preface

At the very beginning, Spring was just referring to the Spring Framework, but after so many years of development, Spring has expanded into a giant. When we talk about Spring, we refer to the Spring ecosystem, which contains the whole Spring family. Although it is huge, it is not bloated. Consumers can choose on demand within the Spring ecosystem without having to import all of its dependencies. Spring can do just that, thanks to its excellent design. This series of articles focuses on some of the best code, design, and design goals in the Spring Framework.

Spring Framework design principles

Before learning about a Framework, we should first understand its design philosophy, or design principles. The design principles of the Spring Framework are as follows:

  • Provide choice at every level. Spring lets you defer design decisions as late as possible. For example, you can switch persistence providers through configuration without changing your code. The same is true for many other infrastructure concerns and integration with third-party APIs.

Offer options at all levels. Spring allows you to defer design decisions as much as possible, such as not changing your code, rather than changing the configuration file to switch persistence. The same goes for other tripartite frameworks for basic functionality and integration.

  • Accommodate diverse perspectives. Spring embraces flexibility and is not opinionated about how things should be done. It supports a wide range of application needs with different perspectives.

Multi-angle compatibility. Spring embraces change (with flexibility) and doesn’t dictate what you should do. Supports wide compatibility with different perspectives.

  • Maintain strong backward compatibility. Spring’s evolution has been carefully managed to force few breaking changes between versions. Spring supports a carefully chosen range of JDK versions and third-party libraries to facilitate maintenance of applications and libraries that depend on Spring.

Continuous and strong backward compatibility. Spring carefully manages its evolution (upgrades) to avoid major differences between releases. Spring supports a select number of JDK versions and tripartite libraries to facilitate the maintenance of spring-dependent applications and libraries.

  • Care about API design. The Spring team puts a lot of thought and time into making APIs that are intuitive and that hold up across many versions and many years.

Focus on API design. The Spring team spent a lot of time and effort designing an API that was intuitive and could withstand multiple versions and time.

  • Set high standards for code quality. The Spring Framework puts a strong emphasis on meaningful, current, and accurate javadoc. It is one of very few projects that can claim clean code structure with no circular dependencies between packages.

Code quality is high. The Spring framework emphasizes javadoc that is meaningful, up to date, and accurate in meaning. The Spring framework can proudly say that it is one of the few projects that has a clean code structure without cyclic dependencies between packages.

In short, Spring’s design principles are five: extensibility, compatibility, maintainability, user-friendly API design, and high-quality code. The entire Spring Framework is designed and implemented based on these five principles.

IoC Container

IoC Container is the core and cornerstone of the Spring Framework. Without it, the Spring Framework will lose its skeleton and soul.

What is the core of the Spring Framework? Just as Java is an object-oriented language, the Spring Framework is a bean-oriented Framework. Almost all of its functions are achieved through Bean management, enhancement and other operations. What were the core features of the Spring Framework when it was first launched? The dependency inversion/dependency injection mechanism (also implemented in EJBs) solves the critical problem of having dependencies between objects associated through configuration files. As stated in the first design rule above, postponing your design decisions, which implementation to use, does not need to be decided at code time. How are these beans managed? IoC Containers are used to manage relationships and states between beans.

In terms of code structure, IoC Container has the following functions:

  • srping-beans
  • spring-context
  • spring-core

These three modules are the three core packages of the Spring Framework when it was originally designed

spring-beans

Beans are at the heart of the Spring Framework, and this module deals with three things: Bean definition, creation, and parsing.

BeanFactory

The Bean is created using the typical factory method pattern, and its core interface should be familiar:BeanFactory

The above is the core of its class diagram (excluding the ApplictionContext related interfaces and classes, the subsequent say), you can see the final implementation class is DefaultListableBeanFactory, why to define so many interface and abstract class? There are four layers between the implementation class and the top interface, BeanFactory. Wouldn’t it be simpler? Using the interface name and corresponding annotations, we can see that different interfaces actually correspond to different scenarios. For example, ListableBeanFactory means that the factory implementing this interface can iterate over all of its beans, HierarchicalBeanFactory representatives to realize its Factory is existence of inheritance, that may be the Parent Factory, AutowireCapableBeanFactory mean it has the function of the automatic assembly Bean. There are so many classes that collectively define the collection, relationships, and behavior of beans.

One of the six principles of design pattern is the Interface Segregation principle: to establish a single interface, a client should not rely on interfaces it does not need and dependencies between classes should be established on the smallest interface. How do you understand that? That is, when we create an interface, we should not create a large and complete interface, but according to the function it should be divided into multiple independent and simple interface, the client only needs to rely on the interface it needs.

The smaller the interface design granularity is, the more flexible the system will be, but correspondingly, the complexity of the system structure will be higher, and the development cost and maintenance cost will increase

Bean registered

We know that beans can be configured in a number of ways, whether through XML configuration, annotations, or other means. The IoC Container must use different tools to read these configurations, but the results read by these tools must be uniform so that they can be handed to the BeanFactory for initialization.

The BeanFactory has only one purpose, which is to produce beans, and it doesn’t care about getting the differences between beans or where they come from.

The result of this unification isBeanDefinitonNo matter how you get the Bean, you eventually need to convert it toBeanDefinitionAnd then toBeanFactoryProcess. DefaultListableBeanFactoryAnd it’s doneBeanDefinitionRegistryInterfaces, differentApplicationContextEventually, you need to generate a BeanDefinition object and pass itBeanDefinitionRegistrytheregisterBeanDefinitionTo register the Bean, in effectbeanNameAs the key,BeanDefinitionIt is stored in a map as value. And then the BeanFactory goes throughgetBeanMethod to create a bean and place it in the container.

Is this implementation somewhat similar to the adapter pattern? The target interface accepts only the BeanDefinition, and each different configuration implements its own adapter that converts what it processes into the Implementation class of the BeanDefinition. To add a new configuration method, you only need to add a new adapter, without changing the original code.

Knowing the Bean registration process, it is easy to create a Bean dynamically when using the Spring Framework:

    @Resource
    private ApplicationContext ctx;

    public DynamicBean create(a) {
        ((BeanDefinitionRegistry)ctx).registerBeanDefinition("dynamicBean",
                             BeanDefinitionBuilder
                                .rootBeanDefinition(DynamicBean.class)
                                .addConstructorArgValue("something...")
                                .setRole(BeanDefinition.ROLE_APPLICATION)
                                .setScope(BeanDefinition.SCOPE_SINGLETON)
                                .getBeanDefinition());
        return (DynamicBean) ctx.getBean("dynamicBean");
    }
Copy the code

You can also use the BeanProcessor:

public class DynamicBeanCreator implements BeanFactoryPostProcessor.BeanDefinitionRegistryPostProcessor {

    /*** * registers beans */ by implementing BeanFactoryPostProcessor, converting the factory to BeanDefinitionRegistry
    @Override
    public void postProcessBeanFactory(ConfigurableListableBeanFactory configurableListableBeanFactory) throws BeansException {
        BeanDefinitionRegistry registry = (BeanDefinitionRegistry) configurableListableBeanFactory;
        registry.registerBeanDefinition("dynamicBean", BeanDefinitionBuilder... getBeanDefinition()); }/ * * * by implementing BeanDefinitionRegistryPostProcessor, can be directly obtained BeanDefinitionRegistry to register the bean * /
    @Override
    public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry beanDefinitionRegistry) throws BeansException {
        beanDefinitionRegistry.registerBeanDefinition("dynamicBean", BeanDefinitionBuilder...getBeanDefinition());
    }
}
Copy the code

Initialization of the bean

After the BeanDefinition is registered, it is ready to initialize the bean. The getBean method calling the BeanFactory returns the corresponding bean instance. If it does not exist, it will attempt to initialize the bean on getBean

  1. We saw earlier that the final implementation of the BeanFactory isDefaultListableBeanFactory, the callgetBeanMethod, after multiple layers of overloading, is calledresolveBeanMethod, you can see that if you can’t get the bean from the current beanFactory, you’ll try to get it from the parent or objectProvider implementation.

The following figure simplifies some of the code

  1. Next upresolveNamedBeanMethod, here is mainly implemented to determine the unique bean logic, if there is only a unique bean, then will enter the real logic to create the bean, if there are more than one, then will first according to@PrimaryTo find a unique bean; If it doesn’t exist@Primary, will be based on@OrderPriority to get the bean with the highest priority; If none exists, it will be thrownNoUniqueBeanDefinitionExceptionException:We usually see"expected single matching bean but found 2...This is where the exception is generated.

