As a Spring 1.2 dude, I have often been in a situation where you needed a Spring bean to be different based on different environments. The most common scenario is this. Lets say you are configuring your MySQL database access like this:
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="
http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-3.0.xsd">
<bean id="properties" class="org.springframework.beans.factory.config.PropertyPlaceholderConfigurer">
<property name="ignoreResourceNotFound" value="true"/>
<property name="locations">
<list>
<value>classpath:app.properties</value>
</list>
</property>
</bean>
<bean id="dataSource" class="org.apache.commons.dbcp.BasicDataSource" destroy-method="close">
<property name="driverClassName" value="com.mysql.jdbc.Driver"/>
<property name="url" value="${app.db.url}"/>
<property name="username" value="${app.db.username}"/>
<property name="password" value="${app.db.password}"/>
<property name="validationQuery" value="SELECT 1 FROM DUAL"/>
</bean>
</beans>
Now when running local unit test, you don't want them to mess with a real MySQL server, even though this is sort of the only way to surface these nasty MySQL integration problems. However, you want unit tests to run super fast, i.e. in memory. So what you are thinking about is something like this:
<bean id="dataSource" class="org.apache.commons.dbcp.BasicDataSource" destroy-method="close">
<property name="driverClassName" value="org.hsqldb.jdbcDriver"/>
<property name="url" value="jdbc:hsqldb:mem:data"/>
<property name="username" value="sa"/>
<property name="password" value=""/>
</bean>
A HSQLDB that runs in memory. Now there are several approaches to switch the Spring beans. What I like to do is to use the bean overriding feature. That is to create a second main file for the ApplicationContext which will re-use the first applicationContext.xml file. Lets call the second file testApplicationContext.xml. It will look like this:
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="
http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-3.0.xsd">
<import resource="applicationContext.xml" />
<bean id="dataSource" class="org.apache.commons.dbcp.BasicDataSource" destroy-method="close">
<property name="driverClassName" value="org.hsqldb.jdbcDriver"/>
<property name="url" value="jdbc:hsqldb:mem:data"/>
<property name="username" value="sa"/>
<property name="password" value=""/>
</bean>
</beans>
The bean definitions in this file will overwrite the previous bean definitions. The testApplicationContext.xml file is used like this in unit tests:
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations="/testApplicationContext.xml")
public class SomeTest {
All good. This works well whenever you can overwrite the beans. There are alternatives. In the classpath, you could have subdirectories named after your environment, i.e. integration, production etc. Then you write some intelligent bean loader mechanism that dynamically includes the correct files. The same is possible by using <import resource="beans-${environment}.xml" />. Spring 3.1 is addressing the problem and comes with a solution in form of the so called bean definition profiles. It will allow you to do 2 new things. You can specify a profile attribute in a beans element and you are allowed to nest beans-elements, so you can have all profiles in one XML file.
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="
http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-3.1.xsd">
<bean id="properties" class="org.springframework.beans.factory.config.PropertyPlaceholderConfigurer">
<property name="ignoreResourceNotFound" value="true"/>
<property name="locations">
<list>
<value>classpath:app.properties</value>
</list>
</property>
</bean>
<beans profile="testing">
<bean id="dataSource" class="org.apache.commons.dbcp.BasicDataSource" destroy-method="close">
<property name="driverClassName" value="org.hsqldb.jdbcDriver"/>
<property name="url" value="jdbc:hsqldb:mem:data"/>
<property name="username" value="sa"/>
<property name="password" value=""/>
</bean>
</beans>
<beans profile="production">
<bean id="dataSource" class="org.apache.commons.dbcp.BasicDataSource" destroy-method="close">
<property name="driverClassName" value="com.mysql.jdbc.Driver"/>
<property name="url" value="${app.db.url}"/>
<property name="username" value="${app.db.username}"/>
<property name="password" value="${app.db.password}"/>
<property name="validationQuery" value="SELECT 1 FROM DUAL"/>
</bean>
</beans>
</beans>
Now you need to tell Spring which profile is active. Spring will be looking for a property called spring.profiles.active, which can be a single profile or a comma-separated list of profiles. In Spring 3.1 each ApplicationContext has a reference to a new Object which implements the new Environment Interface. This Environment instance knows all the active profiles and can therefore select the right beans out of the configuration files. To programmatically set the active profiles you would do:
final ApplicationContext applicationContext = new GenericXmlApplicationContext();
applicationContext.getEnvironment().setActiveProfiles("integration");
IntelliJ 10.5 already supports this by the way. You specify an active bean definition profile and IntelliJ will grey out the sections within the Spring configuration that are not affected by this profile - very cool. Back to the spring.profiles.active property. Spring 3.1 comes also with something new called PropertySource, which is as the name suggests a source for Properties. To identify the value for spring.profiles.active it will go through a hierarchy of property sources. The StandardEnvironment class has a PropertySource for system environment variables and JVM system properties, the latter one being more specific so it will be checked first. If you have a web application, there will also be a PropertySource for properties within the web.xml. This is done through the StandardServletEnvironment. So there are different ways to set spring.profiles.active for your application. You can have this on system level:
export spring.profiles.active=production
or start your application using:
-D spring.profiles.active=production
or have a web.xml like this:
<servlet>
<servlet-name>dispatcher</servlet-name>
<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
<init-param>
<param-name>spring.profiles.active</param-name>
<param-value>production</param-value>
</init-param>
</servlet>
These PropertySources are a really great feature. I like them even more than the bean definition profiles. I could imagine that your XML configuration can get a bit messy and hard to read if you use the profiles over excessively. I think I will stick with the bean override stuff for a while and see if the profiles are really better.
Totally unrelated and I don't remember why exactly, but recently I needed a mechanism to copy the properties for a Spring application back into System.properties when the application was loading. I think I used these properties from Spring powered beans but there were some legacy parts that were expecting them to be system properties. Here is a class that did this for me, maybe you find this useful.
import org.springframework.beans.factory.config.PropertyPlaceholderConfigurer;
import java.io.IOException;
import java.util.Properties;
/**
* A {@link PropertyPlaceholderConfigurer} merging loaded {@link Properties} with System {@link Properties}.
*
* Author: reik, 3/15/11
*/
public class SystemPropertiesMergingPropertyPlaceholderConfigurer extends PropertyPlaceholderConfigurer {
/**
* Does the same as {@link PropertyPlaceholderConfigurer#mergeProperties()} but additionally merges the properties
* back into {@link System#getProperties()} before returning the loaded {@link Properties}.
*
* @return Properties
* @throws IOException
*/
@Override
protected Properties mergeProperties() throws IOException {
final Properties springProperties = super.mergeProperties();
System.getProperties().putAll(springProperties);
return springProperties;
}
}
1 Kommentare:
Can spring.profiles.active also be specified in a .properties file? Shouldn't that make it's way into Env?
Kommentar veröffentlichen