Drop XML: configure your Spring web application in Java

Since Spring 3.0, configuration of your application context is possible in Java (using the @Configuration annotation). But it is only now maturing to a point where you can fully configure your web application, with the coming release of Spring 3.1 (milestone 2 was released just a few days ago). This blog post describes how.


Introduction



Our Spring web application is a simple one seen from a configuration perspective: we only need Spring beans, plus transactions and the Spring MVC framework. This is what we need to configure:



  • A component scan to autodiscover our @Components — this cuts the configuration size considerably.

  • Transaction management

  • A DataSource

  • Spring MVC

  • A default view (Spring MVC)

  • Static resources (Spring MVC)

  • Message converters (REST support from Spring MVC)

  • Argument resolvers (Spring MVC; also see this post)

  • View resolvers (Spring MVC)



Still quite some list, but as we’ll see it has now become easy.



Auto-discovery, transactions and data



The first part of our configuration actually consists of three components, as we’ll want our unit tests to use a different DataSource than our running application. The source code is below. Please note the comments: they explain what happens.




@Configuration // This is a configuration class: @Bean methods are intercepted, and it's return values cached as needed for the bean scope.
@ComponentScan(basePackages = "base.package", excludeFilters=@Filter(Configuration.class)) // Find our @Component-classes; the excludeFilters is no longer necessary with 3.1 RC2
@EnableTransactionManagement // Transaction management
@PropertySource("classpath:persistence.properties") // External properties: in development our environment decides we want to log more.
@Import(SpringMvc.class) // Use this configuration as well.
public class ApplicationConfig {

@Autowired
private DataSource dataSource; // From one of our nested classes
@Autowired
private String hibernateDialect; // From one of our nested classes
@Autowired
private Environment environment;


@Bean
public FactoryBean<EntityManagerFactory> entityManagerFactory() {

Map<String, Object> jpaProperties = new HashMap<String, Object>();
jpaProperties.put("hibernate.dialect", hibernateDialect);
jpaProperties.put("hibernate.hbm2ddl.auto", environment.getProperty("hibernate.hbm2ddl.auto"));
jpaProperties.put("hibernate.show_sql", environment.getProperty("hibernate.show_sql"));
jpaProperties.put("hibernate.format_sql", environment.getProperty("hibernate.format_sql"));
jpaProperties.put("hibernate.use_sql_comments", environment.getProperty("hibernate.use_sql_comments"));

LocalContainerEntityManagerFactoryBean localContainerEntityManagerFactoryBean = new LocalContainerEntityManagerFactoryBean();
localContainerEntityManagerFactoryBean.setJpaPropertyMap(jpaProperties);
localContainerEntityManagerFactoryBean.setDataSource(dataSource);

return localContainerEntityManagerFactoryBean;
}


@Bean(name = AbstractApplicationContext.MESSAGE_SOURCE_BEAN_NAME) // Nice: no hardcoded name.
public MessageSource messageSource() {

ResourceBundleMessageSource messageSource = new ResourceBundleMessageSource();
messageSource.setBasename("messages"); // Our i18n translations are in messages.properties, messages_nl.properties, ...
return messageSource;
}


@Bean
// We need to define a transaction manager ourselves. For production use we could name it anything we want (unlike when
// using XML), but the TransactionalTestExecutionListener in spring-test requires us to either specify this name, or use
// the default name ("transactionManager").
public PlatformTransactionManager transactionManager() {

EntityManagerFactory factory;
try {
// Needed because our method returns a factory; not the object itself.
factory = entityManagerFactory().getObject();
} catch (Exception e) {
throw new IllegalStateException("Failed to create an EntityManagerFactory", e);
}
return new JpaTransactionManager(factory);
}


/**
* Beans for production use. This is a nested class, and as such is automatically imported.
*/
@Configuration
@Profile("production") // For normal use.
public static class JndiConnections {

@Resource(mappedName = "jdbc/OurAppDataSource")
private DataSource dataSource;
@Resource(mappedName = "OurAppHibernateDialect")
private String hibernateDialect;


@Bean
public DataSource dataSource() {

return dataSource;
}


@Bean
public String hibernateDialect() {

return hibernateDialect;
}
}

/**
* Beans for testing.
*/
@Configuration
@Profile("testing") // Only unit tests.
public static class LocalConnections {

@Bean
public DataSource dataSource() {

DriverManagerDataSource delegate = new DriverManagerDataSource();
delegate.setDriverClassName("org.hsqldb.jdbcDriver"); // As String, as the dependency is test (not compile).
delegate.setUrl("jdbc:hsqldb:mem:postduif;sql.enforce_strict_size=true");
delegate.setUsername("sa");
delegate.setPassword("");

// We use Liquibase; see: http://blog.42.nl/articles/automate-liquibase-migrations
LiquibaseMigrator migrator = new LiquibaseMigrator();
migrator.setChangeLogPath("src/main/db/changelog.groovy");

MigratingDataSource dataSource = new MigratingDataSource();
dataSource.setMigrator(migrator);
dataSource.setDelegate(delegate);
return dataSource;
}


@Bean
public String hibernateDialect() {

// As String, as the dependency is runtime.
return "org.hibernate.dialect.HSQLDialect";
}
}
}


This configures the basics of our application. All @Components are discovered using a classpath scan, and we’ve added transaction management, a DataSource and referenced our translations. Next up: Spring MVC.



Spring MVC with REST support



In addition to the @EnableTransactionManagement annotation, the Spring 3.1 milestones also introduce the @EnableWebMvc annotation. Here however, we’re also more likely to use the accompanying *Configurer interface. We use the class WebMvcConfigurerAdapter, which contains empty implementations of all it’s methods.




