/* * Copyright 2011 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.springframework.remoting.jbr; import org.apache.commons.logging.LogFactory; import org.jboss.remoting.InvocationRequest; import org.jboss.remoting.InvokerLocator; import org.jboss.remoting.ServerInvocationHandler; import org.jboss.remoting.ServerInvoker; import org.jboss.remoting.callback.InvokerCallbackHandler; import org.jboss.remoting.invocation.NameBasedInvocation; import org.jboss.remoting.transport.Connector; import org.springframework.beans.factory.BeanNameAware; import org.springframework.beans.factory.InitializingBean; import org.springframework.context.SmartLifecycle; import org.springframework.remoting.support.RemoteExporter; import org.springframework.util.Assert; import org.springframework.util.ReflectionUtils; import org.springframework.util.StringUtils; import javax.management.MBeanServer; import java.lang.reflect.Method; import java.util.Arrays; import java.util.HashMap; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; /** * <P> Exporter that delegates to the JBoss Remoting (http://community.jboss.org/en/jbossremoting) project to handle the RPC support. <p/> * * @author Josh Long * @see org.springframework.remoting.rmi.RmiServiceExporter */ public class JbossRemotingExporter extends RemoteExporter implements InitializingBean, BeanNameAware, SmartLifecycle { // private TransporterServer server; private volatile boolean running = false; // configuration to use in setting up the server private Map<String, String> configuration = new HashMap<String, String>(); // the type of serialization - two values:'jboss' and 'java' private JbossSerialization serializationType = JbossSerialization.JBOSS; private String host = "127.0.0.1"; private String serverName; private boolean clustered; private int port = 5400; private String beanName; private String url; private String transport = "socket"; private Connector connector; @Override public void setBeanName(String name) { this.beanName = name; } protected Connector setupServer(String locatorUrl) throws Exception { InvokerLocator locator = new InvokerLocator(locatorUrl); if (logger.isDebugEnabled()) { logger.debug("Starting remoting server with locator uri of: " + locatorUrl); } Connector connector = new Connector(locator); connector.create(); ReflectiveInvocationHandler invocationHandler = new ReflectiveInvocationHandler(getService()); connector.addInvocationHandler(beanName, invocationHandler); return connector; } @Override public void afterPropertiesSet() throws Exception { Assert.notNull(this.beanName, "the 'beanName' can't be null"); if (!StringUtils.hasText(this.serverName)) { this.serverName = this.beanName; } if (!StringUtils.hasText(this.url)) { this.url = buildUrl(); } connector = setupServer(this.url); } public void setSerializationType(JbossSerialization serializationType) { this.serializationType = serializationType; } public void setHost(String host) { this.host = host; } public void setServerName(String serverName) { this.serverName = serverName; } public void setClustered(boolean clustered) { this.clustered = clustered; } public void setPort(int port) { this.port = port; } public void setUrl(String url) { this.url = url; } public void setTransport(String transport) { this.transport = transport; } public void setConfiguration(Map<String, String> configuration) { this.configuration = configuration; } @Override public boolean isAutoStartup() { return true; } @Override public void stop(Runnable callback) { stop(); if (null != callback) { callback.run(); } } @Override public void start() { try { connector.start(); this.running = true; if (logger.isDebugEnabled()) { logger.debug("just server#start()'d the exporter."); } } catch (Exception e) { if (logger.isErrorEnabled()) { logger.error(String.format("could not start the server for url '%s'", this.url), e); } throw new RuntimeException(e); } } @Override public void stop() { connector.stop(); if (logger.isDebugEnabled()) { logger.debug("stopping the exporter."); } } @Override public boolean isRunning() { return running; } @Override public int getPhase() { return 0; } protected String buildUrl() { Assert.hasText(this.transport); Assert.hasText(this.host); Assert.notNull(this.serializationType, "you must specify a serialization type (" + JbossSerialization.JBOSS.name() + "," + JbossSerialization.JAVA.name() + ")"); Assert.isTrue(this.port > 0, "you must specify a non-zero port"); return transport + "://" + host + ":" + port + "/?serializationtype=" + serializationType.name().toLowerCase(); } /** * <P> This {@link ServerInvocationHandler} crawls every method on the {@link #service} and * constructs a {@link Map} which has as its keys a value object ({@link MethodNameAndArguments}), and * as its key the correct method to call on the target {@link #service} object, cached. * </p> * <P> The {@link MethodNameAndArguments} value object simply contains the String forms of * the method to invoke and the names of the types of the method signature (e.g., {"long","a.Class"}). * </P> * <P> This is a best faith effort, and no guarantees are made as to the correct resolution of the methods. * To avoid any complications (and this advice is valid for most RPC systems, I think you'll find), * prefer methods with unique signatures. Method overloading and var-args don't translate well. * </p> * * @author Josh Long */ public static class ReflectiveInvocationHandler implements ServerInvocationHandler { Object service; org.apache.commons.logging.Log log = LogFactory.getLog(getClass()); Map<MethodNameAndArguments, Method> methodMap = new ConcurrentHashMap<MethodNameAndArguments, Method>(); public ReflectiveInvocationHandler(Object target) throws Exception { this.service = target; buildMapOfMethods(this.service); } /** * this is a simple value object that we use to create a unique key for every method that * can be retrieved safely from a JDK collection because the {@link #equals(Object)} implementation * is stable / predictable. */ static private class MethodNameAndArguments { private String methodName; private String[] classOfArguments; @Override public String toString() { return "MethodNameAndArguments{" + "methodName='" + methodName + '\'' + ", classOfArguments=" + (classOfArguments == null ? null : Arrays.asList(classOfArguments)) + '}'; } private MethodNameAndArguments(String methodName, String[] classOfArguments) { this.methodName = methodName; this.classOfArguments = classOfArguments; } @Override public boolean equals(Object o) { if (this == o) { return true; } if (o == null || getClass() != o.getClass()) { return false; } MethodNameAndArguments that = (MethodNameAndArguments) o; return Arrays.equals(classOfArguments, that.classOfArguments) && !(methodName != null ? !methodName.equals(that.methodName) : that.methodName != null); } @Override public int hashCode() { int result = methodName != null ? methodName.hashCode() : 0; result = 31 * result + (classOfArguments != null ? Arrays.hashCode(classOfArguments) : 0); return result; } } /** * convenience method to build a {@link MethodNameAndArguments} value object given a unique * {@link NameBasedInvocation}. * * @param i the invocation * @return the value object that - for any two invocations of the same method on the server * and given the same argument types - should be {@link #equals(Object) equal to one another}. */ MethodNameAndArguments fromNameBasedInvocation(NameBasedInvocation i) { Class[] classes = new Class[i.getParameters().length]; int indx = 0; for (Object p : i.getParameters()) { classes[indx++] = p.getClass(); } String[] clzzNames = fromClasses(classes); String methodName = i.getMethodName(); if (log.isDebugEnabled()) { log.debug("searching for " + methodMap); } return new MethodNameAndArguments(methodName, clzzNames); } /** * Turn a collection of {@link Class classes} into a collection of {@link String strings}. * * @param clzz the classes * @return the array of strings */ String[] fromClasses(Class[] clzz) { Assert.notNull(clzz, "the classes can't be null"); String[] classes = new String[clzz.length]; int indx = 0; for (Class p : clzz) { classes[indx++] = p.getClass().getName(); } return classes; } MethodNameAndArguments fromMethod(Method m) { return new MethodNameAndArguments(m.getName(), fromClasses(m.getParameterTypes())); } void buildMapOfMethods(Object svc) throws Exception { Method[] methods = ReflectionUtils.getAllDeclaredMethods(svc.getClass()); for (Method m : methods) { methodMap.put(fromMethod(m), m); } } Method findWinningMethod(Object s, NameBasedInvocation parameter) throws Throwable { MethodNameAndArguments nameAndArguments = fromNameBasedInvocation(parameter); return methodMap.get(nameAndArguments); } public Object invoke(InvocationRequest invocation) throws Throwable { Object parm = invocation.getParameter(); Assert.isInstanceOf(NameBasedInvocation.class, parm); NameBasedInvocation parameter = (NameBasedInvocation) invocation.getParameter(); Method method = findWinningMethod(service, parameter); Assert.notNull(method, "we couldn't find the method to invoke!"); return method.invoke(this.service, ((NameBasedInvocation) invocation.getParameter()).getParameters()); } public void addListener(InvokerCallbackHandler callbackHandler) { } public void removeListener(InvokerCallbackHandler callbackHandler) { } public void setMBeanServer(MBeanServer server) { } public void setInvoker(ServerInvoker invoker) { } } }