Seiten

Simple Remote Access with JEE 6 Web Profile


Problem

A simple HTTP based remote access solution is not part of the JEE 6 Web Profile. Unfortunately this is one of the important parts that must be available in a rich client architecture using EclipseRCP/Spring.


Solution

Therefore I started to build a solution for myself. The solution should be
  • simple to use by an annotation,
  • based on Hessian and
  • plugging into the CDI container.
To use the remote access solution you have simply to
  1. add a library to your project,
  2. annotate your beans that should be remote accessible and
  3. define a ServletContextListener in web.xml

The usage of the annotation is quite simple. You just need to define the interface to be remote exposed and the (relative) url at the bean that must implement this interface:

@RemoteCallable(type=IPersonService.class, url="/personService")
public class PersonService implements IPersonService {

  // some service methods

}

In case of an EJB you need to define an JNDI name for your EJB. This EJB is looked up in JNDI under java:app/[webapp-url]/[ejb-name] (the root path is hard coded but vendor specific - this should be improved):

@Stateless(name="ejb/PersonService")
@RemoteCallable(type=IPersonService.class, url="/personService")
public class PersonService implements IPersonService {

  // some service methods

}

You have can access @RemoteCallable beans from client side as follows:
        
        String url = "http://localhost:8080/eercp.server/personService";
        HessianProxyFactory factory = new HessianProxyFactory();
        this.remotePersonService = (IPersonService) factory.create(IPersonService.class,url);
        
        Person person = new Person();
        
        person = this.remotePersonService.update(person);


Implementation

The implementation approach:
  1. During web application startup a CDI extension collects all bean types with @RemoteCallable annotations and stores them in an instance variable.
  2. A ServletContextListener retrieves the CDI extension bean from the CDI context and
  3. uses the stored bean types to retrieve the corresponding bean/EJB implementation instances using CDI/JNDI  and
  4. creates a Servlet (dynamic-proxy) as remote accessible delegator for each bean/EJB (delegate).
  5. Each Servlet is dynamically registered in the servlet context with the given url.

For the creation of the dynamic-proxy I used the javassist-library The dynamic proxy
  • extends the HessianServlet,
  • implements the bean service interface,
  • is associated with the bean/EJB implementation instance and
  • delegates all calls to the implementation instance.

Here are some implementation aspects in detail.

Lets start with the CDI extension. Any CDI extension class must be declared in the file ./META-INF/services/javax.enterprise.inject.spi.Extension.

The extension must extend javax.enterprise.inject.spi.Extension and looks like this:

public class RemoteCallableDiscoveryExtension implements Extension {

    /** the list of remote callable annotated types. */
    private Collection<Annotatedtype> annotatedTypes = new ArrayList<Annotatedtype>();

    /** callback (is called for each annotated bean)  */
     <t> void processAnnotatedType(@Observes ProcessAnnotatedType<t> pat) {

        AnnotatedType<t> type = pat.getAnnotatedType();

        Annotation anno = type.getAnnotation(RemoteCallable.class);

        if (anno != null) {
            annotatedTypes.add(type);
        }
    }
    ...
}

The life cycle of the ServletContextListener is determined by the web application. Therefore my listener could not be part of the CDI context. So I have to access the beans using the CDI SPI (Service Provider Interface). I created a helper class to achieve this:

public class BeanManagerAccessor {

    /**
     * bean manager reference.
     */
    private BeanManager cdiBeanManager;

    /** Retrieves and returns the bean reference from cdi context of the given type. ... */
    public <T> T getReference(Class<T> beanClass) {
        BeanManager cdiManager = getBeanManager();

        Bean<t> bean = (Bean<T>) cdiManager.resolve(cdiManager.getBeans(beanClass));
        CreationalContext<T> env = cdiManager.createCreationalContext(bean);

        if (bean != null) {
            return (T) cdiManager.getReference(bean, beanClass, env);
        }
        else {
            throw new RuntimeException("No Bean for class: " + beanClass + " found!");
        }
    }

