<< The 2nd most useful Java-Oracle Tool for 2006 | 首页 | Agile Object to Relational Database Replication with db4o >>

Wire Hibernate Transactions in Spring

This article is intended to show how Spring can be used to assemble components, including their transaction contexts. Connecting to a single data store from within a J2EE application is not a big hurdle. But when it comes to assembly and integration of enterprise-class components, the scenario gets complicated. A single component would be backed up by a single data store or a number of data stores. So, when we speak of assembling two or more components, we are expected to maintain the atomicity of operations done in many data stores, across components. A J2EE server provides a container for these components so that the container is able to take care of transactional atomicity and isolation across components, too. When we are not using a J2EE server, Spring helps us. Spring is based on Inversion of Control (also called Dependency Injection) for wiring not only the component services together, but also their associated transaction contexts. For the purpose of this article, we are using Hibernate as an object/relational persistence and query service.

 

Assembling Component Transactions

Assume that in the enterprise component library, we already have an audit component, with service methods which can be called by clients. Later, when we want to build an order processing system, we discover the design requirement that the OrderListManager component service also needs auditing component services. The OrderListManager creates and manages orders, and hence all OrderListManager services have their own transaction attributes. When we call the audit component from within the OrderListManager service, we are in effect propagating the transaction context of OrderListManager services to audit services. Perhaps in the future, a new business service component will also need the audit component service, but the audit service will be invoked in a different transaction context. The net effect is that, even though the functionality of the audit component remains same, it can be composed with other business service functionalities, with a mix-and-match of transaction attributes to provide different run time transactional behavior.

Two separate call context flows are shown in Figure 1. In flow 1, if the client has a TX context, OrderListManager either takes part in it or starts a new TX, depending on whether the Client is in a TX or not, and also on what TX attributes are specified for OrderListManager methods. The same explanation holds true when OrderListManager service in turn invokes AuditManager methods.

Assembling Component Transactions
Figure 1. Assembling component transactions

The EJB architecture provides this flexibility by allowing the component assembler to give the correct transaction attributes declaratively. We are not exploring the alternative to declarative transaction management (called programmatic transaction control), since this involves code change to affect a different run time transactional behavior. Almost all J2EE application servers provide distributed transaction managers compliant to the Two-Phase Commit protocol as per the X/Open XA specification. Now the question is, can we avail the same functionality out of an EJB server? Spring is one of the alternative solutions here. Let us explore how Spring helps us solve our transaction-assembly problem.

Transaction Management Using Spring

We are going to look at a lightweight transaction infrastructure that can, in effect, manage component-level transaction assembling. Spring is one of the solutions here. The advantage is that we are not hooked to J2EE container services like the JNDI DataSource. The most notable point is that if we feel we need to hook this lightweight transaction infrastructure to an already-available J2EE container infrastructure, there is nothing that will stop us from doing so. It seems like we can leverage best of both worlds.

The other side of Spring lightweight transaction infrastructure is that it uses an Aspect-Oriented Programming (AOP) framework. The Spring AOP framework makes use of an AOP-enabled Spring bean factory. Transactions are demarcated by specifying transaction characteristics at the component service level, in a Spring-specific configuration file, applicationContext.xml.

<beans>
 
<!-- other code goes here... -->
 
<bean id="orderListManager"
        class="org.springframework.transaction
        .interceptor.TransactionProxyFactoryBean">
<property name="transactionManager">
        <ref local="transactionManager1"/>
</property>
<property name="target">
        <ref local="orderListManagerTarget"/>
</property>
<property name="transactionAttributes">
        <props>
                <prop key="getAllOrderList">
                        PROPAGATION_REQUIRED
                </prop>
                <prop key="getOrderList">
                        PROPAGATION_REQUIRED
                </prop>
                <prop key="createOrderList">
                        PROPAGATION_REQUIRED
                </prop>
                <prop key="addLineItem">
                   PROPAGATION_REQUIRED,
                   -com.example.exception.FacadeException
                </prop>
                <prop key="getAllLineItems">
                        PROPAGATION_REQUIRED,readOnly
                </prop>
                <prop key="queryNumberOfLineItems">
                        PROPAGATION_REQUIRED,readOnly
                </prop>
        </props>
</property>
</bean>
 
</beans>

Once we specify the transaction attributes at the service level, they are intercepted and interpreted by a particular implementation of the org.springframework.transaction.PlatformTransactionManager interface. This interface is given below:

public interface PlatformTransactionManager{
        TransactionStatus getTransaction
                (TransactionDefinition definition);
        void commit(TransactionStatus status);
        void rollback(TransactionStatus status);
}
 

Hibernate Transaction Manager

Since we have already decided that we are going to use Hibernate as the ORM tool, we need to wire in a Hibernate-specific transaction manager implementation, which is what we are going to do as the next step.

<beans>
 
<!-- other code goes here... -->
 
<bean id="transactionManager1"
        class="org.springframework.orm.hibernate.
                HibernateTransactionManager">
        <property name="sessionFactory">
                <ref local="sessionFactory1"/>
        </property>
</bean>
 
</beans>
 

Design for Managing Transactions in Multiple Components

Let us now discuss what we mean by "assembling component transactions." You might have noted the different TX attributes specified for OrderListManager, which is a service-level component in our domain. The main objects identified for our domain are shown in the Business Domain Object Model (BDOM) shown in Figure 2:

Business Domain Object Model (BDOM)
Figure 2. Business Domain Object Model (BDOM)

For demonstration purposes, let us list out some Non-Functional Requirements (NFR) for the objects in our domain:

  • Business objects needs to be persisted in a database, appfuse1.
  • Audit needs to be logged in a separate database, appfuse2, behind a firewall, for security reasons.
  • Business components should be reused.
  • Every attempt should be made to audit all activities in the business services layer.

Taking into account of the above requirements, we have decided that the OrderListManager services will delegate any audit log calls to the already-available AuditManager component. This leads us to the detailed design shown in Figure 3:

Design of Component Services
Figure 3. Design of component services

The notable point here is that, due to our NFR, we are mapping OrderListManager-related objects to the appfuse1 database, and Audit-related objects to appfuse2. Then, for any auditing purposes, the OrderListManager component calls the AuditManager component. We all will agree that the methods in OrderListManager component need to be transactional, since we are creating orders and line items using the services. What about the services in the AuditManager component? Since the service we are considering in the AuditManager component is doing an audit trace, we are interested in persisting as much of an audit trail as we can, for every possible business activity in the system. This leads to a requirement like, "We need to make audit entry, even if the main business activity fails." The AuditManager component also needs to be in its own transaction, since it is also interacting with its own database. This is shown below:

     

<beans>

<!-- other code goes here... -->
<bean id="auditManager"
class="org.springframework.transaction.
interceptor.TransactionProxyFactoryBean">
<property name="transactionManager">
<ref local="transactionManager2"/>
</property>
<property name="target">
<ref local="auditManagerTarget"/>
</property>
<property name="transactionAttributes">
<props>
<prop key="log">
PROPAGATION_REQUIRES_NEW
</prop>
</props>
</property>
</bean>

</beans>

 

We will now concentrate in two business services, viz. createOrderList and addLineItem, for our demo purposes. Also note that we are not concentrating on best design strategies--you might have noted that the addLineItem method throws a FacadeException, but createOrderList doesn't. In a production design, you might want every service method to deal with exception scenarios.

