Printemps: recherche de contexte

Il y a quelques mois, un article détaillé sur le chargement des classes sur la JVM a été publié dans mon profil . Après cet exposé, mes collègues ont posé une bonne question: quel mécanisme Spring utilise-t-il pour analyser les configurations et comment charge-t-il les classes à partir du contexte?




Après plusieurs heures de débogage de source au printemps, mon collègue a expérimentalement découvert la vérité très simple et compréhensible.

Un peu de théorie


Déterminez immédiatement que ApplicationContext est l'interface principale d'une application Spring qui fournit des informations de configuration d'application.

Avant de passer directement à la démonstration, examinons les étapes de création d'un ApplicationContext :



dans cet article, nous analyserons la première étape, car nous souhaitons lire les configurations et créer BeanDefinition.

BeanDefinition est une interface qui décrit un bean, ses propriétés, des arguments de constructeur et d'autres méta-informations.

Concernant la configuration des beans eux-mêmes, Spring dispose de 4 méthodes de configuration:

  1. Configuration Xml - ClassPathXmlApplicationContext (”context.xml”);
  2. Configuration Groovy - GenericGroovyApplicationContext (”context.groovy”);
  3. Configuration via des annotations indiquant le package à analyser - AnnotationConfigApplicationContext (”package.name”);
  4. JavaConfig - configuration via des annotations indiquant la classe (ou tableau de classes) marquée avec @Configuration - AnnotationConfigApplicationContext (JavaConfig.class).

Configuration XML


