Thursday, 15 February 2007

Spring-JPA-Tomcat

Created an Eclipse project that follows the 'Introduction to Spring 2 and JPA' pdf. This entails creating POJOs, the business objects of the application, which in this case are the Employee class and the Address class. The POJOs do not contain any business logic, possessing only their data fields and the associated getters and setters. Two constructors are added, a no-arg constructor and another constructor that sets all the fields with data.

The POJOUnitTest class, a Junit test case, is coded to unit test the Employee and Address classes. Their objects are created, inserted with data (via setter methods and also constructor) and then the data is verified in the test.

A service layer containing the business logic to use these POJOs is visualized in the EmployeeService interface, which contains abstract methods for dealing with the Employee class.

Up to now, it's been plain Java coding. Spring and JPA comes into play when one needs to add database support. JPA annotations are added to the POJOs, at the class and field names, to establish the mapping between objects and database counterparts. Spring's JPA support enables one to use its DAO API to code the implementation class which extends JpaDaoSupport and implements the EmployeeService interface, called EmployeeDAO.

Now we use Spring to string these POJOs up as beans in dwspring-service.xml. Here the provider is changed from TopLink to Hibernate and a database change to MySQL by modifying the entityManagerFactory bean's properties as shown below:
From:
property name="jpaVendorAdapter"
bean class="org.springframework.orm.jpa.vendor.TopLinkJpaVendorAdapter"
property name="databasePlatform"
value="oracle.toplink.essentials.platform.database.HSQLPlatform"

To:
property name="jpaVendorAdapter"
bean class="org.springframework.orm.jpa.vendor.HibernateJpaVendorAdapter"
property name="databasePlatform"
value="org.hibernate.dialect.MySQLDialect"

Needless to say, the properties of the dataSource bean (driverClassName, url, username, password) need to be changed to reflect the use of the MySQL database.

Another test, an integration test this time, verifies that the EmployeeDAO implementation of EmployeeService works against an actual database. The classname is EmployeeServiceImplementationTest and it extends AbstractJpaTests. This allows implementation of the getConfigLocations() method, where one or more beans configuration files can be parsed by the Spring engine. This enables automatic dependency injection; when Spring loads the EmployeeServiceImplementationTest class, it will discover an unfulfilled dependency - a property of type EmployeeService. The engine looks through the dwspring2-service.xml file for a configured bean of type EmployeeService and injects it via the setEmployeeService() method. The relevant code is below:

public class EmployeeServiceIntegrationTest extends AbstractJpaTests {
private EmployeeService employeeService;
private long JoeSmithId = 99999;
public void setEmployeeService(EmployeeService employeeService) {
this.employeeService = employeeService;
}
protected String[] getConfigLocations() {
return new String[] {"classpath:/com/ibm/dw/spring2/dwspring2-service.xml"};
}


After that, in order to run the test, one needs to include all the dependency JARs from Spring and Hibernate libraries, which includes the MySQL driver file. A persistence.xml file is required by the JPA specification. It describes a persistence unit, though in this case, it is only there to satisfy specs. The file is placed in the META-INF folder and must be accessible through the classpath. The code is below:

persistence-unit name="dwSpring2Jpa" type="RESOURCE_LOCAL"

Now the web application comes into play. The UI for this is Spring MVC, and the first controller class created is MainController, which handles the initial incoming request. It obtains a list of all the employees in the system. Next the dwspring2-servlet.xml (dwspring2 being the name of the DispatcherServlet) file is configured with all the beans required by Spring MVC. The web.xml file contains the configuration of the DispatcherServlet and is located in the WEB-INF folder. MainController does not display the data it holds, instead it hands the data over to a view which resolves to a jsp file, home.jsp located in the jsp folder. home.jsp displays the id and full name of each employee, it also made links out of the ids. When clicked, the links go to the controller EmpDetailsController which gets all details about a particular employee (in other words, get the employee object). A command class, EmployeeDetailsCommand, is used to parse the link arguments into an object. Here, the only arguments being passed is the employee id, so EmployeeDetailsCommand has only one data field and a getter and setter.

EmpDetailsController passes the employee object it holds to the empdetails view which as before is resolved by the InternalResourceViewResolver by appending the appropriate prefix and suffix and returns /jsp/empdetails.jsp as the view handler. empdetails.jsp displays the employee's details.

home.jsp and empdetails.jsp use the css/dwstyles.css stylesheet to format their HTML. This only affect appearance.

The next step is the Eclipse WTP process, however it has been replaced with an Ant build. This step will compile the code, build a deployable WAR file (for deployment in a J2EE compatible Web tier container) and deploy that file to a Tomcat server. The WAR file consists of the compiled classes, the dependency library jars, the web content and the configuration files (context.xml, persistence.xml, dwspring2-servlet.xml, dwspring-service.xml, web.xml)

Before the WAR file can be deployed, a number of things had to be done. When a Spring JPA application runs on Tomcat, bytecode "weaving" during class loading is required for the JPA support to work properly. The standard Tomcat classloader does not support this. A Spring-specific classloader is needed.

Installing the Spring classloader:
1) Copy spring-tomcat-weaver.jar into the Tomcat's server/lib subdirectory. The spring-tomcat-weaver.jar library can be found in the dist/weaver of the Spring distribution.
2) Configure the context.xml file (located in META-INF) to let Tomcat know to replace the standard classloader for this particular web application.

Spring needed to hook into Tomcat's context loading pipeline so a ContextLoaderListener was added to web.xml.

