Do it yourself to implement a simple IOC container

Time:2021-1-17

Control reversal, Inversion of control (IOC) is an object-oriented design principle, which can effectively reduce the coupling degree of architecture code. From the perspective of object callers, it is also called dependency injection, that is, dependency Injection (DI), through control inversion, when an object is created, a container that regulates all objects in the system passes the references of the objects it depends on to it. In other words, dependencies are injected into the object. This container is the IOC container that we often talk about. The core of spring and spring boot framework is to provide an IOC container based on annotation, which can manage all lightweight JavaBean components. The underlying services provided include component lifecycle management, configuration and assembly services, AOP support, and declarative transaction services based on AOP.

In this article, we will implement a simple IOC container based on annotations by ourselves. Of course, because it is a personal implementation, it will not really follow the design pattern of the springboot framework, and it will not consider too many complex issues such as loop dependency, thread safety, etc The whole implementation principle is very simple. Scan the annotations, create the bean instances we need through reflection, and then put these beans into the collection. Externally, we provide a getBean () method through the IOC container class to obtain the ean instance. Needless to say, let’s start the specific design and implementation.

1. Definition annotation

@Retention(RetentionPolicy.RUNTIME)
public @interface SproutComponet {
    String value() default "";
}
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface SproutRoute {
    RouteEnum value();
}

2. Implement jar package scanning class

According to the incoming jar package, scan and cache all the specified annotation class > class objects under the jar package

public class ClassScanner {

    private static Set> classSet = null;
    
    private static Map> componetMap = null;

    /**
     *Gets all class classes under the specified package name
     * @param packageName
     * @return
     * @throws Exception
     */
    public static Set> getClasses(String packageName) throws Exception {

        if (classSet == null){
            classSet = ReflectUtils.getClasses(packageName);

        }
        return classSet;
    }
    
    
    /**
     *Cache all class > class objects with specified annotations
     * @param packageName
     * @return
     * @throws Exception
     */
    public static Map> getBean(String packageName) throws Exception {

        if (componetMap == null) {
            Set> clsList = getClasses(packageName);

            if (clsList == null || clsList.isEmpty()) {
                return componetMap;
            }

            componetMap = new HashMap<>(16);
            for (Class cls : clsList) {

                Annotation annotation = cls.getAnnotation(SproutComponet.class);
                if (annotation == null) {
                    continue;
                }

                SproutComponet sproutComponet = (SproutComponet) annotation;
                componetMap.put(sproutComponet.value() == null ? cls.getName() : sproutComponet.value(), cls);

            }
        }
        return componetMap;
    }

}

Based on classscanner, the annotated method object is scanned and cached to support the implementation of method routing

public class RouterScanner {

    private String rootPackageName;

    private static Map routes = null;

    private List methods;

    private volatile static RouterScanner routerScanner;

    /**
     * get single Instance
     *
     * @return
     */
    public static RouterScanner getInstance() {
        if (routerScanner == null) {
            synchronized (RouterScanner.class) {
                if (routerScanner == null) {
                    routerScanner = new RouterScanner();
                }
            }
        }
        return routerScanner;
    }

    private RouterScanner() {
    }

    public String getRootPackageName() {
        return rootPackageName;
    }

    public void setRootPackageName(String rootPackageName) {
        this.rootPackageName = rootPackageName;
    }

    /**
     *Specify the method get route method according to the annotation
     *
     * @param queryStringDecoder
     * @return
     * @throws Exception
     */
    public Method routeMethod(Object key) throws Exception {
        if (routes == null) {
            routes = new HashMap<>(16);
            loadRouteMethods(getRootPackageName());
        }

        Method method = routes.get(key);

        if (method == null) {
            throw new Exception();
        }

        return method;

    }

    /**
     *Load the method object under the specified package
     * 
     * @param packageName
     * @throws Exception
     */
    private void loadRouteMethods(String packageName) throws Exception {
        Set> classSet = ClassScanner.getClasses(packageName);

        for (Class> sproutClass : classSet) {
            Method[] declaredMethods = sproutClass.getMethods();

            for (Method method : declaredMethods) {
                SproutRoute annotation = method.getAnnotation(SproutRoute.class);
                if (annotation == null) {
                    continue;
                }
                routes.put(annotation.value(), method);
            }
        }

    }

}

3. Define beanfactory object factory interface

The interface must have three basic methods

  • Init() initializes the registration bean instance
  • Getbean() gets the bean instance
  • Release() to unload the bean instance
public interface ISproutBeanFactory {

    /**
     * Register into bean Factory
     * 
     * @param object
     */
    void init(Object object);

    /**
     * Get bean from bean Factory
     * 
     * @param name
     * @return
     * @throws Exception
     */
    Object getBean(String name) throws Exception;

    /**
     * release all beans
     */
    void release();

}

4. Implementation of beanfactory object factory interface

For the specific implementation of the beanfactory interface, in the beanfactory factory, we need a container, that is, the map collection beans. During initialization, we instantiate and save all the objects that need to be managed by the IOC container to the bean container. When we need to use it, we only need to get it from the container,

To solve this problem, you need to call reflection every time you create a new instancenewInstance()The problem of low efficiency.

public class SproutBeanFactory implements ISproutBeanFactory {

    /**
     *Object map
     */
    private static Map beans = new HashMap<>(8);
    
    /**
     *Object map
     */
    private static List methods = new ArrayList<>(2);

    @Override
    public void init(Object object) {
        beans.put(object.getClass().getName(), object);
    }

    @Override
    public Object getBean(String name) {
        return beans.get(name);
    }
    
    
    public List getMethods() {
        return methods;
    }

    @Override
    public void release() {
        beans = null;
    }
}

