org.jboss.ejb.client.EJBInvocationHandler Maven / Gradle / Ivy
/*
* JBoss, Home of Professional Open Source.
* Copyright 2011, Red Hat, Inc., and individual contributors
* as indicated by the @author tags. See the copyright.txt file in the
* distribution for a full listing of individual contributors.
*
* This is free software; you can redistribute it and/or modify it
* under the terms of the GNU Lesser General Public License as
* published by the Free Software Foundation; either version 2.1 of
* the License, or (at your option) any later version.
*
* This software is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this software; if not, write to the Free
* Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
* 02110-1301 USA, or see the FSF site: http://www.fsf.org.
*/
package org.jboss.ejb.client;
import java.io.NotSerializableException;
import java.io.Serializable;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.rmi.RemoteException;
import java.rmi.UnmarshalException;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.Future;
import javax.ejb.EJBException;
import javax.ejb.EJBHome;
import javax.ejb.EJBObject;
import org.jboss.ejb.client.annotation.CompressionHint;
/**
* @param the proxy view type
* @author David M. Lloyd
*/
@SuppressWarnings({"SerializableClassWithUnconstructableAncestor"})
final class EJBInvocationHandler extends Attachable implements InvocationHandler, Serializable {
private static final long serialVersionUID = 946555285095057230L;
private static final String ENABLE_ANNOTATION_SCANNING_SYSTEM_PROPERTY = "org.jboss.ejb.client.view.annotation.scan.enabled";
private final transient boolean async;
/**
* @serial the associated EJB locator
*/
private final EJBLocator locator;
/**
* @serial the weak affinity
*/
private volatile Affinity weakAffinity = Affinity.NONE;
/**
* An optional association to a EJB client context
*/
// we don't serialize EJB client context identifier
private final transient EJBClientContextIdentifier ejbClientContextIdentifier;
/**
* The {@link AnnotationScanner}s which will be used for scanning annotations on the view class which corresponds to the proxy represented by this invocation handler
*/
// The whole annotation scanning business is for now contained within this invocation handler class and isn't exposed or isn't allowed to be "pluggable" to avoid unnecesary complexity
// in the EJB client API. The only control user applications have over annotation scanning of view class(es), for now, is via setting a system property to enable or disable the scanning.
private final transient AnnotationScanner[] annotationScanners = new AnnotationScanner[]{new CompressionHintAnnotationScanner()};
/**
* map of methods that can be handled on the client side
*/
private static final Map clientSideMethods;
static {
Map methods = new HashMap();
methods.put(new MethodKey("equals", Object.class), new EqualsMethodHandler());
methods.put(new MethodKey("hashCode"), new HashCodeMethodHandler());
methods.put(new MethodKey("toString"), new ToStringMethodHandler());
methods.put(new MethodKey("getPrimaryKey"), new GetPrimaryKeyHandler());
methods.put(new MethodKey("getHandle"), new GetHandleHandler());
methods.put(new MethodKey("isIdentical", EJBObject.class), new IsIdenticalHandler());
methods.put(new MethodKey("getHomeHandle"), new GetHomeHandleHandler());
clientSideMethods = Collections.unmodifiableMap(methods);
}
EJBInvocationHandler(final EJBLocator locator) {
this(null, locator);
}
/**
* Creates an {@link EJBInvocationHandler} for the passed locator
and associates this
* invocation handler with the passed ejbClientContextIdentifier
*
* @param ejbClientContextIdentifier (Optional) EJB client context identifier. Can be null.
* @param locator The {@link EJBLocator} cannot be null.
*/
EJBInvocationHandler(final EJBClientContextIdentifier ejbClientContextIdentifier, final EJBLocator locator) {
if (locator == null) {
throw Logs.MAIN.paramCannotBeNull("EJB locator");
}
this.ejbClientContextIdentifier = ejbClientContextIdentifier;
this.locator = locator;
async = false;
if (locator instanceof StatefulEJBLocator) {
// set the weak affinity to the node on which the session was created
final String sessionOwnerNode = ((StatefulEJBLocator>) locator).getSessionOwnerNode();
if (sessionOwnerNode != null) {
this.setWeakAffinity(new NodeAffinity(sessionOwnerNode));
}
}
// scan for annotations on the view class, if necessary
final String annotationScanEnabledSysPropVal = SecurityActions.getSystemProperty(ENABLE_ANNOTATION_SCANNING_SYSTEM_PROPERTY);
if (annotationScanEnabledSysPropVal != null && Boolean.valueOf(annotationScanEnabledSysPropVal.trim())) {
scanAnnotationsOnViewClass();
} else {
// let's for the sake of potential performance optimization, add an attachment which lets the EJBReceiver(s) and any other decision making
// code to decide whether it can entirely skip any logic related to "hints" (like @org.jboss.ejb.client.annotation.CompressionHint)
putAttachment(AttachmentKeys.HINTS_DISABLED, true);
}
}
EJBInvocationHandler(final EJBInvocationHandler other) {
super(other);
locator = other.locator;
async = true;
ejbClientContextIdentifier = other.ejbClientContextIdentifier;
if (locator instanceof StatefulEJBLocator) {
// set the weak affinity to the node on which the session was created
final String sessionOwnerNode = ((StatefulEJBLocator>) locator).getSessionOwnerNode();
if (sessionOwnerNode != null) {
this.setWeakAffinity(new NodeAffinity(sessionOwnerNode));
}
}
}
public Object invoke(final Object proxy, final Method method, final Object[] args) throws Throwable {
return doInvoke(locator.getViewType().cast(proxy), method, args);
}
void setWeakAffinity(Affinity newWeakAffinity) {
weakAffinity = newWeakAffinity;
}
Affinity getWeakAffinity() {
return weakAffinity;
}
/**
* Returns the {@link EJBClientContextIdentifier} associated with this invocation handler. If this
* invocation handler isn't associated with any {@link EJBClientContextIdentifier} then this method
* returns null
*
* @return
*/
EJBClientContextIdentifier getEjbClientContextIdentifier() {
return this.ejbClientContextIdentifier;
}
Object doInvoke(final T proxy, final Method method, final Object[] args) throws Throwable {
final MethodHandler handler = clientSideMethods.get(new MethodKey(method));
if (handler != null && handler.canHandleInvocation(this, proxy, method, args)) {
return handler.invoke(this, proxy, method, args);
}
final EJBClientContext context;
// check if we are associated with a EJB client context identifier
if (ejbClientContextIdentifier == null) {
// we aren't associated with a EJB client context identifier, so select the "current"
// EJB client context
context = EJBClientContext.requireCurrent();
} else {
// we are associated with a specific EJB client context, so fetch it
context = EJBClientContext.require(this.ejbClientContextIdentifier);
}
return doInvoke(this, async, proxy, method, args, context);
}
@SuppressWarnings("unchecked")
static EJBInvocationHandler extends T> forProxy(T proxy) {
InvocationHandler handler = Proxy.getInvocationHandler(proxy);
if (handler instanceof EJBInvocationHandler) {
return (EJBInvocationHandler extends T>) handler;
}
throw Logs.MAIN.proxyNotOurs(proxy, EJBClient.class.getName());
}
private static Object doInvoke(final EJBInvocationHandler ejbInvocationHandler, final boolean async, final T proxy, final Method method, final Object[] args, EJBClientContext clientContext) throws Throwable {
final EJBClientInvocationContext invocationContext = new EJBClientInvocationContext(ejbInvocationHandler, clientContext, proxy, method, args);
try {
// send the request
sendRequestWithPossibleRetries(invocationContext, true);
if (!async) {
// wait for invocation to complete
final Object value = invocationContext.awaitResponse();
if (value != EJBClientInvocationContext.PROCEED_ASYNC) {
return value;
}
// proceed asynchronously
}
// force async...
if (method.getReturnType() == Future.class) {
return invocationContext.getFutureResponse();
} else if (method.getReturnType() == void.class) {
invocationContext.setDiscardResult();
// Void return
return null;
} else {
// wrap return always
EJBClient.setFutureResult(invocationContext.getFutureResponse());
return null;
}
} catch (Exception e) {
//AS7-5937 prevent UndeclaredThrowableException
if (e instanceof RuntimeException) {
throw e;
}
boolean remoteException = false;
for (Class> exception : method.getExceptionTypes()) {
if (exception.isAssignableFrom(e.getClass())) {
throw e;
} else if (RemoteException.class.equals(exception)) {
remoteException = true;
}
}
if (remoteException) {
throw new RemoteException("Error", e);
}
throw new EJBException(e);
}
}
/**
* Sends a method invocation request to an eligible EJB receiver. If the request sending fails with a {@link RequestSendFailedException} then this method attempts to
* retry sending that request to some other eligible node (if any). It does this till either the request was successfully sent or till there are no more eligible EJB receivers
* which can handle this request
*
* @param clientInvocationContext The EJB client invocation context
* @param firstAttempt True if this is a first attempt at sending the request, false if this is a retry
* @throws Exception
*/
private static void sendRequestWithPossibleRetries(final EJBClientInvocationContext clientInvocationContext, final boolean firstAttempt) throws Exception {
try {
// this is the first attempt so use the sendRequest API
if (firstAttempt) {
clientInvocationContext.sendRequest();
} else {
// retry
clientInvocationContext.retryRequest();
}
} catch (RequestSendFailedException rsfe) {
// check whether the first attempt fails during serialization
if (firstAttempt) {
// EJBCLIENT-106 supress retry and throw the root cause
if (rsfe.getCause() instanceof RuntimeException) {
throw (RuntimeException) rsfe.getCause();
}else if(rsfe.getCause() instanceof NotSerializableException) {
throw (NotSerializableException)rsfe.getCause();
}else if (rsfe.getCause() instanceof UnmarshalException) {
throw (UnmarshalException) rsfe.getCause();
}
}
final String failedNodeName = rsfe.getFailedNodeName();
if (failedNodeName != null) {
Logs.MAIN.debugf(rsfe, "Retrying invocation %s which failed on node: %s due to:", clientInvocationContext, failedNodeName);
// exclude this failed node, during the retry
clientInvocationContext.markNodeAsExcluded(failedNodeName);
// retry
sendRequestWithPossibleRetries(clientInvocationContext, false);
} else {
throw rsfe;
}
}
}
@SuppressWarnings("unused")
protected Object writeReplace() {
return new SerializedEJBInvocationHandler(locator);
}
EJBInvocationHandler getAsyncHandler() {
return async ? this : new EJBInvocationHandler(this);
}
boolean isAsyncHandler() {
return this.async;
}
EJBLocator getLocator() {
return locator;
}
/**
* Scans for annotations on the view class and its methods using pre-configured {@link AnnotationScanner}s (if any)
*/
private void scanAnnotationsOnViewClass() {
// nothing to do
if (annotationScanners == null || annotationScanners.length == 0) {
return;
}
final Class> viewClass = this.locator.getViewType();
final Method[] viewMethods = viewClass.getMethods();
for (final AnnotationScanner annotationScanner : annotationScanners) {
// scan class level annotations
annotationScanner.scanClass(viewClass);
// now method level annotations
for (final Method viewMethod : viewMethods) {
annotationScanner.scanMethod(viewMethod);
}
}
}
private static final class MethodKey {
private final String name;
private final Class>[] parameters;
public MethodKey(final String name, final Class>... parameters) {
this.name = name;
this.parameters = parameters;
}
public MethodKey(final Method method) {
this.name = method.getName();
this.parameters = method.getParameterTypes();
}
@Override
public boolean equals(final Object o) {
//we don't care about the return type
//we want covariant methods to still be handled
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
final MethodKey methodKey = (MethodKey) o;
if (!name.equals(methodKey.name)) return false;
if (!Arrays.equals(parameters, methodKey.parameters)) return false;
return true;
}
@Override
public int hashCode() {
int result = name.hashCode();
result = 31 * result + Arrays.hashCode(parameters);
return result;
}
}
private interface MethodHandler {
boolean canHandleInvocation(final EJBInvocationHandler> thisHandler, final Object proxy, final Method method, final Object[] args) throws Exception;
Object invoke(final EJBInvocationHandler> thisHandler, final Object proxy, final Method method, final Object[] args) throws Exception;
}
private static final class EqualsMethodHandler implements MethodHandler {
@Override
public boolean canHandleInvocation(final EJBInvocationHandler> thisHandler, final Object proxy, final Method method, final Object[] args) {
return true;
}
@Override
public Object invoke(final EJBInvocationHandler> thisHandler, final Object proxy, final Method method, final Object[] args) {
final Object other = args[0];
if (other instanceof Proxy) {
final InvocationHandler handler = Proxy.getInvocationHandler(other);
if (handler instanceof EJBInvocationHandler) {
return thisHandler.locator.equals(((EJBInvocationHandler>) handler).locator);
}
}
return Boolean.FALSE;
}
}
private static final class HashCodeMethodHandler implements MethodHandler {
@Override
public boolean canHandleInvocation(final EJBInvocationHandler> thisHandler, final Object proxy, final Method method, final Object[] args) {
return true;
}
@Override
public Object invoke(final EJBInvocationHandler> thisHandler, final Object proxy, final Method method, final Object[] args) {
return thisHandler.locator.hashCode();
}
}
private static final class ToStringMethodHandler implements MethodHandler {
@Override
public boolean canHandleInvocation(final EJBInvocationHandler> thisHandler, final Object proxy, final Method method, final Object[] args) {
return true;
}
@Override
public Object invoke(final EJBInvocationHandler> thisHandler, final Object proxy, final Method method, final Object[] args) {
return String.format("Proxy for remote EJB %s", thisHandler.locator);
}
}
private static final class GetPrimaryKeyHandler implements MethodHandler {
@Override
public boolean canHandleInvocation(final EJBInvocationHandler> thisHandler, final Object proxy, final Method method, final Object[] args) throws Exception {
return proxy instanceof EJBObject;
}
@Override
public Object invoke(final EJBInvocationHandler> thisHandler, final Object proxy, final Method method, final Object[] args) throws Exception {
final EJBLocator> locator = thisHandler.locator;
if (locator instanceof EntityEJBLocator) {
return ((EntityEJBLocator) locator).getPrimaryKey();
}
throw new RemoteException("Cannot invoke getPrimaryKey() om " + proxy);
}
}
private static final class GetHandleHandler implements MethodHandler {
@Override
public boolean canHandleInvocation(final EJBInvocationHandler> thisHandler, final Object proxy, final Method method, final Object[] args) throws Exception {
return proxy instanceof EJBObject && thisHandler.locator instanceof EJBLocator;
}
@Override
public Object invoke(final EJBInvocationHandler> thisHandler, final Object proxy, final Method method, final Object[] args) throws Exception {
final EJBLocator locator = (EJBLocator) thisHandler.getLocator();
return new EJBHandle(locator);
}
}
private static final class IsIdenticalHandler implements MethodHandler {
@Override
public boolean canHandleInvocation(final EJBInvocationHandler> thisHandler, final Object proxy, final Method method, final Object[] args) throws Exception {
return proxy instanceof EJBObject && thisHandler.locator instanceof EJBLocator;
}
@Override
public Object invoke(final EJBInvocationHandler> thisHandler, final Object proxy, final Method method, final Object[] args) throws Exception {
final EJBLocator> locator = thisHandler.locator;
final Object other = args[0];
if (Proxy.isProxyClass(other.getClass())) {
final InvocationHandler handler = Proxy.getInvocationHandler(other);
if (handler instanceof EJBInvocationHandler) {
return locator.equals(((EJBInvocationHandler>) handler).getLocator());
}
}
return false;
}
}
private static final class GetHomeHandleHandler implements MethodHandler {
@Override
public boolean canHandleInvocation(final EJBInvocationHandler> thisHandler, final Object proxy, final Method method, final Object[] args) throws Exception {
return proxy instanceof EJBHome;
}
@Override
public Object invoke(final EJBInvocationHandler> thisHandler, final Object proxy, final Method method, final Object[] args) throws Exception {
final EJBLocator locator = (EJBLocator) thisHandler.getLocator();
if (locator instanceof EJBHomeLocator) {
return new EJBHomeHandle((EJBHomeLocator) locator);
}
throw new RemoteException("Cannot invoke getHomeHandle() on " + proxy);
}
}
/**
* An {@link AnnotationScanner} is responsible for scanning for (some known) annotations on the view class and its methods. It's up to the implementations of this
* interface to decide what to do with the annotation it finds on the view class or the view method. Typical implementations attach the found annotation(s) as "attachments" to the
* {@link EJBInvocationHandler} so that the annotation information is available for invocations, happening on this invocation handler, through the use of {@link EJBClientInvocationContext#getProxyAttachment(AttachmentKey)}
*/
private interface AnnotationScanner {
void scanClass(final Class> view);
void scanMethod(final Method method);
}
/**
* An {@link AnnotationScanner} responsible for scanning the {@link org.jboss.ejb.client.annotation.CompressionHint} annotation on the view class and its methods. This
* {@link org.jboss.ejb.client.EJBInvocationHandler.CompressionHintAnnotationScanner} attaches the found annotations (if any) to the invocation handler through the use of {@link EJBInvocationHandler#putAttachment(AttachmentKey, Object)}
* method. The class level {@link org.jboss.ejb.client.annotation.CompressionHint} annotation (if any) is attached with {@link AttachmentKeys#VIEW_CLASS_DATA_COMPRESSION_HINT_ATTACHMENT_KEY} as the key and the method level
* {@link org.jboss.ejb.client.annotation.CompressionHint} annotations (if any) is attached with {@link AttachmentKeys#VIEW_METHOD_DATA_COMPRESSION_HINT_ATTACHMENT_KEY} as the key
*/
private final class CompressionHintAnnotationScanner implements AnnotationScanner {
@Override
public void scanClass(Class> view) {
final CompressionHint compressionHint = (CompressionHint) view.getAnnotation(CompressionHint.class);
// nothing to do
if (compressionHint == null) {
return;
}
// add it as an attachment to the invocation handler so that it's available during an invocation via the EJBClientInvocationContext#getProxyAttachment()
EJBInvocationHandler.this.putAttachment(AttachmentKeys.VIEW_CLASS_DATA_COMPRESSION_HINT_ATTACHMENT_KEY, compressionHint);
}
@Override
public void scanMethod(Method method) {
final CompressionHint compressionHint = (CompressionHint) method.getAnnotation(CompressionHint.class);
// nothing to do
if (compressionHint == null) {
return;
}
Map annotatedMethods = EJBInvocationHandler.this.getAttachment(AttachmentKeys.VIEW_METHOD_DATA_COMPRESSION_HINT_ATTACHMENT_KEY);
if (annotatedMethods == null) {
// Intentionally avoiding IdentityHashMap since it doesn't work out because the methods being scanned here and the methods being invoked upon later, aren't the "same" (i.e. this method != that method)
annotatedMethods = new HashMap();
EJBInvocationHandler.this.putAttachment(AttachmentKeys.VIEW_METHOD_DATA_COMPRESSION_HINT_ATTACHMENT_KEY, annotatedMethods);
}
annotatedMethods.put(method, compressionHint);
}
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy