Главная
Блог разработчиков phpBB
 
+ 17 предустановленных модов
+ SEO-оптимизация форума
+ авторизация через соц. сети
+ защита от спама

Spring изнутри

Anna | 1.06.2014 | нет комментариев
Доброго времени суток уважаемые програвчане. Теснее 3 года я тружусь на плане в котором мы используем Spring. Мне неизменно было увлекательно разобраться с тем, как он устроен внутри. Я поискал статьи про внутреннее устройство Spring, но, к сожалению, ничего не обнаружил.

Всех, кого волнует внутреннее устройство Spring, умоляю под кат.

На схеме изображены основные этапы поднятия ApplicationContext. В этом посте мы остановимся на всяком из этих этапов. Какой-то этап будет рассмотрен детально, а какой-то будет описан в всеобщих чертах.

1. Парсирование конфигурации и создание BeanDefinition

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

  1. Xml конфигурация — ClassPathXmlApplicationContext(“context.xml”)
  2. Конфигурация через аннотации с указанием пакета для сканирования — AnnotationConfigApplicationContext(“package.name”)
  3. Конфигурация через аннотации с указанием класса (либо массива классов) помеченного аннотацией @Configuration -AnnotationConfigApplicationContext(JavaConfig.class). Данный метод конфигурации именуется — JavaConfig.
  4. Groovy конфигурация — GenericGroovyApplicationContext(“context.groovy”)

Про все четыре метода дюже отлично написано здесь.

Цель первого этапа — это создание всех BeanDefinitionBeanDefinition — это особый интерфейс, через тот, что дозволено получить доступ к метаданным грядущего бина. В зависимости от того, какая у вас конфигурация, будет применяться тот либо другой механизм парсирования конфигурации.

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

Для Xml конфигурации применяется класс — XmlBeanDefinitionReader, тот, что реализует интерфейсBeanDefinitionReader. Здесь все довольно прозрачно. XmlBeanDefinitionReader получает InputStream и загружает Document через DefaultDocumentLoader. Дальше обрабатывается всякий элемент документа и если он является бином, то создается BeanDefinition на основе заполненных данных (id, name, class, alias, init-method, destroy-method и др.). Всякий BeanDefinition помещается в Map. Map хранится в классеDefaultListableBeanFactory. В коде Map выглядит вот так.

/** Map of bean definition objects, keyed by bean name */
private final Map<String, BeanDefinition> beanDefinitionMap = new ConcurrentHashMap<String, BeanDefinition>(64);
Конфигурация через аннотации с указанием пакета для сканирования либо JavaConfig

Конфигурация через аннотации с указанием пакета для сканирования либо JavaConfig в корне отличается от конфигурации через xml. В обоих случаях применяется класс AnnotationConfigApplicationContext.

new AnnotationConfigApplicationContext(JavaConfig.class);

либо

new AnnotationConfigApplicationContext(“package.name”);

Если заглянуть во вовнутрь AnnotationConfigApplicationContext, то дозволено увидеть два поля.

private final AnnotatedBeanDefinitionReader reader;
private final ClassPathBeanDefinitionScanner scanner;

ClassPathBeanDefinitionScanner сканирует указанный пакет на присутствие классов помеченных аннотацией@Component (либо всякий иной аннотацией которая включает в себя @Component). Обнаруженные классы парсируются и для них создаются BeanDefinition.
Дабы сканирование было запущено, в конфигурации должен быть указан пакет для сканирования.

@ComponentScan({"package.name"})

либо

<context:component-scan base-package="package.name"/>

AnnotatedBeanDefinitionReader работает в несколько этапов.

  1. 1-й этап — это регистрация всех @Configuration для последующего парсирования. Если в конфигурации применяются Conditional, то будут зарегистрированы только те конфигурации, для которых Conditionвернет true. Аннотация Conditional возникла в четвертой версии спринга. Она применяется в случае, когда на момент поднятия контекста необходимо решить, создавать бин/конфигурацию либо нет. Причем решение принимает особый класс, тот, что обязан реализовать интерфейс Condition.
  2. 2-й этап — это регистрация особого BeanFactoryPostProcessor, а именноBeanDefinitionRegistryPostProcessor, тот, что при помощи класса ConfigurationClassParser парсирует JavaConfig и создает BeanDefinition.
Groovy конфигурация

Данная конфигурация дюже схожа на конфигурацию через Xml, за исключением того, что в файле не XML, а Groovy. Чтением и парсированием groovy конфигурации занимается класс GroovyBeanDefinitionReader.

2. Настройка сделанных BeanDefinition

Позже первого этапа у нас имеется Map, в котором хранятся BeanDefinition. Зодчество спринга построена таким образом, что у нас есть вероятность повлиять на то, какими будут наши бины еще до их фактического создания, напротив говоря мы имеем доступ к метаданным класса. Для этого существует особый интерфейсBeanFactoryPostProcessor, реализовав тот, что, мы получаем доступ к сделанным BeanDefinition и можем их изменять. В этом интерфейсе каждого один способ.

