简体   繁体   English

Maven插件验证Spring配置?

[英]Maven plugin to validate Spring configuration?

Does anyone know of a Maven plugin that can be used to validate Spring configuration files? 有谁知道可以用来验证Spring配置文件的Maven插件? By validation, I mean: 通过验证,我的意思是:

  • Verify all beans reference a class on the build path 验证所有bean引用构建路径上的类
  • Verify all bean references refer to a valid bean definition 验证所有bean引用是否引用了有效的bean定义
  • Verify no orphaned beans exist 验证没有孤立的bean存在
  • Other configuration mistakes I'm sure I'm missing. 其他配置错误我敢肯定我错过了。

I searched around and didn't come up with anything. 我四处搜索,没有想出任何东西。

A Maven plugin would be ideal for my purposes, but any other tools (Eclipse plugin, etc.) would be appreciated. Maven插件非常适合我的目的,但是任何其他工具(Eclipse插件等)都会受到赞赏。

What we do on our project is simply write a JUnit test which loads the Spring configuration. 我们在项目中所做的只是编写一个加载Spring配置的JUnit测试。 This does a few of the things you described like: 这会做一些你描述的事情:

  • Validate the XML 验证XML
  • Ensures beans can be loaded with classes on the classpath (at least beans which aren't lazy-loaded) 确保bean可以在类路径上加载类(至少bean不是延迟加载的)

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. 无论如何,考虑到代码中的任何地方,都没有可靠的方法来执行此操作,您可以直接根据ID查找bean。 Just because a bean is not referenced by any other beans does not mean it is not used. 仅仅因为bean没有被任何其他bean引用并不意味着它没有被使用。 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. 事实上,所有Spring配置都至少有一个bean没有被其他bean引用,因为总是必须有层次结构的根。

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. 如果您的bean依赖于数据库等实际服务,并且您不希望在JUnit测试中连接到这些服务,则只需抽象配置以允许测试值。 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. 这可以通过PropertyPlaceholderConfigurer之类的东西轻松完成,它允许您为每个环境在单独的配置文件中指定不同的属性,然后由一个bean定义文件引用。

EDIT (to include sample code): 编辑(包括示例代码):
The way we do this is have at least 3 different spring files... 我们这样做的方式是至少有3个不同的弹簧文件......

  • src/main/resources/applicationContext.xml SRC /主/资源/ applicationContext.xml中
  • src/main/resources/beanDefinitions.xml SRC /主/资源/ beanDefinitions.xml
  • src/test/resources/testContext.xml SRC /测试/资源/ testContext.xml

applicationContext.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 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 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. applicationContext.xml文件是整个应用程序的主要spring文件。 It contains an PropertyPlaceHolder bean to allow certain property values to be configurable between different environments we deploy to (test vs. prod). 它包含一个PropertyPlaceHolder bean,允许在我们部署到的不同环境(test vs. prod)之间配置某些属性值。 It imports all of the main beans that the app needs to run. 它导入应用程序需要运行的所有主bean。 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. 任何不应在测试中使用的bean,如DB bean,或与外部服务/资源通信的其他类,都应在此文件中定义。
  • The beanDefinitions.xml file has all of your normal beans in it which don't rely on external things. beanDefinitions.xml文件包含所有正常的bean,它们不依赖于外部事物。 These beans can and will reference beans defined in the appContext.xml file. 这些bean可以并将引用appContext.xml文件中定义的bean。
  • The testContext.xml file is the test version of the appContext. testContext.xml文件是appContext的测试版本。 It needs versions of all beans defined in the appContext.xml file but we used a mocking library to instantiate these beans. 它需要appContext.xml文件中定义的所有bean的版本,但我们使用模拟库来实例化这些bean。 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. 此文件也不需要属性占位符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; SpringContextTest.java包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. ApplicationContext类是加载spring上下文的推荐方法。 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. 从那里,您可以像以前一样加载bean并断言。

Hope this helps! 希望这可以帮助!

Here's the URL of Spring IDE update site (Eclipse plugin). 这是Spring IDE更新站点 (Eclipse插件)的URL。 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. 我已经编写了一个( 非常未经测试的 )Maven插件来执行此操作。 It currently only supports WARs but could easily be extended. 它目前仅支持WAR,但可以轻松扩展。 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. 另外,我不打算实际加载bean,因为我不想为了满足这个插件而必须维护大量属性的麻烦。

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: 而插件的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: 安装后,在WAR模块的根级别运行:

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

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM