Learning summary on September 1, 2021

What is the difference between factory mode and DI container

In fact, the most basic design idea of DI container bottom layer is based on factory mode. DI container is equivalent to a large factory class, which is responsible for creating objects in advance according to the configuration (which class objects to create and which other class objects to rely on for the creation of each class object) when the program starts. When an application needs to use a class object, it can be obtained directly from the container. It is precisely because it holds a pile of objects that this framework is called a "container".
DI container, which deals with larger object creation projects. A factory class is only responsible for the creation of a class object or a group of related class objects (subclasses inherited from the same abstract class or interface), while DI container is responsible for the creation of all class objects in the whole application.
In addition, the DI container is responsible for more than the simple factory mode. For example, it also includes configuration resolution and object life cycle management. Next, let's talk in detail about what core functions a simple DI container should contain.

What are the core functions of DI container

To sum up, a simple DI container generally has three core functions: configuration resolution, object creation and object lifecycle management

Configure resolution.

We put the class object that needs to be created by the DI container and the necessary information for creating the class object (which constructor to use, what the corresponding constructor parameters are, etc.) into the configuration file. The container reads the configuration file and creates objects according to the information provided by the configuration file.
The following is the configuration file of a typical Spring container. The Spring container reads the configuration file, parses the two objects to be created: ratelimit and redisCounter, and obtains their dependencies: ratelimit depends on redisCounter.

public class RateLimiter {
    private RedisCounter redisCounter;
    public RateLimiter(RedisCounter redisCounter) {
        this.redisCounter = redisCounter;
    }
    public void test() {
        System.out.println("Hello World!");
    }
//...
}
public class RedisCounter {
    private String ipAddress;
    private int port;
    public RedisCounter(String ipAddress, int port) {
        this.ipAddress = ipAddress;
        this.port = port;
    }
//...
}
configuration file beans.xml: 
<beans>
<bean id="rateLimiter" class="com.xzg.RateLimiter">
<constructor-arg ref="redisCounter"/>
</bean>
<bean id="redisCounter" class="com.xzg.redisCounter">
<constructor-arg type="String" value="127.0.0.1">
<constructor-arg type="int" value=1234>
</bean>
</beans>

Object creation.

In the DI container, if we create a factory class for each class, the number of classes in the project will increase exponentially, which will increase the maintenance cost of the code. It is not difficult to solve this problem. We just need to create all class objects in a factory class

Life cycle management.

There are two ways to implement the simple factory mode. One is to return the newly created object every time, and the other is to return the same pre created object every time, that is, the so-called singleton object. In the Spring framework, we can distinguish these two different types of objects by configuring the scope attribute. scope=prototype means to return the newly created object, and scope=singleton means to return the singleton object.
In addition, we can also configure whether the object supports lazy loading. If lazy init = true, the object is created only when it is actually used (for example, BeansFactory.getBean("userService"); If lazyinit=false, the object is created in advance when the application starts.
Not only that, we can also configure the init method and destroy method methods of the object. After the DI container creates the object, it will actively call the method specified by the init method attribute to initialize the object. Before the object is finally destroyed, the DI container will actively call the method specified by the destroy method attribute to do some cleaning work, such as releasing the database connection pool and closing the file.

How to implement a simple DI container

In fact, to implement a simple DI container in Java language, the core logic only needs to include two parts: configuration file parsing and creating objects through "reflection" syntax according to the configuration file.

1. Minimum prototype design

Because we mainly explain design patterns, in today's explanation, we only implement a minimum prototype of DI container. DI containers like the Spring framework support very flexible and complex configuration formats. In order to simplify the code implementation and focus on the principle, in the minimum prototype, we only support the configuration syntax involved in the following configuration file.

configuration file beans.xml: 
<beans>
<bean id="rateLimiter" class="com.xzg.RateLimiter">
<constructor-arg ref="redisCounter"/>
</bean>
<bean id="redisCounter" class="com.xzg.redisCounter">
<constructor-arg type="String" value="127.0.0.1">
<constructor-arg type="int" value=1234>
</bean>
</beans>

The usage of the minimum prototype is very similar to the Spring framework. The example code is as follows:

public class Demo {
    public static void main(String[] args) {
        ApplicationContext applicationContext = new ClassPathXmlApplicationContext
        "beans.xml");
        RateLimiter rateLimiter = (RateLimiter) applicationContext.getBean("rateLim
                rateLimiter.test();
        //...
    }
}

2. Provide execution access

The final step of object-oriented design is to assemble classes and provide execution entry. Here, an execution entry is a set of interfaces and classes exposed for external use.
From the example code used by the minimal prototype just now, we can see that the execution entry mainly consists of two parts: ApplicationContext and ClassPathXmlApplicationContext. Where ApplicationContext is the interface and ClassPathXmlApplicationContext is the implementation class of the interface. The specific implementations of the two classes are as follows:

public interface ApplicationContext {
    Object getBean(String beanId);
}
public class ClassPathXmlApplicationContext implements ApplicationContext {
    private BeansFactory beansFactory;
    private BeanConfigParser beanConfigParser;
    public ClassPathXmlApplicationContext(String configLocation) {
        this.beansFactory = new BeansFactory();
        this.beanConfigParser = new XmlBeanConfigParser();
        loadBeanDefinitions(configLocation);
    }
    private void loadBeanDefinitions(String configLocation) {
        InputStream in = null;
        try {
            in = this.getClass().getResourceAsStream("/" + configLocation);
            if (in == null) {
                throw new RuntimeException("Can not find config file: " + configLocatio
            }
            List<BeanDefinition> beanDefinitions = beanConfigParser.parse(in);
            beansFactory.addBeanDefinitions(beanDefinitions);
        } finally {
            if (in != null) {
                try {
                    in.close();
                } catch (IOException e) {
// TODO: log error
                }
            }
        }
    }
    @Override
    public Object getBean(String beanId) {
        return beansFactory.getBean(beanId);
    }
}

From the above code, we can see that ClassPathXmlApplicationContext is responsible for assembling BeansFactory and BeanConfigParser, and executing the process in series: load the configuration file in XML format from classpath, parse it into a unified BeanDefinition format through BeanConfigParser, and then BeansFactory creates objects according to BeanDefinition.

3. Configuration file analysis

Configuration file parsing mainly includes the BeanConfigParser interface and XmlBeanConfigParser implementation class, which is responsible for parsing the configuration file into BeanDefinition structure so that BeansFactory can create objects according to this structure.

4. Core plant design

In fact, the main technical point used by BeansFactory to create objects is the reflection syntax in Java: a dynamic addition
Mechanisms for loading classes and creating objects. We know that the JVM will automatically load classes and create pairs according to the code when it starts
Elephant. As for which classes to load and which objects to create, these are written dead in the code, or written in advance. However, if the creation of an object is not written in the code, but in the configuration file, we need to dynamically load classes and create objects according to the configuration file during the program running, then this part of the work can not be completed automatically by the JVM. We need to use the reflection syntax provided by Java to write our own code.

Keywords: Java Hibernate Spring

Added by Lucidnight on Thu, 02 Sep 2021 03:53:42 +0300