public interface BeanFactoryPostProcessor {
	void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException;
}

Способ postProcessBeanFactory принимает параметром ConfigurableListableBeanFactory. Данная фабрика содержит много пригодных способов, в том числе getBeanDefinitionNames, через тот, что мы можем получить все BeanDefinitionNames, а теснее потом по определенному имени получить BeanDefinition для последующей обработки метаданных.

Давайте разберем одну из родных реализаций интерфейса BeanFactoryPostProcessor. Традиционно, настройки подключения к базе данных выносятся в обособленный property файл, потом при помощиPropertySourcesPlaceholderConfigurer они загружаются и делается inject этих значений в необходимое поле. Так как inject делается по ключу, то до создания экземпляра бина необходимо заменить данный ключ на само значение из property файла. Эта замена происходит в классе, тот, что реализует интерфейсBeanFactoryPostProcessor. Наименование этого класса — PropertySourcesPlaceholderConfigurer. Каждый данный процесс дозволено увидеть на рисунке ниже.

Давайте еще раз разберем что же у нас здесь происходит. У нас имеется BeanDefinition для класса ClassName. Код класса приведен ниже.

@Component
public class ClassName {

    @Value("${host}")
    private String host;

    @Value("${user}")
    private String user;

    @Value("${password}")
    private String password;

    @Value("${port}")
    private Integer port;
} 

Если PropertySourcesPlaceholderConfigurer не обработает данный BeanDefinition, то позже создания экземпляра ClassName, в поле host проинжектится значение — “${host}” (в остальные поля проинжектятся соответсвующие значения). Если PropertySourcesPlaceholderConfigurer все таки обработает данныйBeanDefinition, то позже обработки, метаданные этого класса будут выглядеть дальнейшим образом.

@Component
public class ClassName {

    @Value("127.0.0.1")
    private String host;

    @Value("root")
    private String user;

    @Value("root")
    private String password;

    @Value("27017")
    private Integer port;
} 

Соответственно в эти поля проинжектятся положительные значения.

Для того что бы PropertySourcesPlaceholderConfigurer был добавлен в цикл настройки сделанных BeanDefinition, необходимо сделать одно из следующих действий.

Для XML конфигурации.


<context:property-placeholder location="property.properties" />

Для JavaConfig.

@Configuration
@PropertySource("classpath:property.properties")
public class DevConfig {
	@Bean
	public static PropertySourcesPlaceholderConfigurer configurer() {
	    return new PropertySourcesPlaceholderConfigurer();
	}
}

PropertySourcesPlaceholderConfigurer непременно должен быть объявлен как static. Без static у вас все будет трудиться до тех пор, пока вы не испробуете применять @ Value внутри класса @Configuration.

3. Создание кастомных FactoryBean

FactoryBean — это generic интерфейс, которому дозволено делегировать процесс создания бинов типа . В те времена, когда конфигурация была экстраординарно в xml, разработчикам был нужен механизм с поддержкой которого они бы могли руководить процессом создания бинов. Именно для этого и был сделан данный интерфейс. Для того что бы отличнее осознать задачу, приведу пример xml конфигурации.


<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:context="http://www.springframework.org/schema/context"
       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd">

    <bean id="redColor" scope="prototype">
        <constructor-arg name="r" value="255" />
        <constructor-arg name="g" value="0" />
        <constructor-arg name="b" value="0" />
    </bean>

</beans>

На 1-й взор, здесь все типично и нет никаких задач. А что делать если необходим иной цвет? Сделать еще один бин? Не вопрос.


<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:context="http://www.springframework.org/schema/context"
       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd">

    <bean id="redColor" scope="prototype">
        <constructor-arg name="r" value="255" />
        <constructor-arg name="g" value="0" />
        <constructor-arg name="b" value="0" />
    </bean>

    <bean id="green" scope="prototype">
        <constructor-arg name="r" value="0" />
        <constructor-arg name="g" value="255" />
        <constructor-arg name="b" value="0" />
    </bean>

</beans>

А что делать если я хочу всякий раз беспричинный цвет? Вот здесь то и приходит на поддержка интерфейсFactoryBean.

Сделаем фабрику которая будет отвечать за создание всех бинов типа — Color.

package com.malahov.factorybean;

import org.springframework.beans.factory.FactoryBean;
import org.springframework.stereotype.Component;

import java.awt.*;
import java.util.Random;

/**
 * User: malahov
 * Date: 18.04.14
 * Time: 15:59
 */
public class ColorFactory implements FactoryBean<Color> {
    @Override
    public Color getObject() throws Exception {
        Random random = new Random();
        Color color = new Color(random.nextInt(255), random.nextInt(255), random.nextInt(255));
        return color;
    }

    @Override 
    public Class<?> getObjectType() {
        return Color.class;
    }

    @Override
    public boolean isSingleton() {
        return false;
    }
}