public class OrderListManagerImpl
        implements OrderListManager{
 
private AuditManager auditManager;
 
public Long createOrderList
 (OrderList orderList){
        Long orderId = orderListDAO.
                createOrderList(orderList);
       auditManager.log(new AuditObject
                (ORDER + orderId, CREATE));
 
        return orderId;
}
 
public void addLineItem
 (Long orderId, LineItem lineItem)
        throws FacadeException{
 
        Long lineItemId = orderListDAO.
                addLineItem(orderId, lineItem);
       auditManager.log(new AuditObject
                (LINE_ITEM + lineItemId, CREATE));
 
        int numberOfLineItems = orderListDAO.
                queryNumberOfLineItems(orderId);
        if(numberOfLineItems > 2){
                log("Added LineItem " + lineItemId +
                        " to Order " + orderId + ";
                        But rolling back *** !");
                throw new FacadeException("Make a new
                        Order for this line item");
        }
        else{
                log("Added LineItem " + lineItemId +
                        " to Order " + orderId + ".");
        }
}
 
//Other code goes here...
 
}
 

To create an exception scenario for the purposes of our demonstration, we have introduced another business rule that states that a particular order cannot contain more than two line items. We should now note the call to auditManager.log() method from within createOrderList and addLineItem. You should have already noted the transaction attributes specified for the above methods.

<bean id="orderListManager"
        class="org.springframework.transaction.
         interceptor.TransactionProxyFactoryBean">
        <property name="transactionAttributes">
                <props>
<prop key="createOrderList">
                                PROPAGATION_REQUIRED
                        </prop>
                        <prop key="addLineItem">
                                PROPAGATION_REQUIRED,-com.
                                example.exception.FacadeException
                        </prop>
 
                </props>
        </property>
</bean>
 
<bean id="auditManager" class="org.
        springframework.transaction.interceptor.
                TransactionProxyFactoryBean">
        <property name="transactionAttributes">
                <props>
<prop key="log">
                                PROPAGATION_REQUIRES_NEW
                        </prop>
 
                </props>
        </property>
</bean>
 

PROPAGATION_REQUIRED is equivalent to TX_REQUIRED, and PROPAGATION_REQUIRES_NEW is equivalent to TX_REQUIRES_NEW in EJB. If we want the service methods to always run in a transaction, we can use PROPAGATION_REQUIRED. When we use PROPAGATION_REQUIRED, if a TX is already running, the bean method joins in that TX, or else the Spring lightweight TX manager starts one for you. If we always want a new transaction to begin when the component services are called, we can use the PROPAGATION_REQUIRES_NEW attribute.

We have also specified that addLineItem should always roll back the transaction if the method throws any exception of the type FacadeException. This is another level of granularity by which we can finely control how a TX should end, in case we have an exception scenario. Prefixing a - sign specifies to roll back the TX, and a + sign specifies to commit the TX.

The next question is, why we have given PROPAGATION_REQUIRES_NEW for the log method? This is driven by our requirement that, whatever happens to our main service methods, we need to log audit trails for every attempt of order creation and adding line items in our system. This means that even if we come across an exception scenario at any point from within the implementation of createOrderList and addLineItem, we have to log an audit trail. This is possible if and only if we start a new TX and call log in this new TX context. That's why log is given the PROPAGATION_REQUIRES_NEW TX attribute: if the call to

auditManager.log(new AuditObject(LINE_ITEM +              
								lineItemId, CREATE));    

is successful, auditManager.log() will happen in a new TX context, which will be committed anyway if auditManager.log() is successful by itself (i.e., doesn't throw an exception).

Setting up the Demo Environment

To prepare the demonstration environment, I have been following the reference book Spring Live.

  1. Download and install the following components. When doing so, do note the exact versions, otherwise you may have to sort out any version mismatch incompatibilities.
  2. Set up the following environment variables in the system:
    • JAVA_HOME
    • CATALINA_HOME
    • ANT_HOME
  3. Add the following to your PATH environment variable, or use full paths to execute your scripts:
    • JAVA_HOME\bin
    • CATALINA_HOME\bin
    • ANT_HOME\bin
  4. To set up Tomcat, open the $CATALINA_HOME/conf/tomcat-users.xml file in a text editor and verify that the following lines exist. If they do not, you have to add them:
  5.        <role rolename="manager"/>            
    			<user username="admin" password="admin" roles="manager"/>    
  6. To create a web application based on Struts, Spring, and Hibernate, we have to use Equinox to make a bare-bones starter application--this will have the predefined folder structure, all of the required .jar files, and the Ant build script. Extract Equinox into a folder, so that it will create an equinox folder. Change the directory to the equinox folder, and type the command ANT_HOME\bin\ant new -Dapp.name=myusers. This will create a folder called myusers at the same level as equinox. The contents of it are shown below:

    Equinox Myusers Application Folder Template
    Figure 4. Equinox myusers application folder template

  1. Delete all .xml files from the myusers\web\WEB-INF folder.
  2. Copy equinox\extras\struts\web\WEB-INF\lib\struts*.jar files to the myusers\web\WEB-INF\lib, folder so that the example application is Struts-ready, too.
  3. From the sample code in the Resources section on page 3, unzip myusersextra.zip to some convenient location. Change the directory to the newly created myusersextra folder. Copy all of the contents in the myusersextra folder, and paste (overwrite) them into the myusers folder.
  4. Open up a command prompt and change the directory to myusers. Execute CATALINA_HOME\bin\startup. It is important to start Tomcat from the myusers folder, otherwise the database will not be created inside of the myusers folder, which will cause problems when we execute some tasks defined in build.xml.
  5. Open up a second command prompt, and change the directory to myusers. Execute ANT_HOME\bin\ant install. This will build the application and deploy it in Tomcat. When we do this we can note that a directory db is created in myusers, for the databases appfuse1 and appfuse2.
  6. Open up the browser and verify that the myusers application is deployed at http://localhost:8080/myusers/
  7. To reinstall the application, execute ANT_HOME\bin\ant remove, and then shut down Tomcat by executing CATALINA_HOME\bin\shutdown. Now delete any myusers folder from the CATALINA_HOME\webapps folder. Then restart Tomcat by executing CATALINA_HOME\bin\startup and install the application by executing ANT_HOME\bin\ant install.

Running The Demo

For running the test cases, a JUnit test class, OrderListManagerTest, is provided within myusers\test\com\example\service. To execute it, run the following command from the command prompt where we built the application:

CATALINA_HOME\bin\ant test -Dtestcase=OrderListManager  

The test case is divided into two main sections: the first section creates an order consisting of two line items, and then links the two line items to the order. This is shown below, and will run successfully:

OrderList orderList1 = new OrderList();
Long orderId1 = orderListManager.
        createOrderList(orderList1);
log("Created OrderList with id '"
        + orderId1 + "'...");
orderListManager.addLineItem(orderId1,lineItem1);
orderListManager.addLineItem(orderId1,lineItem2);
 

The next section performs a similar action, but this time we are trying to add three line items to the order, which will generate an exception:

OrderList orderList2 = new OrderList();
Long orderId2 = orderListManager.
        createOrderList(orderList2);
log("Created OrderList with id '"
        + orderId2 + "'...");
orderListManager.addLineItem(orderId2,lineItem3);
orderListManager.addLineItem(orderId2,lineItem4);
//We know, we will have an exception here,
        still want to proceed
try{
  orderListManager.addLineItem
        (orderId2,lineItem5);
}
catch(FacadeException facadeException){
  log("ERROR : " + facadeException.getMessage());
} 
 

The console print out is shown in Figure 5:

Client Side Console Output
Figure 5. Client-side console output

We have created Order1, and added two line items with Line Item IDs 1 and 2 to it. Then we created Order2, and tried to add three items. Adding the first two line items (with Line Item IDs 3 and 4) was successful, but Figure 5 shows that when we tried to add a third item (with Line Item ID 5) to Order2, the business method met an exception. Hence, the business method TX is rolled back, and no Line Item with Line Item ID 5 is persisted in the database. This can be validated as shown in Figure 6 and Figure 7, which is possible by executing the following command from the console:

CATALINA_HOME\bin\ant browse1  

Orders Created in appfuse1 DB
Figure 6. Orders created in the appfuse1 database

Line Items Created in appfuse1 DB
Figure 7. Line items created in the appfuse1 database

The next, and most important, part of the demonstration shows that orders and line items are persisted in appfuse1 database, whereas audit objects are persisted in appfuse2 database. In effect, the service methods in OrderListManager talk to multiple databases. Open up the appfuse2 database to see the audit trail, as shown below:

CATALINA_HOME\bin\ant browse2

Audit trail created in the appfuse2 database, with entry for failed TX
Figure 8. Audit trail created in the appfuse2 database, with entry for failed TX

The last row in Figure 8 needs special attention. The RESOURCE column says "this row corresponds to LineItem5." But if we go back to Figure 7, there is no line item corresponding to LineItem5. Has something gone wrong here? In fact, everything worked perfectly, and this extra row in Figure 7 is what this whole article has been about. We will now discuss what has happened here.

We know that addLineItem() method has PROPAGATION_REQUIRED and the log() method has PROPAGATION_REQUIRES_NEW. Further, addLineItem() internally calls the log() method. So when we tried to add a third line item to Order2, an exception is raised (as per our business rule), which will roll back both the creation of this line item and the linking of this line item with Order2. But, since a call to log() is also made from within addLineItem(), and since log() has the PROPAGATION_REQUIRES_NEW TX attribute, the rolling back of addLineItem() will not roll back log(), since log() happens in a new TX.

Let us now make a change in the TX attribute of log(). Instead of PROPAGATION_REQUIRES_NEW, change the TX attribute to PROPAGATION_SUPPORTS. The PROPAGATION_SUPPORTS attribute allows the service method to run in a client TX, if the client has a TX context, otherwise the method runs with no TX at all. You may have to reinstall the application so that the already-available data in the databases are automatically flushed. To reinstall, follow step 12 in the "Setting up the Demo Environment" section above.

If we repeat the run, we will experience a slightly different behavior this time. This time, we will still get an exception scenario when we try to add a third line item to Order 2. This will roll back the transaction that attempts to add the third line item. This method, in turn, calls the method log(). But since log() has a TX attribute of PROPAGATION_SUPPORTS, log() will be invoked in the same TX context as that of addLineItem() method. Since addLineItem() rolls back, the log() also rolls back, leaving no audit trace for the rolled-back TX. So there is no audit trail entry in Figure 9, corresponding to the failed TX!

Audit Trail Created In appfuse2 DB, Without Entry For Failed TX
Figure 9. Audit trail created in the appfuse2 database, without entry for failed TX

The only change we have done for this different transaction behavior is that we changed the TX attribute in the Spring configuration, as shown below:

<bean id="auditManager"
        class="org.springframework.transaction.
        interceptor.TransactionProxyFactoryBean">
    <property name="transactionAttributes">
        <props>
<!-- prop key="log">
                PROPAGATION_REQUIRES_NEW
            </prop -->
            <prop key="log">
                PROPAGATION_SUPPORTS
            </prop>
 
        </props>
    </property>
</bean>
 

This is the effect of declarative transaction management, which we have been leveraging since the inception of EJB. But we know that we need a high-end application server to host our EJB components. Now we have seen that similar results can be attained even without an EJB server, using Spring.

Summary

This article throws light onto one of the powerful combinations in the J2EE world: Spring and Hibernate. By leveraging the capabilities of both, we have alternate technology options for Container-Managed Persistence (CMP), Container-Managed Relationships (CMR), and Declarative Transaction Management. Even though Spring wasn't conceived as an alternative to EJB, it provides features, such as declarative transaction management for plain Java objects, that enable users to dispense with EJB in many projects.

It is not the aim of this article to find out an alternative to EJB, but we are trying to find out the best available technology options for the problems at hand. Hence, we need to explore further capabilities of the lightweight combination of Spring and Hibernate, and this is left as a subject for further exploration for the reader.

Resources

Binildas Christudas is a Senior Technical Architect at Communication Service Providers Practice (CSP) of Infosys, and is a Sun Microsystems Certified Enterprise Architect and a Microsoft Certified Professional.

 




发表评论 发送引用通报