Tear spring by Hand: Implement a simple IOC container

From the ground up, a simple IOC container is essentially an implementation: reading configuration files and extracting beans; registering beans to containers; loading beans from containers; this chapter implements the basic BeanFactory: supporting xml configuration beans, supporting loading single beans.

Start with the most basic IOC usage:

(1) Define spring-bean.xml:

<bean id="bean1" class="com.rui.test.TestBean1" scope="singleton">

</bean>

(2) TestBean1.java:

public class TestBean1 {
    public void test(){
        System.out.println("hello...");
    }
}

(3) Testing BeanFactoryTest.java:

public class BeanFactoryTest {
    @Test
    public void testGetBean(){
        Resource resource=new ClassPathResource("spring-bean.xml");
        BeanFactory factory=new XmlBeanFactory(resource);
        TestBean1 bean1 = (TestBean1)factory.getBean("bean1");
        bean1.test();
    }

(4) Results:

hello...

Beginning design: Type of container?

As you can see from the previous definition xml file, a bean contains attributes such as id, class, scope, and so on, so it needs to be defined with a class (BeanDefinition). In order to support annotation in addition to xml definition beans, BeanDefinition is abstracted as an interface, and finally, IOC container is designed as a map <String.BeanDefinition>, which is added after the scope attribute for a simple demonstration.

public interface BeanDefinition {
    //Get the full class name of the bean
    String getBeanClassName();
}

GenericBeanDefinition is the most basic implementation class of BeanDefinition:

public class GenericBeanDefinition implements BeanDefinition {

    private String beanId;
    private String beanClassName;

    public GenericBeanDefinition(String beanId,String beanClassName){
        this.beanId=beanId;
        this.beanClassName=beanClassName;
    }

    @Override
    public String getBeanClassName() {
        return this.beanClassName;
    }
}

Abstract: Implement Resource

Let's start with Resource resource=new ClassPathResource("spring-bean.xml");

Referring to the statement in "Spring Source Deep Resolution", the Resource interface abstracts all the underlying resources used internally in Spring. Encapsulate resource files from different sources (such as File,ClassPath) into corresponding Resource implementations: FileSystemResource, ClassPathResource, etc. Here, the Resource interface mainly provides getInputStream() The getDescription () method is also provided to print error information.

public interface Resource {

     InputStream getInputStream() throws IOException;

     String getDesciption();
}

The most central idea

BeanFactory=new XmlBeanFactory(resource);What exactly did you do in this sentence?

Master the core ideas, other abstractions are derived from this core.
At the beginning of this chapter, it is said that IOC containers are essentially: (1) reading configuration files to register extracted bean information with IOC containers; (2) loading beans from containers when beans are needed. Referring to the spring source code, there are two very core roles in the xmlBeanFactory, and the responsibilities of the XmlBeanDefinitionReader correspond to those above (1)., DefaultBeanFactory's responsibilities correspond to (2), which is inherited by xmlBeanFactory.

public class XmlBeanFactory extends DefaultBeanFactory {

    private final XmlBeanDefinitionReader reader=new XmlBeanDefinitionReader(this);

    public XmlBeanFactory(Resource resource){
        this.reader.loadBeanDefinition(resource);
    }

}
  • Bean Factory: Provides an interface for registering and loading beans
  • DefaultBeanFactory: Implement the Registered Bean method, load the Bean method [very important]
  • XmlBeanDefinitionReader: Parse the xml file and register the Bean using the DefaultBeanFactory registration method

Abstract: Implement BeanDefinitionRegistry

For factory BeanFactories, the intent of the design is to obtain information about the bean object, and the registration and retrieval of BeanDefinition are "internal". We do not want the BeanFactory interface to expose BeanDefinition registration and retrieval methods in the future, but only operations about beans, so the role of BeanDefinition Registry appears.

DefaultBeanFactory implements a method of the BeanDefinitionRegistry interface:

    @Override
    public void registerBeanDefinition(String beanId, BeanDefinition bd) {
        this.beanDefinitionMap.put(beanId,bd);
    }