The following figure simplifies some of the code

  1. Let’s go to the logic for creating the bean,resolveNamedBeanMethod calls the parent classAbstractBeanFactorythegetBeanMethod, here is mainly implemented by@DependsOnRecursive initialization of dependencies for annotation identifiers solves the problem of circular dependencies through three map caches, as explained later. Eventually both Singleton and Prototype will entercreateBeanMethods.

The following figure simplifies some of the code

  1. createBeanMethod gives an extension point before the bean is initialized, which can be implementedInstantiationAwareBeanPostProcessorInterface to implement its own initialization bean logic and then enterdoCreateBean.

The following figure simplifies some of the code

  1. doCreateBeanThe final initialization and publication of the bean is implemented and three main things are done:
    1. Instantiate the bean object
    2. Initialize the bean’s properties and assign values
    3. To initialize the bean, we basically perform a series of extension points, including the ones we use most oftenInitialazingBean,initMethods, variousBeanAwareInterface andBeanProcessorEtc.

The following figure simplifies some of the code

The implementation of the above series of extension points is a bit like a mix of callback, Listener, and Observer patterns (the first two are not part of GoF23), as long as a bean implements the corresponding interface, The bean can then be notified by the IoC Container via the callback method at a specific point in time. For example, after the bean properties are set, all beans that implement the InitialazingBean interface are scanned and retrieved, and their callback method afterPropertiesSet() is called, as well as various Aware interfaces, and so on. A large number of extension points in the Spring Framework are implemented in this way.

Loop dependent processing

Spring’s loop dependencies are handled by three map caches:

  1. SingletonObjects holds bean objects that have been initialized
  2. earlySingletonObjectsThere are uninitialized bean objects (polulateBeanNot yet executed)
  3. SingletonFactories stores the factory object of the bean

So how does Spring rely on these three maps to solve the problem of circular dependencies? Suppose objects A and B depend on each other A -> B -> A

  1. When initializing A (getBean(A)), while initializing the property (populateBean()Before putting your own factory objects insingletonFactories(addSingletonFactory()Methods)
  2. inpopulateBeanWhen it finds that it depends on A, initialize A(getBean(A))
  3. When initializing A, we first get from the cache:
    1. From the firstsingletonObjectsIf it exists, return it directly
    2. If A is in InCreation, the sequence is incrementedearlySingletonObjectsandsingletonFactoriesTo derive
    3. Found in thesingletonFactoriesThe factory bean is used to get the semi-finished bean and put it inearlySingletonObjectsAnd fromsingletonFactoriesRemove the
  4. Initialize A successfully (semi-finished product), assign A reference to B, create B successfully, put B insingletonObjectsIn the
  5. A is created successfullysingletonObjectsAnd fromearlySingletonObjectsRemove the

So let me simplify this a little bitgetBeanAnd use only one cache (this is problematic, but this is just to demonstrate the handling of loop dependencies) :

FactoryBean

A FactoryBean differs from a regular bean in that it is a FactoryBean and its getObject method can return or create an instance of a bean of a certain type. To put it simply, after the bean is initialized, it returns the object returned by getObject if it is an ordinary bean, or if it is a bean that implements the FactoryBean interface.

So what does this thing do? Why bother? Why don’t we just go back? It actually provides a way for you to customize the bean implementation. You can return the bean you created in the implementation of getObject, and it can also enhance the bean, which is the proxy.

Here’s an example: Spring Framework implements many FactoryBean object itself, such as TransactionProxyFactoryBean, return is a transaction proxy class, ListFactoryBean casts the value from the generic before returning the object (if necessary).

conclusion

The main process for creating beans at the BeanFactory and some of the design patterns and methods involved were described above. Some of the designs might not make much sense when reading Spring Framework code, but if we could look at them from the consumer’s point of view, we might understand them differently. Or we can think or realize that logic in our own way, and maybe in the process of thinking or realizing, we can really understand its design goal.

Question 1: Why can’t the Prototype schema and constructor handle loop dependencies? Question 2: Why do I need three cache maps when dealing with circular dependencies? Is one ok? Would two do?

If you like this article, please give it a thumbs up. Thanks