Geek Time-The Beauty of Design Patterns Factory Pattern (Part 2): How to design and implement a Dependency Injection framework?

When creating an object is a "big project", we generally choose to use the factory pattern to encapsulate the complex creation process of the object, separate the creation and use of the object, and make the code clearer. So what is a "big project"? In the last lesson, we talked about two situations. One is that the creation process involves complex if-else branch judgments, and the other is that object creation requires the assembly of multiple objects of other types or a complex initialization process.

Let's talk about a "big project" for creating objects, a dependency injection framework, or Dependency Injection Container, or DI container for short. In today's explanation, I will take you to clarify the following questions: What is the difference and connection between the DI container and the factory model we talked about? What are the core functions of the DI container and how to implement a simple DI container?

What is the difference between factory mode and DI container?

In fact, the most basic design idea at the bottom of the DI container is based on the factory pattern. The DI container is equivalent to a large factory class, which is responsible for creating objects in advance according to the configuration (which class objects are to be created and which other class objects need to be created for each class object) when the program starts. When an application needs to use a certain class object, it can be obtained directly from the container. It is precisely because it holds a bunch of objects that this framework is called a "container".

Compared with the factory pattern example we talked about in the previous lesson, the DI container deals with a larger object creation project. In the factory model described in the previous lesson, a factory class is only responsible for the creation of a certain class object or a certain group of related class objects (inherited from the same abstract class or interface subclass), while the DI container is responsible for all the applications in the entire application. Creation of class objects.

In addition, the DI container is responsible for more than the simple factory model. For example, it also includes configuration analysis and object life cycle management. Next, we will talk in detail about what core functions a simple DI container should contain.

What are the core functions of the DI container?

To sum up, there are generally three core functions of a simple DI container: configuration analysis, object creation, and object lifecycle management.

First, let's look at the configuration analysis.

In the factory pattern discussed in the previous lesson, which class object the factory class will create is determined in advance and is hard-coded in the factory class code. As a general framework, the framework code and application code should be highly decoupled. The DI container does not know in advance which objects the application will create, and it is impossible to write the objects created by an application in the framework code. Therefore, we need a form for the application to tell the DI container which objects to create. This form is the configuration we are going to talk about.

We will need to create the class object created by the DI container and the necessary information to create the class object (which constructor is used and what are the corresponding constructor parameters, etc.), into the configuration file. The container reads the configuration file and creates objects based on the information provided by the configuration file.

The following is a typical Spring container configuration file. The Spring container reads this configuration file, parses out the two objects to be created: rateLimiter and redisCounter, and obtains the dependency between the two: rateLimiter 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;
  }
  //...
}

配置文件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>

Second, let’s look at 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 only need to create all class objects in a factory class, such as BeansFactory.

You might say, if there are a lot of class objects to be created, will the code in BeansFactory expand linearly (the amount of code is proportional to the number of objects created)? Actually not. When we talk about the specific implementation of the DI container, we will talk about the "reflection" mechanism, which can dynamically load classes and create objects during the running of the program, without having to write down the code in advance. Object. So, whether it is to create one object or ten objects, the BeansFactory factory class code is the same.

Finally, let's look at the life cycle management of objects.

There are two ways to implement the simple factory pattern. One is to return the newly created object each time, and the other is to return the same pre-created object each time, which is the so-called singleton object. In the Spring framework, we can distinguish these two different types of objects by configuring the scope property. scope=prototype means to return a newly created object, scope=singleton means to return a singleton object.

In addition, we can also configure whether the object supports lazy loading. If lazy-init=true, the object is created when it is actually used (for example: BeansFactory.getBean("userService")); if lazy-init=false, the object is created in advance when the application starts.

Not only that, we can also configure the object's init-method and destroy-method methods, such as init-method=loadProperties(), destroy-method=updateConfigFile(). After the DI container has created 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 cleanup 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 object creation based on the configuration file through "reflection" syntax.

1. Minimal prototype design

Because we are mainly explaining design patterns, in today's explanation, we only implement a minimal prototype of a DI container. The configuration format supported by DI containers like the Spring framework is very flexible and complex. In order to simplify the code implementation and focus on the principle, in the minimal prototype, we only support the configuration syntax involved in the following configuration files.


配置文件beans.xml
<beans>
   <bean id="rateLimiter" class="com.xzg.RateLimiter">
      <constructor-arg ref="redisCounter"/>
   </bean>
 
   <bean id="redisCounter" class="com.xzg.redisCounter" scope="singleton" lazy-init="true">
     <constructor-arg type="String" value="127.0.0.1">
     <constructor-arg type="int" value=1234>
   </bean>
</bean

The use of the minimal prototype is very similar to the Spring framework. The sample code is as follows:


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

As we mentioned earlier, the last step of object-oriented design is to assemble the class and provide an entry point for execution. Here, the execution entry is a set of interfaces and classes exposed to external use.

Using the sample code for the minimal prototype just now, we can see that the execution entry consists of two parts: ApplicationContext and ClassPathXmlApplicationContext. Among them, ApplicationContext is the interface, and ClassPathXmlApplicationContext is the implementation class of the interface. The specific implementation of the two classes is 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: " + configLocation);
      }
      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 the two classes of BeansFactory and BeanConfigParser, serializing the execution process: loading the configuration file in XML format from the classpath, parsed into a unified BeanDefinition format through BeanConfigParser, and then BeansFactory according to BeanDefinition Create the object.

3. Configuration file analysis

The configuration file analysis mainly includes the BeanConfigParser interface and the XmlBeanConfigParser implementation class, which is responsible for parsing the configuration file into a BeanDefinition structure, so that the BeansFactory can create objects based on this structure

