Пару месяцев назад я поделился подробным руководством о загрузке классов на виртуальной машине Java. После отчета мои коллеги спросили, какой механизм использует Spring для анализа конфигураций и как загружает классы из контекста.

После многих часов отладки исходного кода Spring мой коллега экспериментальным путем дошел до этой очень простой и ясной истины.

Немного теории

ApplicationContext - это основной интерфейс в приложении Spring, который предоставляет информацию о конфигурации приложению.

Прежде чем приступить непосредственно к демонстрации, давайте рассмотрим этапы создания ApplicationContext:

В этом посте мы проанализируем первый этап, поскольку нас интересует чтение файлов конфигурации и создание BeanDefinition.

BeanDefinition - это интерфейс, который описывает компонент, его свойства, аргументы конструктора и другую метаинформацию.

Что касается конфигурации самих bean-компонентов, в Spring есть 4 метода настройки:

  1. Конфигурация XML - ClassPathXmlApplicationContext («context.xml»);
  2. Конфигурация Groovy - GenericGroovyApplicationContext («context.groovy»);
  3. Конфигурация на основе аннотаций, в которой вы указываете пакет для сканирования - AnnotationConfigApplicationContext («package.name»);
  4. JavaConfig - конфигурация на основе аннотаций, которая указывает (Java) класс (или массив классов), аннотированный с помощью @Configuration - AnnotationConfigApplicationContext (JavaConfig.class).


Конфигурация XML

За основу берем простой проект:

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

Здесь мы должны объяснить, какие методы используются и для чего они используются:

  • printLoadedClasses (String… filters) отображает имя загрузчика и классов JVM, загруженных из пакета, передаваемых в качестве параметра в консоль. Дополнительно есть информация о количестве всех загруженных классов.
  • doSomething (Object o) - метод, выполняющий примитивную работу, но не позволяющий исключить указанные классы при оптимизации на этапе компиляции.

Подключаем к нашему проекту Spring (здесь и далее Spring 4):

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

Строка 25 - это объявление и инициализация ApplicationContext через конфигурацию XML.

Конфигурационный XML-файл выглядит следующим образом:

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

При настройке bean-компонента мы указываем реально существующий класс. Обратите внимание на данное свойство lazy-init = «true»: в этом случае bean-компонент будет создан только после запроса его из контекста.

Давайте посмотрим, как Spring справится с ситуацией с классами, объявленными в конфигурационном файле, при парсинге контекста:

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

После выполнения printLoadedClasses (calssFilter) вместе с контекстом было загружено 343 класса, 3 из которых являются классами из нашего пакета. Это означает, что к ранее использовавшимся классам добавлен еще один и он был упомянут в XML-файле конфигурации как класс film.Villian.

Давайте подробно рассмотрим конфигурацию XML:

  • Файл конфигурации считывается классом XmlBeanDefinitionReader, который реализует интерфейс BeanDefinitionReader.
  • XmlBeanDefinitionReader получает InputStream на входе и загружает Document через DefaultDocumentLoader:
Document doc = doLoadDocument(inputSource, resource);
    return registerBeanDefinitions(doc, resource);
  • После этого каждый элемент этого документа обрабатывается и, если это bean-компонент, BeanDefinition создается на основе заполненных данных (id, name, class, alias, init-method, destroy-method и т. Д.):
this.beanDefinitionMap.put(beanName, beanDefinition);
        this.beanDefinitionNames.add(beanName);
  • Каждое BeanDefinition помещается в Map, которая хранится в классе DefaultListableBeanFactory:
this.beanDefinitionMap.put(beanName, beanDefinition);
        this.beanDefinitionNames.add(beanName);

В коде карта выглядит следующим образом:

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

Теперь давайте добавим еще одно объявление bean-компонента, содержащее класс film.BadVillain, в тот же файл конфигурации:

<beans xmlns = "http://www.spingframework.org/schema/beans" xmlns:xsi = "link">
    <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>

Посмотрим, что произойдет, если напечатать список созданных BeanDefenitionNames и загруженных классов:

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

Несмотря на то, что указанный в конфигурационном файле класс film.BadVillain не существует, Spring работал без ошибок:

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

Список BeanDefenitionNames содержит 2 элемента; то есть эти 2 определения bean-компонентов, настроенные в нашем файле, были созданы.

Конфигурации обоих компонентов по сути одинаковы. Но, существующий класс загрузился, никаких проблем не возникло. Поэтому можно сделать вывод, что также была попытка загрузить несуществующий класс, но неудачная попытка ни на что не повлияла.

Попробуем получить сами бобы по их именам:

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

Вот что мы получаем:

Если в первом случае был получен действительный bean-компонент, то во втором случае возникла исключительная ситуация.

Обратите внимание на трассировку стека: сработала отложенная загрузка. Все загрузчики классов были обойдены при попытке найти искомый класс среди ранее загруженных. А после того, как нужный класс не был найден, была предпринята попытка найти несуществующий класс с помощью метода Utils.forName, что привело к логической ошибке.

При поднятии контекста загружался только один класс, однако попытка загрузить несуществующий файл не привела к ошибке. Почему так случилось?

Это потому, что мы указали lazy-init: true и запретили Spring создавать экземпляр компонента, в котором генерируется ранее полученное исключение. Если мы удалим это свойство из конфигурации или изменим его значение на lazy-init: false, то также возникнет описанная выше ошибка, но она не будет проигнорирована и приложение остановится. В нашем случае контекст был инициализирован, но мы не смогли создать экземпляр компонента, так как указанный класс не был найден.



Отличная конфигурация

