What opportunities does Spring provide for customizing its behavior?

Hello everyone. In touch Vladislav Rodin. Currently, I am the head of the High Load Architect course at OTUS, and I also teach courses on software architecture.

In addition to teaching, I am also writing copyright material for the OTUS blog on the Haber and I want to coincide with today's article to launch the course "Developer on the Spring Framework" , which is now open for recruitment.




Introduction


From the readerโ€™s point of view, the application code that uses Spring looks quite simple: some beans are declared, classes are marked with annotations, and then the beans are injected where necessary, everything works fine. But an inquisitive reader has a question: โ€œHow does it work? What's happening?". In this article we will try to answer this question, but not for the sake of satisfying idle curiosity.

Spring framework is known for being flexible enough and provides options for customizing the behavior of the framework. Spring also abounds with a number of rather interesting rules for applying certain annotations (for example, Transactional). In order to understand the meaning of these rules, to be able to derive them, and also to understand what and how to configure in Spring, you need to understand several principles of what is in Spring under the hood. As you know, the knowledge of several principles exempts from the knowledge of many facts. I suggest that you familiarize yourself with these principles below if, of course, you do not already know them.

Reading configurations


At the very beginning, you need to parse the configurations that are in your application. Since there are several types of configurations (xml-, groovy-, java-configurations, configuration based on annotations), different methods are used to read them. One way or another, a map of the form Map <String, BeanDefinition> is collected, in which the names of beans are assigned to their bean definitions. The objects of the BeanDefinition class are meta-information on beans and contain the bean's id-id, its name, its class, destroy- and init-methods.

Examples of classes involved in this process: GroovyBeanDefinitionReader, XmlBeanDefinitionReader, AnnotatedBeanDefinitionReader, implementing the BeanDefinitionReader interface .

Setting up Beandefinitions


So, we have descriptions of beans, but there are no beans themselves, they have not been created yet. Before creating beans, Spring provides the ability to customize the resulting BeanDefinitions. For these purposes, the BeanFactoryPostProcessor interface is used . It looks like this:

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

The method parameter of this interface allows using its own getBeanDefinitionNames method to get the names by which you can get BeanDefinitions from the map and edit them.

Why might this be needed? Suppose that some beans require details to connect to an external system, such as a database. We want the beans to be created already with the details, but the details themselves are stored in the property-file. We can apply one of the standard BeanFactoryPostProcessors - PropertySourcesPlaceholderConfigurer, which will replace the name property in the BeanDefinition with the actual value stored in the property file. That is, it will actually replace Value ("user") with Value ("root") in BeanDefinion. For this to work, the PropertySourcesPlaceholderConfigurer, of course, needs to be connected. But this is not limited to this; you can register your BeanFactoryPostProcessor, in which you can implement any logic you need to process BeanDefinitions.

Creating Beans


At this stage, we have a map that has the names of beans located by the keys, and the configured BeanDefinitions by the values. Now you need to create these beans. This is what BeanFactory does . But here you can add customization by writing and registering your FactoryBean . FactoryBean is an interface of the form:

public interface FactoryBean {
    T getObject() throws Exception;
    Class<?> getObjectType();
    boolean isSingleton();
}

Thus, a BeanFactory creates a bean itself if there is no FactoryBean corresponding to the bean class, or asks the FactoryBean to create this bean. There is a small nuance: if the scope of the bean is singletone, then the bean is created at this stage, if prototype, then every time this bean is needed, it will be requested from the BeanFactory.

As a result, we again get a map, but itโ€™s already a little different: the keys contain the names of the beans, and the values โ€‹โ€‹of the beans themselves. But this is true only for singletones.

Configuring Beans


Now comes the most interesting stage. We have a map containing the created beans, but these beans have not yet been configured. That is, we did not process annotations that set the state of the bean: Autowired, Value. We also did not process annotations that change the behavior of the bean: Transactional, Async. To solve this problem, BeanPostProcessor's allow , which again are implementations of the corresponding interface:

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

We see 2 methods with scary but comprehensive names. Both methods take a bean as input, from which you can ask which class it is, and then use the Reflection API to process annotations. Return bean methods, possibly replaced by proxy.

For each bean before placing it in the context, the following happens: the postProcessBeforeInitialization methods are triggered for all BeanPostProcessors, then the init method is triggered, and then the postProcessAfterInitialization methods are triggered for all BeanPostProcessors too.

These two methods have different semantics: postProcessBeforeInitialization processes state annotations, postProcessAfterInitialization processes behavior, because proxying is used to process the behavior, which can lead to loss of annotations. That is why behavior changes in the last place.

Where is customization? We can write our annotation, BeanPostProcessor for it, and Spring will process it. However, for BeanPostProcessor to work, it also needs to be registered as a bean.

For example, to embed a random number in a field, we create an InjectRandomInt annotation (hung on the fields), create and register an InjectRandomIntBeanPostProcessor, in the first method of which we process the created annotation, and in the second method, we simply return the incoming bean.

To profile the beans, create a Profile annotation that broadcasts to methods, create and register a ProfileBeanPostProcessor, in the first method of which we return the incoming bean, and in the second method, we return a proxy that wraps the call to the original method with clipping and execution time logging.



Learn more about the course



All Articles