Guess spring boot: dependency injection

Keywords: Java github Spring

Dependency injection

Citation and target

This is the second article in the spring boot series (Oh, I have written 10 articles, which is not easy).

In the previous article, we implemented the creation of bean, but it was just the creation, there was no real implementation of bean injection. So in today's article, we are going to implement automatic bean injection.

We have defined two classes of SimpleService and SimpleController in the project before. In this article, we want to automatically inject SimpleService into SimpleController;

SimpleController.java

@Service
public class SimpleController {

    @Autowired
    private SimpleService simpleService;

    public SimpleController(){
        System.out.println("the controller is created!");
    }
}

Because it's just a console program at present, and there's no way to make a real call and display, so I add an output to SimpleService to represent the uniqueness of this class.
In this way, SimpleService becomes like this:

SimpleService.java

@Service
public class SimpleService {
    
    private String serviceId ;
    public SimpleService(){
        serviceId= UUID.randomUUID().toString();
        System.out.println("the service :"+serviceId+"is created!");
    }
    public String getServiceId(){
        return this.serviceId;
    }
}

Now although there are calls, there is still no way to verify our ideas.

Let's add a poststruct note:)

SimpleController.java

public class SimpleController{
    // other code
    @PostContruct
    public void init(){
        System.out.println("the service id is :"+this.simpleService.getServiceId());
    }   
}

Demand analysis of dependency injection

From the changes of our target program, we can see that our changes to mini boot are mainly as follows:

  1. Define PostConstruct and Autowired
  2. Using Autowired as a marker to implement dependency injection
  3. With postconstruct as a marker, the automatic initialization of Bean creation is realized

Three are the intuitive goals we want to achieve this time. However, due to the simplistic design between us, we have a problem to solve, namely:

We need a place to store and find the bean s that have already been generated!

So this question is obviously more important than 1,2,3, so

Dig the pit before and fill the pit later

Our goal has become:

  1. Realize the storage and management of Bean
  2. Define PostConstruct and Autowired
  3. Using Autowired as a marker to implement dependency injection
  4. With postconstruct as a marker, the automatic initialization of Bean creation is realized

Storage and management of Step 0 Bean

In Spring, to get a bean from the outside:

  1. Get ApplicationContext through dependency injection or other ways
  2. Get the corresponding bean through ApplicationContext.getBean

Through these two, it is obvious:

  1. We also follow an application context
  2. In this way, our ApplicationContext can be implemented very well:

ApplicationContext.java

package com.github.yfge.miniboot.context;

import java.util.Collection;
import java.util.LinkedHashMap;
import java.util.Map;

public class ApplicationContext {
    private Map<String,Object> beanMap ;
    public ApplicationContext(){
        beanMap=new LinkedHashMap<>();
    }
    public void addBean( Object ob){
        this.beanMap.put(ob.getClass().getName(),ob);
    }

    public Object getBean(String beanName){
        return this.beanMap.get(beanName);
    }
    public Object getBean(Class beanClass){
        return this.beanMap.get(beanClass.getName());
    }
}

Well, with ApplicationContext, we can save all beans with an ApplicationContext while generating beans.
At this time, our Application.loadBeans function changes a little:

Application.loadBeans

public class Application{
// other code 
  private static void LoadBeans(Class source) {
        ClassUtils util = new ClassUtils();
        List<String> classNames = util.loadClass(source);
        /** Instantiate a context**/
        ApplicationContext applicationContext = new ApplicationContext();
        for (String name : classNames) {
            try {
                var classInfo = Class.forName(name);
                /**
                 * Check if @ Service is declared
                 **/
                if (classInfo.getDeclaredAnnotation(Service.class) != null) {
                    /**
                     * Get default constructor
                     */
                    var constructor = classInfo.getConstructor();
                    if (constructor != null) {
                        /**
                         * Create examples
                         */
                        var obj = constructor.newInstance();
                        /** Save bean**/
                        applicationContext.addBean(obj);
                    }
                }
            } catch (Throwable e) {
                e.printStackTrace();
            }
        }
  }

//other code
}

As you can see, the change at this time is very small. We just initialized a context, and then save the bean in the context after the bean is generated

OK, the function of saving the beean is completed (just filling in the previous pits). The next step is to start our formal work.

Step1 define Annotation

As we have analyzed before, we need to define two annotations: Autowired and postconstruct. They are very simple. Just make the following Declaration:

Autowired.java

package com.github.yfge.miniboot.autoconfigure;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
public @interface Autowired {
}

PostConstruct.java

package com.github.yfge.miniboot.autoconfigure;

import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;

@Retention(RetentionPolicy.RUNTIME)
public @interface PostConstruct {
}