The analysis of the configuration file is relatively cumbersome, does not involve the theoretical knowledge that our column will explain, and is not the focus of our explanation, so here I only give the general design ideas of the two classes, and do not give the specific implementation code. If you are interested, you can complete it yourself. The specific code framework is as follows:


public interface BeanConfigParser {
    
    
  List<BeanDefinition> parse(InputStream inputStream);
  List<BeanDefinition> parse(String configContent);
}

public class XmlBeanConfigParser implements BeanConfigParser {
    
    

  @Override
  public List<BeanDefinition> parse(InputStream inputStream) {
    
    
    String content = null;
    // TODO:...
    return parse(content);
  }

  @Override
  public List<BeanDefinition> parse(String configContent) {
    
    
    List<BeanDefinition> beanDefinitions = new ArrayList<>();
    // TODO:...
    return beanDefinitions;
  }

}

public class BeanDefinition {
    
    
  private String id;
  private String className;
  private List<ConstructorArg> constructorArgs = new ArrayList<>();
  private Scope scope = Scope.SINGLETON;
  private boolean lazyInit = false;
  // 省略必要的getter/setter/constructors
 
  public boolean isSingleton() {
    
    
    return scope.equals(Scope.SINGLETON);
  }


  public static enum Scope {
    
    
    SINGLETON,
    PROTOTYPE
  }
  
  public static class ConstructorArg {
    
    
    private boolean isRef;
    private Class type;
    private Object arg;
    // 省略必要的getter/setter/constructors
  }
}
4. Core factory design

Finally, let's see how BeansFactory is designed and implemented. This is also the core class of our DI container. It is responsible for creating objects based on the BeanDefinition parsed from the configuration file.

If the scope attribute of the object is singleton, the object will be cached in a map of singletonObjects after it is created. Next time this object is requested, it will be retrieved and returned directly from the map without re-creating it. If the scope property of the object is prototype, then every time the object is requested, the BeansFactory will create a new object and return it.

In fact, the main technical point used by BeansFactory to create objects is the reflection syntax in Java: a mechanism for dynamically loading classes and creating objects. We know that the JVM will automatically load classes and create objects based on the code when it starts. As for which classes to load and which objects to create, these are all hard-coded in the code, or written in advance. However, if the creation of an object is not written in the code, but placed in the configuration file, we need to dynamically load the class and create the object according to the configuration file during the running of the program, then this part of the work will not work Let the JVM do it automatically for us, we need to use the reflection syntax provided by Java to write the code ourselves.

Knowing the principle of reflection, the code of BeansFactory is not difficult to understand. The specific code implementation is as follows:


public class BeansFactory {
    
    
  private ConcurrentHashMap<String, Object> singletonObjects = new ConcurrentHashMap<>();
  private ConcurrentHashMap<String, BeanDefinition> beanDefinitions = new ConcurrentHashMap<>();

  public void addBeanDefinitions(List<BeanDefinition> beanDefinitionList) {
    
    
    for (BeanDefinition beanDefinition : beanDefinitionList) {
    
    
      this.beanDefinitions.putIfAbsent(beanDefinition.getId(), beanDefinition);
    }

    for (BeanDefinition beanDefinition : beanDefinitionList) {
    
    
      if (beanDefinition.isLazyInit() == false && beanDefinition.isSingleton()) {
    
    
        createBean(beanDefinition);
      }
    }
  }

  public Object getBean(String beanId) {
    
    
    BeanDefinition beanDefinition = beanDefinitions.get(beanId);
    if (beanDefinition == null) {
    
    
      throw new NoSuchBeanDefinitionException("Bean is not defined: " + beanId);
    }
    return createBean(beanDefinition);
  }

  @VisibleForTesting
  protected Object createBean(BeanDefinition beanDefinition) {
    
    
    if (beanDefinition.isSingleton() && singletonObjects.contains(beanDefinition.getId())) {
    
    
      return singletonObjects.get(beanDefinition.getId());
    }

    Object bean = null;
    try {
    
    
      Class beanClass = Class.forName(beanDefinition.getClassName());
      List<BeanDefinition.ConstructorArg> args = beanDefinition.getConstructorArgs();
      if (args.isEmpty()) {
    
    
        bean = beanClass.newInstance();
      } else {
    
    
        Class[] argClasses = new Class[args.size()];
        Object[] argObjects = new Object[args.size()];
        for (int i = 0; i < args.size(); ++i) {
    
    
          BeanDefinition.ConstructorArg arg = args.get(i);
          if (!arg.getIsRef()) {
    
    
            argClasses[i] = arg.getType();
            argObjects[i] = arg.getArg();
          } else {
    
    
            BeanDefinition refBeanDefinition = beanDefinitions.get(arg.getArg());
            if (refBeanDefinition == null) {
    
    
              throw new NoSuchBeanDefinitionException("Bean is not defined: " + arg.getArg());
            }
            argClasses[i] = Class.forName(refBeanDefinition.getClassName());
            argObjects[i] = createBean(refBeanDefinition);
          }
        }
        bean = beanClass.getConstructor(argClasses).newInstance(argObjects);
      }
    } catch (ClassNotFoundException | IllegalAccessException
            | InstantiationException | NoSuchMethodException | InvocationTargetException e) {
    
    
      throw new BeanCreationFailureException("", e);
    }

    if (bean != null && beanDefinition.isSingleton()) {
    
    
      singletonObjects.putIfAbsent(beanDefinition.getId(), bean);
      return singletonObjects.get(beanDefinition.getId());
    }
    return bean;
  }
}

Guess you like

Origin blog.csdn.net/zhujiangtaotaise/article/details/110475682