Spring: mencari konteks

Beberapa bulan yang lalu, sebuah pos terperinci tentang memuat kelas di JVM diterbitkan di profil saya . Setelah ceramah ini, kolega saya mengajukan pertanyaan yang bagus: mekanisme apa yang digunakan Spring untuk mengurai konfigurasi dan bagaimana cara memuat kelas dari konteks?




Setelah berjam-jam debug sumber, kolega saya secara eksperimental sampai pada kebenaran yang sangat sederhana dan dapat dimengerti.

Sedikit teori


Segera tentukan bahwa ApplicationContext adalah antarmuka utama dalam aplikasi Spring yang menyediakan informasi konfigurasi aplikasi.

Sebelum melanjutkan langsung ke demonstrasi, mari kita lihat langkah-langkah yang terlibat dalam membuat ApplicationContext :



Dalam posting ini kami akan menganalisis langkah pertama, karena kami tertarik membaca konfigurasi dan membuat BeanDefinition.

BeanDefinition adalah antarmuka yang menggambarkan kacang, propertinya, argumen konstruktor, dan informasi meta lainnya.

Mengenai konfigurasi kacang itu sendiri, Spring memiliki 4 metode konfigurasi:

  1. Konfigurasi Xml - ClassPathXmlApplicationContext ("context.xml");
  2. Konfigurasi Groovy - GenericGroovyApplicationContext ("context.groovy");
  3. Konfigurasi melalui anotasi yang menunjukkan paket untuk pemindaian - AnnotationConfigApplicationContext ("package.name");
  4. JavaConfig - konfigurasi melalui anotasi yang menunjukkan kelas (atau array kelas) yang ditandai dengan @Configuration - AnnotationConfigApplicationContext (JavaConfig.class).

Konfigurasi xml


Kami mengambil sebagai proyek sederhana:

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

Di sini Anda harus menjelaskan sedikit metode apa dan apa yang digunakan:

  • printLoadedClasses (Filter ... filter) - metode ini mencetak ke konsol nama loader dan memuat kelas JVM dari paket yang diteruskan sebagai parameter. Selain itu, ada informasi tentang jumlah semua kelas yang dimuat;
  • doSomething (Obyek o) adalah metode yang melakukan pekerjaan primitif, tetapi tidak memungkinkan untuk mengecualikan kelas yang disebutkan selama optimasi selama kompilasi.

Kami terhubung ke proyek Pegas (selanjutnya, Pegas 4 bertindak sebagai subjek uji):

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

Baris 25 adalah deklarasi dan inisialisasi ApplicationContext melalui konfigurasi Xml .

File konfigurasi xml adalah sebagai berikut:

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

Saat mengonfigurasi kacang, kami menentukan kelas yang benar-benar ada. Perhatikan properti yang ditentukan lazy-init = ”true” : dalam hal ini, nampan akan dibuat hanya setelah memintanya dari konteks.

Kami melihat bagaimana Spring, ketika meningkatkan konteks, akan menjernihkan situasi dengan kelas yang dideklarasikan dalam file konfigurasi:

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

Mari kita periksa detail konfigurasi Xml:

- Membaca file konfigurasi telah kelas XmlBeanDefinitionReader , yang mengimplementasikan antarmuka BeanDefinitionReader ;

- XmlBeanDefinitionReader pada input menerima InputStream dan memuat Dokumen melalui DefaultDocumentLoader :

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

- Setelah itu, setiap elemen dokumen ini diproses dan, jika itu adalah sebuah bin, BeanDefinition dibuat berdasarkan data yang diisi (id, nama, kelas, alias, metode init, metode-destr, dll.):

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

- Setiap BeanDefinition ditempatkan di Peta, yang disimpan di kelas DefaultListableBeanFactory:

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

Dalam kode tersebut, Peta terlihat seperti ini:

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

Sekarang di file konfigurasi yang sama, tambahkan deklarasi bean lain dengan film.BadVillain class :

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

Kami akan melihat apa yang terjadi jika Anda mencetak daftar BeanDefenitionNames dan kelas yang dimuat:

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

Terlepas dari kenyataan bahwa kelas film.BadVillain yang ditentukan dalam file konfigurasi tidak ada, Spring berfungsi tanpa kesalahan:

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

Daftar BeanDefenitionNames berisi 2 elemen; yaitu, 2
BeanDefinition yang dikonfigurasi dalam file kami dibuat.

Konfigurasi kedua nampan pada dasarnya sama. Tapi, sementara kelas yang ada dimuat, tidak ada masalah yang muncul. Dari mana kita dapat menyimpulkan bahwa ada juga upaya untuk memuat kelas yang tidak ada, tetapi upaya yang gagal tidak merusak apa pun.

Mari kita coba mendapatkan kacang sendiri dengan nama mereka:

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

Kami mendapatkan yang berikut:



Jika dalam kasus pertama bean yang valid diterima, maka dalam kasus kedua pengecualian tiba.

Perhatikan jejak stack: pemuatan kelas ditangguhkan bekerja. Semua pemuat kelas dirayapi dalam upaya untuk menemukan kelas yang mereka cari di antara yang dimuat sebelumnya. Dan setelah kelas yang diinginkan tidak ditemukan, dengan memanggil metode Utils.forName , upaya dilakukan untuk menemukan kelas yang tidak ada dengan nama, yang menyebabkan kesalahan alami.