Datasources are managed by Tomcat and are available through a standard Java Naming and Directory Interface (JNDI) lookup mechanism. The employee system runs as a Web application inside Tomcat and should obtain its datasource through Tomcat's JNDI. To accomplish this, the MySQL driver needs to be copied to Tomcat's common/lib subdirectory. Then configure the context.xml file, adding a JNDI resource. With a resource name of "jdbc/dwspring2", the configuration makes the JNDI datasource available through the name java:comp/env/jdbc/dwspring2. Next, add a resource reference in web.xml, making it available for use within the web application. Finally, dwspring2-service.xml must be modified to use the JNDI datasource.

The Spring engine needs to locate and process the bean configuration file (dwspring2-service.xml) for the POJOs in order to wire them up with the web application. The context parameter in web.xml must be configured with the location of the file.

The final step before deployment is to fill the database with data. The FillTableWithEmployeeInfo class was coded to do this. This class extends AbstractJpaTests. A great feature of tests based on AbstractJpaTests is that all database changes are rolled back upon completion of a test, allowing the next test to run quickly. However calling the setComplete() method within the test commits the transaction instead of rolling it back and makes the changes permanent. Which is what is done with FillTableWithEmployeeInfo.

Finally the WAR file (spring2web.war) can be deployed to Tomcat's webapps subdirectory and loaded. However the deployment was unsuccessful.

From Tomcat's Catalina log, it was a puzzling one-liner error about the context listener. Set up the web application logging by adding log4j.properties in order to find out what was wrong. The error was more verbose, essentially a ClassDefNotFoundException of a class in jasper-compiler.jar, which is located in Tomcat's common/lib subdirectory. Adding that file to the web application library only made it a ClassCastException. The project was stuck for a while.

Got hold of the springjpa project from MemeStorm, and tried to deploy that as well. Whilst doing that, changed the loadTimeWeaver property in dwspring2-service.xml from SimpleLoadTimeWeaver to InstrumentationLoadTimeWeaver to follow the springjpa project. Read that SimpleLoadTimeWeaver was only suited for testing, so for real deployment, InstrumentationLoadTimeWeaver or ReflectiveLoadTimeWeaver should be used. Whatever the problem was, it wasn't the loadTimeWeaver property as both applications still refused to deploy.

Eventually learned that it was the Spring class loader at fault. When Tomcat replaced the standard classloader with the Spring classloader, the Spring classloader does not have the classpath the standard classloader possess in order to access Tomcat's library jars. The solution was to add the attribute useSystemClassLoaderAsParent="false" to the Loader in context.xml. The error was hurdled but a new one popped up.

The new error was that no persistence units were parsed from META-INF/persistence.xml. This one was simple to solve, the META-INF/persistence.xml needed to be in the root of the class package. So a move to the web application's WEB-INF/classes and it is solved.

The next error was talking about a java agent and prior reading indicates this had to do with the InstrumentationLoadTimeWeaver, basically it needs a Spring agent to be loaded into the JVM. To accomplish this in Tomcat, the line set JAVA_OPTS=%JAVA_OPTS% -javaagent:"%CATALINA_BASE%\server\lib\spring-agent.jar" was inserted into catalina.bat in Tomcat's bin subdirectory. The spring-agent.jar mentioned in the line can be found in the dist/weaver of the Spring distribution. The file was copied to Tomcat's server/lib subdirectory.

Success! Deployment went without a hitch and the web application can be accessed, displaying the list of employees previously inserted into the database and their details when their link was clicked.

Messed around with the loadTimeWeaver property, changing it back to SimpleLoadTimeWeaver and even commenting it out as Hibernate apparently does not require it. The web application still runs fine.

One last change was the addition of index.jsp and include.jsp, which were from a previous project. A slight modification to the welcome file in web.xml and one can now access the web application via http://localhost:8080/spring2web without any need for a filename.

The file structure of the spring2web web application:

spring2web
|-css
| |-dwstyles.css
|
|-jsp
| |-empdetails.jsp
| |-home.jsp
| |-include.jsp
|
|-META-INF
| |-context.xml
|
|-WEB-INF
| |-classes
| | |-com.ibm.dw.spring2.*
| | |-com.ibm.dw.spring2.web.*
| | |
| | |-META-INF
| | | |-persistence.xml
| | |
| | |-log4j.properties
| |
| |-lib
| | |-*.jar
| |
| |-dwspring2-service.xml
| |-dwspring2-servlet.xml
| |-web.xml
|
|-index.jsp


The steps taken in Spring-JPA-Tomcat:
- POJOs
- Unit Test
- dwspring2-service.xml
- Integration Test
- META-INF/persistence.xml
- Spring MVC
- dwspring2-servlet.xml
- web.xml
- META-INF/context.xml (not at the same location as persistence.xml)
- (Tomcat)/common/lib <-- mysql-connector-java-5.0.4-bin.jar
- (Tomcat) /server/lib <-- spring-tomcat-weaver.jar, spring-agent.jar
- (Tomcat)/bin/catalina.bat <-- Java agent command

3 comments:

Matthew said...

Any chance you have your finished source tree available?

I've been driving myself batty for the past couple days trying to get this to work.

I'm getting:
java.lang.IllegalArgumentException: Unknown entity: com.parkinghero.domain.Car

Thank you...
Matt

Turgay Zengin said...

Hi,
Thank you, this information was really useful for me.
I managed to configure Spring-JPA-Tomcat 6, also defining multiple persistence units. I tried to explain this in my blog

Regards,
Turgay

TheAlchemist said...

Awesome!

This was helpful, thank you. I've got Spring + JPA + Tomcat working, finally, even using AspectJ annotations!

The "org.springframework.web.context.ContextLoaderListener" in web.xml was important, as well as the customer loader in the context file (i.e., Loader loaderClass="org.springframework.instrument.classloading.tomcat.TomcatInstrumentableClassLoader"
useSystemClassLoaderAsParent="false").