    /** Returns the bean manager... */
    public BeanManager getBeanManager() {
        if (cdiBeanManager == null) {
            try {
                InitialContext ic = new InitialContext();
                cdiBeanManager = (BeanManager) ic.lookup("java:comp/BeanManager");
            } catch (Exception anEx) {
                throw new RuntimeException(anEx);
            }
        }
        return cdiBeanManager;
    }
}
At least all things are put together in my ServletContextListener implementation. Important is the contextInitialized() method that is called when the web application context is initialzed what also includes the creation of the CDI application context:

public class BeanManagerAccessor {

public class RemoteHessianInitializer implements ServletContextListener {

    ...

    /** Context listener callback. ... */
    @Override
    public void contextInitialized(ServletContextEvent sce) {

        BeanManagerAccessor accessor = new BeanManagerAccessor()

        // retrieve extension from CDI
        RemoteCallableDiscoveryExtension extension = 
                 accessor.getReference(RemoteCallableDiscoveryExtension.class);

        Collection<Annotatedtype> annotatedTypes = extension.getRemoteCallableAnnotatedTypes();

        // process all @RemoteCallable annotated beans
        for (AnnotatedType annoType : annotatedTypes) {

            RemoteCallable anno = annoType.getAnnotation(RemoteCallable.class);
            Class iface = anno.type();
            String url = anno.url();
            Class implClass = annoType.getJavaClass();

            // bean instance to be made remote accessible
            Object instance = null;

            // check if its an ejb
            Stateless stateless = annoType.getAnnotation(Stateless.class);
            Stateful statefull = annoType.getAnnotation(Stateful.class);

            // try to retrieve ejb from jndi
            if (stateless != null || statefull != null) {
                try {
                    String ejbName = null;
                    if (stateless != null) {
                        ejbName = stateless.name();
                    } else if (statefull != null) {
                        ejbName = statefull.name();
                    }
                    if (ejbName != null) {

                        InitialContext ctx = new InitialContext();
                        String webAppName = sce.getServletContext().getContextPath();
                        // TODO the jndi-ejb root path (path building) should be configurable
                        instance = ctx.lookup("java:app" + webAppName + "/" + ejbName);
                    } else {
                        throw new RuntimeException("...");
                    }

                } catch (Exception ex) {
                    ...
                }
            } else {
                try {
                    instance = accessor.getReference(implClass);
                } catch (Exception ex) {
                    ...
                }
            }
            if (instance != null) {

                // create hession servlet proxy
                Object proxyImpl = ExtendedHessianServletProxy.createProxy(iface, instance);

                // register as servlet
                ServletRegistration.Dynamic dynamic = 
                       sce.getServletContext().addServlet(iface.getName(), (HessianServlet) proxyImpl);
                // register url mapping
                dynamic.addMapping(url);
            }
        }
    }
}

The dynamic proxy class looks like this:

public class ExtendedHessianServletProxy extends HessianServlet {

    /**
     * the delegate.
     */
    Object delegate;

    ...

    /** The returned proxy inherits a hession servlet that can dynamically 
     * used as a hession business proxy for the given interface.... */
    public static Object createProxy(final Class anInterfaceToImplement, final Object anImplementor) {

        ProxyFactory f = new ProxyFactory();
        f.setSuperclass(ExtendedHessianServletProxy.class);
        f.setInterfaces(new Class[]{anInterfaceToImplement});
        f.setFilter(new MethodFilter() {

            public boolean isHandled(Method aMethod) {

                // only accept methods of the given interface
                for (Method method : anInterfaceToImplement.getDeclaredMethods()) {
                    if (method.equals(aMethod)) {
                        return true;
                    }
                }
                return false;
            }
        });
        Class c = f.createClass();
        MethodHandler mi = new MethodHandler() {

            public Object invoke(Object self, Method aMethodCalled, Method proceed,
                    Object[] args) throws Throwable {

                ExtendedHessianServletProxy proxy = (ExtendedHessianServletProxy) self;
                return aMethodCalled.invoke(proxy.getDelegate(), args);  // execute the delegate method.
            }
        };
        ExtendedHessianServletProxy proxy;
        try {
            proxy = (ExtendedHessianServletProxy) c.newInstance();
            ((ProxyObject) proxy).setHandler(mi);
        } catch (Exception ex) {
            .....
        }
        proxy.setDelegate(anImplementor);
        return proxy;
    }
}

The complete source code you find at My GitHub repository.

Keine Kommentare:

Kommentar veröffentlichen