Simple IOC container implementation

Keywords: Java Spring github Gradle Junit

Preface

This article is written to learn the execution process of the Spring IOC container. It can't fully represent the Spring IOC container, but simply implements the dependency injection and control inversion functions of the container, which can't be used in production. It can only play a certain role in understanding the Spring container.

start

Create project

Create the Gradle project and modify build.gradle

plugins {
    id 'java'
    id "io.franzbecker.gradle-lombok" version "3.1.0"
}

group 'io.github.gcdd1993'
version '1.0-SNAPSHOT'

sourceCompatibility = 1.8

repositories {
    mavenCentral()
}

dependencies {
    testCompile group: 'junit', name: 'junit', version: '4.12'
}

Create BeanFactory

BeanFactory is the core interface used to store bean instances and get beans in IOC. Its core methods are getBean and overload methods of getBean. Here, two methods of getBean are simply implemented.

package io.github.gcdd1993.ioc.bean;

/**
 * bean factory interface
 *
 * @author gaochen
 * @date 2019/6/2
 */
public interface BeanFactory {

    /**
     * Get bean by bean name
     *
     * @param name bean Name
     * @return bean
     */
    Object getBean(String name);

    /**
     * Get bean by bean type
     *
     * @param tClass bean type
     * @param <T>    Generic T
     * @return bean
     */
    <T> T getBean(Class<T> tClass);

}

Create ApplicationContext context

ApplicationContext, as we often say, is actually the Spring container itself.

We create the ApplicationContext class and implement the BeanFactory interface.

public class ApplicationContext implements BeanFactory {
}

getBean method

Since it's a container, there must be a place to hold our bean instance. Use two maps as containers.

/**
 * Group by beanName
 */
private final Map<String, Object> beanByNameMap = new ConcurrentHashMap<>(256);

/**
 * Group by bean class
 */
private final Map<Class<?>, Object> beanByClassMap = new ConcurrentHashMap<>(256);

Then, we can finish our getBean method first.

@Override
public Object getBean(String name) {
    return beanByNameMap.get(name);
}

@Override
public <T> T getBean(Class<T> tClass) {
    return tClass.cast(beanByClassMap.get(tClass));
}

Is it easy to get bean instances directly from the Map? Of course, in a real Spring container, it won't be so simple, but this time we want to simplify the complexity and understand the IOC container.

constructor

Spring provides @ ComponentScan to scan the components under the package. For convenience, we directly specify the package to scan in the constructor.

private final Set<String> basePackages;
/**
 * Default constructor, scan the current package by default
 */
public ApplicationContext() {
    this(new HashSet<>(Collections.singletonList(ApplicationContext.class.getPackage().getName())));
}

/**
 * All parameter constructor
 * @param basePackages List of scanned package names
 */
public ApplicationContext(Set<String> basePackages) {
    this.basePackages = basePackages;
}

refresh method

The process of refresh basically follows the following process

  1. Scan all classes with @ Bean annotation (in Spring, @ Component annotation) under the specified package.
List<Class> beanClasses = PackageScanner.findClassesWithAnnotation(packageName, Bean.class);
System.out.println("scan classes with Bean annotation : " + beanClasses.toString());

for (Class beanClass : beanClasses) {
    try {
        createBean(beanClass);
    } catch (ClassNotFoundException | NoSuchMethodException | IllegalAccessException | InvocationTargetException | InstantiationException e) {
        e.printStackTrace();
    }
}
  1. Traverse the class, get the constructor of the class and all fields.
Constructor constructor = beanClass.getDeclaredConstructor();
Object object = constructor.newInstance();
Field[] fields = beanClass.getDeclaredFields();
  1. Determine whether the field is dependent on the injected or normal field.

  2. If it is a normal field, initialize the field through the field type and try to get the Value from the @ Value annotation and plug it into the field.

Value value = field.getAnnotation(Value.class);
if (value != null) {
    // injection
    field.setAccessible(true);
    // Some type conversion is needed, from String to corresponding type
    field.set(object, value.value());
}
  1. If it is a dependency injected field, try to get the corresponding instance from beanByClassMap. If not, first instantiate the corresponding type of the field.
Autowired autowired = field.getAnnotation(Autowired.class);
if (autowired != null) {
    // Dependency injection
    String name = autowired.name();
    // Inject by name
    Object diObj;
    if (!name.isEmpty()) {
        diObj = beanByNameMap.get(name) == null ?
                createBean(name) :
                beanByNameMap.get(name);
    } else {
        // Inject by type
        Class<?> aClass = field.getType();
        diObj = beanByClassMap.get(aClass) == null ?
                createBean(aClass) :
                beanByClassMap.get(aClass);
    }
    // injection
    field.setAccessible(true);
    field.set(object, diObj);
}

Test our IOC container

Create Address

@Data
@Bean
public class Address {
    @Value("2222")
    private String longitude;

    @Value("1111")
    private String latitude;
}

Create Person and inject Address

@Data
@Bean
public class Person {
    @Autowired
    private Address address;

    @Value("gaochen")
    private String name;

    @Value("27")
    private String age;
}

Create test class ApplicationContextTest

public class ApplicationContextTest {

    @Test
    public void refresh() {
        Set<String> basePackages = new HashSet<>(1);
        basePackages.add("io.github.gcdd1993.ioc");
        ApplicationContext ctx = new ApplicationContext(basePackages);
        ctx.refresh();

        Person person = ctx.getBean(Person.class);
        System.out.println(person);

        Object person1 = ctx.getBean("Person");
        System.out.println(person1);
    }
}

The console will output:

scan classes with Bean annotation : [class io.github.gcdd1993.ioc.util.Address, class io.github.gcdd1993.ioc.util.Person]
scan classes with Bean annotation : [class io.github.gcdd1993.ioc.util.Address, class io.github.gcdd1993.ioc.util.Person]
Person(address=Address(longitude=2222, latitude=1111), name=gaochen, age=27)
Person(address=Address(longitude=2222, latitude=1111), name=gaochen, age=27)

As you can see, we successfully injected the Address instance into the Person instance and stored them in our own IOC container. In fact, the principle of Spring container is basically the same. In order to cope with enterprise level development, it provides many convenient functions, such as bean scope, bean custom methods and so on.

Get source code

The full source code can be obtained from my github repository Simple-IOC-Container

Posted by ExpendableDecoy on Mon, 10 Feb 2020 05:03:49 -0800