Добавим ее в xml и удалим объявленные до этого бины типа — Color.


<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:context="http://www.springframework.org/schema/context"
       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd">

	<bean id="colorFactory"></bean>

</beans>

Сейчас создание бина типа Color.class будет делегироваться ColorFactory, у которого при всяком создании нового бина будет вызываться способ getObject.

Для тех кто пользуется JavaConfig, данный интерфейс будет безусловно непотребен.

4. Создание экземпляров бинов

Созданием экземпляров бинов занимается BeanFactory при этом, если необходимо, делегирует это кастомнымFactoryBean. Экземпляры бинов создаются на основе ранее сделанных BeanDefinition.

5. Настройка сделанных бинов

Интерфейс BeanPostProcessor разрешает вклиниться в процесс настройки ваших бинов до того, как они попадут в контейнер. Интерфейс несет в себе несколько способов.

public interface BeanPostProcessor {
	Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException;
	Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException;
}

Оба способа вызываются для всякого бина. У обоих способов параметры безусловно идентичные. Разница только в порядке их вызова. 1-й вызывается до init-способа, воторой, позже. Значимо понимать, что на данном этапе экземпляр бина теснее сделан и идет его донастройка. Здесь есть два значимых момента:

  1. Оба способа в результате обязаны воротить бин. Если в способе вы вернете null, то при приобретении этого бина из контекста вы получите null, а от того что через бинпостпроцессор проходят все бины, позже поднятия контекста, при запросе всякого бина вы будете получать фиг, в смысле null.
  2. Если вы хотите сделать прокси над вашим объектом, то имейте ввиду, что это принято делать позже вызова init способа, напротив говоря это необходимо делать в способе postProcessAfterInitialization.

Процесс донастройки показан на рисунке ниже. Порядок в котором будут вызваны BeanPostProcessor не знаменит, но мы верно знаем что исполнены они будут ступенчато.

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

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

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

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
public @interface InjectRandomInt {
    int min() default 0;
    int max() default 10;
}

По умолчанию, диапазон случайных числе будет от 0 до 10.

После этого, необходимо сделать обработчик этой аннотации, а именно реализацию BeanPostProcessor для обработки аннотации InjectRandomInt.

@Component
public class InjectRandomIntBeanPostProcessor implements BeanPostProcessor {

    private static final Logger LOGGER = LoggerFactory.getLogger(InjectRandomIntBeanPostProcessor.class);

    @Override
    public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {

        LOGGER.info("postProcessBeforeInitialization::beanName = {}, beanClass = {}", beanName, bean.getClass().getSimpleName());

        Field[] fields = bean.getClass().getDeclaredFields();

        for (Field field : fields) {
            if (field.isAnnotationPresent(InjectRandomInt.class)) {
                field.setAccessible(true);
                InjectRandomInt annotation = field.getAnnotation(InjectRandomInt.class);
                ReflectionUtils.setField(field, bean, getRandomIntInRange(annotation.min(), annotation.max()));
            }
        }

        return bean;
    }

    @Override
    public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
        return bean;
    }

    private int getRandomIntInRange(int min, int max) {
        return min   (int)(Math.random() * ((max - min)   1));
    }
}

Код данного BeanPostProcessor довольно прозрачен, следственно мы не будем на нем останавливаться, но здесь есть один значимый момент.

BeanPostProcessor непременно должен быть бином, следственно мы его либо помечаем аннотацией@Component, либо регестрируем его в xml конфигурации как обыкновенный бин.

Первая группа разработчиков свою задачу исполнила. Сейчас вторая группа может применять эти наработки.

@Component
@Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE)
public class MyBean {

    @InjectRandomInt
    private int value1;

    @InjectRandomInt(min = 100, max = 200)
    private int value2;

    private int value3;

    @Override
    public String toString() {
        return "MyBean{"  
                "value1="   value1  
                ", value2="   value2  
                ", value3="   value3  
                '}';
    }
}

В результате, все бины типа MyBean, получаемые из контекста, будут создаваться с теснее проинициализированными полями value1 и value2. Также здесь стоить подметить, этап на котором будет протекать инжект значений в эти поля будет зависеть от того какой @ Scope у вашего бина. SCOPE_SINGLETON— инициализация произойдет один раз на этапе поднятия контекста. SCOPE_PROTOTYPE — инициализация будет выполняться всякий раз по запросу. Причем во втором случае ваш бин будет проходить через все BeanPostProcessor-ы что может гораздо стукнуть по продуктивности.

Полный код программы вы можете обнаружить здесь.

Хочу сказать отдельное спасибо EvgenyBorisov. Вследствие его курсу, я решился на написание этого поста.

Также советую посмотреть его отчет с JPoint 2014.

Источник: programmingmaster.ru

Оставить комментарий
Форум phpBB, русская поддержка форума phpBB
Рейтинг@Mail.ru 2008 - 2017 © BB3x.ru - русская поддержка форума phpBB