Как стать автором
Обновить

Spring: в поисках контекста

Время на прочтение 9 мин
Количество просмотров 38K

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




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

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


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

Перед тем, как перейти непосредственно к демонстрации, взглянем на этапы формирования ApplicationContext:



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

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

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

  1. Xml конфигурация — ClassPathXmlApplicationContext(”context.xml”);
  2. Groovy конфигурация — GenericGroovyApplicationContext(”context.groovy”);
  3. Конфигурация через аннотации с указанием пакета для сканирования — AnnotationConfigApplicationContext(”package.name”);
  4. JavaConfig — конфигурация через аннотации с указанием класса (или массива классов) помеченного аннотацией @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 = "http..."
        <bean id = "villain" class = "film.Villain" lazy-init= "true">
                <property name = "name" value = "Vasily"/>
        </bean>
</beans> 

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

Смотрим, как 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     /*

Разберемся с деталями Xml конфигурации:

— Чтением файла конфигурации занимается класс XmlBeanDefinitionReader, который реализует интерфейс BeanDefinitionReader;

XmlBeanDefinitionReader на входе получает InputStream и загружает Document через DefaultDocumentLoader:

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

— После этого каждый элемент этого документа обрабатывается и, если он является бином, создается BeanDefinition на основе заполненных данных (id, name, class, alias, init- method, destroy-method и др.):

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

— Каждый BeanDefinition помещается в Map, который хранится в классе DefaultListableBeanFactory:

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

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

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

Теперь в том же конфигурационном файле добавим еще одно объявление бина с классом 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>

Смотрим, что получится, если распечатать список созданных 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   /*

Cписок BeanDefenitionNames содержит 2 элемента; то есть, те 2
BeanDefinition, сконфигурированные в нашем файле, были созданы.

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

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

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

Получаем следующее:



Если в первом случае был получен валидный бин, то во втором случае прилетел exception.

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

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

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

Groovy конфигурация


При конфигурации контекста с помощью Groovy-файла, необходимо сформировать GenericGroovyApplicationContext, который принимает на вход строку с конфигурацией контекста. Чтением контекста в данном случае занимается класс 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, создает BeanDefenition'ы, но в данном случае постпроцессор сразу выдаёт ошибку.

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


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

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

В контексте AnnotationConfigApplicationContext есть 2 приватных поля:

  • private final AnnotatedBeanDefinitionReader reader (работает с JavaConfig);
  • private final ClassPathBeanDefinitionScanner scanner (сканирует пакет).

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

  1. Регистрация всех @Configuration-файлов для дальнейшего парсинга;
  2. Регистрация специального BeanFactoryPostProcessor, а именно BeanDefinitionRegistryPostProcessor, который при помощи класса ConfigurationClassParser парсит JavaConfig и создает 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);

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



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

Еще один момент: в случае с 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, второй — с кастомной аннотацией:

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

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

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



Вывод


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

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

    У каждой реализации контекста свой инструментарий, но в основном используется сканирование. До момента создания BeanDefinition, мы не пытаемся загружать классы: сначала происходит сканирование по указанным параметрам, а после создаются подходящие BeanDefinition’ы на основании результатов сканирования. Затем уже постпроцессоры пытаются донастроить сам BeanDefinition, загрузить к нему класс и т.д.
  2. Как Spring загружает классы из контекста?

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

P.S. Надеюсь, в этом посте мне удалось “приоткрыть завесу тайны” и подробно показать, как происходит формирование спрингового контекста на первом этапе. Оказывается, не все так «страшно». Но это только небольшая часть большого фреймворка, значит впереди нас ждет еще много нового и интересного.
Теги:
Хабы:
+4
Комментарии 1
Комментарии Комментарии 1

Публикации

Истории

Работа

Java разработчик
359 вакансий

Ближайшие события

Московский туристический хакатон
Дата 23 марта – 7 апреля
Место
Москва Онлайн
Геймтон «DatsEdenSpace» от DatsTeam
Дата 5 – 6 апреля
Время 17:00 – 20:00
Место
Онлайн
PG Bootcamp 2024
Дата 16 апреля
Время 09:30 – 21:00
Место
Минск Онлайн
EvaConf 2024
Дата 16 апреля
Время 11:00 – 16:00
Место
Москва Онлайн