Stage aware spring contexts
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 auto detection 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 environments. And in true Open Source style, I thought.. well if they do not add it. then I have to do it
Environment Aware Spring Contexts
So what I did was to create a little add on to the Spring framework providing the necessary hooks to make this happen. it consists of just a few classes, and one annotation.
@Environment annotation
First, the @Environment annotation. This annotation serves as the discriminator, and can be registered on types. It has a String value, and defines 3 default stages :
- Environment.DEVELOPMENT
- Environment.TEST
- Environment.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.
EnvironmentAwareListableBeanFactory
This is a spring bean factory that when autowiring is happening, finds all the candidate classes, checks if they have a @Environment annotation and if there are more than one candidate, select the one that have the correct environment 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 Environment Aware Spring Contexts EnvironmentAwareClassPathXmlContext and EnvironmentAwareXmlWebApplicationContext. To enable stage detection in spring you will have to use one of these contexts
EnvironmentAwareClassPathXmlContext
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 defaultEnvironmentResolve() {
ApplicationContext ctx = new EnvironmentAwareClassPathXmlApplicationContext(“classpath:applicationContext.xml”);
ConfigurationService configurationService = (ConfigurationService) ctx.getBean(“configurationService”);
assertEquals(Stage.PRODUCTION, configurationService.getRunningEnvironment());
}
EnvironmentAwareXmlWebApplicationContext
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.EnvironmentAwareXmlWebApplicationContext</param-value>
</context-param>
Finally, we need to know what environment we are actually running in, and that is done with a configurable EnvironmentResolver strategy.
EnvironmentResolver
This is an interface that you could use to tell Spring what environment we are running in. A default strategy is supplied, and it uses simply a system property to resolve the environment, defaulting to Environment.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 Environment Aware 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 EnvironmentAwareClassPathXmlApplicationContext or EnvironmentAwareXmlWebApplicationContext
- Annotate the classes you want to be stage aware with the @Environment annotation as in the example below
- (optionally) supply your own EnvironmentResolver strategy
@Service
@Environment(Environment.PRODUCTION)
public class ProductionEnvironmentService implements EnvironmentService {
public String getEnvironment() {
return “production”;
}
}
@Service
@Environment(Environment.DEVELOPMENT)
public class DevelopmentEnvironmentService implements EnvironmentService {
public String getEnvironment() {
return “development”;
}
}
Where to get it
Environment Aware Spring Contexts is available from the project website, where you will find reference documentation, source code and a link to a public maven repository containing the binaries
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.
About this entry
You’re currently reading “Stage aware spring contexts,” an entry on Different aspects of life
- Published:
- Wednesday, April 2nd, 2008 at 10:34 pm
- Author:
- Kaare Nilsen
- Category:
- Open Source
- Tags:
- Java, Open Source, Spring
1 Comment
Jump to comment form | comments rss | trackback uri