The Spring framework has in our parts of the world become the kind of de-facto standard for almost all enterprise Java pplications. There are close to zero projects starting up in the Oslo area not using it. In my role as consultant and Spring Instructor I have over the years seen my share of Spring based projects, and one of the discussions that I see in these
projects are how should we configure our Spring applications ?
Well of course there are no one answer to that question, and the options we have when we use Spring are many, and sufficient for most use cases. But there is one use case that too often have been a kind of pain in the….
How do we manage configurations in various stages of development?
Let us say that we are going to use one implementation of a interface when we are developing our application, and another one in production. Let us say for e.g. mocking purposes. How should we configure our application to support that.
There is always the option of having your build system do it for you with ant filters, or Maven resource filtering. But hey. That often require us to build the application one time for each environment.
Then we have PropertyPlaceholderConfigurer. It is a very little know fact, that it also can be used to select bean classes using property files. But again. We would need one property file for each stage, and it can be somewhat difficult to make sure the correct one is used.
Also we have the option of having more bean xml files. one for tests, one for development and then include the right one, but that gives us the hassle of keeping them in sync.
I have also tried using special factory beans to solve this, but that gave me quite large xml files.
And there is another common complaint
There are way to much xml when using Spring!
To the rescue
In Spring 2.5 they introduced autodetection of components in the classpath by scanning for annotated classes. The annotation used for detecting components are called stereotypes, and Spring ships with the follwing out of the box :
- @Component
- @Service
- @Controller
- @Component
What the component scanner does, is looking for these stereotypes, and registers the classes as beans in the running Spring container. By default it uses the class name as bean id e.g. the class DevelopmentEnvironmentService annotated with the @Service annotation would be registered as a bean with the id developmentEnvironmentService :
@Service
public class DevelopmentEnvironmentService implements EnvironmentService {
public String getEnvironment() {
return “development”;
}
}
Well all good so far. But how to get a hold of this bean, and still keep us free from xml configuration files ?
Let us say we have a service that is supposed to use the EnvironmentService interface to query for the environment we are running in. First we need to create the ConfigurationService and let it be known to the container with the @Service annotation.
@Service
public class ConfigurationService {
}
Then we need to inject it with an instance of the EnvironmentService. To do this, we use the @Autowired annotation. And after we have provided the dependency and implemented the business call the class may now looks like this :
@Service
public class ConfigurationService {
@Autowired EnvironmentService environmentService;
public String getEnvironment() {
return “development”;
}
}
That’s it. basically all you need to wire up spring applications using annotations instead of xml. ehh. well almost. You may want to add this little snippet to your xml configuration file :
<beans
xmlns=“http://www.springframework.org/schema/beans”
xmlns:xsi=“http://www.w3.org/2001/XMLSchema-instance”
xmlns:context=“http://www.springframework.org/schema/context”
xsi:schemaLocation=
“http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans-2.5.xsd
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context-2.5.xsd”>
<context:component-scan base-package=“no.arktekk.stagedspring”/>
</beans>
With that said, there are other options like JavaConfig, or by creating your application context in Java, setting the correct scanners in code, but hey. the above snippet is kind a good enough for me.
Half way there
Ok now we have taken the xml configuration files out of the equation, but what happens if we have 2 instances of the EnvironmentService interface in the container ?
Well I can tell you… you get this stacktrace :
org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'configurationService': Autowiring of fields failed; nested exception is org.springframework.beans.factory.BeanCreationException: Could not autowire field: no.arktekk.stagedspring.service.EnvironmentService no.arktekk.stagedspring.service.ConfigurationService.environmentService; nested exception is org.springframework.beans.factory.NoSuchBeanDefinitionException: No unique bean of type [no.arktekk.stagedspring.service.EnvironmentService] is defined: expected single matching bean but found 2: [developmentEnvironmentService, productionenvironmentService]
at org.springframework.beans.factory.annotation.AutowiredAnnotationBeanPostProcessor.postProcessAfterInstantiation(AutowiredAnnotationBeanPostProcessor.java:240)
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.populateBean(AbstractAutowireCapableBeanFactory.java:876)
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.doCreateBean(AbstractAutowireCapableBeanFactory.java:437)
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory$1.run(AbstractAutowireCapableBeanFactory.java:383)
at java.security.AccessController.doPrivileged(Native Method)
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBean(AbstractAutowireCapableBeanFactory.java:353)
at org.springframework.beans.factory.support.AbstractBeanFactory$1.getObject(AbstractBeanFactory.java:245)
at org.springframework.beans.factory.support.DefaultSingletonBeanRegistry.getSingleton(DefaultSingletonBeanRegistry.java:169)
at org.springframework.beans.factory.support.AbstractBeanFactory.getBean(AbstractBeanFactory.java:242)
at org.springframework.beans.factory.support.AbstractBeanFactory.getBean(AbstractBeanFactory.java:164)
at org.springframework.beans.factory.support.DefaultListableBeanFactory.preInstantiateSingletons(DefaultListableBeanFactory.java:400)
at org.springframework.context.support.AbstractApplicationContext.finishBeanFactoryInitialization(AbstractApplicationContext.java:736)
at org.springframework.context.support.AbstractApplicationContext.refresh(AbstractApplicationContext.java:369)
at org.springframework.context.support.ClassPathXmlApplicationContext.(ClassPathXmlApplicationContext.java:123)
at org.springframework.context.support.ClassPathXmlApplicationContext.(ClassPathXmlApplicationContext.java:66)
at no.arktekk.stagedspring.context.StagedClassPathXmlApplicationContext.(StagedClassPathXmlApplicationContext.java:37)
at no.arktekk.stagedspring.StageTest.defaultStageResolve(StageTest.java:38)
at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:39)
at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:25)
at java.lang.reflect.Method.invoke(Method.java:585)
at org.junit.internal.runners.TestMethod.invoke(TestMethod.java:59)
at org.junit.internal.runners.MethodRoadie.runTestMethod(MethodRoadie.java:98)
at org.junit.internal.runners.MethodRoadie$2.run(MethodRoadie.java:79)
at org.junit.internal.runners.MethodRoadie.runBeforesThenTestThenAfters(MethodRoadie.java:87)
at org.junit.internal.runners.MethodRoadie.runTest(MethodRoadie.java:77)
at org.junit.internal.runners.MethodRoadie.run(MethodRoadie.java:42)
at org.junit.internal.runners.JUnit4ClassRunner.invokeTestMethod(JUnit4ClassRunner.java:88)
at org.junit.internal.runners.JUnit4ClassRunner.runMethods(JUnit4ClassRunner.java:51)
at org.junit.internal.runners.JUnit4ClassRunner$1.run(JUnit4ClassRunner.java:44)
at org.junit.internal.runners.ClassRoadie.runUnprotected(ClassRoadie.java:27)
at org.junit.internal.runners.ClassRoadie.runProtected(ClassRoadie.java:37)
at org.junit.internal.runners.JUnit4ClassRunner.run(JUnit4ClassRunner.java:42)
at org.eclipse.jdt.internal.junit4.runner.JUnit4TestReference.run(JUnit4TestReference.java:38)
at org.eclipse.jdt.internal.junit.runner.TestExecution.run(TestExecution.java:38)
at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.runTests(RemoteTestRunner.java:460)
at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.runTests(RemoteTestRunner.java:673)
at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.run(RemoteTestRunner.java:386)
at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.main(RemoteTestRunner.java:196)
Caused by: org.springframework.beans.factory.BeanCreationException: Could not autowire field: no.arktekk.stagedspring.service.EnvironmentService no.arktekk.stagedspring.service.ConfigurationService.environmentService; nested exception is org.springframework.beans.factory.NoSuchBeanDefinitionException: No unique bean of type [no.arktekk.stagedspring.service.EnvironmentService] is defined: expected single matching bean but found 2: [developmentEnvironmentService, productionenvironmentService]
at org.springframework.beans.factory.annotation.AutowiredAnnotationBeanPostProcessor$AutowiredFieldElement.inject(AutowiredAnnotationBeanPostProcessor.java:433)
at org.springframework.beans.factory.annotation.InjectionMetadata.injectFields(InjectionMetadata.java:79)
at org.springframework.beans.factory.annotation.AutowiredAnnotationBeanPostProcessor.postProcessAfterInstantiation(AutowiredAnnotationBeanPostProcessor.java:237)
… 38 more
Caused by: org.springframework.beans.factory.NoSuchBeanDefinitionException: No unique bean of type [no.arktekk.stagedspring.service.EnvironmentService] is defined: expected single matching bean but found 2: [developmentEnvironmentService, productionenvironmentService]
at org.springframework.beans.factory.support.DefaultListableBeanFactory.resolveDependency(DefaultListableBeanFactory.java:583)
at org.springframework.beans.factory.annotation.AutowiredAnnotationBeanPostProcessor$AutowiredFieldElement.inject(AutowiredAnnotationBeanPostProcessor.java:409)
… 40 more
What have I done to end up here ?
What I did is that I added this class below to my project :
@Service
public class ProductionEnvironmentService implements EnvironmentService {
public String getEnvironment() {
return “production”;
}
}
And then, suddenly I am back to square one. I know this is somewhat a strange and constructed example, but is has been quite common in projects I have been in, that we need different implementations of an interface. Most often for mocking purposes. So
what I think Spring need to have support for is to have a discriminator between stages. And in true Open Source style, I thought.. well if they do not add it. then I have to do it
Stage Enabled Spring Contexts
So what I did was to create a little addon to the Spring framework providing the neccecary hooks to make this happen. it consists of just a few classes, and one annotation.
@Stage annotation
First, the @Stage annotation. This annotation serves as the discriminator, and can be registered on types. It has a String value, and defines 3 default stages :
- Stage.DEVELOPMENT
- Stage.TEST
- Stage.PRODUCTION
When this annotation is present on a Java class, it tells the new stage enabled bean factory which stage this particular class should apply to.
StagedListableBeanFactory
This is a spring bean factory that when autowiring is happening, finds all the candidate classes, checks if they have a @Stage annotation and if there are more than one candidate, select the one that have the correct stage set and gives the reference back to the running application context.
Application Context
There are 2 application contexts supplied in the 1.0 version of Staged Enabled Spring Contexts StagedClassPathXmlContext and StagedXmlWebApplicationContext. To enable stage detection in spring you will have to use one of these contexts
StagedClassPathXmlContext
Used when a set of xml based configuration file is used to assemble a context in Java code. e.g. for embedding Spring in a server application, or test cases :
@Test
public void defaultStageResolve() {
ApplicationContext ctx = new StagedClassPathXmlApplicationContext(“classpath:applicationContext.xml”);
ConfigurationService configurationService = (ConfigurationService) ctx.getBean(“configurationService”);
assertEquals(Stage.PRODUCTION, configurationService.getRunningEnvironment());
}
StagedXmlWebApplicationContext
Used when you are creating a web application. To enable this context instead of the default spring context, you will need to set a context param in you web application web.xml file :
<context-param>
<param-name>contextClass</param-name>
<param-value>no.arktekk.stagedspring.context.StagedXmlWebApplicationContext</param-value>
</context-param>
Finally, we need to know what environment we are actually running in, and that is done with a configurable StageResolver strategy.
StageResolver
This is an interface that you could use to tell the stage enabled what environment we are running in. A default strategy is supplied, and it uses simply a system property to resolve the environment, defaulting to Stage.PRODUCTION. Of course there is nothing keeping you to supply your own environment check, by simply implementing that interface and register it as a bean in the context. The bean factory will automatically choose the one supplied by you if found.
Putting it all together
Then I have shown how to use the Stage Enabled Spring Contexts project, and it is time to summarize a little :
- Enable the Spring component scanner in your bean xml configuration file
- Annotate you classes with Spring stereotypes to make them discoverable
- Instanciate the application context using the StagedClassPathXmlApplicationContext or StagedXmlWebApplicationContext
- Annotate the classes you want to be stage aware with the @Stage annotation as in the example below
- (optionally) supply your own StageResolver strategy
@Service
@Stage(Stage.PRODUCTION)
public class ProductionEnvironmentService implements EnvironmentService {
public String getEnvironment() {
return “production”;
}
}
@Service
@Stage(Stage.DEVELOPMENT)
public class DevelopmentEnvironmentService implements EnvironmentService {
public String getEnvironment() {
return “development”;
}
}
Where to get it
Stage Enabled Spring Contexts is currently only available from my own public Subversion repository at :
http://kaare-nilsen.com/subversion/public/labs/staged-spring
Is it completly finished
The short answer. Nope. But for a 1.0 release I think it is sufficient. What I would like to point out is that it currently does not work well with the @Qualifier annotation, so if you are using it, the stage may not be correctly resolved.
I aim to fix that in a 1.1 release in a short while.
So as the last ting I would say, is that I hope you find it interesting and please drop me questions and suggestions to make it even better.