Maven plugin to validate Spring configuration? Maven plugin to validate Spring configuration? spring spring

Maven plugin to validate Spring configuration?


What we do on our project is simply write a JUnit test which loads the Spring configuration. This does a few of the things you described like:

  • Validate the XML
  • Ensures beans can be loaded with classes on the classpath (at least beans which aren't lazy-loaded)

It does not check that there are no orphan beans. There is no reliable way of doing this anyway considering from anywhere in your code, you can lookup beans directly given their ID. Just because a bean is not referenced by any other beans does not mean it is not used. In fact all Spring configs will have at least one bean which is not referenced by other beans because there always has to be a root to the hierarchy.

If you have beans which rely on real services like databases or something and you don't want to connect to these services in a JUnit test, you simply need to abstract the configuration to allow for test values. This can be easily accomplished with something like the PropertyPlaceholderConfigurer which allows you to have different properties specified in separate config files for each environment and then referenced by one beans definition file.

EDIT (to include sample code):
The way we do this is have at least 3 different spring files...

  • src/main/resources/applicationContext.xml
  • src/main/resources/beanDefinitions.xml
  • src/test/resources/testContext.xml

applicationContext.xml

<?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.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd">    <import resource="classpath:beanDefinitions.xml"/>    <bean class="org.springframework.beans.factory.config.PropertyPlaceholderConfigurer">        <property name="location" value="file:path/environment.properties" />    </bean>    <bean id="dataSource" class="org.springframework.jdbc.datasource.DriverManagerDataSource">        <property name="driverClassName" value="${driver}" />        ...    </bean>    ... <!-- more beans which shouldn't be loaded in a test go here --></beans>

beanDefinitions.xml

<?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.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd">    <bean id="myBean" class="com.example.MyClass">        ...    </bean>    <bean id="myRepo" class="com.example.MyRepository">        <property name="dataSource" ref="dataSource"/>        ...    </bean>    ... <!-- more beans which should be loaded in a test --></beans>

testContext.xml

<?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.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd">    <import resource="classpath:beanDefinitions.xml"/>    <bean id="dataSource" class="org.mockito.Mockito" factory-method="mock">        <constructor-arg value="org.springframework.jdbc.datasource.DriverManagerDataSource"/>    </bean></beans>

There are many things going on here, let me explain...

  • The applicationContext.xml file is the main spring file for your whole application. It contains an PropertyPlaceHolder bean to allow certain property values to be configurable between different environments we deploy to (test vs. prod). It imports all of the main beans that the app needs to run. Any beans which should not be used in a test, like DB beans, or other classes which communicate with external services/resources should be definied in this file.
  • The beanDefinitions.xml file has all of your normal beans in it which don't rely on external things. These beans can and will reference beans defined in the appContext.xml file.
  • The testContext.xml file is the test version of the appContext. It needs versions of all beans defined in the appContext.xml file but we used a mocking library to instantiate these beans. This way the real classes aren't used and there is no risk of access external resources. This file also doesn't need the property placeholder bean.

Now that we have a test context which we aren't afraid to load from a test, here is the code to do it...

SpringContextTest.java package com.example;

import org.junit.Test;import org.springframework.beans.factory.xml.XmlBeanFactory;import org.springframework.core.io.ClassPathResource;public class SpringContextTest {    @Test    public void springContextCanLoad() {        XmlBeanFactory factory = new XmlBeanFactory(new ClassPathResource("testContext.xml"));        for (String beanName : factory.getBeanDefinitionNames()) {            Object bean = factory.getBean(beanName);            // assert anything you want        }    }}

This may not be the optimal way of doing it; the ApplicationContext class is the recommended way of loading spring contexts. The above might be able to be replaced by:

    @Test    public void springContextCanLoad() {        ApplicationContext context = new FileSystemXmlApplicationContext("classpath:testContext.xml");    }

I believe that one line will accomplish everything you need to verify your spring context is wired correctly. From there, you can load beans and assert like before.

Hope this helps!


Here's the URL of Spring IDE update site (Eclipse plugin). It does what you described above. Their site seems to be unavailable.


I came across this question when googling - I had exactly the same question.

I've written a (very much untested) Maven plugin to do this this. It currently only supports WARs but could easily be extended. In addition, I don't bother actually loading the beans since I don't want the hassle of having to maintain a large set of properties just to satisfy this plugin.

Here it is if it's ever any use:

package myplugins;import org.apache.maven.plugin.AbstractMojo;import org.apache.maven.plugin.MojoExecutionException;import org.apache.maven.project.MavenProject;import org.springframework.beans.MutablePropertyValues;import org.springframework.beans.PropertyValue;import org.springframework.beans.factory.config.BeanDefinition;import org.springframework.beans.factory.config.ConstructorArgumentValues;import org.springframework.beans.factory.support.DefaultListableBeanFactory;import org.springframework.beans.factory.xml.XmlBeanDefinitionReader;import org.springframework.core.io.FileSystemResource;import org.springframework.util.ClassUtils;import java.io.File;import java.lang.reflect.Constructor;import java.lang.reflect.Method;import java.net.URL;import java.net.URLClassLoader;import java.util.Collection;import java.util.HashSet;import java.util.List;import java.util.Set;/** * Validates Spring configuration resource and class references * using a classloader that looks at the specified WAR's lib and classes * directory. * <p/> * It doesn't attempt to load the application context as to avoid the * need to supply property files * <br/> * TODO: maybe one day supplying properties will become an optional part of the validation. * * @goal validate * @aggregator * @phase install */public class WarSpringValidationMojo extends AbstractMojo{    private final static String FILE_SEPARATOR = System.getProperty("file.separator");    /**     * Project.     * @parameter expression="${project}"     * @readonly     */    private MavenProject project;    /**     * The WAR's root Spring configuration file name.     *     * @parameter expression="${applicationContext}" default-value="webAppConfig.xml"     */    private String applicationContext;    /**     * The WAR's directory.     *     * @parameter expression="${warSourceDirectory}" default-value="${basedir}/target/${project.build.finalName}"     */    private File warSourceDirectory;    @SuppressWarnings("unchecked")    public void execute() throws MojoExecutionException    {        try        {            if ("war".equals(project.getArtifact().getType()))            {                File applicationContextFile = new File(warSourceDirectory, "WEB-INF" + FILE_SEPARATOR + applicationContext);                File classesDir = new File(warSourceDirectory, "WEB-INF" + FILE_SEPARATOR + "classes");                File libDir = new File(warSourceDirectory, "WEB-INF" + FILE_SEPARATOR + "lib");                Set<URL> classUrls = new HashSet<URL>();                if (classesDir.exists())                {                    classUrls.addAll(getUrlsForExtension(classesDir, "class", "properties"));                }                if (libDir.exists())                {                    classUrls.addAll(getUrlsForExtension(libDir, "jar", "zip"));                }                ClassLoader parentClassLoader = Thread.currentThread().getContextClassLoader();                ClassLoader classLoader = new URLClassLoader(classUrls.toArray(new URL[classUrls.size()]), parentClassLoader);                ClassUtils.overrideThreadContextClassLoader(classLoader);                DefaultListableBeanFactory factory = new DefaultListableBeanFactory();                factory.setBeanClassLoader(classLoader);                XmlBeanDefinitionReader reader = new XmlBeanDefinitionReader(factory);                reader.setValidating(true);                reader.loadBeanDefinitions(new FileSystemResource(applicationContextFile));                for (String beanName : factory.getBeanDefinitionNames())                {                    validateBeanDefinition(classLoader, factory.getBeanDefinition(beanName), beanName);                }                getLog().info("Successfully validated Spring configuration (NOTE: validation only checks classes, " +                        "property setter methods and resource references)");            }            else            {                getLog().info("Skipping validation since project artifact is not a WAR");            }        }        catch (Exception e)        {            getLog().error("Loading Spring beans threw an exception", e);            throw new MojoExecutionException("Failed to validate Spring configuration");        }    }    private void validateBeanDefinition(ClassLoader beanClassloader, BeanDefinition beanDefinition, String beanName) throws Exception    {        Class<?> beanClass = validateBeanClass(beanClassloader, beanDefinition, beanName);        validateBeanConstructor(beanDefinition, beanName, beanClass);        validateBeanSetters(beanDefinition, beanName, beanClass);    }    private Class<?> validateBeanClass(ClassLoader beanClassloader, BeanDefinition beanDefinition, String beanName) throws Exception    {        Class<?> beanClass;        try        {            beanClass = beanClassloader.loadClass(beanDefinition.getBeanClassName());        }        catch (ClassNotFoundException e)        {            throw new ClassNotFoundException("Cannot find " + beanDefinition.getBeanClassName() +                    " for bean '" + beanName + "' in " + beanDefinition.getResourceDescription(), e);        }        return beanClass;    }    private void validateBeanConstructor(BeanDefinition beanDefinition, String beanName,            Class<?> beanClass) throws Exception    {        boolean foundConstructor = false;        ConstructorArgumentValues constructorArgs = beanDefinition.getConstructorArgumentValues();        Class<?>[] argTypes = null;        if (constructorArgs != null)        {            Constructor<?>[] constructors = beanClass.getDeclaredConstructors();            int suppliedArgCount = constructorArgs.getArgumentCount();            boolean isGenericArgs = !constructorArgs.getGenericArgumentValues().isEmpty();            for (int k = 0; k < constructors.length && !foundConstructor; k++)            {                Constructor<?> c = constructors[k];                knownConstructorLoop:                {                    Class<?>[] knownConstructorsArgTypes = c.getParameterTypes();                    if (knownConstructorsArgTypes.length == suppliedArgCount)                    {                        if (isGenericArgs)                        {                            foundConstructor = true; // TODO - support generic arg checking                        }                        else                        {                            for (int i = 0; i < knownConstructorsArgTypes.length; i++)                            {                                Class<?> argType = knownConstructorsArgTypes[i];                                ConstructorArgumentValues.ValueHolder valHolder = constructorArgs.getArgumentValue(i,                                        argType);                                if (valHolder == null)                                {                                    break knownConstructorLoop;                                }                            }                            foundConstructor = true;                        }                    }                }            }        }        else        {            try            {                Constructor c = beanClass.getConstructor(argTypes);                foundConstructor = true;            }            catch (Exception ignored) { }        }        if (!foundConstructor)        {            throw new NoSuchMethodException("No matching constructor could be found for bean '" +                        beanName + "' for " + beanClass.toString() + " in " + beanDefinition.getResourceDescription());        }    }    private void validateBeanSetters(BeanDefinition beanDefinition, String beanName, Class<?> beanClass) throws Exception    {        MutablePropertyValues properties = beanDefinition.getPropertyValues();        List<PropertyValue> propList = properties.getPropertyValueList();        try        {            Method[] methods = beanClass.getMethods();            for (PropertyValue p : propList)            {                boolean foundMethod = false;                String propName = p.getName();                String setterMethodName = "set" + propName.substring(0, 1).toUpperCase();                if (propName.length() > 1)                {                    setterMethodName += propName.substring(1);                }                for (int i = 0; i < methods.length && !foundMethod; i++)                {                    Method m = methods[i];                    foundMethod = m.getName().equals(setterMethodName);                }                if (!foundMethod)                {                    throw new NoSuchMethodException("No matching setter method " + setterMethodName                            + " could be found for bean '" +    beanName + "' for " + beanClass.toString() +                            " in " + beanDefinition.getResourceDescription());                }            }        }        catch (NoClassDefFoundError e)        {            getLog().warn("Could not validate setter methods for bean " + beanName +                    " since getting the methods of " + beanClass + " threw a NoClassDefFoundError: "                    + e.getLocalizedMessage());        }    }    private Collection<? extends URL> getUrlsForExtension(File file, String... extensions) throws Exception    {        Set<URL> ret = new HashSet<URL>();        if (file.isDirectory())        {            for (File childFile : file.listFiles())            {                ret.addAll(getUrlsForExtension(childFile, extensions));            }        }        else        {            for (String ex : extensions)            {                if (file.getName().endsWith("." + ex))                {                    ret.add(file.toURI().toURL());                    break;                }            }        }        return ret;    }}

And the plugin's pom.xml:

<?xml version="1.0"?><project xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd" xmlns="http://maven.apache.org/POM/4.0.0"         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">    <modelVersion>4.0.0</modelVersion>    <parent>        ... <my project's parent> ...    </parent>    <groupId>myplugins</groupId>    <artifactId>maven-spring-validation-plugin</artifactId>    <version>1.0</version>    <packaging>maven-plugin</packaging>    <name>Maven Spring Validation Plugin</name>    <url>http://maven.apache.org</url>    <dependencies>    <dependency>        <groupId>org.apache.maven</groupId>        <artifactId>maven-plugin-api</artifactId>        <version>2.0</version>    </dependency>    <dependency>        <groupId>org.apache.maven</groupId>        <artifactId>maven-project</artifactId>        <version>2.0.8</version>    </dependency>        <dependency>            <groupId>org.springframework</groupId>            <artifactId>spring-beans</artifactId>            <version>3.0.7.RELEASE</version>        </dependency>    </dependencies></project>

Once installed, run like so at the root level of your WAR module:

mvn myplugins:maven-spring-validation-plugin:validate