Also note that we need to add @ Retention(RetentionPolicy.RUNTIME) here. For specific functions and differences, we will find opportunities to explain in detail (digging holes again, not digging uncomfortable bases)

Step 2 is marked with @ Autowired to implement dependency injection

Well, we're going to do the most exciting part, implementing dependency injection, that is, we're going to automatically inject SimpleService into SimpleController.

But ....

It's all very natural. What's so exciting about the company? All we have to do is:

  1. Take all bean s out of context;
  2. Get every field of the bean by reflection;
  3. Check whether this field is annotated with autowire;
  4. If yes, check whether the field type is a bean;
  5. If yes, take it out and assign it with reflection;

Because we use Map for storage, steps 4 and 5 can be combined into:

4-5. Get the bean according to the field type. If it is not empty, assign a value.

Because I think clearly, the code will be completed in one go, that is, add the injection logic after Application.loadBeans:

Application.loadBeans

public class Application {
    /**
     * Load the corresponding bean(Service)
     *
     * @param source
     */
    private static void LoadBeans(Class source) {
        //other code 
        //That's the logic
        
        /** Injecting bean **/
        for (Object ob : applicationContext.getAllBeans()) {
            var classInfo = ob.getClass();
            for (var field : classInfo.getDeclaredFields()) {
                if (field.getDeclaredAnnotation(Autowired.class) != null) {
                    field.setAccessible(true);
                    var filedBean = applicationContext.getBean(field.getType());
                    if (filedBean != null) {
                        try {
                            field.set(ob, filedBean);
                        } catch (IllegalAccessException e) {
                            e.printStackTrace();
                        }
                    }
                }
            }
        }
    }
    //other code
}

Here, we add a new method for ApplicationContext to get all bean s

ApplicationContext.java

public class ApplicationContext {
    //other code
    
    public Collection<Object> getAllBeans(){
        return this.beanMap.values();
    }
}

Note that there is a point in this, that is, we have cycled twice:

  • The first time is to generate bean s and store them
  • The second time is to extract all beans and assign values to each bean in the form of dependency injection

The reasons for this are also clear:

To ensure that we can get the beans we want at the time of injection, we must process them after all beans are generated

Step 3 is marked with @ PostConstruct to realize the automatic operation of initialization methods

By the last step, our dependency injection has actually been completed, but at present, our mini boot framework is too simple to verify our success.

Therefore, we also implement @ postconstruct mechanism here, that is, automatic initialization after class loading.

With the previous work, you will find that our work has become very stylized and tasteless, namely:

  1. Take all bean s out of context;
  2. Get every method of bean by reflection;
  3. Check whether the method is annotated with postconstruct;
  4. If so, use reflection to perform this method;

You will notice that I have some bold fonts here. The reason for the bold is that: 1-4 here is copied from above, and the bold ones are only the changed parts.

Even if the implementation ideas can be copied, then the code has become very easy.

Application.loadBeans

public class Application {
    /**
     * Load the corresponding bean(Service)
     *
     * @param source
     */
    private static void LoadBeans(Class source) {
        //other code 
        //That's the logic
        //Include
        //1. Generate all bean s and store them
        //2. Traverse the generated bean s for dependency injection
        /** Execute initialization method**/
        for (Object ob : applicationContext.getAllBeans()) {
            var classInfo = ob.getClass();
            if (classInfo.getDeclaredAnnotation(Service.class) != null) {
                for (var method : classInfo.getDeclaredMethods()) {
                    if (method.getDeclaredAnnotation(PostConstruct.class) != null) {
                        try {
                            method.invoke(ob);
                        } catch (IllegalAccessException e) {
                            e.printStackTrace();
                        } catch (InvocationTargetException e) {
                            e.printStackTrace();
                        }
                    }
                }
            }
        }
    }
    // other code
}

OK, now compile the project and execute it. You will see the following output:

the controller is created!
the service :580fbd69-d82b-44b6-847a-b4c5cbc4d97b is created!
the service Id is :580fbd69-d82b-44b6-847a-b4c5cbc4d97b
The Mini-Boot Application Is Run! The Name is Hello

In this output:

  • The first line is automatically created by the controller
  • In the second row of the table, the service is created automatically, and the unique id is xxxx
  • The third line is the output of init function in our controller. You can see that the service here is not empty, and it is the one created automatically between us!

in other words:

  1. We've achieved simple automatic injection as expected
  2. At the same time, we implement a simple automatic initialization function as expected

Now, quietly think about the content of our article, whether there is a kind of door that has been opened to you:)

Other

Do not share the source code are playing hooligans!

Therefore, our project address is:
https://github.com/yfge/mini-boot
Because, with the release of the article, the code will be constantly updated, so the tag of this chapter is: article-02 (forgive me for naming the level)

Posted by SilverFoxDesign on Sat, 07 Mar 2020 20:43:40 -0800