    @Override
    public BeanDefinition getBeanDefinition(String beanId) {
        return this.beanDefinitionMap.get(beanId);
    }

Important To Resolve and Register XmlBeanDefinitionReader

public class XmlBeanDefinitionReader {
    //Dependent on BeanDefinition Registrar
    private BeanDefinitionRegistry registry;
    //Constructor to pass in the Registrar
    public XmlBeanDefinitionReader(BeanDefinitionRegistry registry){
        this.registry=registry;
    }
    //Resolve Resources
    public void loadBeanDefinition(Resource resource){

        try {
            InputStream inputStream = resource.getInputStream();
            SAXReader saxReader = new SAXReader();
            Document doc = saxReader.read(inputStream);//Reading the input stream of resource using the dom4j tool
            Element root= doc.getRootElement();//beans tag
            Iterator<Element> iterator = root.elementIterator();
            while(iterator.hasNext()){
                Element element= iterator.next();//bean Label
                String beanId = element.attributeValue("id");//id attribute inside bean tag
                String beanClassName = element.attributeValue("class");//class attribute inside bean tag
                //Register the bean
                GenericBeanDefinition bd = new GenericBeanDefinition(beanId, beanClassName);//Implementation class for BeanDefinition
                registry.registerBeanDefinition(beanId,bd);//Register BeanDefinition using the registrar's registration method
            }
        } catch (Exception e) {
            throw new BeanDefinitionStoreException("IOException parsing XML document from " + resource.getDesciption(),e);
        }
    }
}

Important Implement DefaultBeanFactory Load Bean

TestBean1 bean1 = (TestBean1)factory.getBean("bean1");What exactly did this sentence go through?

As mentioned earlier, DefaultBeanFactory essentially implements the getBean(String beanId) method.

DefaultBeanFactory:

public class DefaultBeanFactory implements BeanFactory,BeanDefinitionRegistry{

    private final ConcurrentHashMap<String,BeanDefinition> beanDefinitionMap= new ConcurrentHashMap<String,BeanDefinition>(64);

    private ClassLoader classLoader;

    @Override
    public Object getBean(String beanId) {
        BeanDefinition bd = this.getBeanDefinition(beanId);
        if (bd==null){
            return null;
        }
        return this.createBean(bd);
    }

    private Object createBean(BeanDefinition bd){
        String beanClassName = bd.getBeanClassName();
        ClassLoader classLoader = this.getClassLoader();
        try {
            Class<?> clazz = classLoader.loadClass(beanClassName);
            return clazz.newInstance();
        } catch (Exception e) {
            throw new BeanCreationException("create bean for "+ beanClassName +" failed",e);
        }
    }
    public ClassLoader getClassLoader(){
        return this.classLoader!=null?this.classLoader: ClassUtils.getDefaultClassLoader();
    }

Here, the embryo of a simple IOC container has been completed, supporting the registration and loading of beans.

Important Implement Single singleton

Singleton singleton mode means that when xml declares a bean, the scope property is set to singleton (default is also singleton), when only one instance of the bean exists in the IOC container, and no one loading of the bean will result in a new object. How do you achieve this?

First, add settings and acquisitions of scope properties to the BeanDefinition interface, then there are the single state (isSingleton) and the multiple state (isPrototype)

public interface BeanDefinition {

    public static final String SCOPE_SINGLETON="singleton";
    public static final String SCOPE_PROTOTYPE="prototype";
    public static final String SCOPE_DEFAULT="";

    boolean isSingleton();
    boolean isPrototype();

    String getScope();
    void setScope(String scope);

    String getBeanClassName();
}

Second, GenericBeanDefinition implements these methods, which means that the properties of the scope and the singleton/multiple state can be set when the reader parses the registered bean s.

public class GenericBeanDefinition implements BeanDefinition {

    private String beanId;
    private String beanClassName;
    private boolean isSingleton=true;
    private boolean isPrototype=false;
    private String scope=SCOPE_DEFAULT;

    @Override
    public void setScope(String scope) {
        this.scope=scope;
        this.isSingleton=SCOPE_SINGLETON.equals(scope)||SCOPE_DEFAULT.equals(scope);
        this.isPrototype=SCOPE_PROTOTYPE.equals(scope);
    }
    ......
}

Then, you need to maintain an instance when a getBean is loaded, that is, to determine whether the bean instance has been loaded each time it is loaded. SingletonBeanRegistry is abstracted here.

The implementation class DefaultSingletonBeanDefinition maintains a single object with a map (singletonBeanObjectMap).

public class DefaultSingletonBeanRegistry implements SingletonBeanRegistry {
    public final ConcurrentHashMap<String,Object> singletonBeanObjectMap=new ConcurrentHashMap<>(64);

    @Override
    public void registerSingleton(String beanId, Object singletonObject) {
        singletonBeanObjectMap.put(beanId,singletonObject);
    }

