Primavera: buscando contexto

Hace un par de meses, se publicó en mi perfil una publicación detallada sobre las clases de carga en la JVM . Después de esta charla, mis colegas hicieron una buena pregunta: ¿qué mecanismo usa Spring para analizar las configuraciones y cómo carga las clases del contexto?




Después de muchas horas de depuración de fuentes de primavera, mi colega llegó experimentalmente a la verdad muy simple y comprensible.

Poco de teoría


Determine de inmediato que ApplicationContext es la interfaz principal en una aplicación Spring que proporciona información de configuración de la aplicación.

Antes de continuar directamente con la demostración, echemos un vistazo a los pasos necesarios para crear un ApplicationContext :



en esta publicación analizaremos el primer paso, ya que estamos interesados ​​en leer configuraciones y crear BeanDefinition.

BeanDefinition es una interfaz que describe un bean, sus propiedades, argumentos de constructor y otra metainformación.

En cuanto a la configuración de los propios beans, Spring tiene 4 métodos de configuración:

  1. Configuración Xml - ClassPathXmlApplicationContext ("context.xml");
  2. Configuración Groovy - GenericGroovyApplicationContext ("context.groovy");
  3. Configuración mediante anotaciones que indican el paquete para escanear - AnnotationConfigApplicationContext ("package.name");
  4. JavaConfig : configuración mediante anotaciones que indican la clase (o matriz de clases) marcada con @Configuration: AnnotationConfigApplicationContext (JavaConfig.class).

Configuración XML


Tomamos como base un proyecto simple:

public class SpringContextTest{
   private static String classFilter = "film.";
   
