Using the Spring AOP Framework with EJB Components
Pages: 1, 2, 3, 4, 5

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 {
     
                        
tradeHome = ServiceLocator.getTradeLocalHome();
  }
  
  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 (refer to the article on CMP performance tuning in the Resources section). 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.

Pages: 1, 2, 3, 4, 5

Next Page »