5. Implement bean container class

The entry and top-level implementation class of the IOC container, declare the Bena factory instance, scan the specified jar package, obtain the “class > collection based on the annotation, and inject the beanfactory object factory after instantiation

public class SproutApplicationContext {

    private SproutApplicationContext() {
    }

    private static volatile SproutApplicationContext sproutApplicationContext;

    private static ISproutBeanFactory sproutBeanFactory;

    
    public static SproutApplicationContext getInstance() {
        if (sproutApplicationContext == null) {
            synchronized (SproutApplicationContext.class) {
                if (sproutApplicationContext == null) {
                    sproutApplicationContext = new SproutApplicationContext();
                }
            }
        }
        return sproutApplicationContext;
    }

    /**
      *Declare the Bena factory instance, scan the specified jar package, and load the instance under the specified jar package
     * 
     * @param packageName
     * @throws Exception
     */
    public void init(String packageName) throws Exception {
        //Get the map of the specified annotation class
        Map> sproutBeanMap = ClassScanner.getBean(packageName);

        sproutBeanFactory = new SproutBeanFactory();
         
        //Injection instance factory
        for (Map.Entry> classEntry : sproutBeanMap.entrySet()) {
            Object instance = classEntry.getValue().newInstance();
            sproutBeanFactory.init(instance);
        }

    }

    /**
     *Get the corresponding instance by name
     * 
     * @param name
     * @return
     * @throws Exception
     */
    public Object getBean(String name) throws Exception {
        return sproutBeanFactory.getBean(name);
    }

    /**
     * release all beans
     */
    public void releaseBean() {
        sproutBeanFactory.release();
    }

}

6. Implementation method route

Provide methods, accept incoming annotations, obtain corresponding method objects and bean instances through routerscanner and sproutapplicationcontext, and call specific methods to realize method routing function.

public class RouteMethod {

    private volatile static RouteMethod routeMethod;

    private final SproutApplicationContext applicationContext = SproutApplicationContext.getInstance();

    public static RouteMethod getInstance() {
        if (routeMethod == null) {
            synchronized (RouteMethod.class) {
                if (routeMethod == null) {
                    routeMethod = new RouteMethod();
                }
            }
        }
        return routeMethod;
    }

    /**
     *Call method
     * @param method
     * @param annotation
     * @param args
     * @throws Exception
     */
    public void invoke(Method method, Object[] args) throws Exception {
        if (method == null) {
            return;
        }
        Object bean = applicationContext.getBean(method.getDeclaringClass().getName());
        if (args == null) {
            method.invoke(bean);
        } else {
            method.invoke(bean, args);
        }
    }
    
    
    /**
     *Call method according to annotation
     * @param method
     * @param annotation
     * @param args
     * @throws Exception
     */
    public void invoke(RouteEnum routeEnum, Object[] args) throws Exception {
        Method method = RouterScanner.getInstance().routeMethod(routeEnum);
        if (method == null) {
            return;
        }
        Object bean = applicationContext.getBean(method.getDeclaringClass().getName());
        if (args == null) {
            method.invoke(bean);
        } else {
            method.invoke(bean, args);
        }
    }

}

7. Specific use

Here, the main interfaces and implementation classes of the IOC container are basically implemented. Let’s see how to use them

First, initialize the IOC container. Here, scan all the classes under the application package according to the main method, and inject the annotated bean instance into the instance container

public void start() {
        try {
            
            resolveMainClass();
            if(mainClass!=null) {
                SproutApplicationContext.getInstance().init(mainClass.getPackage().getName());
            }

        }catch (Exception e) {
            // TODO: handle exception
        }
    }
    /**
     *Query the class class of main method
     *
     */
    private Class> resolveMainClass() {
        try {
            if(!StringUtils.isEmpty(config().getRootPackageName())) {
                mainClass = Class.forName(config().getRootPackageName());
            }else {
                StackTraceElement[] stackTrace = new RuntimeException().getStackTrace();
                for (StackTraceElement stackTraceElement : stackTrace) {
                    if ("main".equals(stackTraceElement.getMethodName())) {
                        mainClass = Class.forName(stackTraceElement.getClassName());
                        break;
                    }
                }
            }

        } catch (Exception ex) {
            // ignore this ex
        }
        return mainClass;
    }

Get the bean instance and call the method

/**
     *Call method according to annotation
     * @param method
     * @param annotation
     * @param args
     * @throws Exception
     */
    public void invoke(RouteEnum routeEnum, Object[] args) throws Exception {
        Method method =  RouterScanner.getInstance (). Routemethod (routeenum); // routing method based on IOC
        if (method == null) {
            return;
        }
        Object bean =  applicationContext.getBean ( method.getDeclaringClass (). Getname ()); // get the instance directly through the bean container
        if (args == null) {
            method.invoke(bean);
        } else {
            method.invoke(bean, args);
        }
    }

8. Summary

In the above content, we have implemented the most basic IOC container function around “reflection” + “cache”. The overall code is simple and clear, without considering other complex situations, which is suitable for use or learning in specific scenarios, At the same time, it can also let you have a preliminary understanding of the definition and implementation principle of IOC. It will get twice the result with half the effort to further study the relevant code in the sping framework. I hope this article can help you. If there are deficiencies and inaccuracies, I also hope to point out and discuss them.

 

WeChat official account for more technical articles.

 

Recommended Today

SDS of redis data structure

SDS(simple dynamic string), simple dynamic string. S and it’s called hacking string. Where hack is stored is the length of the string and the remaining space in SDS. The implementation of SDS insds.cIn the middle. C language string uses a character array of length N + 1 to represent the string of length N, and […]