Matt RaibleMatt Raible is a Web Architecture Consultant specializing in open source frameworks.

The JHipster Mini-Book The JHipster Mini-Book is a guide to getting started with hip technologies today: AngularJS, Bootstrap, and Spring Boot. All of these frameworks are wrapped up in an easy-to-use project called JHipster.

This book shows you how to build an app with JHipster, and guides you through the plethora of tools, techniques and options you can use. Furthermore, it explains the UI and API building blocks so you understand the underpinnings of your great application.

For book updates, follow @jhipster-book on Twitter.

10+ YEARS


Over 10 years ago, I wrote my first blog post. Since then, I've authored books, had kids, traveled the world, found Trish and blogged about it all.

AppFuse Refactorings Part II: Spring Integration

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 = (UserManagergetBean("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.

Posted in Java at Feb 05 2004, 12:52:18 PM MST 17 Comments
Comments:

Oh yes. A little XDoclet template magic is a must to generate Spring and Hibernate configs. It's a nice way to switch your SessionFactory DS between JNDI and JDBC datasources when running unit tests. I don't even bother to create separate Spring xml files. I just make liberal use of <XDtConfig:ifConfigParamEquals/> in my one Spring file and run XDoclet over it. Actually, I do create separate Spring files (datasourceContext.xml, securityContext.xml, hibernateContext.xml, etc) and put them in a merge dir, just like webdoclet. Its strictly for development convenience though. I like more manageable file sizes. One thing I do dislike is how the Log4jWebConfigurer forces you to use the webapp root as the root of your log dir. I'd much rather be able to set my own log root dir through a context param (mainly because I'm on weblogic, and I don't expand the war's). So I had to create my own ContextListener that uses Log4jConfigurer so I could set my own custom log dir. Oh well, this is a minor gripe.

Posted by Jason on February 05, 2004 at 03:08 PM MST #

Good info Matt. Since there isn't a big body of work on Spring best practices yet, thanks for adding your take on it. I followed up here as well.

Posted by Patrick on February 05, 2004 at 04:08 PM MST #

!Matt, if you haven't already, you may want to have a look at Don Brown's "Struts Spring Plugin" available [here|http://struts.sourceforge.net/struts-spring/index.html]. It requires virtually no reference to the Spring framework from within your actions. The only caveat is that you will need to add accessors and mutators to your Struts' actions.

Posted by Joe O'Pecko on February 05, 2004 at 06:36 PM MST #

Matt, instead of including the other context files as XML entities in the main context file, you could specify a "contextConfigLocation" context-param in web.xml, containing multiple file paths separated by spaces or commas, e.g. "WEB-INF/applicationContext.xml WEB-INF/dataAccessContext.xml". This way, there won't be any hassle with your XML parser's entity handling.

Posted by Juergen Hoeller on February 10, 2004 at 10:16 AM MST #

Jason, if you don't work with an expanded WAR, Log4jWebConfigurer (which checks for changes in the log4j.properties file and exposes the web app root directory as system property) isn't appropriate anyway: I'd use default Log4J initialization from log4j.properties in the classpath then, resolving any system properties via Log4J's ${...} syntax. Those system properties could also be set at server startup.

Posted by Juergen Hoeller on February 10, 2004 at 10:25 AM MST #

Juergen - your suggestion to use "contextConfigLocation" will work when I'm running in-container - but I'm also using the entity includes to refer to other context files when running Unit Tests. In my setUp() method, I have:

ClassPathXmlApplicationContext ctx = 
    new ClassPathXmlApplicationContext("/applicationContext.xml");

There's probably something similar I could do (to specify different files) here, but I like the consistency b/w the <em>webapp</em> and unit tests - even if I do have to do some ant-replacing.

Posted by Matt Raible on February 10, 2004 at 11:52 AM MST #

ClassPathXmlApplicationContext and FileSystemXmlApplicationContext both have overloaded constructors that take an array of file locations - the exact equivalent of "contextConfigLocation" in web.xml. I personally simply specify the same list of file locations in my unit tests - if I need the full list at all; for testing the DAO layer, the business layer shouldn't be necessary, so one can just load the DAO context there.

Posted by Juergen Hoeller on February 11, 2004 at 05:12 AM MST #

!Cool, thanks for the clarification Juergen. I tried using this instead of <em>entity includes</em> and found a couple things. * Each XML file needs to have the DTD and root <beans> element (of course). * You cannot share beans across files. For instance, my dataSource has to be defined in applicationContext-hibernate.xml, instead of living in a separate applicationContext-database.xml. I think I'll stick with Entity Includes because (1) it works for me and (2) I like the ability to swap out database configs based on whether I'm running JUnit or Tomcat.

Posted by Matt Raible on February 11, 2004 at 08:39 AM MST #

why i cann't run "ant test-dao" in eclipse? flowing is the stack track: [junit] junit.framework.AssertionFailedError: Exception in constructor: testRemoveUser (org.springframework.beans.factory.BeanDefinitionStoreException: Line 4 in XML document from classpath resource [applicationContext.xml] is invalid; nested exception is org.xml.sax.SAXParseException: 相关的 URI "applicationContext-database.xml"; 没有基本的 URI,不能解决。 [junit] org.xml.sax.SAXParseException: 相关的 URI "applicationContext-database.xml"; 没有基本的 URI,不能解决。 [junit] at org.apache.crimson.parser.Parser2.fatal(Parser2.java:3339) [junit] at org.apache.crimson.parser.Parser2.fatal(Parser2.java:3333) [junit] at org.apache.crimson.parser.Parser2.resolveURI(Parser2.java:2915) [junit] at org.apache.crimson.parser.Parser2.maybeExternalID(Parser2.java:2887) [junit] at org.apache.crimson.parser.Parser2.maybeEntityDecl(Parser2.java:2789) [junit] at org.apache.crimson.parser.Parser2.maybeMarkupDecl(Parser2.java:1357) [junit] at org.apache.crimson.parser.Parser2.maybeDoctypeDecl(Parser2.java:1291) [junit] at org.apache.crimson.parser.Parser2.parseInternal(Parser2.java:623) [junit] at org.apache.crimson.parser.Parser2.parse(Parser2.java:333) [junit] at org.apache.crimson.parser.XMLReaderImpl.parse(XMLReaderImpl.java:448) [junit] at org.apache.crimson.jaxp.DocumentBuilderImpl.parse(DocumentBuilderImpl.java:185) [junit] at javax.xml.parsers.DocumentBuilder.parse(DocumentBuilder.java:76) [junit] at org.springframework.beans.factory.xml.XmlBeanDefinitionReader.loadBeanDefinitions(XmlBeanDefinitionReader.java:110) [junit] at org.springframework.context.support.FileSystemXmlApplicationContext.loadBeanDefinitions(FileSystemXmlApplicationContext.java:61) [junit] at org.springframework.context.support.AbtractXmlApplicationContext.refreshBeanFactory(AbstractXmlApplicationContext.java:57) [junit] at org.springframework.context.support.AbstractApplicationContext.refresh(AbstractApplicationContext.java:240) [junit] at org.springframework.context.support.FileSystemXmlApplicationContext.<init>(FileSystemXmlApplicationContext.java:32) [junit] at org.springframework.context.support.ClassPathXmlApplicationContext.<init>(ClassPathXmlApplicationContext.java:32) [junit] at org.appfuse.persistence.BaseDAOTestCase.<init>(BaseDAOTestCase.java:28) [junit] at org.appfuse.persistence.UserDAOTest.<init>(UserDAOTest.java:10) [junit] at sun.reflect.NativeConstructorAccessorImpl.newInstance0(Native Method) [junit] at sun.reflect.NativeConstructorAccessorImpl.newInstance(NativeConstructorAccessorImpl.java:39) [junit] at sun.reflect.DelegatingConstructorAccessorImpl.newInstance(DelegatingConstructorAccessorImpl.java:27) [junit] at java.lang.reflect.Constructor.newInstance(Constructor.java:274) [junit] )

Posted by Unknown on March 11, 2004 at 07:58 PM MST #

I haven't tried to run any of the tests in Eclipse recently. I only use Eclipse to edit my files - I use command-line Ant to compile and run tests. I don't know if it's possible with the current version to even run JUnit tests in Eclipse. If you'd like to see this functionality, please enter an enhancement request at https://appfuse.dev.java.net.

Posted by Matt Raible on March 11, 2004 at 08:19 PM MST #

Dear All, I deploy appfuse, but occur to as follows errors: [appfuse] DEBUG [Thread-4] StartupListener.contextInitialized(34) | contextIniti alized... [appfuse] DEBUG [Thread-4] StartupListener.contextInitialized(59) | daoType: hib ernate [appfuse] DEBUG [Thread-4] StartupListener.contextInitialized(63) | populating d rop-downs... [appfuse] ERROR [Thread-4] StartupListener.contextInitialized(79) | Error popula ting drop-downs failed!Line 4 in XML document from resource [/WEB-INF/applicatio nContext.xml] of ServletContext is invalid; nested exception is org.xml.sax.SAXP arseException: 相关的 URI "WEB-INF/applicationContext-database.xml"; 没有基本的 URI,不能解决。 org.springframework.beans.factory.BeanDefinitionStoreException: Line 4 in XML do cument from resource [/WEB-INF/applicationContext.xml] of ServletContext is inva lid; nested exception is org.xml.sax.SAXParseException: 相关的 URI "WEB-INF/appl icationContext-database.xml"; 没有基本的 URI,不能解决。 org.xml.sax.SAXParseException: 相关的 URI "WEB-INF/applicationContext-database.x ml"; 没有基本的 URI,不能解决。 at org.apache.crimson.parser.Parser2.fatal(Parser2.java:3232) at org.apache.crimson.parser.Parser2.fatal(Parser2.java:3226) at org.apache.crimson.parser.Parser2.resolveURI(Parser2.java:2808) at org.apache.crimson.parser.Parser2.maybeExternalID(Parser2.java:2780) at org.apache.crimson.parser.Parser2.maybeEntityDecl(Parser2.java:2682) at org.apache.crimson.parser.Parser2.maybeMarkupDecl(Parser2.java:1255) at org.apache.crimson.parser.Parser2.maybeDoctypeDecl(Parser2.java:1189) at org.apache.crimson.parser.Parser2.parseInternal(Parser2.java:523) at org.apache.crimson.parser.Parser2.parse(Parser2.java:318) at org.apache.crimson.parser.XMLReaderImpl.parse(XMLReaderImpl.java:442) at org.apache.crimson.jaxp.DocumentBuilderImpl.parse(DocumentBuilderImpl .java:185) at javax.xml.parsers.DocumentBuilder.parse(DocumentBuilder.java:76) at org.springframework.beans.factory.xml.XmlBeanDefinitionReader.loadBea nDefinitions(XmlBeanDefinitionReader.java:110) at org.springframework.web.context.support.XmlWebApplicationContext.load BeanDefinitions(XmlWebApplicationContext.java:115) at org.springframework.context.support.AbstractXmlApplicationContext.ref reshBeanFactory(AbstractXmlApplicationContext.java:57) at org.springframework.context.support.AbstractApplicationContext.refres h(AbstractApplicationContext.java:240) at org.springframework.web.context.support.XmlWebApplicationContext.refr esh(XmlWebApplicationContext.java:106)

Posted by yecailiu on April 09, 2004 at 02:22 AM MDT #

Please don't post errors like this on my website. Use the mailing lists or forums at http://appfuse.dev.java.net. ;-)

Try running "ant clean deploy" and see if that fixes your problem.

Posted by Matt Raible on April 09, 2004 at 04:22 AM MDT #

In my envirement, Tomcat's XMLparser told me that there is no base uri defined for those 3 seperated entity xml files,so I have no choice but merge them into applicationContext.xml run appfuse successfully. May this helps to those error-page-producer :)

Posted by Shawn on April 10, 2004 at 12:17 AM MDT #

Shawn - I'm curious, what's your environment?

Posted by Matt Raible on April 10, 2004 at 12:17 PM MDT #

Matt . . . I can get your applicationContext.xml with Entity Includes scheme working in Tomcat 5.0.24 with Xerces 1.4.4 in the endorsed directory (instead of the Xerces 2 libs that come with Tomcat). I just swap out the *new* and in with the *old* and works like a charm. But it seems a little, I don't know . . . backwards? What's your configuration?

Posted by Jeremy Kassis on May 19, 2004 at 05:32 PM MDT #

I've never had to change any of Tomcat's "endorsed" JARs for this to work. Environments tested on: WinXP/Tomcat 4.1.29/5.0.19, OS X/Tomcat 4.1.29/5.0.19, Fedora Core 1/Tomcat 4.1.29/5.0.19. ... wierd ...

Posted by Matt Raible on May 19, 2004 at 05:40 PM MDT #

xerces 1.3.x & 1.4.x seem to resolve unspecified, relative paths differently from the new xerces 2.x. 1.3/1.4 use the path that the original file was loaded from as 'root' for resolving relative SYSTEM values, while the 2.x seem to use the JVM root - where the process was kicked.

that's my quickie take on it. not sure if others could describe it differently, but i'm seeing differently SYSTEM values for my EntityResolver impl that are coming into it for resolution.

i would love to hear more about this and if there is a way to configure xerces 2.x to behave similarly to the 1.3.x and 1.4.x versions. i'd like to have my app runnable on both tomcat and weblogic, but wl doesn't like newer versions of xerces for this reason.

http://edocs.bea.com/wls/docs81/xml/xml_admin.html

yikes!

c

Posted by chris butler on June 14, 2004 at 02:36 PM MDT #

Post a Comment:
Comments are closed for this entry.