При настройке контекста с помощью Groovy-файла необходимо сформировать GroovyBeanDefinitionReader, который получает на входе строку с конфигурацией контекста. В этом случае класс GroovyBeanDefinitionReader участвует в чтении контекста. Фактически, эта конфигурация работает так же, как XML, но с файлами Groovy. Кроме того, GroovyApplicationContext также хорошо работает с XML-файлом.

Вот пример простого конфигурационного Groovy-файла:

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

Попробуем сделать то же самое, что и с XML:

Ошибка возникает сразу: Groovy, как и XML, создает определения bean-компонентов, но в этом случае постпроцессор сразу выдает ошибку.



Конфигурация на основе аннотаций с указанием пакета для сканирования или JavaConfig

Такая конфигурация отличается от предыдущих. В конфигурации на основе аннотаций используются два варианта: JavaConfig и аннотации к классам.

Здесь используется тот же контекст: AnnotationConfigApplicationContext («пакет» /JavaConfig.class). Работает в зависимости от того, что было передано в конструктор.

В AnnotationConfigApplicationContext есть 2 частных поля:

  • закрытый финальный читатель AnnotatedBeanDefinitionReader (работает с JavaConfig)
  • частный финальный сканер ClassPathBeanDefinitionScanner (сканирует пакет)

Особенность AnnotatedBeanDefinitionReader заключается в том, что он работает в несколько этапов:

  1. Регистрация @ Configuration-файлов для дальнейшего разбора;
  2. Регистрация специального BeanFactoryPostProcessor, а именно BeanDefinitionRegistryPostProcessor, который анализирует JavaConfig с помощью класса ConfigurationClassParser и создает BeanDefinition.

Вот простой пример:

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

Мы создаем файл конфигурации с максимально простым bean-компонентом. И смотрим, что загрузится:

Если в случае с XML и Groovy было загружено столько BeanDefinitions, сколько было объявлено, то в этом случае в процессе повышения контекста загружаются как объявленные, так и дополнительные определения bean. В случае реализации через JavaConfig сразу загружаются все классы, включая сам класс JavaConfig, поскольку это bean-компонент.

Более того, вот еще кое-что. В случае конфигураций XML и Groovy было загружено 343 файла; здесь произошла более тяжелая загрузка 631 дополнительного файла.



Этапы операции ClassPathBeanDefinitionScanner:

  • Указанный пакет определяет список файлов для проверки. Все файлы попадают в каталоги;
  • Сканер просматривает каждый файл, получает InputStream и сканирует их с помощью org.springframework.asm.ClassReader.class;
  • На 3-м этапе сканер проверяет, проходят ли найденные объекты через фильтры аннотации org.springframework.core.type.filter.AnnotationTypeFilter. Spring ищет классы, помеченные @Component или любой другой аннотацией, которая по умолчанию включает @Component;
  • Если проверка прошла успешно, будет создано и зарегистрировано новое определение BeanDefinition.

Вся «магия» работы с аннотациями, как в случае с XML и Groovy, заключается именно в ClassReader.class из пакета springframework.asm. Специфика этого ридера в том, что он умеет работать с байт-кодом. То есть считыватель берет InputStream из байт-кода, просматривает его и ищет там аннотации.

Давайте посмотрим, как работает сканер, на простом примере. Мы создаем собственную аннотацию для поиска подходящих классов:

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

Затем мы создаем 2 класса: один со стандартной аннотацией @Component, а другой со специальной аннотацией.

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

В результате мы получаем сгенерированные определения bean-компонентов для этих классов, а также успешно загруженные классы.

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

Заключение

Исходя из вышеизложенного, на поставленные вопросы можно ответить следующим образом:

1. Какой механизм использует Spring для анализа конфигураций?

Каждая реализация контекста имеет свой собственный инструментарий, но в основном используется сканирование. Пока не создано BeanDefinition, мы не пытаемся загружать классы: сначала выполняется сканирование в соответствии с заданными параметрами, а затем на основе результатов сканирования создаются подходящие определения bean-компонентов. Затем постпроцессоры сами пытаются «настроить» BeanDefinition, загружают в него класс и так далее.

2. Как Spring загружает классы из контекста?

Используется стандартный механизм загрузки классов Java: загрузчики классов обходятся при попытке найти нужный класс среди ранее загруженных, а если класс не может быть найден, делается попытка загрузить его.



Другие статьи о Java и Spring, которые могут вам понравиться

  • 5 функций Spring Boot, которые должен знать каждый Java-разработчик (функции)
  • 5 лучших бесплатных курсов для изучения Spring и Spring Boot (курсы)
  • 5 Курс по освоению Spring Boot онлайн (курсы)
  • 10 вещей, которые должен усвоить Java-разработчик (цели)
  • 10 инструментов, которые Java-разработчики используют в повседневной жизни (инструменты)
  • 10 советов, как стать лучшим Java-разработчиком (советы)
  • 5 лучших курсов для изучения микросервисов на Java? (Курсы)
  • 3 передовых метода, которым программисты на Java могут научиться у Spring (лучшие практики)
  • 5 курсов по изучению Spring Boot и Spring Cloud (курсы)
  • 3 способа изменить порт Tomcat в Spring Boot (учебник)
  • 10 продвинутых курсов Spring Boot для Java-программистов (курсы)
  • 10 аннотаций Spring MVC, которые следует изучить Java-разработчикам (аннотации)
  • 15 вопросов для собеседований по Spring Boot для Java-программистов (вопросы)

Спасибо, что прочитали эту статью. Если вы найдете эту статью полезной, поделитесь ею со своими друзьями и коллегами. Если у вас есть какие-либо вопросы или отзывы, напишите нам.

Первоначально опубликовано на https://intexsoft.com 5 марта 2020 г.