Saat meningkatkan konteks, hanya satu kelas yang dimuat, sementara upaya untuk memuat file yang tidak ada tidak mengarah ke kesalahan. Kenapa ini terjadi?

Itu karena kami mendaftar lazy-init: truedan melarang Spring untuk membuat instance kacang, tempat pengecualian yang diterima sebelumnya dihasilkan. Jika Anda menghapus properti ini dari konfigurasi atau mengubah nilainya lazy-init: false , maka kesalahan yang dijelaskan di atas juga akan macet, tetapi aplikasi tidak akan diabaikan. Dalam kasus kami, konteksnya diinisialisasi, tetapi kami tidak dapat membuat instance kacang, karena Kelas yang ditentukan tidak ditemukan.

Konfigurasi asyik


Saat mengonfigurasi konteks menggunakan file Groovy, Anda perlu membuat GenericGroovyApplicationContext , yang menerima string dengan konfigurasi konteks sebagai input. Dalam hal ini, kelas GroovyBeanDefinitionReader terlibat dalam membaca konteks . Konfigurasi ini pada dasarnya berfungsi sama dengan Xml, hanya dengan file Groovy. Selain itu, GroovyApplicationContext berfungsi dengan baik dengan file Xml.

Contoh file konfigurasi Groovy sederhana:

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

Kami mencoba melakukan hal yang sama dengan Xml:



Kesalahan langsung crash: Groovy, seperti Xml, membuat BeanDefenitions, tetapi dalam hal ini postprocessor segera memberikan kesalahan.

Konfigurasi melalui anotasi yang mengindikasikan paket untuk pemindaian atau JavaConfig


Konfigurasi ini berbeda dari dua sebelumnya. Konfigurasi melalui anotasi menggunakan 2 opsi: JavaConfig dan anotasi atas kelas.

Konteks yang sama digunakan di sini: AnnotationConfigApplicationContext ("package" /JavaConfig.class) . Ini bekerja tergantung pada apa yang diteruskan ke konstruktor.

Dalam konteks AnnotationConfigApplicationContext ada 2 bidang pribadi:

  • pembaca AnnotatedBeanDefinitionReader akhir pribadi (berfungsi dengan JavaConfig);
  • private final ClassPathBeanDefinitionScanner scanne r (memindai paket).

Kekhasan AnnotatedBeanDefinitionReader adalah ia bekerja dalam beberapa tahap:

  1. Pendaftaran semua file konfigurasi untuk parsing lebih lanjut;
  2. Daftarkan BeanFactoryPostProcesso r khusus , yaitu BeanDefinitionRegistryPostProcessor , yang menggunakan kelas ConfigurationClassParser , mem-parsing JavaConfig dan membuat BeanDefinition .

Pertimbangkan contoh sederhana:

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

Kami membuat file konfigurasi dengan nampan sesederhana mungkin. Kami melihat apa yang akan dimuat:



Jika dalam kasus Xml dan Groovy, sebanyak BeanDefinition dimuat seperti yang diumumkan, maka dalam kasus ini, baik BeanDefinition yang dideklarasikan maupun tambahan dimuat dalam proses meningkatkan konteks. Dalam hal implementasi melalui JavaConfig, semua kelas dimuat dengan segera, termasuk kelas JavaConfig itu sendiri, karena itu sendiri merupakan sebuah kacang.

Poin lain: dalam hal konfigurasi Xml dan Groovy, 343 file diunggah, di sini terjadi 631 file tambahan yang lebih “berat”. Langkah-langkah

kerja ClassPathBeanDefinitionScanner :

  • Paket yang ditentukan menentukan daftar file untuk pemindaian. Semua file masuk ke direktori;
  • , InputStream org.springframework.asm.ClassReader.class;
  • 3- , org.springframework.core.type.filter.AnnotationTypeFilter. Spring , Component , Component;
  • , BeanDefinition.

Semua "keajaiban" bekerja dengan anotasi, seperti halnya dengan Xml dan Groovy, tepatnya terletak pada kelas ClassReader.class dari paket springframework.asm . Kekhasan pembaca ini adalah ia dapat bekerja dengan bytecode. Artinya, pembaca mengambil InputStream dari bytecode, memindai dan mencari anotasi di sana.

Pertimbangkan pemindai menggunakan contoh sederhana.

Buat anotasi Anda sendiri untuk mencari kelas yang sesuai:

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

Kami membuat 2 kelas: satu dengan anotasi Komponen standar , yang kedua dengan anotasi khusus:

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

Sebagai hasilnya, kami mendapatkan BeanDefinition yang dihasilkan untuk kelas-kelas ini dan berhasil memuat kelas-kelas.

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



Kesimpulan


Dari hal tersebut di atas, pertanyaan yang diajukan dapat dijawab sebagai berikut:

  1. Spring ?

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

    Java: , , , .

PS Saya berharap bahwa dalam posting ini saya bisa "membuka tabir kerahasiaan" dan menunjukkan secara rinci bagaimana pembentukan konteks pegas terjadi pada tahap pertama. Ternyata tidak semuanya begitu "menakutkan". Tetapi ini hanya sebagian kecil dari kerangka besar, yang berarti masih banyak hal baru dan menarik di masa depan.

All Articles