    @Override
    public Object getSingleton(String beanId) {
        return this.singletonBeanObjectMap.get(beanId);
    }
}

Finally, when loading bean s in the DefaultBeanFactory, you can get the singleton in the singletonBeanObjectMap first, and if not, createBean().

public class DefaultBeanFactory extends DefaultSingletonBeanRegistry
        implements BeanFactory,BeanDefinitionRegistry{

    @Override
    public Object getBean(String beanId) {
        BeanDefinition bd = this.getBeanDefinition(beanId);
        if (bd==null){
            return null;
        }
        if (bd.isSingleton()){
            Object singleton = this.getSingleton(beanId);
            if (singleton==null){
                singleton = this.createBean(bd);
                this.registerSingleton(beanId, singleton);
            }
            return singleton;
        }

        return this.createBean(bd);
    }

Small extension: implementation of ApplicationContext

In practice, most of the applications use the ApplicationContext instead of the BeanFactory. The ApplicationContext inherits the BeanFactory from the spring source and extends some functionality to specify where it has been expanded. To cater for the use of the ApplicationContext, here is a simple design and implementation of the ApplicationContext.

Write a test case first:

public class ApplicationContextTest {
    @Test
    public void testApplicationContext(){
        ApplicationContext context=new ClassPathXmlApplicationContext("spring-bean.xml");
        TestBean1 bean1=(TestBean1)context.getBean("bean1");
        bean1.test();
    }
}

ApplicationContext inherits the getBean functionality of BeanFactory:

ClassPathXmlApplicationContext implementation class:

public class ClassPathXmlApplicationContext implements ApplicationContext {

    private DefaultBeanFactory factory;

    public ClassPathXmlApplicationContext(String configFile) {
        this.factory=new DefaultBeanFactory();
        XmlBeanDefinitionReader reader = new XmlBeanDefinitionReader(factory);
        Resource resource = new ClassPathResource(configFile);
        reader.loadBeanDefinition(resource);
    }

    @Override
    public Object getBean(String beanId) {
        return factory.getBean(beanId);
    }
}

At this point, the test cases are running smoothly, but there are two minor issues that need to be optimized.

Design Patterns: Use of Template Method Patterns

Next, the first issue is the extension of the ApplicationContext, which mentioned earlier that our resource resources can come from different sources, including ClassPathResource and FileSystemResource.

The corresponding ApplicationContext has ClassPathXmlApplicationContext as well as FileSystemXmlApplicationContext. If there are other implementation classes of the ApplicationContext in the future, such as FileSystemXmlApplicationContext, you will have to rewrite the contents of the ClassPathXmlApplicationContext in it, except that FileSystemResource is used when the input is converted to a Resource.

public class FileSystemXmlApplicationContext extends AbstractApplicationContext {

    private DefaultBeanFactory factory;

    public FileSystemXmlApplicationContext(String configFile) {
        this.factory=new DefaultBeanFactory();
        XmlBeanDefinitionReader reader = new XmlBeanDefinitionReader(factory);
        Resource resource = new FileSystemResource(configFile);
        reader.loadBeanDefinition(resource);
    }

    @Override
    public Object getBean(String beanId) {
        return factory.getBean(beanId);
    }
}

To address this issue, you first need to abstract a new class AbstractApplicationContext to solve the problem of duplicate code for each implementation class, and then use the template method pattern, which is the template/skeleton (including the abstract method) that executes well defined in the parent class.Resolves the problem of using different Resource implementation classes (ClassPathResource and FileSystemResource) when converting files from different sources into Resources.

AbstractApplicationContext Abstract class:

public abstract class AbstractApplicationContext implements ApplicationContext {

	private DefaultBeanFactory factory = null;
	
	public AbstractApplicationContext(String configFile){
		factory = new DefaultBeanFactory();
		XmlBeanDefinitionReader reader = new XmlBeanDefinitionReader(factory);	
		Resource resource = this.getResourceByPath(configFile);
		reader.loadBeanDefinitions(resource);
		
	}
	
	public Object getBean(String beanID) {
		
		return factory.getBean(beanID);
	}
	
	protected abstract Resource getResourceByPath(String path);

}

ClassPathXmlApplicationContext:

public class ClassPathXmlApplicationContext extends AbstractApplicationContext {
	public ClassPathXmlApplicationContext(String configFile) {
		super(configFile);
		
	}

	@Override
	protected Resource getResourceByPath(String path) {
		
		return new ClassPathResource(path);
	}

}

Abstract: Implement Configurable BeanFactory

The second minor issue is about ClassLoader, where getBeanClassLoader is used in many places, such as when ClassPathResource fetches streams and DefaultBeanFactory when creating teBeans, but the current implementations are all write-dead by default (ClassUtils.getDefaultClassLoader())., we want to support the import of ClassLoader, so abstract the ConfigurableBeanFactory interface.

This Section Class Diagram Full Family

Tags: Java Spring ioc

Posted on Tue, 21 Sep 2021 14:31:28 -0400 by jesse24