Nous prenons comme base un projet 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     /*

Ici, vous devez expliquer un peu quelles méthodes et quelles méthodes sont utilisées:

  • printLoadedClasses (filtres String ...) - la mĂ©thode imprime sur la console le nom du chargeur et des classes JVM chargĂ©es Ă  partir du package passĂ© en paramètre. En outre, il existe des informations sur le nombre de toutes les classes chargĂ©es;
  • doSomething (Object o) est une mĂ©thode qui fait un travail primitif, mais ne permet pas d'exclure les classes mentionnĂ©es lors de l'optimisation lors de la compilation.

Nous nous connectons au projet Spring (ci-après, Spring 4 fait office de sujet de test):

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 ligne 25 est la déclaration et l'initialisation d'ApplicationContext via la configuration Xml .

Le fichier de configuration xml est le suivant:

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

Lors de la configuration du bean, nous spécifions une classe réellement existante. Faites attention à la propriété spécifiée lazy-init = ”true” : dans ce cas, le bac ne sera créé qu'après l'avoir demandé à partir du contexte.

Nous regardons comment Spring, en augmentant le contexte, clarifiera la situation avec les classes déclarées dans le fichier de configuration:

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     /*

Examinons les détails de la configuration Xml:

- La lecture du fichier de configuration a été la classe XmlBeanDefinitionReader , qui implémente l'interface BeanDefinitionReader ;

- Le XmlBeanDefinitionReader à l'entrée reçoit un InputStream et charge le document via le DefaultDocumentLoader :

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

- Après cela, chaque élément de ce document est traité et, s'il s'agit d'un bac, BeanDefinition est créé en fonction des données renseignées (id, nom, classe, alias, méthode init, méthode destroy, etc.):

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

- Chaque BeanDefinition est placée dans une carte, qui est stockée dans la classe DefaultListableBeanFactory:

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

Dans le code, Map ressemble Ă  ceci:

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

Maintenant dans le même fichier de configuration, ajoutez une autre déclaration de bean avec la classe 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>

Nous verrons ce qui se passe si vous imprimez une liste des BeanDefenitionNames créés et des classes chargées:

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

Malgré le fait que la classe film.BadVillain spécifiée dans le fichier de configuration n'existe pas, Spring fonctionne sans erreur:

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 liste BeanDefenitionNames contient 2 éléments; c'est-à-dire que ces 2
BeanDefinition configurés dans notre fichier ont été créés.

Les configurations des deux bacs sont essentiellement les mêmes. Mais, alors que la classe existante était chargée, aucun problème ne s'est posé. D'où nous pouvons conclure qu'il y a également eu une tentative de chargement d'une classe inexistante, mais qu'une tentative échouée n'a rien cassé.

Essayons d'obtenir les beans eux-mĂŞmes par leurs noms:

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"));

Nous obtenons ce qui suit:



Si dans le premier cas un bean valide a été reçu, alors dans le second cas une exception est arrivée.

Faites attention à la trace de la pile: le chargement différé des classes a fonctionné. Tous les chargeurs de classe sont explorés pour tenter de trouver la classe qu'ils recherchent parmi celles précédemment chargées. Et après que la classe souhaitée n'a pas été trouvée, en appelant la méthode Utils.forName , une tentative est effectuée pour trouver une classe inexistante par nom, ce qui a conduit à une erreur naturelle.

Lors de l'élévation du contexte, une seule classe a été chargée, tandis qu'une tentative de chargement d'un fichier inexistant n'a pas entraîné d'erreur. Pourquoi est-ce arrivé?

C'est parce que nous avons enregistré lazy-init: trueet interdit à Spring de créer une instance du bean, où l'exception précédemment reçue est générée. Si vous supprimez cette propriété de la configuration ou modifiez sa valeur lazy-init: false , l'erreur décrite ci-dessus se bloquera également, mais l'application ne sera pas ignorée. Dans notre cas, le contexte a été initialisé, mais nous n'avons pas pu créer une instance du bean, car La classe spécifiée est introuvable.

Configuration Groovy


Lors de la configuration d'un contexte à l'aide d'un fichier Groovy, vous devez générer un GenericGroovyApplicationContext , qui reçoit une chaîne avec une configuration de contexte. Dans ce cas, la classe GroovyBeanDefinitionReader est engagée dans la lecture du contexte . Cette configuration fonctionne essentiellement de la même manière que Xml, uniquement avec les fichiers Groovy. En outre, GroovyApplicationContext fonctionne bien avec le fichier Xml.

Un exemple de fichier de configuration Groovy simple:

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

Nous essayons de faire la mĂŞme chose qu'avec Xml: l'



erreur se bloque immédiatement: Groovy, comme Xml, crée BeanDefenitions, mais dans ce cas le postprocesseur donne immédiatement une erreur.

Configuration via des annotations indiquant le package pour l'analyse ou JavaConfig


Cette configuration est différente des deux précédentes. La configuration par annotations utilise 2 options: JavaConfig et annotation sur les classes.

Le même contexte est utilisé ici: AnnotationConfigApplicationContext («package» /JavaConfig.class) . Cela fonctionne en fonction de ce qui a été transmis au constructeur.

Dans le contexte d' AnnotationConfigApplicationContext, il existe 2 champs privés:

  • lecteur AnnotatedBeanDefinitionReader final privĂ© (fonctionne avec JavaConfig);
  • scanne r ClassPathBeanDefinitionScanner finale privĂ©e (analyse le paquet).

La particularité de AnnotatedBeanDefinitionReader est qu'il fonctionne en plusieurs étapes:

  1. Enregistrement de tous les fichiers @Configuration pour une analyse ultérieure;
  2. Enregistrez un BeanFactoryPostProcesso r spécial , à savoir BeanDefinitionRegistryPostProcessor , qui, à l'aide de la classe ConfigurationClassParser , analyse JavaConfig et crée un BeanDefinition .

Prenons un exemple 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);

Nous créons un fichier de configuration avec le bac le plus simple possible. Nous regardons ce qui va se charger:



si dans le cas de Xml et Groovy, autant de BeanDefinition ont été chargés que prévu, alors dans ce cas, les deux BeanDefinition déclarés et supplémentaires sont chargés dans le processus d'élévation du contexte. Dans le cas d'une implémentation via JavaConfig, toutes les classes sont chargées immédiatement, y compris la classe de JavaConfig elle-même, car elle est elle-même un bean.

Autre point: dans le cas des configurations Xml et Groovy, 343 fichiers ont été téléchargés, ici une charge plus «lourde» de 631 fichiers supplémentaires s'est produite. Étapes de

travail de ClassPathBeanDefinitionScanner :

  • Le package spĂ©cifiĂ© dĂ©termine la liste des fichiers Ă  analyser. Tous les fichiers tombent dans des rĂ©pertoires;
  • , InputStream org.springframework.asm.ClassReader.class;
  • 3- , org.springframework.core.type.filter.AnnotationTypeFilter. Spring , Component , Component;
  • , BeanDefinition.

Tous les « magie » de travailler avec des annotations, comme cela est le cas avec Xml et Groovy, réside précisément dans la ClassReader.class classe du springframework.asm package . La spécificité de ce lecteur est qu'il peut fonctionner avec le bytecode. Autrement dit, le lecteur prend un InputStream du bytecode, le scanne et y recherche des annotations.

Considérez le scanner en utilisant un exemple simple.

Créez votre propre annotation pour rechercher les classes correspondantes:

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

Nous créons 2 classes: une avec l'annotation de composant standard , la seconde avec l'annotation personnalisée:

@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(){}

En conséquence, nous obtenons la définition BeanDefined générée pour ces classes et les classes chargées avec succès.

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



Conclusion


De ce qui précède, les questions posées peuvent être répondues comme suit:

  1. Spring ?

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

    Java: , , , .

PS J'espère que dans ce post j'ai pu «ouvrir le voile du secret» et montrer en détail comment se produit la formation du contexte jaillissant dans la première étape. Il s'avère que tout n'est pas si «effrayant». Mais ce n'est qu'une petite partie d'un grand cadre, ce qui signifie qu'il reste encore beaucoup de nouveautés intéressantes.

All Articles