@Configuration
@EnableWebMvc
public class SpringMvc extends WebMvcConfigurerAdapter {

@Autowired
private ApplicationInfoController applicationInfoController; // Used to provide version information on all JSP's.
private List<HttpMessageConverter<?>> messageConverters; // Cached: this is not a bean.


@Override
public void configureViewControllers(ViewControllerConfigurer configurer) {

// Our default view.
configurer.mapViewName("/", "welcome");
}


@Override
public void configureResourceHandling(ResourceConfigurer configurer) {

// We've mapped the DispatcherServlet to '/', so we need to tell it where our static resources are.
configurer.addResourceLocation("/images/").addPathMapping("/images/**");
configurer.addResourceLocation("/styles/").addPathMapping("/styles/**");
configurer.addResourceLocation("/scripts/").addPathMapping("/scripts/**");
}


@Override
public void configureMessageConverters(List<HttpMessageConverter<?>> converters) {

// Note: this overwrites the default message converters.
converters.addAll(getMessageConverters());
}


/**
* The message converters for the content types we support.
*
* @return the message converters; returns the same list on subsequent calls
*/
private List<HttpMessageConverter<?>> getMessageConverters() {

if (messageConverters == null) {
messageConverters = new ArrayList<HttpMessageConverter<?>>();

MappingJacksonHttpMessageConverter mappingJacksonHttpMessageConverter = new MappingJacksonHttpMessageConverter();
mappingJacksonHttpMessageConverter.setObjectMapper(objectMapper());
messageConverters.add(mappingJacksonHttpMessageConverter);
}
return messageConverters;
}


@Bean // Also used in our integration tests.
public ObjectMapper objectMapper() {

return new JpaAwareObjectMapper();
}


@Override
public void addArgumentResolvers(List<HandlerMethodArgumentResolver> argumentResolvers) {

// Extra argument resolvers (the default ones are added as well).
argumentResolvers.add(pathEntityMethodArgumentResolver());
argumentResolvers.add(requestEntityMethodArgumentResolver());
argumentResolvers.add(validatingRequestBodyMethodArgumentResolver());
}


@Bean // As bean to have Spring autowire it, and inject it into our unit test.
public PathEntityMethodArgumentResolver pathEntityMethodArgumentResolver() {

return new PathEntityMethodArgumentResolver();
}


@Bean // As bean to have Spring autowire it, and inject it into our unit test.
public RequestEntityMethodArgumentResolver requestEntityMethodArgumentResolver() {

return new RequestEntityMethodArgumentResolver();
}


@Bean // As bean to have Spring autowire it, and inject it into our unit test.
public ValidatingRequestBodyMethodArgumentResolver validatingRequestBodyMethodArgumentResolver() {

return new ValidatingRequestBodyMethodArgumentResolver(getMessageConverters());
}


@Bean
public InternalResourceViewResolver viewResolver() {

// Location of our JSP pages, and to expose the bean with version information
InternalResourceViewResolver internalResourceViewResolver = new InternalResourceViewResolver();
internalResourceViewResolver.setViewClass(JstlView.class);
internalResourceViewResolver.setPrefix("/WEB-INF/views/");
internalResourceViewResolver.setSuffix(".jsp");
internalResourceViewResolver.setExposedContextBeanNames(new String[]);
return internalResourceViewResolver;
}


@Bean
public Map<String, String> versionInfo() {

return applicationInfoController.getVersionInformation();
}
}


Testing (unit tests)



No application is complete unless it has been tested thouroughly. Integration tests use the application as it is build, and hence use the production configuration inside the application (outside, the server environment must obviously be a test environment tailored to the tests). This leaves the unit tests.



To ensure your application context is used during unit tests, we need to do two things: tell Spring to use the annotation configuration, and tell what configuration to use. We also do one extra thing: tell it to use the testing profile. Also note that this is an empty abstract superclass: our unit tests simply extend it. Note that an interface won’t work, as annotations are never inherited from interfaces (annotations describe what a class is, interfaces define only behavior).




@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(loader = AnnotationConfigContextLoader.class, classes = Application.class)
@ActiveProfiles("testing")
public abstract class SpringContextTestCase {
// Subclass for unit tests.
}


Also note that the application context we’ve defined above is in Java. We won’t forget to test that as well, won’t we? It’s even more proof that our application will behave as it should, and an extra help to disgnose problems if it doesn’t.



Using our @Configuration in a web application



Spring MVC has a default location (and handling) for an application context, and it is backwards compatible. So it uses XML. Below is the XML fragment in web.xml we use to load our Java-based application context:




<context-param>
<param-name>contextClass</param-name>
<param-value>org.springframework.web.context.support.AnnotationConfigWebApplicationContext</param-value>
</context-param>
<context-param>
<param-name>contextConfigLocation</param-name>
<param-value>base.package.configuration.ApplicationConfig</param-value>
</context-param>
<context-param>
<param-name>spring.profiles.active</param-name>
<param-value>production</param-value>
</context-param>
<listener>
<listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
</listener>


This fragment tells Spring to load the application context at startup using the AnnotationConfigWebApplicationContext. It has it look at the class ApplicationConfig as a starting point, using the profile production. Note that this profile remains the same for any environment, as it is the profile that reads the environment specific configuration from JNDI.



Conclusion



Using a proof by example, this blog post demonstrates that with the upcoming Spring 3.1 we can configure our application context in Java.

No comments:

Post a Comment