
Good day, dear habravchane. For 3 years I have been working on a project in which we use Spring. It has always been interesting to me to deal with how it is arranged inside. I looked for articles about Spring's internal device, but, unfortunately, I did not find anything.
Anyone who is interested in the internal structure of Spring, please under the cat.
The diagram shows the main steps in raising the ApplicationContext. In this post we will focus on each of these stages. Some stage will be considered in detail, and some will be described in general terms.
')

1. Parsing the configuration and creating a BeanDefinition
After the release of the fourth version of the spring, we have four ways to configure the context:
- Xml configuration - ClassPathXmlApplicationContext (“context.xml”)
- Configuration via annotations with package for scanning - AnnotationConfigApplicationContext (“package.name”)
- Configuration via annotations indicating the class (or array of classes) annotated with @Configuration -AnnotationConfigApplicationContext (JavaConfig.class). This configuration method is called JavaConfig.
- Groovy configuration - GenericGroovyApplicationContext (“context.groovy”)
About all four ways very well written
here .
The purpose of the first stage is to create all
BeanDefinition .
BeanDefinition is a special interface through which you can access the metadata of a future bean. Depending on your configuration, one or another configuration parsing mechanism will be used.
Xml configuration
For the Xml configuration, the class is used -
XmlBeanDefinitionReader , which implements the
BeanDefinitionReader interface. It's all quite transparent.
The XmlBeanDefinitionReader receives an
InputStream and loads a
Document via
DefaultDocumentLoader . Then each element of the document is processed and if it is a bin, then a
BeanDefinition is created on the basis of the completed data (id, name, class, alias, init-method, destroy-method, etc.). Each
BeanDefinition is placed in a Map. The map is stored in the
DefaultListableBeanFactory class. In code Map looks like this.
private final Map<String, BeanDefinition> beanDefinitionMap = new ConcurrentHashMap<String, BeanDefinition>(64);
Configuration via annotations with package for scanning or JavaConfig
Configuration via annotations specifying a package for scanning or JavaConfig is completely different from configuration via xml. In both cases, the
AnnotationConfigApplicationContext class is used.
new AnnotationConfigApplicationContext(JavaConfig.class);
or
new AnnotationConfigApplicationContext(“package.name”);
If you look inside the AnnotationConfigApplicationContext, you can see two fields.
private final AnnotatedBeanDefinitionReader reader; private final ClassPathBeanDefinitionScanner scanner;
ClassPathBeanDefinitionScanner scans the specified package for the classes marked with the annotation
@Component (or any other annotation that includes
@Component ). Found classes are parsed and BeanDefinition are created for them.
In order for the scan to be started, the package for scanning must be specified in the configuration.
@ComponentScan({"package.name"})
or
<context:component-scan base-package="package.name"/>
AnnotatedBeanDefinitionReader works in several stages.
- The first step is to register all @Configurations for further parsing. If the configuration uses Conditional , then only those configurations will be registered for which Condition returns true. Conditional abstract appeared in the fourth version of the spring. It is used when, at the time of raising the context, you need to decide whether to create a bin / configuration or not. Moreover, the decision is made by a special class, which must implement the Condition interface.
- The second step is to register a special BeanFactoryPostProcessor , namely, BeanDefinitionRegistryPostProcessor , which uses the ConfigurationClassParser class to parse the JavaConfig and create the BeanDefinition .
Groovy configuration
This configuration is very similar to the configuration via Xml, except that Groovy is not XML in the file. The GroovyBeanDefinitionReader class deals with reading and parsing the groovy configuration.
2. Setup created by BeanDefinition
After the first stage, we have a Map in which
BeanDefinition is stored. The architecture of the spring is built in such a way that we have an opportunity to influence what our bins will be before they are actually created, in other words, we have access to the class metadata. To do this, there is a special interface
BeanFactoryPostProcessor , having implemented that, we get access to the created
BeanDefinition and can change them. There is only one method in this interface.
public interface BeanFactoryPostProcessor { void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException; }
The
postProcessBeanFactory method accepts a
ConfigurableListableBeanFactory parameter. This factory contains many useful methods, including
getBeanDefinitionNames , through which we can get all the BeanDefinitionNames, and only then with a specific name to get
BeanDefinition for further metadata processing.
Let's analyze one of the native implementations of the
BeanFactoryPostProcessor interface. Usually, the database connection settings are transferred to a separate property file, then with the help of
PropertySourcesPlaceholderConfigurer they are loaded and the inject of these values is made in the desired field. Since the inject is done by key, before creating an instance of a bin, it is necessary to replace this key with the actual value from the property file. This replacement occurs in a class that implements the
BeanFactoryPostProcessor interface. The name of this class is
PropertySourcesPlaceholderConfigurer . This whole process can be seen in the figure below.

