What features does Spring provide for customizing it?

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 Habré and I want to coincide with today’s article to launch the course “Developer on the Spring Framework”which the set is open right now.


Introduction

From the reader’s point of view, the application code using 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.

The Spring framework is known for being flexible enough to provide options for customizing its behavior. 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 you, of course, do not yet 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 type map is going, in which the names of beans are mapped 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 that implement the interface BeanDefinitionReader.

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. An interface is used for these purposes. BeanFactoryPostProcessor. 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 any 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 to 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. Deals with this Beanfactory. 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. This problem can be solved Beanpostprocessor‘s, 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 as well.

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. True, for the BeanPostProcessor to work, it also needs to be registered as a bean.

For example, to embed a random number in the field, we write 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


Similar Posts

Leave a Reply

Your email address will not be published. Required fields are marked *