2010-11-21 13 views
17

¿Alguien sabe de un plugin Maven que pueda usarse para validar los archivos de configuración de Spring? Por la validación, es decir:¿Plugin Maven para validar la configuración de Spring?

  • Compruebe Referencia todos los granos de una clase en la trayectoria de la estructura
  • Compruebe que todas las referencias de frijol se refieren a una definición de frijol válida
  • Verificar no judías huérfanos existen
  • Otros errores de configuración I Estoy seguro de que me estoy perdiendo.

He buscado y no he encontrado nada.

Un plugin de Maven sería ideal para mis propósitos, pero cualquier otra herramienta (plugin de Eclipse, etc.) sería apreciada.

Respuesta

9

Lo que hacemos en nuestro proyecto es simplemente escribir una prueba JUnit que carga la configuración de Spring. Esto hace unas pocas de las cosas que ha descrito como:

  • Validar el código XML
  • Asegura granos se pueden cargar con clases en la ruta de clase (por lo menos los granos que no son perezosos-cargado)

No comprueba que no haya granos huérfanos. No existe una manera confiable de hacer esto de todos modos, considerando desde cualquier parte de su código, puede buscar beans directamente dado su ID. Solo porque un frijol no esté referenciado por ningún otro frijol no significa que no se use. De hecho, todas las configuraciones de Spring tendrán al menos un bean al que no hacen referencia otros beans porque siempre debe haber una raíz para la jerarquía.

Si tiene granos que se basan en reales servicios como bases de datos o algo y no desea conectarse a estos servicios en una prueba unitaria, simplemente necesita para abstraer la configuración para permitir valores de prueba. Esto se puede lograr fácilmente con algo como el PropertyPlaceholderConfigurer que le permite tener diferentes propiedades especificadas en archivos de configuración separados para cada entorno y luego referenciadas por un archivo de definición de beans.

EDITAR (incluir código de ejemplo):
La forma de hacer esto es tener al menos 3 archivos diferentes primavera ...

  • 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> 

Hay muchas cosas que suceden aquí, vamos a explicar ...

  • El archivo applicationContext.xml es la archivo principal de primavera para toda su aplicación. Contiene un bean PropertyPlaceHolder para permitir que ciertos valores de propiedades sean configurables entre diferentes entornos en los que implementamos (prueba frente a prod). Importa todos los beans principales que la aplicación necesita para ejecutarse. Cualquier frijol que no se deba usar en una prueba, como beans de DB u otras clases que se comuniquen con servicios/recursos externos, debe definirse en este archivo.
  • El archivo beanDefinitions.xml tiene todos sus beans normales que no dependen de elementos externos. Estos beans pueden y harán referencia a beans definidos en el archivo appContext.xml.
  • El archivo testContext.xml es la versión de prueba de appContext. Necesita versiones de todos los beans definidos en el archivo appContext.xml, pero usamos una biblioteca de burla para crear una instancia de estos beans. De esta forma, las clases reales no se utilizan y no existe el riesgo de acceder a recursos externos. Este archivo tampoco necesita la propiedad placeholder bean.

Ahora que tenemos un contexto de prueba que no tenemos miedo a cargar de una prueba, aquí está el código para hacerlo ...

SpringContextTest.java paquete 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 
     } 
    } 
} 

Esta puede no ser la forma óptima de hacerlo; la clase ApplicationContext es la forma recomendada de cargar contextos de primavera. Lo anterior podría ser capaz de ser reemplazado por:

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

creo que una línea va a lograr todo lo que necesita para verificar su contexto muelle está conectado correctamente. A partir de ahí, puede cargar frijoles y afirmar como antes.

Espero que esto ayude!

+0

En medio de un proyecto marcha de la muerte y tenía la esperanza de encontrar una solución automagic-pensamiento cero, pero haces buenos puntos. Supongo que en realidad tengo que pensar un poco. Queja. Gracias. – Ickster

+0

Gweebz, ¿te molesta compartir el código fuente para esto? Me encantaría tener esta prueba JUnit en mi proyecto. –

+0

@Cory - Trataré de encontrar este código para ti, era un proyecto en el que trabajé hace un tiempo, así que tendré que buscar en SVN. –

0

Me encontré con esta pregunta cuando busqué en Google: tenía exactamente la misma pregunta.

He escrito un (muy no probado) plugin Maven para hacer esto. Actualmente solo es compatible con WAR, pero podría ampliarse fácilmente. Además, no me molesto en cargar los granos ya que no quiero tener que mantener un gran conjunto de propiedades solo para satisfacer este plugin.

Aquí es si alguna vez cualquier uso:

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; 
    } 
} 

Y pom del plugin.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> 

Una vez instalado, ejecute al igual que en el nivel raíz de su módulo WAR:

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