Frühling: Kontext suchen

Vor ein paar Monaten wurde in meinem Profil ein ausführlicher Beitrag zum Laden von Klassen in der JVM veröffentlicht . Nach diesem Vortrag stellten meine Kollegen eine gute Frage: Welchen Mechanismus verwendet Spring zum Parsen von Konfigurationen und wie werden Klassen aus dem Kontext geladen?




Nach vielen Stunden des Debuggens von Frühlingsquellen kam mein Kollege experimentell zu der sehr einfachen und verständlichen Wahrheit.

Ein bisschen Theorie


Stellen Sie sofort fest, dass ApplicationContext die Hauptschnittstelle in einer Spring-Anwendung ist, die Informationen zur Anwendungskonfiguration bereitstellt.

Bevor wir direkt zur Demonstration übergehen, werfen wir einen Blick auf die Schritte zum Erstellen eines ApplicationContext :



In diesem Beitrag analysieren wir den ersten Schritt, da wir daran interessiert sind, Konfigurationen zu lesen und BeanDefinition zu erstellen.

BeanDefinition ist eine Schnittstelle, die eine Bean, ihre Eigenschaften, Konstruktorargumente und andere Metainformationen beschreibt.

In Bezug auf die Konfiguration der Beans selbst verfügt Spring über vier Konfigurationsmethoden:

  1. XML-Konfiguration - ClassPathXmlApplicationContext (”context.xml”);
  2. Groovy-Konfiguration - GenericGroovyApplicationContext (”context.groovy”);
  3. Konfiguration über Anmerkungen, die das zu scannende Paket angeben - AnnotationConfigApplicationContext (”package.name”);
  4. JavaConfig - Konfiguration über Annotationen, die die mit @Configuration - AnnotationConfigApplicationContext (JavaConfig.class) gekennzeichnete Klasse (oder das Klassenarray) angeben.

XML-Konfiguration


Wir nehmen ein einfaches Projekt als Grundlage:

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

Hier sollten Sie ein wenig erklären, welche Methoden und welche verwendet werden:

  • printLoadedClasses (String ... -Filter) - Die Methode druckt den Namen des Loaders an die Konsole und lädt JVM-Klassen aus dem als Parameter übergebenen Paket. Darüber hinaus gibt es Informationen zur Anzahl aller geladenen Klassen.
  • doSomething (Object o) ist eine Methode, die primitive Arbeit leistet, jedoch nicht zulässt, dass die genannten Klassen während der Optimierung während der Kompilierung ausgeschlossen werden.

Wir verbinden uns mit dem Spring-Projekt (im Folgenden fungiert Spring 4 als Testperson):

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

Zeile 25 ist die Deklaration und Initialisierung von ApplicationContext über die XML- Konfiguration .

Die Konfigurations-XML-Datei lautet wie folgt:

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

Bei der Konfiguration der Bean geben wir eine wirklich vorhandene Klasse an. Beachten Sie die angegebene Eigenschaft lazy-init = ”true” : In diesem Fall wird der Bin erst erstellt, nachdem er vom Kontext angefordert wurde.

Wir sehen uns an, wie Spring beim Erhöhen des Kontexts die Situation mit den in der Konfigurationsdatei deklarierten Klassen aufklärt:

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

Lassen Sie uns die Details der XML-Konfiguration untersuchen:

- Das Lesen der Konfigurationsdatei war die Klasse XmlBeanDefinitionReader , die die Schnittstelle BeanDefinitionReader implementiert .

- Der XmlBeanDefinitionReader am Eingang empfängt einen InputStream und lädt das Dokument über den DefaultDocumentLoader :

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

- Danach wird jedes Element dieses Dokuments verarbeitet und, wenn es sich um einen Bin handelt, wird BeanDefinition basierend auf den gefüllten Daten (ID, Name, Klasse, Alias, Init-Methode, Zerstörungsmethode usw.) erstellt:

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

- Jede BeanDefinition wird in einer Map platziert, die in der DefaultListableBeanFactory-Klasse gespeichert ist:

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

Im Code sieht Map folgendermaßen aus:

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

Fügen Sie nun in derselben Konfigurationsdatei eine weitere Bean-Deklaration mit der film.BadVillain- Klasse hinzu :

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

Wir werden sehen, was passiert, wenn Sie eine Liste der erstellten BeanDefenitionNames und geladenen Klassen drucken :

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

Trotz der Tatsache, dass die in der Konfigurationsdatei angegebene film.BadVillain- Klasse nicht vorhanden ist, funktioniert Spring fehlerfrei:

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

Die BeanDefenitionNames-Liste enthält 2 Elemente. Das heißt, diese 2
in unserer Datei konfigurierten BeanDefinition wurden erstellt.

Die Konfigurationen beider Behälter sind im Wesentlichen gleich. Während die vorhandene Klasse geladen wurde, traten jedoch keine Probleme auf. Daraus können wir schließen, dass es auch einen Versuch gab, eine nicht vorhandene Klasse zu laden, aber ein fehlgeschlagener Versuch hat nichts gebrochen.

Versuchen wir, die Bohnen selbst beim Namen zu bekommen:

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

Wir erhalten Folgendes:



Wenn im ersten Fall eine gültige Bean empfangen wurde, ist im zweiten Fall eine Ausnahme aufgetreten.

Achten Sie auf die Stapelverfolgung: verzögertes Laden der bearbeiteten Klassen. Alle Klassenlader werden gecrawlt, um die gesuchte Klasse unter den zuvor geladenen zu finden. Und nachdem die gewünschte Klasse nicht gefunden wurde, wird durch Aufrufen der Utils.forName- Methode versucht, eine nicht vorhandene Klasse nach Namen zu finden, was zu einem natürlichen Fehler führte.

