Saturday, April 21, 2007

Partitioning Spring configuration with JavaConfig

There has been a lot of buzz about annotation based dependency-injection lately. Spring offers JavaConfig (still in its 1.0 milestone release), there is Guice from Google, and PicoContainer is adding in annotation based configuration support as well. In this post, I am going to write about my experience converting the XML based ApplicationContext for a small web application to one thats a hybrid of XML and JavaConfig annotations.

If you have worked on moderate to large Spring enabled web applications, you will be familiar with the XML bloat in the Spring configuration files. Lots of time, the bloat can be addressed by partitioning the context into a two-level hierarchy of XML files. The top level contains a single file with global configurations, and the next level contains module configurations, one file for each module. Something like this:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
<!-- Top level configuration -->
<beans>
  <!-- common bean definitions -->
  <bean id="common1" class="com.mycompany.common.Bean1"/>
  ...
  <!-- module definitions -->
  <import resource="classpath:applicationContext-foo.xml"/>
  <import resource="classpath:applicationContext-bar.xml"/>
  ...
</beans>

Most of the applicationContext-*.xml files represent beans being injected into other beans, which can now live in the Java layer with Spring-Javaconfig annotations. This is probably nicer, since Java IDEs typically have better tool support for Java code than for XML files. For example, using an IDE such as Eclipse, it is easier to find a bean definition method in a Configuration file than it is in XML. Also, you get immediate feedback that a bean is incorrectly built in Java, rather than wait for the application to complain during startup, as is the case with XML.

Here is the applicationContext.xml file I started with. As you can see, nothing fancy. There is a database DataSource configuration, a reference to Lucene index directory, and a bunch of bean definitions that use dependency injection to build more complex beans based on the previously defined beans.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
<beans ...>

  <!-- Database Datasource configuration -->
  <bean id="dataSource" class="org.springframework.jdbc.datasource.DriverManagerDataSource">
    <property name="driverClassName" value="com.mysql.jdbc.Driver" />
    <property name="url" value="jdbc:mysql://localhost:3306/facetdb" />
    <property name="username" value="root" />
    <property name="password" value="secret" />
  </bean>

  <!-- Lucene index datasource configuration -->
  <bean id="fsDirectory" class="org.springmodules.lucene.index.support.FSDirectoryFactoryBean">
    <property name="location" value="file:/tmp/soapindex" />
  </bean>

  <bean id="searcherFactory" class="org.springmodules.lucene.search.factory.SimpleSearcherFactory">
    <property name="directory" ref="fsDirectory" />
  </bean>

  <!-- DAO -->
  <bean id="facetsDao" class="com.mycompany.mymodule.db.FacetsDao">
    <property name="dataSource" ref="dataSource" />
  </bean>

  <!-- Searcher -->
  <bean id="facetedSoapSearcher" class="com.mycompany.mymodule.search.FacetedSoapSearcher">
    <property name="searcherFactory" ref="searcherFactory" />
    <property name="analyzer">
      <bean class="org.apache.lucene.analysis.SimpleAnalyzer" />
    </property>
    <property name="facetsDao" ref="facetsDao" />
  </bean>

  <!-- Controller -->
  <bean id="facetedSearchController" class="com.mycompany.mymodule.controller.FacetedSearchController">
    <property name="facetedSoapSearcher" ref="facetedSoapSearcher" />
  </bean>

</beans>

My plan was to leave the dataSource and fsDirectory definitions in Spring, since they represent the part of the application that can be customized by operations folks, and move the rest of the definitions out to a Java class. After I was done, this was what the Java class looked like:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
@Configuration
public class MyConfiguration extends ConfigurationSupport {

  @Bean(scope = Scope.SINGLETON)
  public SearcherFactory searcherFactory() throws Exception {
    SimpleSearcherFactory searcherFactory = new SimpleSearcherFactory();
    searcherFactory.setDirectory((FSDirectory) getBean("fsDirectory"));
    return searcherFactory;
  }

  @Bean(scope = Scope.SINGLETON)
  public FacetsDao facetsDao() {
    FacetsDao facetsDao = new FacetsDao();
    facetsDao.setDataSource((DataSource) getBean("dataSource"));
    return facetsDao;
  }

  @Bean(scope = Scope.SINGLETON)
  public FacetedSoapSearcher facetedSoapSearcher() throws Exception {
    FacetedSoapSearcher facetedSoapSearcher = new FacetedSoapSearcher();
    facetedSoapSearcher.setSearcherFactory(searcherFactory());
    facetedSoapSearcher.setAnalyzer(new SimpleAnalyzer());
    facetedSoapSearcher.setFacetsDao(facetsDao());
    return facetedSoapSearcher;
  }

  @Bean(scope = Scope.SINGLETON)
  public FacetedSearchController facetedSearchController() throws Exception {
    FacetedSearchController facetedSearchController = new FacetedSearchController();
    facetedSearchController.setFacetedSoapSearcher(facetedSoapSearcher());
    return facetedSearchController;
  }
}

Notice that our configuration class subclasses ConfigurationSupport which gives us access to the getBean(String) method that can pull out beans from the main application context. For unit-testing, this class can be used standalone. You can get bean references out of the AnnotationApplicationContext, like so:

1
2
  ApplicationContext context = new AnnotationApplicationContext(MyConfiguration.class);
  MyBean myBean = (MyBean) context.getBean("myBean");

On the XML side, you need to define this class in the applicationContext, and define a PostProcessor that will read these annotated configuration files and put it into the applicationContext. The new applicationContext.xml file is shown below:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<beans ...>

  <!-- common definitions -->

  <!-- Database Datasource configuration -->
  <bean id="dataSource" class="org.springframework.jdbc.datasource.DriverManagerDataSource">
    <property name="driverClassName" value="com.mysql.jdbc.Driver" />
    <property name="url" value="jdbc:mysql://localhost:3306/facetdb" />
    <property name="username" value="root" />
    <property name="password" value="secret" />
  </bean>

  <!-- Lucene index datasource configuration -->
   <bean id="fsDirectory" class="org.springmodules.lucene.index.support.FSDirectoryFactoryBean">
    <property name="location" value="file:/tmp/soapindex" />
  </bean>

  <!-- module definitions -->
  <bean class="com.mycompany.mymodule.MyConfiguration"/>

  <bean class="org.springframework.beans.factory.java.ConfigurationPostProcessor"/>

</beans>

The conversion is almost too easy, but it all works. My unit tests behave identically under the old and new configuration setups. My initial concern was that the annotations did not have all the features that I was used to with the bean elements attributes. I use the init-method and destroy-method quite frequently, but could not find any documentation on it. A quick look at the source code for the Bean annotation reveals a TODO to include an initMethod and destroyMethod as well. So I am guessing that a later release would have these features. I think it is important to include these methods, more important than Autowire anyway, which is already included.

One small nit. I realize this is beta software, but It would be nice if JavaConfig could be made available from ibiblio's maven2 repository. For those of us who use maven2, trying to figure out the dependencies from the currently available JAR is like going back to writing Java code in vim after having used an IDE for a while - possible, but not fun. FWIW, I had to add aspectj-runtime-1.0.jar into my classpath from the libraries included with the distribution.

Resources

  1. Craig Walls, the author of Spring in Action, compares Spring-JavaConfig and Guice in his "Guice vs Spring" blog entry.
  2. A page from Sun describing Annotations in JDK 1.5

No comments:

Post a Comment

Comments are moderated to prevent spam.