Thursday, 2 July 2009

Reacquiring a Stateful Session Bean on NoSuchEJBException

In the current project I'm working on I'm using a Tomcat server connecting to a remote JBoss instance. We're using a Stateful Session Bean (SFSB) to hold the session information for the current user for authentication/access control purposes. The bean is stored in tomcat within the session (within a session-scoped managed bean actually) and generally it works fine.
However, if the bean is destroyed on the server then, as per the EJB specification, an NoSuchEJBException is thrown. This could be handled individually by catching the exception, printing a user friendly message and getting a new instance of the session bean. However, it you've got several method calls across multiple classes, having this try/catch code throughout all the classes bloats the code and just plain looks ugly.

I wanted a solution where I didn't need to touch the module variable that stored the EJB given to me by jboss - I wanted it to obtain a new instance itself. The reason being, if I have this instance shared across multiple JSF managed beans (some stored in session-scope, some in request), the other classes shouldn't have to know that a new instance of the EJB was obtained.

The solution is to use a nifty feature of Java reflection API - a Proxy that passes calls to a InvocationHandler instance that you write. Actually the EJB instance JBoss's context factory provides you is a Proxy (and the same is true for other app servers), which makes wrapping it even easier. Observe:
First create a class that implements InvocationHandler and a custom interface we'll use to directly access the handler called StatefulProxy:
package devgrok.proxy;

import java.io.Serializable;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;

import javax.ejb.NoSuchEJBException;
import javax.naming.Context;
import javax.naming.InitialContext;
import javax.naming.NamingException;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
* Utility wrapper, automatically obtains a new instance of the session bean when the existing instance is lost. Uses
* jndi to obtain new instance.
* @author Chris Watts 2009
*/
public class JndiRemoteProxyInvocationHandler implements InvocationHandler, StatefulProxy, Serializable
{
/** Logger for this class */
private static final Logger log = LoggerFactory.getLogger(JndiRemoteProxyInvocationHandler.class);

private Proxy remoteBean = null;
private final String jndiName;

/*
* (non-Javadoc)
* 
* @see java.lang.reflect.InvocationHandler#invoke(java.lang.Object, java.lang.reflect.Method, java.lang.Object[])
*/
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable
{
if (method.getDeclaringClass().equals(StatefulProxy.class))
return method.invoke(this, args);

if (remoteBean == null)
getNewInstance();

try
{
//relay it on to JBoss's handler
InvocationHandler handler = Proxy.getInvocationHandler(remoteBean);
return handler.invoke(remoteBean, method, args);
}
catch (NoSuchEJBException e)
{
log.warn("caught exception {}", e.toString());
remoteBean = null;
throw e;
}
}

/**
* Constructs the proxy, obtaining the instance from the given jndi name.
* @param jndiName
*/
public JndiRemoteProxyInvocationHandler(String jndiName)
{
this.jndiName = jndiName;
getNewInstance();
}

/**
* Obtains a new instance of the bean from the context.
* @return
*/
public void getNewInstance()
{
try
{
Context context = new InitialContext();
this.remoteBean = (Proxy) context.lookup("java:/comp/env/" + jndiName);
}
catch (NamingException e)
{
log.warn("caught exception {}", e.toString());
throw new RuntimeException(e);
}
}

public Object getRemoteBean()
{
return remoteBean;
}

public void clearInstance()
{
remoteBean = null;
}
}
Next the interface to provide access to the utility methods:
package devgrok.proxy;

/**
* Interface to allow calls to the InvocationHandler for the stateful bean proxy.
* @author Chris Watts 2009
*/
public interface StatefulProxy<T>
{
/**
* Get a new instance of the underlying class.
*/
public void getNewInstance();

/**
* Returns the remote bean.
* 
* @return
*/
public T getRemoteBean();

/**
* Clears the stored instance of the bean (useful for logout/cleanup).
*/
public void clearInstance();
}
Now all that is left is obtaining an instance. Since I'm using Google Guice I wrote a provider (aka a factory) but you could easily adapt it:
package devgrok.proxy;

import java.lang.reflect.Proxy;

import com.google.inject.Provider;

/**
* A guice provider, providing a proxied EJB looked up by JNDI.
* @author Chris Watts 2009
*/
public class ProxyProvider<T> implements Provider<T>
{
private final String jndiName;
private final Class<T> clazz;

/**
* @param jndiName
* @param clazz
*/
public ProxyProvider(String jndiName, Class<T> clazz)
{
this.jndiName = jndiName;
this.clazz = clazz;
}

/*
* (non-Javadoc)
* 
* @see com.google.inject.Provider#get()
*/
public T get()
{
return (T) Proxy.newProxyInstance(clazz.getClassLoader(), new Class[] { clazz, StatefulProxy.class },
new JndiRemoteProxyInvocationHandler(jndiName));
}

}
To bind it:
bind(MySessionRemote.class).toProvider(new ProxyProvider<MySessionRemote>("ejb/myEjb", MySessionRemote.class)).in(ServletScopes.SESSION);

4 comments:

  1. Correct me if I'm wrong, but that example doesn't silently retrieve a new session bean, right? If the old session bean is gone, it throws the exception back to the caller, and only fetches a new session bean the next time a method is called, right? So you'd still need retry code in your callers.

    You could try to fetch a new session bean in the invoke method, or recurse with a retry limit or something.

    ReplyDelete
  2. True, that could easily be changed to reacquire the bean within the proxy on NoSuchEJBException.

    However in my case I wanted the exception cascaded up to the caller so special logic can be performed, as it was used to store the user's session state. Thus if it is lost then the user has been logged out.
    In my case the calling code doesn't need to reacquire the bean, the next call on the bean will automatically get a new one.

    ReplyDelete
  3. Hi. I am using JBoss 5.1.0 and I have Stateful Session Beans that are destroyed but I do not want the user to get an exception. I would like for the bean to be recreated when this occurs. How would I go about writing the ProxyProvider and how to bind it to my bean? Thanks.

    ReplyDelete