Beim Auslösen des Kontexts wurde nur eine Klasse geladen, während der Versuch, eine nicht vorhandene Datei zu laden, nicht zu einem Fehler führte. Warum ist das geschehen?

Das liegt daran, dass wir faul-init registriert haben : wahrund verbot Spring, eine Instanz der Bean zu erstellen, in der die zuvor empfangene Ausnahme generiert wird. Wenn Sie diese Eigenschaft aus der Konfiguration entfernen oder ihren Wert lazy-init: false ändern , stürzt der oben beschriebene Fehler ebenfalls ab, die Anwendung wird jedoch nicht ignoriert. In unserem Fall wurde der Kontext initialisiert, aber wir konnten keine Instanz der Bean erstellen, weil Die angegebene Klasse wurde nicht gefunden.

Groovige Konfiguration


Wenn Sie einen Kontext mithilfe einer Groovy-Datei konfigurieren, müssen Sie einen GenericGroovyApplicationContext generieren , der eine Zeichenfolge mit der Kontextkonfiguration als Eingabe empfängt. In diesem Fall liest die GroovyBeanDefinitionReader- Klasse den Kontext . Diese Konfiguration funktioniert im Wesentlichen genauso wie XML, nur mit Groovy-Dateien. Darüber hinaus funktioniert GroovyApplicationContext gut mit der XML-Datei.

Ein Beispiel für eine einfache Groovy-Konfigurationsdatei:

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

Wir versuchen das Gleiche wie bei Xml: Der



Fehler stürzt sofort ab: Groovy erstellt wie Xml BeanDefenitions, aber in diesem Fall gibt der Postprozessor sofort einen Fehler aus.

Konfiguration über Anmerkungen, die das Paket für den Scan oder JavaConfig angeben


Diese Konfiguration unterscheidet sich von den beiden vorherigen. Die Konfiguration durch Annotationen verwendet zwei Optionen: JavaConfig und Annotation über Klassen.

Hier wird derselbe Kontext verwendet: AnnotationConfigApplicationContext ("package" /JavaConfig.class) . Dies funktioniert abhängig davon, was an den Konstruktor übergeben wurde.

Im Kontext von AnnotationConfigApplicationContext gibt es zwei private Felder:

  • private final AnnotatedBeanDefinitionReader reader (funktioniert mit JavaConfig);
  • private final ClassPathBeanDefinitionScanner scanne r (scannt das Paket).

Die Besonderheit von AnnotatedBeanDefinitionReader ist, dass es in mehreren Stufen funktioniert:

  1. Registrierung aller @ Configuration- Dateien zur weiteren Analyse;
  2. Registrieren Sie einen speziellen BeanFactoryPostProcesso , nämlich BeanDefinitionRegistryPostProcessor , der mithilfe der ConfigurationClassParser- Klasse JavaConfig analysiert und eine BeanDefinition erstellt .

Betrachten Sie ein einfaches Beispiel:

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

Wir erstellen eine Konfigurationsdatei mit dem einfachsten möglichen Bin. Wir schauen uns an, was geladen wird:



Wenn im Fall von Xml und Groovy so viele BeanDefinition geladen wurden, wie angekündigt wurde, werden in diesem Fall sowohl deklarierte als auch zusätzliche BeanDefinition geladen, um den Kontext zu erhöhen. Bei der Implementierung über JavaConfig werden alle Klassen sofort geladen, einschließlich der Klasse von JavaConfig selbst, da es sich selbst um eine Bean handelt.

Ein weiterer Punkt: Bei XML- und Groovy-Konfigurationen wurden 343 Dateien hochgeladen, hier trat eine „schwerere“ Last von 631 zusätzlichen Dateien auf.

Arbeitsschritte von ClassPathBeanDefinitionScanner :

  • Das angegebene Paket bestimmt die Liste der zu scannenden Dateien. Alle Dateien fallen in Verzeichnisse;
  • , InputStream org.springframework.asm.ClassReader.class;
  • 3- , org.springframework.core.type.filter.AnnotationTypeFilter. Spring , Component , Component;
  • , BeanDefinition.

Die ganze „Magie“ der Arbeit mit Anmerkungen, wie dies bei Xml und Groovy der Fall ist, liegt genau in der ClassReader.class- Klasse aus dem springframework.asm- Paket . Die Besonderheit dieses Lesegeräts besteht darin, dass es mit Bytecode arbeiten kann. Das heißt, der Leser nimmt einen InputStream aus dem Bytecode, scannt ihn und sucht dort nach Anmerkungen.

Betrachten Sie den Scanner anhand eines einfachen Beispiels.

Erstellen Sie Ihre eigene Anmerkung, um nach den entsprechenden Klassen zu suchen:

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

Wir erstellen zwei Klassen: eine mit der Standard- Komponentenanmerkung , die zweite mit der benutzerdefinierten Anmerkung:

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

Als Ergebnis erhalten wir die generierte BeanDefinition für diese Klassen und haben Klassen erfolgreich geladen.

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



Fazit


Aus dem Vorstehenden können die gestellten Fragen wie folgt beantwortet werden:

  1. Spring ?

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

    Java: , , , .

PS Ich hoffe, dass ich in diesem Beitrag „den Schleier der Geheimhaltung öffnen“ und detailliert zeigen konnte, wie die Bildung des entspringenden Kontextes in der ersten Phase erfolgt. Es stellt sich heraus, dass nicht alles so "beängstigend" ist. Dies ist jedoch nur ein kleiner Teil eines großen Rahmens, was bedeutet, dass noch viel Neues und Interessantes vor uns liegt.

All Articles