Why do you need Spring? What is Spring?
For such a problem, most people are in a hazy state, said, but not completely said, today we will try to unravel the mystery of Spring from the perspective of architectural design.
This post is presented in a step-by-step manner, so there’s no need to panic, I can assure you that you can read it if you know how to program.
This post is based on Spring 5.2.8 and will take about 20 minutes to read!Let’s take a look at a case study: there’s a guy who owns a Jeep, and he usually drives his Jeep to work.
Code Implementation:
public class GeelyCar {
public void run(){
System.out.println("geely running");
}
}
public class Boy {
private final GeelyCar geelyCar = new GeelyCar();
public void drive(){
geelyCar.run();
}
}
One day, the guy makes money and buys another red flag and wants to drive a new car.
Simple, replace the dependencies with HongQiCar
Code Implementation:
public class HongQiCar {
public void run(){
System.out.println("hongqi running");
}
}
public class Boy {
private final HongQiCar hongQiCar = new HongQiCar();
public void drive(){
hongQiCar.run();
}
}
When you get tired of driving your new car and want to switch back to your old one, there’s a problem: this code keeps changing back and forth
Clearly, this case violates our Dependency Inversion Principle (DIP): programs should not depend on implementations, but on abstractions
Now we optimize the code as follows:
Boy
Dependent on the Car
interface, whereas the previous GeelyCar
and HongQiCar
are Car
interface implementations.
Code Implementation:
Define the Car interface
public interface Car {
void run();
}
Replace the previous GeelyCar
and HongQiCar
with the implementation class of Car
.
public class GeelyCar implements Car {
@Override
public void run(){
System.out.println("geely running");
}
}
HongQiCar same
Person relies on the Car
interface at this point
public class Boy {
private final Car car;
public Person(Car car){
this.car = car;
}
public void drive(){
car.run();
}
}
At this point the guy can just pass in whatever parameters he wants to change the car to drive and the code no longer changes.
The above case remodeling does seem to have no more problems, but there are still some limitations, if new scenes are added at this time:
One day a guy can’t drive because he’s been drinking and needs a chauffeur. The chauffeur doesn’t care which guy he’s driving for or what kind of car he’s driving, so the guy suddenly becomes an abstraction, at which point the code has to be changed again, and the code for the chauffeur’s dependence on the guy might look something like this:
private final Boy boy = new YoungBoy(new HongQiCar());
As the complexity of the system increases, such problems become more and more difficult to maintain, so what should we do to solve this problem?
First of all, we can be sure: there is no problem with using the principle of dependency inversion, which solves our problem to some extent.
Where we feel things go wrong is in the process of passing in parameters: we pass in whatever the program needs, and as soon as there are multiple dependent class relationships in the system, this passing in of parameters becomes extremely complicated.
Perhaps we can reverse the thinking: the program uses whatever we have!
When we implement only HongQiCar
and YoungBoy
, the valet is using YoungBoy
with HongQiCar
on!
When we implement only GeelyCar
and OldBoy
, the valet naturally changes to OldBoy
with GeelyCar
on!
And how to reverse it is one of the big challenges that Spring solves.
Introduction to Spring
Spring is a one-stop lightweight heavyweight development framework , the purpose is to address the complexity of enterprise-level application development , it provides comprehensive infrastructure support for the development of Java applications , so that Java developers no longer need to care about the dependencies between classes and classes , you can focus on the development of applications (crud).
Spring for enterprise-level development provides to a rich set of features , and the underlying functionality of these features are dependent on its two core features : dependency injection (DI) and cutter-oriented programming (AOP).
Core Concepts of Spring
IoC container
The full name of IoC is Inversion of Control
, meaning Inversion of Control, and IoC is also known as Dependency Injection (DI), which is the process of injecting an object through its dependencies: objects define their dependencies (i.e., the other objects with which they are combined) only through constructors, factory methods, or attributes that are set on the object by its instantiation, and the container then injects those needed dependencies when the bean is created. This process is fundamentally the inverse of the bean itself controlling the instantiation or location of its dependencies through the use of direct construction of classes or mechanisms such as the Service Location Pattern (hence the term control inversion).
The principle of dependency inversion is the design principle of IoC, and dependency injection is the implementation of IoC.In Spring, we can use XML, Java annotations or Java code to write the configuration information, and through the configuration information, access to instructions on instantiating, configuring and assembling objects to instantiate, configure and assemble the application object is called the container.
Typically, we only need to add a few annotations so that after the container is created and initialized, we have a configurable, executable system or application.
Bean
In Spring, the objects instantiated -> assembled and managed by the Spring IOC container -> forming the skeleton of the program are called beans. a bean is one of the many objects in an application.
The above three points in series is: Spring is an internal IoC container to place beans, through dependency injection to deal with the dependencies between beans.
AOP
Cutting-oriented programming (Aspect-oriented Programming), is relative to object-oriented programming (OOP), a functional complement , OOP is oriented to the main object is the class , and AOP is the cut. AOP is an important component of the Spring Framework , although the IOC container does not rely on AOP, but AOP provides a very powerful feature , used to supplement the IOC.
AOP allows us to make enhancements to our business functionality without modifying our original code: cutting a piece of functionality into a location we specify, such as printing logs between chains of method calls.
Advantages of Spring
1, Spring through DI, AOP to simplify enterprise Java development
2, Spring’s low-invasive design, so that the code is extremely low pollution
3, Spring’s IoC container reduces the complexity of business objects, so that components are decoupled from each other
4, Spring’s AOP support allows some common tasks such as security, transactions, logging, etc. for centralized processing, thus improving the reusability of better
5, Spring’s high degree of openness, and does not force the application is completely dependent on Spring, developers are free to use the Spring framework part or all of the
6, Spring’s highly scalable, so that developers can easily make their own framework for integration on Spring
7, Spring’s ecology is extremely complete, integrated with a variety of excellent frameworks, so that developers can easily use them
We can do without Java, but not without Spring~!
Transforming Cases with Spring
Now that we’ve been introduced to what Spring is, let’s try to revamp the case a bit using Spring
The original structure remains unchanged, just add the @Component
annotation to GeelyCar
or HongQiCar
, and the @Autowired
annotation to Boy
when it is used.
Code Style:
@Component
public class GeelyCar implements Car {
@Override
public void run() {
System.out.println("geely car running");
}
}
HongQiCar same
In Spring, when a class is marked with the @Component annotation it indicates that it is a bean and can be managed by the IoC container
@Component
public class Boy {
@Autowired
private Car car;
public void driver(){
car.run();
}
}
What we said before: the program uses whatever we implement, which in this case is the same as marking theComponent
annotation on whichever class is a bean, which Spring will use to inject into theCar
attribute of theBoy
class.
So when we labelGeelyCar
with the annotationComponent
, the car driven byBoy
isGeelyCar
and when we labelHongQiCar
with the annotationComponent
, the car driven byBoy
isHongQiCar
Of course, we can’t mark theComponent
annotation on bothGeelyCar
andHongQiCar
, because then Spring won’t know whichCar
to use for injection – Spring has a problem with choosing (or can’t one boy drive two cars?).
Starting the program with Spring
Here's where we can unpack what we've just learned from introducing Spring
Passing theMain
class with theComponentScan
annotation (configuration information) toAnnotationConfigApplicationContext
(IoC container) for initialization is the same as: the IoC container instantiates, manages, and assembles beans by obtaining configuration information.
Instead, how dependency injection is performed is done inside the IoC container, which is what this article will focus on
We have a complete understanding of the basic functions of Spring through a transformation of the case, but also on the previous concept of a figurative experience, and we do not know how Spring’s dependency injection of this internal action is accomplished, the so-called know more to know the reason why, combined with our existing knowledge, as well as the understanding of Spring, boldly conjecture speculate about it (this is a very important) ability oh)
Actually, guessing means: if we were to realize it ourselves, how would we realize the process?
First, let’s be clear about what we need to do: scan the classes under the specified package, instantiate them, and combine them according to the dependencies.
Step Breakdown:
Scan for classes under a given package -> if the class identifies a Component annotation (it’s a bean) -> store information about the class
Instantiate -> Iterate over stored classes -> Instantiate these classes via reflection
Compose based on dependencies -> Parse class information -> Determine if there are fields in the class that require dependency injection -> Inject the fields
Program realization
We now have a solution that looks like that, and will now try to make that happen
Defining Annotations
First we need to define the annotations we need to use: ComponentScan
, Component
, Autowired
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface ComponentScan {
String basePackages() default "";
}
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface Component {
String value() default "";
}
@Target(ElementType.FIELD)@Retention(RetentionPolicy.RUNTIME)public @interface Autowired {}
Scans the classes under the specified package
Scanning all classes under a given package may sound like a momentary puzzlement, but in fact it amounts to another question: how do you traverse a file directory?
So what should be used to store class information? Let’s take a look at the getBean
method in the above example, is it like getting the value from the key in a Map? There are many other implementations of Maps, but there is only one thread-safe one, and that is ConcurrentHashMap
(don’t even get me started on HashTable).
Define a map to hold class information
private final Map<String, Class<?>> classMap = new ConcurrentHashMap<>(16);
For the specific process, the code implementation is similarly attached below:
The code implementation can be viewed in conjunction with the flowchart:
Scan class information
private void scan(Class<?> configClass) {
String basePackages = this.getBasePackages(configClass);
this.doScan(basePackages);
}
private String getBasePackages(Class<?> configClass) {
ComponentScan componentScan = configClass.getAnnotation(ComponentScan.class);
return componentScan.basePackages();
}
private void doScan(String basePackages) {
URI resource = this.getResource(basePackages);
File dir = new File(resource.getPath());
for (File file : dir.listFiles()) {
if (file.isDirectory()) {
doScan(basePackages + "." + file.getName());
}
else {
// com.my.spring.example + . + Boy.class -> com.my.spring.example.Boy
String className = basePackages + "." + file.getName().replace(".class", "");
this.registerClass(className);
}
}
}
private void registerClass(String className){
try {
Class<?> clazz = classLoader.loadClass(className);
if(clazz.isAnnotationPresent(Component.class)){
String beanName = this.generateBeanName(clazz);
classMap.put(beanName, clazz);
}
} catch (ClassNotFoundException ignore) {}
}
Now that all the appropriate classes have been parsed, the next step is the instantiation process
Define the Map that holds the bean
private final Map<String, Object> beanMap = new ConcurrentHashMap<>(16);
The specific process, the code implementation of the same is given below:
The code implementation can be viewed in conjunction with the flowchart:
Iterate over classMap
to instantiate the bean
public void instantiateBean() {
for (String beanName : classMap.keySet()) {
getBean(beanName);
}
}
public Object getBean(String beanName){
Object bean = beanMap.get(beanName);
if(bean != null){
return bean;
}
return this.createBean(beanName);
}
private Object createBean(String beanName){
Class<?> clazz = classMap.get(beanName);
try {
Object bean = this.doCreateBean(clazz);
beanMap.put(beanName, bean);
return bean;
} catch (IllegalAccessException e) {
throw new RuntimeException(e);
}
}
private Object doCreateBean(Class<?> clazz) throws IllegalAccessException {
Object bean = this.newInstance(clazz);
this.populateBean(bean, clazz);
return bean;
}
private Object newInstance(Class<?> clazz){ try { return clazz.getDeclaredConstructor().newInstance(); } catch (InstantiationException | IllegalAccessException | InvocationTargetException | NoSuchMethodException e) { throw new RuntimeException(e); }}
private void populateBean(Object bean, Class<?> clazz) throws IllegalAccessException { final Field[] fields = clazz.getDeclaredFields(); for (Field field : fields) { Autowired autowired = field.getAnnotation(Autowired.class); if(autowired != null){ // bean Object value = this.resolveBean(field.getType()); field.setAccessible(true); field.set(bean, value); } }}
private Object resolveBean(Class<?> clazz){
if(clazz.isInterface()){
for (Map.Entry<String, Class<?>> entry : classMap.entrySet()) {
if (clazz.isAssignableFrom(entry.getValue())) {
return getBean(entry.getValue());
}
}
throw new RuntimeException("bean");
}else {
return getBean(clazz);
}
}
public Object getBean(Class<?> clazz){
String beanName = this.generateBeanName(clazz);
return this.getBean(beanName);
}
The two core methods have been written, next to put them together, I implemented them in a custom ApplicationContext
class with the following constructor:
public ApplicationContext(Class<?> configClass) {
this.scan(configClass);
this.instantiateBean();
}
UML class diagrams:
The code structure is the same as the case, and here we show whether our own Spring can work properly
It’s working fine. Chinese people don’t lie to Chinese people.
The source code will be given at the end of the article
Now, we have implemented it according to the envisioned scenario, and it runs as expected. However, if we take a closer look and combine it with our usual scenarios of using Spring, we will find quite a few problems with this piece of code:
1, can not support constructor injection , of course, did not support method injection , which is a functionality of the lack of .
2, the problem of loading class information, load the class we use classLoader.loadClass
way, although this avoids the initialization of the class (can never use Class.forName
way), but still inevitably load the class meta-information into the meta-space, when we scan the package under the unwanted class, which wastes our memory.
3, can not solve the cycle of dependency between beans, such as an A object depends on the B object, the B object depends on the A object, this time we come back to look at the code logic, you will find that at this time will fall into a dead loop.
4, poor scalability, we write all the features in a class, when you want to improve the function (such as the above three issues), you need to frequently modify the class, the class will become more and more bloated, not to mention iterating on new features, maintenance will be a headache.
Optimization solutions
For the first three questions are similar to the functionality of the problem, functionality, change a change is good.
We need to focus on the fourth issue, a framework to become good, then its iterative ability must be good, so that the function can become rich, and the iterative ability of the influence of a number of factors, one of which is its scalability.
So how should we improve the scalability of our programs? The six design principles give us good guidance.
In the program, ApplicationContext
does a lot of things, which can be divided into two main blocks
1. Scan the classes under the specified package
2、Instantiate Bean
With the help of the idea of the single duty principle: a class does only one thing, a method does only one thing.
We scan the specified package under the class of this matter to use a separate processor to deal with, because the scanning configuration is from the configuration class, then we call him the configuration class processor: ConfigurationCalssProcessor
The same goes for instantiating beans, which is divided into two things: instantiation and dependency injection.
Instantiation of beans is equivalent to a process of producing beans, we will use a factory class to deal with this matter, it is called: BeanFactory, since it is in the production of beans, then we need raw materials (Class), so we will classMap
and beanMap
are defined here
The dependency injection process is actually processing the Autowired
annotation, which is called: AutowiredAnnotationBeanProcessor.
We are still in the know, in Spring, not only only this way of using, there are xml, mvc, SpringBoot way, so we will be ApplicationContext
for abstraction, only to achieve the main process, the original annotations handed over to the way AnnotationApplicationContext
to achieve.
By means of the inversion of reliance principle: the program should rely on the abstraction
In the future, class information can come not only from class information, but also from configuration files, so we will ConfigurationCalssProcessor abstraction
Dependency injection doesn’t have to be done with the Autowried
annotation identifier, it can be done with other annotations, such as Resource
, so we’ll make the AutowiredAnnotationBeanProcessor abstraction
Bean types can also be many, can be a single instance, can make multiple instances, can also be a factory bean, so we will BeanFactory abstraction
Now, we have optimized our solution with the help of two design principles, and it is “transformed” compared to the previous one.
Spring’s design
In the previous step, we implemented our own scenario and optimized it for scalability based on some assumptions, and now, let’s get acquainted with what is actually Spring’s design
So, what are the “roles” in Spring?
1, Bean: Spring as an IoC container, of course, the most important is Bean
2, BeanFactory: production and management of bean factory
3, BeanDefinition: Bean’s definition, that is, our program in the Class, Spring encapsulated it
4, BeanDefinitionRegistry: similar to the relationship between Bean and BeanFactory, BeanDefinitionRegistry used to manage BeanDefinition
5, BeanDefinitionRegistryPostProcessor: used to resolve the configuration class processor, similar to our program in the ClassProcessor
6, BeanFactoryPostProcessor: BeanDefinitionRegistryPostProcessor parent class, so that we can then parse the configuration class after the post-processing
7, BeanPostProcessor: Bean’s post-processor, used in the production of Bean in the process of some processing, such as dependency injection, similar to our AutowiredAnnotationBeanProcessor
8, ApplicationContext: If the above roles are in the factory to produce Bean workers, then ApplicationContext is our Spring facade, ApplicationContext and BeanFactory is a combination of the relationship, so it completely extended the BeanFactory functionality, and on its basis to add more enterprise-specific features, such as the familiar ApplicationListener ( event listener )
Above said similar to actually have some of the cart before the horse, because in fact it should be the implementation of our program is similar to the implementation of Spring, so that just to make you better understand the
We actually have a very easy understanding of these roles after going through the design and optimization of our own programs
Next, we’ll take a closer look at one by one
BeanFactory
BeanFactory is a top-level interface in Spring, which defines the way to get the bean, Spring has another interface called SingletonBeanRegistry, which defines the way to operate a single instance of the bean, here I will put these two together for the introduction, because they are largely the same, the SingletonBeanRegistry notes also written on the implementation of the interface with the BeanFactory, to facilitate unified management.
BeanFactory
1, ListableBeanFactory: interface that defines the methods related to obtaining a list of beans/BeanDefinition, such as getBeansOfType(Class type)
2, AutowireCapableBeanFactory: interface, defines the bean life cycle related methods, such as creating beans, dependency injection, initialization
3, AbstractBeanFactory: Abstract class, basically implements all the methods related to the operation of the bean, the definition of the abstract methods related to the life cycle of the bean
4, AbstractAutowireCapableBeanFactory: abstract class, inherited from AbstractBeanFactory, the implementation of the content related to the life cycle of the bean, although it is an abstract class, but it does not have abstract methods
5, DefaultListableBeanFactory: Inherit and implement all the above classes and interfaces, is the lowest level of Spring’s BeanFactory, its own implementation of the ListableBeanFactory interface
6, ApplicationContext: is also an interface, we will have a special introduction to it in the following
SingletonBeanRegistry
1, DefaultSingletonBeanRegistry: defines the Bean cache pool, similar to our BeanMap, the realization of the operation of the single instance, such as getSingleton
(interview often asked about the third level of cache here)
2, FactoryBeanRegistrySupport: provides support for FactoryBean, such as from the FactoryBean to get Bean
BeanDefinition
BeanDefinition is actually an interface (I can’t imagine, right?), which defines a number of operations related to class information, so that it can be used directly in the production of beans, such as getBeanClassName
Its approximate structure is as follows (here is an example of the RootBeanDefinition subclass):
I’m sure you’re no stranger to the various attributes in there.
Again, it has many implementation classes:
1, AnnotatedGenericBeanDefinition: parse the configuration class and parse the Import annotation to bring in the class, it will be used to encapsulate the
2, ScannedGenericBeanDefinition: encapsulate the class information obtained by scanning the package through @ComponentScan
3, ConfigurationClassBeanDefinition: encapsulate the class information obtained through the @Bean annotation
4, RootBeanDefinition: ConfigurationClassBeanDefinition parent class, generally used within Spring, the other BeanDefinition into this class
BeanDefinitionRegistry
Defines operations related to BeanDefiniton, such as registerBeanDefinition
, getBeanDefinition
, in the BeanFactory, the implementation class is DefaultListableBeanFactory
BeanDefinitionRegistryPostProcessor
Interpolation: Speaking of this, have you found that Spring’s naming is extremely standardized, Spring team has said that Spring in the class name are repeated before confirming that really live up to their name ah, so look at the Spring source code is really a very comfortable thing, look at the name of the class and method name will be able to guess the function of them.
The interface defines only one function: processing BeanDefinitonRegistry, that is, parsing the annotations Import
, Component
, ComponentScan
, etc. in the configuration class for corresponding processing, and registering these classes into the corresponding BeanDefinition
Within Spring, there is only one implementation: ConfigurationClassPostProcessor
BeanFactoryPostProcessor
The so-called BeanFactory post-processor, which defines the processing logic that can be invoked after parsing the configuration class, is similar to a slot, if we want to do something after the configuration class is parsed, we can implement the interface.
Inside Spring, again, only the ConfigurationClassPostProcessor implements it: it is used to specifically process classes annotated with Configuration
.
Here string field a small problem, such as know the following code:
@Configuraiton
public class MyConfiguration{
@Bean
public Car car(){
return new Car(wheel());
}
@Bean
public Wheel wheel(){
return new Wheel();
}
}
Q: How many times is the Wheel object being NEW at Spring startup? Why?
BeanPostProcessor
Jianghu translation: Bean’s post-processor
The post-processor throughout the entire process of the life cycle of the bean, in the process of creating the bean, a total of nine times, as for which nine times we will explore next time, the following is the implementation of the class and its role
1, AutowiredAnnotationBeanPostProcessor: used to infer the constructor for instantiation, as well as processing Autowired and Value annotations
2, CommonAnnotationBeanPostProcessor: dealing with the Java specification of the annotations, such as Resource, PostConstruct
3, ApplicationListenerDetector: used after the initialization of the bean, the implementation of the ApplicationListener interface will be added to the list of event listeners bean
4, ApplicationContextAwareProcessor: used to call back the implementation of the Aware interface Bean
5, ImportAwareBeanPostProcessor: used to call back the implementation of the ImportAware interface Bean
ApplicationContext
ApplicationContext as the core of Spring, the facade mode isolates the BeanFactory, the template method mode defines the skeleton of the Spring startup process, and the strategy mode calls a variety of Processor …… is really intricate and subtle!
Its implementation class is as follows:
1, ConfigurableApplicationContext: interface, defines the configuration and life cycle related operations, such as refresh
2, AbstractApplicationContext: abstract class, the implementation of the refresh method, refresh method as the core of Spring core, it can be said that the entire Spring are in the refresh, all subclasses are through the refresh method to start after calling the method, will instantiate all the single-case
3, AnnotationConfigApplicationContext: start using the relevant annotation reader and scanner, to the Spring container to register the need to use the processor, and then in the refresh method in the mainstream process can be called
4、AnnotationConfigWebApplicationContext:loadBeanDefinitions,BeanDefintion
5、ClassPathXmlApplicationContext:
As you can see from the subclasses, the difference between the subclasses is how to load BeanDefiniton, AnnotationConfigApplicationContext is loaded through the configuration class processor (ConfigurationClassPostProcessor), while the AnnotationConfigWebApplicationContext and ClassPathXmlApplicationContext is through their own implementation of loadBeanDefinitions method, the other processes are identical
Spring’s Process
Above, we’ve got a clear picture of the main players and their roles in Spring, now let’s try to put them together and build a Spring startup process
Again, let’s take the AnnotationConfigApplicationContext as an example.
The figure only draws a part of the approximate process in Spring, we will expand the details in later chapters
The so-called difficult at the beginning of everything, the original purpose of this article is to allow you to recognize Spring in a shallow to deep way, the initial establishment of Spring’s cognitive system, understand the internal structure of Spring, Spring’s knowledge is no longer on the surface.
Now that the head has been started, I believe that the learning of the later content will be watertight.
This post is both about Spring’s architectural design and will hopefully serve as a manual that we can use later to review Spring as a whole.
Finally, after reading the article, I believe it will be easy to answer the following frequently asked questions in interviews
1、BeanDefinition?
2、BeanFactory ApplicationContext?
3、Classification and role of post-processor?
4, Spring’s main process is how?
If you don’t think you can answer it well read the article again or leave your own insights in the comments section!