I took some time last weekend and refactored AppFuse to use Spring
to replace my Factories and Hibernate configuration. It only took me a couple of hours, which says a lot for Spring. I was amazed at how many things just worked. It actually lifted me out of my flu symptoms and made me feel euphoric. Or it could have been the Sudafed. In reality, I only replaced one Factory class (DAOFactory) - a fairly large class that instantiated DAOs using reflection and constructor variable inspection. I was also able to get rid of the ServiceLocator class, the getConnnection() stuff in ActionFilter and the hibernate.cfg.xml file.
The one thing I found when looking at the Petclinic and JPetstore apps was that they used an applicationContext.xml file for unit tests, and a (very similar) one for running the app in a container. To me, this was a warning sign. DRY (Don't Repeat Yourself) is a big reason for using XDoclet and I'm beginning to think that Spring could benefit from a little XDoclet lovin'. Anyway, back to the story.
I wanted to find a way to use the same XML files for testing and in-container execution. As you might know from Part I
, AppFuse has 3 different tiers: dao, service and web. To run unit tests for the dao and service layers, I simply load a applicationContext.xml file in my JUnit test's setUp() method and go from there. I saw this in the petclinic app and found that it works pretty well. In the end, I decided to setup different XML files for each layer - applicationContext-hibernate.xml, applicationContext-service.xml and applicationContext.xml for the web layer. The main applicationContext.xml uses entity includes to reference the other two files.
The main pain I found was that the entity includes required different paths for tests vs. running in container. Basically, for tests, I had to use:
<!ENTITY database SYSTEM "applicationContext-database.xml">
While tests, using the ClassPathXmlApplicationContext required:
<!ENTITY database SYSTEM "WEB-INF/applicationContext-database.xml">
Using Ant to do a little replace logic allowed me to jump over this hurdle.
Using this setup, any new DAO definitions are added in src/dao/org/appfuse/persistence/hibernate/applicationContext-hibernate.xml, new Manager definitions (and declarative transaction settings) are be added in /src/service/org/appfuse/service/applicationContext-service.xml. The test-specific applicationContext-database.xml sits in the "test" directory and contains the following:
<bean id="propertyConfigurer"
class="org.springframework.beans.factory.config.PropertyPlaceholderConfigurer">
<property name="location"><value>database.properties</value></property>
</bean>
<bean id="dataSource"
class="org.springframework.jdbc.datasource.DriverManagerDataSource">
<property name="driverClassName">
<value>${hibernate.connection.driver_class}</value>
</property>
<property name="url">
<value>${hibernate.connection.url}</value>
</property>
<property name="username">
<value>${hibernate.connection.username}</value>
</property>
<property name="password">
<value>${hibernate.connection.password}</value>
</property>
</bean>
While the applicationContext-database.xml for the web is simply:
<bean id="dataSource" class="org.springframework.jndi.JndiObjectFactoryBean">
<property name="jndiName"><value>jdbc/appfuse</value></property>
</bean>
To integrate Spring with my web layer (Struts), I just used the ContextLoaderListener in my web.xml file. I didn't see any point in bringing yet another JAR file into the mix.
<listener>
<listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
</listener>
Finally, to expose Spring's context to my Struts Actions, I added the following to my BaseAction.java class:
private WebApplicationContext ctx = null;
public Object getBean(String name) {
if (ctx == null) {
ctx = WebApplicationContextUtils
.getRequiredWebApplicationContext(servlet.getServletContext());
}
return ctx.getBean(name);
}
|
This way, the UserManager implementation can be easier retrieved using:
UserManager userMgr = (UserManager) getBean("userManager");
|
The best part about the Spring integration in AppFuse is: (IMO) its Hibernate support and how it drastically simplifies my Hibernate DAOs (as if Hibernate wasn't simple enough already). I dig the ability to specify declarative transactions, and this refactoring seems to have reduced the "src" distribution of AppFuse by 2 MB (to 10MB total)! I don't know where this came from since the Spring JAR is almost 1 MB. The appfuse.war is about 500 KB larger, but I can live with that.
Of course, all of this has been checked into CVS if you'd like to take a look.