In my previous entry, I talked about how you could easily use Spring from within EJB 3 beans, thanks to the magic of EJB interceptors. But what about the other way? How do you use EJB 3 from Spring?
*Update:* It looks like the Interface21 guys have had similar ideas – check out Project Pitchfork
Well, back in the days of EJB 2, you would make a reference to an EJB using the following incantation:
Simple enough; the LocalStatelessSessionProxyFactoryBean knows how to create a Stateless Session Bean, via the Home interface, which is what the JNDI entry points out. There’s a couple of different proxy factories, for the various types of EJBs.
In EJB3, you don’t need Home interfaces any more. As a result, you don’t need to use a LocalStatelessSessionProxyFactoryBean either. Instead, you use the standard JndiObjectFactoryBean:
So this is pretty simple: look the EJB up in the JNDI, register it as a Spring Bean, and then wire it into other Spring Beans as normal. So far so good.
Or is it? Well, this has got a major problem: you need to specify the EJBs. One of the big selling points of EJB 3 is that you don’t _have_ to manage any XML files, and here comes Spring making you do it all over again! How annoying!
Fortunately, there’s a solution: BeanPostProcessors. BeanPostProcessors lets you do things to a bean after its created. Things like looking for setter methods with an @EJB annotation on them, for example.
Here’s the code:
/** Look for EJB3-style annotations, and provide the corresponding values from the container. */
public class EjbBeanPostProcessor implements BeanPostProcessor {
public Object postProcessAfterInitialization(Object bean, String beanName)
throws BeansException {
// do nothing; the EJBs are configured before initialization.
return bean;
}
public Object postProcessBeforeInitialization(Object bean, String beanName)
throws BeansException {
Class beanClazz = bean.getClass();
for (Method method : beanClazz.getMethods()) {
if (method.isAnnotationPresent(EJB.class)) {
setEjbRef(bean, method);
}
}
return bean;
}
private void setEjbRef(T bean, Method setter) {
Class ejbClass = determineBeanClass(setter);
Object ejbBean = lookupBean(ejbClass, determineJndiName(ejbClass, setter), bean);
invoke(bean, setter, ejbBean);
}
/**
* Determine the JNDI name for the EJB. By default, we assume that it is the simple class
* name, but we allow the setter method to override this, using the 'mappedName' attribute.
*/
private String determineJndiName(Class ejbClass, Method setter) {
EJB ejbAnnotation = setter.getAnnotation(EJB.class);
if (ejbAnnotation.mappedName().equals("")) {
return ejbClass.getSimpleName();
}
return ejbAnnotation.mappedName();
}
private Object lookupBean(Class ejbClass, String beanName, Object bean) throws BeansException {
try {
return new InitialContext().lookup("java:comp/env/ejb/" + beanName);
} catch (NamingException e) {
throw new EjbBeansException(bean, "Could not resolve EJB " + beanName, e);
}
}
/**
* Determine the type of the EJB. This is used to determine the JNDI name to look up. This
* would normally be the type of the argument to the setter, but the EJB spec allows this
* to be overriden (presumably to a subclass)
*/
private Class determineBeanClass(Method setter) {
EJB ejbRefAnnotation = setter.getAnnotation(EJB.class);
if (ejbRefAnnotation.beanInterface() != null &&
Object.class.equals(ejbRefAnnotation.beanInterface()) == false) {
return ejbRefAnnotation.beanInterface();
}
Class otherBeanClass = setter.getParameterTypes()[0];
return otherBeanClass;
}
private void invoke(T bean, Method method, Object ... args) {
try {
method.invoke(bean, args);
} catch (IllegalArgumentException e) {
throw new EjbBeansException(bean, "Could not set EJB", e);
} catch (IllegalAccessException e) {
throw new EjbBeansException(bean, "Could not set EJB", e);
} catch (InvocationTargetException e) {
throw new EjbBeansException(bean, "error setting EJB reference", e.getCause());
}
}
/** Simple exception detailing the type of problem that occured. */
private static class EjbBeansException extends BeansException {
public EjbBeansException(Object bean, String message, Throwable cause) {
super(message + " [bean class: " + bean.getClass().getName() + "]", cause);
}
}
}
Having created this lovely BeanPostProcessor, you enable it in your Spring context file with a simple one-line statement, defining the post-processor:
Now, you simply need to add @EJB references to your Spring beans, and they will magically be set. No need to list the EJBs in your Spring context files or anything – it just works. It also works with anything else using Spring; we have all our WebWork actions configured this way now.
The only caveat to this is that you need to have the JNDI context set up. In a web application, for example, this means you need to have the ejb-ref entries in your web.xml file.
I hope you find this useful; I know I have.
