Using the Spring AOP Framework with EJB Components
by Eugene Kuleshov
12/19/2005
Abstract
A rapidly growing developer community, support for various backend technologies (including JMS, JTA, JDO, Hibernate, iBATIS, and many others), and, more importantly, a non-intrusive lightweight IoC container and a built-in AOP runtime make the Spring Framework very attractive for J2EE application development. Spring-managed components (POJOs) can coexist with EJBs and allow you to use an AOP approach to deal with cross-cutting aspects of enterprise applications—starting from monitoring and auditing, caching, and application-level security, and moving through to handling application-specific business requirements.
This article guides you through several practical uses of Spring's AOP framework within a J2EE application.
Introduction
J2EE technology provides a great foundation for implementing server-side and middleware applications. J2EE containers such as BEA WebLogic Server can take care of system-level elements including application life cycle, security, transactions, remoting, and concurrency, and it can guarantee support for common services like JDBC, JMS, and JTA. However, the greatness and complexity of J2EE makes development and testing more difficult. A traditional J2EE application is usually heavily reliant on the services made available through the container's JNDI. That means a lot of direct JNDI lookups, or as a slight improvement, use of the Service Locator pattern. This architecture increases the coupling between components and makes it almost impossible to test in isolation. For an in-depth analysis of the drawbacks of this architecture you can read the book, J2EE Development without EJB, written by the authors of the Spring Framework.
With the Spring Framework you can wire business logic implemented in plain Java objects with traditional J2EE infrastructure and significantly reduce the amount of code needed for accessing J2EE components and services. On top of that you can mix traditional OO design with orthogonal AOP componentization. Later in this article I demonstrate refactoring of J2EE components to make use of Spring-managed Java objects, and then apply an AOP approach to implement new features maintaining good component separation and testability.
Compared to other AOP tools, Spring provides a limited subset of AOP features. Its goal is to have close integration between AOP implementation and the Spring IoC container to help solve common application problems. Such integration is done in a non-intrusive way, which allows you to mix Spring AOP in the same application with more expressive frameworks, including AspectJ. Spring AOP uses plain Java classes and does not require a special compilation process, control of the class loader hierarchy, or deployment configuration changes. Instead, it uses the Proxy pattern to apply advices to the target object that should be managed by the Spring IoC container.
You can choose between two types of proxies, depending on the situation.
-
The first one is based on the Java dynamic proxy, which can be applied only to interfaces; it is a standard Java feature and provides good performance.
-
The second type can be used when the target object does not implement any interface and such interfaces can't be introduced (for example, in the case of legacy code). It is based on the runtime byte code generation using the CGLIB library.
For a proxied object Spring allows you to assign specific advices using either static (method match based on the exact name or regular expression or annotation-driven) or dynamic (where matching is done in the runtime, that include cflow pointcut types) pointcut definitions, and each pointcut can be associated with one or several advices. Several advice types are supported: around, before, after returning, throws, and introduction advice. Later in this article I'll show an example of the around advice; for more details you can refer to the Spring AOP framework documentation.
As mentioned earlier, you can only advise target objects managed by the Spring IoC container. However, in J2EE applications, a component's lifecycle is managed by the application server, and, depending on the type of integration, J2EE application components can be exposed to remote or local clients using one of the common end-point types:
- Stateless, stateful, or entity beans, local or remote (over RMI-IIOP)
- Message-driven beans (MDB) listening on local or external JMS queues and topics or inbound JCA end-points
- Servlets (including Struts or other end-user UI frameworks, XML-RPC, and SOAP-based interfaces)
Figure 1. Common end-point types
To use Spring's AOP framework on those end-points, you'll have to move all the business logic into Spring-managed beans, using server-managed components to delegate calls and optionally to define transaction demarcation and a security context. Though I'll keep transactional issues aside in this article, you can find several other articles about this in the Resources section.
I'll give you a closer look at how you can refactor a J2EE application to use Spring features. You'll use XDoclet's JavaDoc-based metadata to generate home and bean interfaces as well as EJB deployment descriptors. Complete source code for all sample classes from this article is available in the Download section below.
Refactoring of the EJB Component to Use Spring's EJB Classes
Imagine a simple stock-quote EJB component that returns the current trade price and also allows you to set the new trade price. This example is intended to illustrate various integration aspects and best practices for using the Spring Framework and J2EE services together, and by no means pretends to show how to write stock management applications. Within our requirements, the
TradeManager
business interface could look like this:
public interface TradeManager {
public static String ID = "tradeManager";
public BigDecimal getPrice(String name);
public void setPrice(String name, BigDecimal price);
}
Common design of J2EE applications uses remote stateless session beans as a facade and entity beans in the persistence layer.
TradeManager1Impl
, below, illustrates possible implementation of the
TradeManager
interface in a stateless session bean. Notice that it uses
ServiceLocator
to look up the home interface for a local entity bean
TradeImpl
. XDoclet annotations are used to declare parameters for EJB descriptors and to define exposed methods of the EJB component:
/**
* @ejb.bean
* name="org.javatx.spring.aop.TradeManager1"
* type="Stateless"
* view-type="both"
* transaction-type="Container"
*
* @ejb.transaction type="NotSupported"
*
* @ejb.home
* remote-pattern="{0}Home"
* local-pattern="{0}LocalHome"
*
* @ejb.interface
* remote-pattern="{0}"
* local-pattern="{0}Local"
*/
public class TradeManager1Impl implements SessionBean, TradeManager {
private SessionContext ctx;
private TradeLocalHome tradeHome;
/**
* @ejb.interface-method view-type="both"
*/
public BigDecimal getPrice(String symbol) {
try {
return tradeHome.findByPrimaryKey(symbol).getPrice();
} catch(ObjectNotFoundException ex) {
return null;
} catch(FinderException ex) {
throw new EJBException("Unable to find symbol", ex);
}
}
/**
* @ejb.interface-method view-type="both"
*/
public void setPrice(String symbol, BigDecimal price) {
try {
try {
tradeHome.findByPrimaryKey(symbol).setPrice(price);
} catch(ObjectNotFoundException ex) {
tradeHome.create(symbol, price);
}
} catch(CreateException ex) {
throw new EJBException("Unable to create symbol", ex);
} catch(FinderException ex) {
throw new EJBException("Unable to find symbol", ex);
}
}
public void ejbCreate() throws EJBException {
<strong>
tradeHome = ServiceLocator.getTradeLocalHome();</strong>
}
public void ejbActivate() throws EJBException, RemoteException {
}
public void ejbPassivate() throws EJBException, RemoteException {
}
public void ejbRemove() throws EJBException, RemoteException {
}
public void setSessionContext(SessionContext ctx) throws EJBException, RemoteException {
this.ctx = ctx;
}
}
To test such a component after any code changes, it will be necessary to go through the entire cycle of building, starting the container, and deploying the application before you can run any tests (usually based on special in-container testing frameworks such as Cactus or MockEJB). In simple cases, hot class replacement can save time on redeployment, but it doesn't work when the class schema is changed (for example, fields or methods are added or method signatures are changed). This issue alone is a very good reason to move all the logic into plain Java objects. As you can see from
TradeManager1Impl
code, quite a few lines of the glue code put all the things together in EJB, and you can't get away from duplication around JNDI access and exception handling. However, Spring provides abstract convenience classes that can be extended by the custom EJB beans instead of directly implementing J2EE interfaces. These abstract super classes allow you to eliminate most of the glue code from custom beans and also provide methods to retrieve an instance of the Spring application context.
Start by moving all logic from
TradeManager1Impl
into the new plain Java class
TradeDao
that would also implement a
TradeManager
interface. You'll keep the
TradeImpl
CMP entity bean as a persistence mechanism not only because it is out of scope for this article, but also because WebLogic Server provides a number of tuning options for tweaking performance of the CMP beans, and, in specific use cases, these beans can deliver very good performance. You'll also use the Spring IoC container to inject the home interface of the
TradeImpl
entity bean into the constructor of the
TradeDao
, as you can see in the code below:
public class TradeDao implements TradeManager {
private TradeLocalHome tradeHome;
public TradeDao(TradeLocalHome tradeHome) {
this.tradeHome = tradeHome;
}
public BigDecimal getPrice(String symbol) {
try {
return tradeHome.findByPrimaryKey(symbol).getPrice();
} catch(ObjectNotFoundException ex) {
return null;
} catch(FinderException ex) {
throw new EJBException("Unable to find symbol", ex);
}
}
public void setPrice(String symbol, BigDecimal price) {
try {
try {
tradeHome.findByPrimaryKey(symbol).setPrice(price);
} catch(ObjectNotFoundException ex) {
tradeHome.create(symbol, price);
}
} catch(CreateException ex) {
throw new EJBException("Unable to create symbol", ex);
} catch(FinderException ex) {
throw new EJBException("Unable to find symbol", ex);
}
}
}
Now you can rewrite
TradeManager1Impl
using Spring's
AbstractStatelessSessionBean
abstract class that would also help you obtain a Spring-managed instance of the
TradeDao
bean you created above:
/**
* @ejb.home
* remote-pattern="TradeManager2Home"
* local-pattern="TradeManager2LocalHome"
* extends="javax.ejb.EJBHome"
* local-extends="javax.ejb.EJBLocalHome"
*
* @ejb.transaction type="NotSupported"
*
* @ejb.interface
* remote-pattern="TradeManager2"
* local-pattern="TradeManager2Local"
* extends="javax.ejb.SessionBean"
* local-extends="javax.ejb.SessionBean, org.javatx.spring.aop.TradeManager"
*
*
@ejb.env-entry
* name="BeanFactoryPath"
* value="applicationContext.xml"
*/
public class TradeManager2Impl extends AbstractStatelessSessionBean implements TradeManager {
private TradeManager tradeManager;
public void setSessionContext(SessionContext sessionContext) {
super.setSessionContext(sessionContext);
// make sure there will be the only one Spring bean config
setBeanFactoryLocator(ContextSingletonBeanFactoryLocator.getInstance());
}
public void onEjbCreate() throws CreateException {
tradeManager = (TradeManager) getBeanFactory().getBean(TradeManager.ID);
}
/**
* @ejb.interface-method view-type="both"
*/
public BigDecimal getPrice(String symbol) {
return tradeManager.getPrice(symbol);
}
/**
* @ejb.interface-method view-type="both"
*/
public void setPrice(String symbol, BigDecimal price) {
tradeManager.setPrice(symbol, price);
}
}
Your EJB now delegates all the calls to a
TradeManager
instance obtained from Spring in the
onEjbCreate()
method. The
getBeanFactory()
method, implemented in the
AbstractEnterpriseBean
, handles all the magic needed to look up and create a Spring application context. However, you have to declare a
BeanFactoryPath
env-entry in the EJB deployment descriptor for your EJB to tell Spring where me put the configuration file with bean declarations. The above example used XDoclet annotations to generate this information.
Also, note that you overwrote the
setSessionContext()
method in order to tell
AbstractStatelessSessionBean
to use a single instance of Spring's application context across all EJB beans.
Now you can declare a
tradeManager
bean in the
applicationContext.xml
. Basically, you need to create a new instance of the above
TradeDao
class passing to its constructor an instance of the
TradeLocalHome
obtained from JNDI. Here is a possible definition:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE beans PUBLIC "-//SPRING//DTD BEAN//EN" "spring-beans.dtd">
<beans>
<bean id="tradeManager" class="org.javatx.spring.aop.TradeDao">
<constructor-arg index="0">
<bean class="org.springframework.jndi.JndiObjectFactoryBean">
<property name="jndiName">
<bean id="
org.javatx.spring.aop.TradeLocalHome.JNDI_NAME"
class="org.springframework.beans.factory.config.FieldRetrievingFactoryBean"/>
</property>
<property name="proxyInterface" value="org.javatx.spring.aop.TradeLocalHome"/>
</bean>
</constructor-arg>
</bean>
</beans>
Here you used an anonymously defined
TradeLocalHome
instance, retrieved from the JNDI context using Spring's
JndiObjectFactoryBean
, and then injected it into
tradeManager
as a constructor parameter. You also used a
FieldRetrievingFactoryBean
to avoid hard-coding the actual JNDI name for
TradeLocalHome
and instead retrieved it from the static field (
TradeLocalHome.JNDI_NAME
in this case). Generally, it is a good idea to declare the
proxyInterface
property when using
JndiObjectFactoryBean
as shown in the above example.
This method of obtaining an EJB home interface from the JNDI using
JndiObjectFactoryBean
will also work for all other services exposed by the J2EE container, including JDBC data sources, JMS and JCA connection factories, and JavaMail sessions.
There is another, simplified way to access session beans. Spring provides a
LocalStatelessSessionProxyFactoryBean
that allows you to obtain a session bean right away without going through a home interface. For instance, here is how you can use the
MyComponentImpl
session bean accessed through a local interface in another Spring-managed bean:
<bean id="tradeManagerEjb"
class="org.springframework.ejb.access.LocalStatelessSessionProxyFactoryBean">
<property name="jndiName">
<bean id="
org.javatx.spring.aop.TradeManager2LocalHome.JNDI_NAME"
class="org.springframework.beans.factory.config.FieldRetrievingFactoryBean"/>
</property>
<property name="businessInterface" value="org.javatx.spring.aop.TradeManager"/>
</bean>
The nice thing about this approach is that you can easily switch from the local to the remote interface by changing only a bean declaration in the Spring context using
SimpleRemoteStatelessSessionProxyFactoryBean
. For example:
<bean id="tradeManagerEjb"
class="org.springframework.ejb.access.SimpleRemoteStatelessSessionProxyFactoryBean">
<property name="jndiName">
<bean id="
org.javatx.spring.aop.TradeManager2Home.JNDI_NAME"
class="org.springframework.beans.factory.config.FieldRetrievingFactoryBean"/>
</property>
<property name="businessInterface" value="org.javatx.spring.aop.TradeManager"/>
<property name="lookupHomeOnStartup" value="false"/>
</bean>
Note that the
lookupHomeOnStartup
property is set to
false
to enable lazy initialization.
I'll summarize what you have achieved at this point:
- The above refactorings have created a foundation for using advanced Spring features, namely dependency injection and AOP.
- Without changing the client API I moved all business logic out of the facade session bean, which make this EJB very resistant to change and easier to test.
- The business logic now resides in a plain Java object that can be tested outside the container as long as its dependencies don't require resources from the JNDI, or you can replace these dependencies by stubs or mocks.
- You can now substitute different
tradeManager
implementations, or change initialization parameters or dependent components, without changes in the Java code.
At this point you have completed all the preliminary steps and can start working on the new requirements for the
TradeManager
service.