   public static void main(String[] args){
        
         printLoadedClasses(classFilter);
         /* Classloader: sun.misc.Launcher$AppClassLoader@18b4aac2
            All - 5 : 0 - Filtered      /*
        doSomething(MainCharacter.num); doSomething(FilmMaker.class);
        printLoadedClasses(classFilter);
        /* Classloader: sun.misc.Launcher$AppClassLoader@18b4aac2
               class film.MainCharacter
               class film.FilmMaker
            All - 7 : 2 - Filtered     /*

Aquí debe explicar un poco qué métodos y qué se utilizan:

  • printLoadedClasses (Cadena ... filtros) : el método imprime en la consola el nombre del cargador y las clases JVM cargadas del paquete pasado como parámetro. Además, hay información sobre el número de todas las clases cargadas;
  • doSomething (Object o) : un método que realiza un trabajo primitivo, pero que no permite excluir las clases mencionadas durante la optimización durante la compilación.

Nos conectamos con el proyecto Spring (en adelante, Spring 4 actúa como sujeto de prueba):

11 public class SpringContextTest{
12    private static String calssFilter = "film.";
13    
14    public static void main(String[] args){
15        
16        printLoadedClasses(classFilter);
17       /* Classloader: sun.misc.Launcher$AppClassLoader@18b4aac2
18           All - 5 : 0 - Filtered      /*
19        doSomething(MainCharacter.num); doSomething(FilmMaker.class);
20        printLoadedClasses(classFilter);
21        /* Classloader: sun.misc.Launcher$AppClassLoader@18b4aac2
22               class film.MainCharacter
23               class film.FilmMaker
24               All - 7 : 2 - Filtered   /*
25        ApplicationContext context = new ClassPathXmlApplicationContext(
26                  configLocation: "applicationContext.xml");
27        printLoadedClasses(classFilter);

La línea 25 es la declaración e inicialización de ApplicationContext a través de la configuración Xml .

El archivo xml de configuración es el siguiente:

<beans xmlns = "http://www.spingframework.org/schema/beans" xmlns:xsi = "http..."
        <bean id = "villain" class = "film.Villain" lazy-init= "true">
                <property name = "name" value = "Vasily"/>
        </bean>
</beans> 

Al configurar el bean, especificamos una clase realmente existente. Preste atención a la propiedad especificada lazy-init = "true" : en este caso, el contenedor se creará solo después de solicitarlo desde el contexto.

Observamos cómo Spring, al plantear el contexto, aclarará la situación con las clases declaradas en el archivo de configuración:

public class SpringContextTest {
    private static String classFilter = "film.";
    
    public static void main(String[] args) {
        
           printLoadedClasses(classFilter);
        /* Classloader: sun.misc.Launcher$AppClassLoader@18b4aac2
           All - 5 : 0 - Filtered      /*
        doSomething(MainCharacther.num); doSomething(FilmMaker.class);
        printLoadedClasses(classFilter);
        /* Classloader: sun.misc.Launcher$AppClassLoader@18b4aac2
               class film.MainCharacter
               class film.FilmMaker
            All - 7 : 2 - Filtered     /*
        ApplicationContext context = new ClassPathXmlApplicationContext(
                  configLocation: "applicationContext.xml");
        printLoadedClasses(classFilter);
        /* Classloader: sun.misc.Launcher$AppClassLoader@18b4aac2
               class film.MainCharacter
               class film.FilmMaker
               class film.Villain

            All - 343 : 3- Filtered     /*

Examinemos los detalles de la configuración Xml:

- La lectura del archivo de configuración ha sido de clase XmlBeanDefinitionReader , que implementa la interfaz BeanDefinitionReader ;

- XmlBeanDefinitionReader en la entrada recibe un InputStream y carga el documento a través de DefaultDocumentLoader :

Document doc = doLoadDocument(inputSource, resource);
return registerBeanDefinitions(doc, resource);

- Después de eso, cada elemento de este documento se procesa y, si es un contenedor, se crea BeanDefinition en función de los datos rellenados (id, nombre, clase, alias, método init, método destructor, etc.):

} else if (delegate.nodeNameEquals(ele, "bean")) {
    this.processBeanDefinition(ele, delegate);

- Cada BeanDefinition se coloca en un Mapa, que se almacena en la clase DefaultListableBeanFactory:

this.beanDefinitionMap.put(beanName, beanDefinition);
this.beanDefinitionNames.add(beanName);

En el código, Map se ve así:

/** Map of bean definition objects, keyed by bean name */
private final Map<String, BeanDefinition> beanDefinitionMap = new ConcurrentHashMap<String, BeanDefinition>(64);

Ahora en el mismo archivo de configuración, agregue otra declaración de bean con la clase film.BadVillain :

<beans xmlns = "http://www.spingframework.org/schema/beans" xmlns:xsi = "http..."
        <bean id = "goodVillain" class = "film.Villain" lazy-init= "true">
                <property name = "name" value = "Good Vasily"/>
        </bean>
        <bean id = "badVillain" class = "film.BadVillain" lazy-init= "true">
                <property name = "name" value = "Bad Vasily"/>
        </bean>

Veremos qué sucede si imprime una lista de BeanDefenitionNames creados y clases cargadas:

ApplicationContext context = new ClassPathXmlApplicationContext(
        configLocation: "applicationContext.xml");
System.out.println(Arrays.asList(context.getBeanDefinitionNames()));
        
printLoadedClasses(calssFilter);

A pesar de que la clase film.BadVillain especificada en el archivo de configuración no existe, Spring funciona sin errores:

ApplicationContext context = new ClassPathXmlApplicationContext(
        configLocation: "applicationContext.xml");
System.out.println(Arrays.asList(context.getBeanDefinitionNames()));
//  [goodVillain, badVillain]
printLoadedClasses(calssFilter);
/* Classloader: sun.misc.Launcher$AppClassLoader@18b4aac2
               class film.MainCharacter
               class film.FilmMaker
               class film.Villain
    All - 343 : 3- Filtered   /*

La lista BeanDefenitionNames contiene 2 elementos; es decir, se
crearon los 2 BeanDefinition configurados en nuestro archivo.

Las configuraciones de ambos contenedores son esencialmente las mismas. Pero, mientras se cargaba la clase existente, no surgieron problemas. De lo cual podemos concluir que también hubo un intento de cargar una clase inexistente, pero un intento fallido no rompió nada.

Tratemos de obtener los frijoles por sus nombres:

ApplicationContext context = new ClassPathXmlApplicationContext(
        configLocation: "applicationContext.xml");
System.out.println(Arrays.asList(context.getBeanDefinitionNames()));
//  [goodVillain, badVillain]
System.out.println(context.getBean( name: "goodVillain"));

System.out.println(context.getBean( name: "badVillain"));

Obtenemos lo siguiente:



si en el primer caso se recibió un bean válido, en el segundo caso llegó la excepción.

Preste atención al seguimiento de la pila: carga diferida de clases trabajadas. Todos los cargadores de clases se rastrean en un intento de encontrar la clase que están buscando entre los cargados previamente. Y después de que no se encontró la clase deseada, al llamar al método Utils.forName , se intenta encontrar una clase inexistente por nombre, lo que condujo a un error natural.

Al generar el contexto, solo se cargó una clase, mientras que un intento de cargar un archivo inexistente no condujo a un error. ¿Por qué sucedió?

Eso es porque registramos lazy-init: truey prohibió a Spring crear una instancia del bean, donde se genera la excepción recibida previamente. Si elimina esta propiedad de la configuración o cambia su valor lazy-init: false , el error descrito anteriormente también se bloqueará, pero la aplicación no será ignorada. En nuestro caso, el contexto se inicializó, pero no pudimos crear una instancia del bean, porque No se encontró la clase especificada.

Configuración maravillosa


Al configurar un contexto utilizando un archivo Groovy, debe generar un GenericGroovyApplicationContext , que recibe una cadena con una configuración de contexto. En este caso, la clase GroovyBeanDefinitionReader se dedica a leer el contexto . Esta configuración funciona esencialmente igual que Xml, solo con archivos Groovy. Además, GroovyApplicationContext funciona bien con el archivo Xml.

Un ejemplo de un archivo de configuración Groovy simple:

beans {
    goodOperator(film.Operator){bean - >
            bean.lazyInit = 'true' >
            name = 'Good Oleg' 
         }
    badOperator(film.BadOperator){bean - >
            bean.lazyInit = 'true' >
            name = 'Bad Oleg' / >
        }
  }

Intentamos hacer lo mismo que con Xml: el



error se bloquea inmediatamente: Groovy, como Xml, crea BeanDefenitions, pero en este caso el postprocesador da un error de inmediato.

Configuración mediante anotaciones que indican paquete para escaneo o JavaConfig


Esta configuración es diferente de las dos anteriores. La configuración a través de anotaciones utiliza 2 opciones: JavaConfig y anotación sobre clases.

Aquí se usa el mismo contexto: AnnotationConfigApplicationContext ("paquete" /JavaConfig.class) . Funciona dependiendo de lo que se pasó al constructor.

En el contexto de AnnotationConfigApplicationContext hay 2 campos privados:

  • lector final privado AnnotatedBeanDefinitionReader (funciona con JavaConfig);
  • escaneo privado de ClassPathBeanDefinitionScanner final (escanea el paquete).

La peculiaridad de AnnotatedBeanDefinitionReader es que funciona en varias etapas:

  1. Registro de todos los archivos de @Configuration para su posterior análisis;
  2. Registre un BeanFactoryPostProcesso r especial , concretamente BeanDefinitionRegistryPostProcessor , que, utilizando la clase ConfigurationClassParser , analiza JavaConfig y crea una BeanDefinition .

Considere un ejemplo simple:

@Configuration
public class JavaConfig {
    
    @Bean
    @Lazy
    public MainCharacter mainCharacter(){
        MainCharacter mainCharacter = new MainCharacter();
        mainCharacter.name = "Patric";
        return mainCharacter;        
   }
}

public static void main(String[] args) {

     ApplicationContext javaConfigContext = 
               new AnnotationConfigApplicationContext(JavaConfig.class);
     for (String str : javaConfigContext.getBeanDefinitionNames()){
          System.out.println(str);
     }
     printLoadedClasses(classFilter);

Creamos un archivo de configuración con el bin más simple posible. Observamos lo que se cargará:



si en el caso de Xml y Groovy, se cargaron tantos BeanDefinition como se anunció, entonces, en este caso, tanto BeanDefinition declarados como adicionales se cargan en el proceso de plantear el contexto. En el caso de la implementación a través de JavaConfig, todas las clases se cargan inmediatamente, incluida la clase de JavaConfig, ya que es un bean.

Otro punto: en el caso de las configuraciones Xml y Groovy, se cargaron 343 archivos, aquí se produjo una carga más "pesada" de 631 archivos adicionales. Pasos de

trabajo de ClassPathBeanDefinitionScanner :

  • El paquete especificado determina la lista de archivos para escanear. Todos los archivos caen en directorios;
  • , InputStream org.springframework.asm.ClassReader.class;
  • 3- , org.springframework.core.type.filter.AnnotationTypeFilter. Spring , Component , Component;
  • , BeanDefinition.

Toda la “magia” de trabajar con anotaciones, como es el caso de XML y maravillosa, se debe precisamente a ClassReader.class clase de la springframework.asm paquete . La especificidad de este lector es que puede funcionar con bytecode. Es decir, el lector toma un InputStream del bytecode, lo escanea y busca anotaciones allí.

Considere el escáner con un ejemplo simple.

Cree su propia anotación para buscar las clases correspondientes:

import org.springframework.stereotype.Component
import java.lang.annotation.*;
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Component
public @interface MyBeanLoader{
       String value() default "";

Creamos 2 clases: una con la anotación de Componente estándar , la segunda con la anotación personalizada:

@Component 
public class MainCharacter {
      public static int num = 1;
      @Value("Silence")
      public String name;
      public MainCharacter() { }

MyBeanLoader("makerFilm")
@Lazy 
public class FilmMaker {
      public static int staticInt = 1;
      @Value("Silence")
      public String filmName;
      public FilmMaker(){}

Como resultado, obtenemos el BeanDefinition generado para estas clases y las clases cargadas con éxito.

ApplicationContext annotationConfigContext =
       new AnnotationConfigApplicationContext(...basePackages: "film");
for (String str : annotationConfigContext.getBeanDefinitionNames()){
     System.out.println(str);
}
printLoadedClasses(classFilter);



Conclusión


De lo anterior, las preguntas planteadas se pueden responder de la siguiente manera:

  1. Spring ?

    , . BeanDefinition, : , BeanDefinition’ . BeanDefinition, ..
  2. Spring ?

    Java: , , , .

PD: Espero que en esta publicación haya podido "abrir el velo del secreto" y mostrar en detalle cómo se produce la formación del contexto emergente en la primera etapa. Resulta que no todo es tan "aterrador". Pero esto es solo una pequeña parte de un gran marco, lo que significa que todavía hay muchas novedades nuevas e interesantes por delante.

All Articles