用于验证 Spring 配置的 Maven 插件?

Posted

技术标签:

【中文标题】用于验证 Spring 配置的 Maven 插件?【英文标题】:Maven plugin to validate Spring configuration? 【发布时间】:2011-05-13 06:47:58 【问题描述】:

有人知道可以用来验证 Spring 配置文件的 Maven 插件吗?通过验证,我的意思是:

验证所有 bean 引用构建路径上的类 验证所有 bean 引用是否引用了有效的 bean 定义 验证不存在孤立的 bean 其他配置错误我确定我错过了。

我四处寻找,没有找到任何东西。

Maven 插件非常适合我的用途,但任何其他工具(Eclipse 插件等)都会受到赞赏。

【问题讨论】:

【参考方案1】:

这是Spring IDE update site(Eclipse 插件)的 URL。它执行您上面描述的操作。他们的site 似乎不可用。

【讨论】:

【参考方案2】:

我们在项目中所做的只是编写一个加载 Spring 配置的 JUnit 测试。这做了一些你描述的事情:

验证 XML 确保 bean 可以与类路径上的类一起加载(至少不是延迟加载的 bean)

它不会检查是否没有孤立的 bean。无论如何,考虑到代码中的任何地方,都没有可靠的方法来执行此操作,您可以直接根据它们的 ID 查找 bean。仅仅因为一个 bean 没有被任何其他 bean 引用并不意味着它没有被使用。事实上,所有的 Spring 配置都至少有一个 bean 没有被其他 bean 引用,因为层次结构总是必须有一个根。

如果您有依赖于真实服务(如数据库或其他东西)的 bean,并且您不想在 JUnit 测试中连接到这些服务,您只需抽象配置以允许测试价值观。这可以通过PropertyPlaceholderConfigurer 之类的方法轻松完成,它允许您在每个环境的单独配置文件中指定不同的属性,然后由一个 bean 定义文件引用。

编辑(包括示例代码): 我们这样做的方式是至少有 3 个不同的 spring 文件...

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>

这里发生了很多事情,让我解释一下......

applicationContext.xml 文件是整个应用程序的主要 spring 文件。它包含一个 PropertyPlaceHolder bean,以允许在我们部署到的不同环境(测试与产品)之间配置某些属性值。它导入应用程序需要运行的所有主要 bean。任何不应在测试中使用的 bean,如 DB bean,或与外部服务/资源通信的其他类,都应在此文件中定义。 beanDefinitions.xml 文件中包含所有不依赖外部事物的普通 bean。这些 bean 可以并且将引用 appContext.xml 文件中定义的 bean。 testContext.xml 文件是 appContext 的测试版本。它需要 appContext.xml 文件中定义的所有 bean 的版本,但我们使用了一个模拟库来实例化这些 bean。这样就不会使用真正的类,也没有访问外部资源的风险。该文件也不需要属性占位符 bean。

现在我们有了一个不害怕从测试中加载的测试上下文,下面是执行此操作的代码...

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
        
    

这可能不是最佳的方法; ApplicationContext 类是加载弹簧上下文的推荐方式。以上内容或许可以替换为:

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

我相信一行代码就可以完成验证弹簧上下文是否正确连接所需的一切。从那里,您可以像以前一样加载 bean 和断言。

希望这会有所帮助!

【讨论】:

在一个死亡行军项目的中间,希望找到一个零思维的自动解决方案,但你说得很好。我想我实际上必须考虑一下。叽。谢谢。 Gweebz,介意分享源代码吗?我很想在我的项目中进行这个 JUnit 测试。 @Cory - 我会尝试为您找到此代码,这是我前一段时间从事的一个项目,所以我将不得不通过 SVN 进行查看。 @Cory Gagliardi - 我编辑了答案以包含一些示例代码来帮助您入门。【参考方案3】:

我在谷歌搜索时遇到了这个问题 - 我有完全相同的问题。

我已经编写了一个(非常未经测试)Maven 插件来做到这一点。它目前仅支持 WAR,但可以轻松扩展。此外,我不关心实际加载 bean,因为我不想为了满足这个插件而不得不维护大量属性的麻烦。

如果有用的话,就在这里:

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;
    

以及插件的 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>

安装后,在 WAR 模块的根级别运行:

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

【讨论】:

以上是关于用于验证 Spring 配置的 Maven 插件?的主要内容,如果未能解决你的问题,请参考以下文章

Mybatis-Generator插件的使用与Spring集成Mybatis的配置

markdown Spring(后端)+ Angular(前端) - Maven插件配置

使用 maven 插件设置时 Spring Boot 配置文件不活动

简单两步快速实现shiro的配置和使用,包含登录验证角色验证权限验证以及shiro登录注销流程(基于spring的方式,使用maven构建)

当我有多个用于该目标的配置时,如何在 Maven 插件中使用特定配置运行特定目标

eclipse3.5如何配置spring web开发环境