Let's look at what is happening here. We have a
BeanDefinition for the class ClassName. The class code is shown below.
@Component public class ClassName { @Value("${host}") private String host; @Value("${user}") private String user; @Value("${password}") private String password; @Value("${port}") private Integer port; }
If
PropertySourcesPlaceholderConfigurer does not process this
BeanDefinition , then after creating a ClassName instance, the value of $ {host} will be injected into the host field (the corresponding values will be injected into the other fields). If
PropertySourcesPlaceholderConfigurer still processes this
BeanDefinition , then after processing, the metadata of this class will look like this.
@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; }
Accordingly, the correct values will be injected into these fields.
In order for the
PropertySourcesPlaceholderConfigurer to be added to the configuration loop created by
BeanDefinition , you need to do one of the following.
For XML configuration.
<context:property-placeholder location="property.properties" />
For JavaConfig.
@Configuration @PropertySource("classpath:property.properties") public class DevConfig { @Bean public static PropertySourcesPlaceholderConfigurer configurer() { return new PropertySourcesPlaceholderConfigurer(); } }
PropertySourcesPlaceholderConfigurer must be declared as static. Without static, everything will work for you until you try to use
@ Value inside the
@Configuration class.
3. Create custom FactoryBean
FactoryBean is a generic interface to which you can delegate the process of creating type beans. In those days, when the configuration was exclusively in xml, developers needed a mechanism by which they could manage the process of creating beans. This is what this interface was made for. In order to better understand the problem, I will give an example of the xml configuration.
<?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" class="java.awt.Color"> <constructor-arg name="r" value="255" /> <constructor-arg name="g" value="0" /> <constructor-arg name="b" value="0" /> </bean> </beans>
At first glance, everything is fine and there are no problems. And what if you need a different color? Create another bin? No problem.
<?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" class="java.awt.Color"> <constructor-arg name="r" value="255" /> <constructor-arg name="g" value="0" /> <constructor-arg name="b" value="0" /> </bean> <bean id="green" scope="prototype" class="java.awt.Color"> <constructor-arg name="r" value="0" /> <constructor-arg name="g" value="255" /> <constructor-arg name="b" value="0" /> </bean> </beans>
And what if I want a random color every time? This is where the
FactoryBean interface comes to the rescue.
Create a factory that will be responsible for creating all the beans of the type -
Color .
package com.malahov.factorybean; import org.springframework.beans.factory.FactoryBean; import org.springframework.stereotype.Component; import java.awt.*; import java.util.Random; 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; } }
Add it to xml and remove previously declared bins of the type -
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" class="com.malahov.temp.ColorFactory"></bean> </beans>
Now the creation of a bean of the
Color.class type will be delegated to ColorFactory, in which the
getObject method will be called each time a new bean is
created .
For those who use JavaConfig, this interface will be absolutely useless.
4. Creating Bin instances
Creating Bean instances is handled by the
BeanFactory while, if necessary, delegating this to the custom
FactoryBean . Bean instances are created based on previously created
BeanDefinition .

5. Customize created bins
The
BeanPostProcessor interface allows
you to hook into the process of setting up your beans before they get into the container. The interface carries several methods.
public interface BeanPostProcessor { Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException; Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException; }
Both methods are called for each bean. Both methods have exactly the same parameters. The difference is only in the order of their call. The first is called before the init method, the second, after. It is important to understand that at this stage an instance of the bean has already been created and it is undergoing customization. There are two important points:
- Both methods should eventually return the bin. If you return null in the method, then when you receive this bean from the context, you will get null, and since all the bins pass through the binpost processor, after raising the context, when you query any bean, you will receive a fig, in the sense of null.
- If you want to make a proxy over your object, then keep in mind that this is done after calling the init method, in other words, this should be done in the postProcessAfterInitialization method.
The process of adjusting is shown in the figure below. The order in which the
BeanPostProcessor will be called is not known, but we know for sure that they will be executed sequentially.

In order to better understand what this is for, let's look at some example.
When developing large projects, as a rule, the team is divided into several groups. For example, the first group of developers is engaged in writing the project's infrastructure, and the second group, using the groundwork of the first group, is writing business logic. Suppose a second group needs a functional that allows them to inject some values, for example, random numbers.
At the first stage, an annotation will be created, which will mark the class fields into which the value should be inserted.
@Retention(RetentionPolicy.RUNTIME) @Target(ElementType.FIELD) public @interface InjectRandomInt { int min() default 0; int max() default 10; }
By default, the random number range will be from 0 to 10.
Then, you need to create a handler for this annotation, namely the
BeanPostProcessor implementation to handle the
InjectRandomInt annotation.
@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)); } }
The code of this
BeanPostProcessor is fairly transparent, so we will not dwell on it, but there is one important point.
BeanPostProcessor must be a bin, so we either mark it with the
@Component annotation, or register it in the xml configuration as a regular bin.
The first group of developers completed their task. Now the second group can use these developments.
@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 + '}'; } }
As a result, all
bans of the MyBean type, obtained from the context, will be created with the already initialized fields value1 and value2. It is also worth noting that the stage at which the injection of values into these fields will occur will depend on what
@ Scope of your bean.
SCOPE_SINGLETON - initialization will occur once at the stage of raising the context.
SCOPE_PROTOTYPE - initialization will be performed each time on request. And in the second case, your bin will pass through all BeanPostProcessors, which can significantly hit performance.
The full program code can be found
here .
I want to say a special thanks to
EvgenyBorisov . Thanks to his
course , I decided to write this post.
I also advise you to watch his
report from JPoint 2014.