Spring IOC prototype
The basic core and starting point of the spring framework is undoubtedly IOC. As the core technology provided by spring containers, IOC successfully completed the inversion of dependencies: from the active management of the main class on dependencies to the global control of the dependencies by spring containers.
What are the benefits of doing this?
Of course, it is the so-called "decoupling", which can make the relationship between the modules of the program more independent. Spring only needs to control the dependencies between these modules and create, manage and maintain these modules based on these dependencies during the container startup and initialization process. If you need to change the dependencies between modules, you do not even need to change the program code. You only need to modify the changed dependencies. Spring will re-establish these new dependencies in the process of starting and initializing the container again. In this process, it is necessary to note that the code itself does not need to reflect the declaration of the specific dependency situation of the module but only needs to define the interface of the required module. Therefore, this is a typical interface-oriented idea. At the same time, it is best to express the dependencies in the form of configuration files or annotations. The relevant spring processing classes will assemble modules based on these external configuration files, or scan the annotation to call the internal annotation processor to assemble modules to complete the IOC process.
The purpose of IOC is a dependency injection called DI. Through IOC technology, the ultimate container will help us complete dependency injection between modules.
In addition, the final point is that in the process of spring IOC, we must always be clear about the above main line. No matter how complex the real-time syntax and class structure are, its function and purpose are the same: it is to complete the "assembly" of the module by relying on the described configuration file as the assembly "drawing". Complex syntax is just a means to accomplish this purpose.
The so-called IOC prototype, in order to show the simplest IOC schematic diagram, we might as well make a completely simple prototype to illustrate this process:
First, there are several modules we define, including the main module and the dependency module defined by the two interfaces:
class MainModule{ private DependModuleA moduleA; private DependModuleB moduleB; public DependModuleA getModuleA() { return moduleA; } public void setModuleA(DependModuleA moduleA) { this.moduleA = moduleA; } public DependModuleB getModuleB() { return moduleB; } public void setModuleB(DependModuleB moduleB) { this.moduleB = moduleB; } }interface DependModuleA{ public void funcFromModuleA();}interface DependModuleB{ public void funcFromModuleB();}class DependModuleAImpl implements DependModuleA{ @Override public void funcFromModuleA() { System.out.println("This is func from Module A"); } }class DependModuleBImpl implements DependModuleB{ @Override public void funcFromModuleB() { System.out.println("This is func from Module B"); } }If we do not adopt IOC, but rely on the main module itself to control the creation of its dependent module, then it will be like this:
public class SimpleIOCDemo { public static void main(String[] args) throws ClassNotFoundException { MainModule mainModule = new MainModule(); mainModule.setModuleA(new DependModuleAImpl()); mainModule.setModuleB(new DependModuleBImpl()); mainModule.getModuleA().funcFromModuleA(); mainModule.getModuleB().funcFromModuleB(); }}This is our simplified definition of IOC container prototype. When the container is initialized after startup, it will read the configuration file written by the user. Here we take the simple properties configuration file as an example. Only when the user calls the getBean method, the corresponding bean will be truly assembled and loaded according to the configuration file. A map used to save the assembled bean is maintained inside the container prototype we defined. If there is a bean that meets the requirements, it does not need to be created again:
class SimpleIOCContainer{ private Properties properties = new Properties(); private Map<String, Object> moduleMap = new HashMap<>(); { try { properties.load(new FileInputStream(new File("SimpleIOC.properties"))); } catch (Exception e) { e.printStackTrace(); } } public Object getBean(String moduleName) throws ClassNotFoundException { Object instanceObj; if(moduleMap.get(moduleName)!=null){ System.out.println("return old bean"); return moduleMap.get(moduleName); } System.out.println("create new bean"); String fullClassName = properties.getProperty(moduleName); if(fullClassName == null) throw new ClassNotFoundException(); else{ Class<? extends Object> clazz = Class.forName(fullClassName); try { instanceObj = clazz.newInstance(); instanceObj = buildAttachedModules(moduleName,instanceObj); moduleMap.put(moduleName, instanceObj); return instanceObj; } catch (InstantiationException e) { e.printStackTrace(); } catch (IllegalAccessException e) { e.printStackTrace(); } } return null; } private Object buildAttachedModules(String modulename, Object instanceObj) { Set<String> propertiesKeys = properties.stringPropertyNames(); Field[] fields = instanceObj.getClass().getDeclaredFields(); for (String key : propertiesKeys) { if(key.contains(modulename)&&!key.equals(modulename)){ try { Class<? extends Object> clazz = Class.forName(properties.getProperty(properties.getProperty(key))); for (Field field : fields) { if(field.getType().isAssignableFrom(clazz)) field.set(instanceObj, clazz.newInstance()); } } catch (Exception e) { e.printStackTrace(); } } } return instanceObj; }}This is the dependency configuration file we wrote using the properties configuration file. This configuration file is the "drawing" of our assembly module. The syntax here is completely defined by us. In the real spring IOC container, in order to express more complex dependency logic, a more developed xml format configuration file or newer annotation configuration will be used, and the annotation processor will be used to complete the analysis of the drawing:
mainModule=com.rocking.demo.MainModulemainModule.moduleA=moduleAmainModule.moduleB=moduleBmoduleA=com.rocking.demo.DependModuleAImplmoduleB=com.rocking.demo.DependModuleBImpl
This is the test code. It can be seen that we can completely obtain modules that meet the requirements through the IOC container we defined. At the same time, we can also find that the container we defined can maintain these beans for us. When a bean has been assembled and created, it does not need to be created again.
public class SimpleIOCDemo { public static void main(String[] args) throws ClassNotFoundException { SimpleIOCContainer container = new SimpleIOCContainer(); DependModuleA moduleA = (DependModuleA) container.getBean("moduleA"); moduleA.funcFromModuleA(); DependModuleB moduleB = (DependModuleB) container.getBean("moduleB"); moduleB.funcFromModuleB(); MainModule mainModule = (MainModule) container.getBean("mainModule"); mainModule.getModuleA().funcFromModuleA(); mainModule.getModuleB().funcFromModuleB(); container.getBean("mainModule"); }}This is the IOC container prototype I created based on the basic idea of IOC. Although spring IOC has complex syntax, the tasks completed in the end are the same at the core, so-called "all changes will not be separated from their essence."
Spring IOC specific process
Last time, the prototype of the general implementation of IOC was shown. So how to specifically implement the process of loading POJOs in the Spring framework of this container based on metadata meta information configuration? There are many places in the entire Spring IOC container work process that are designed to be quite flexible, providing users with a lot of space to complete their own tasks, rather than just completing the mechanical process of the container.
This is the process diagram of the entire IOC container working process:
1. Container startup stage (1) Loading configuration file information (2) Analyzing configuration file information (3) Assembly BeanDefinition
(4) Post-processing First, meta-information such as configuration files or annotations and JavaBean class information are loaded into the IOC container. The container reads an xml-format configuration file. This configuration file is a dependency declared by the user and the assembly that needs special attention. It is an early "external drawing" for assembling the bean. The parsing engine in the container can parse the character meta-information in the text form we write into a BeanDefinition that can be recognized inside the container, which can understand the BeanDefinition. It becomes a class structure similar to the reflection mechanism. This BeanDefinition obtained by analyzing JavaBeans and configuration files obtains the basic structure of assembling a JavaBean that meets the requirements. If you need to modify the BeanDefinition in addition to the BeanDefinition, this post-processing is performed. The post-processing is generally processed through the BeanFactoryPostProcessor in the Spring framework.
We still use the examples we used last time to illustrate the operating principle of this BeanDefinition: there are three beans, the main module MainModule and the dependency modules DependModuleA and DependModuleB. The former depends on the latter two modules. In the configuration file, we generally declare dependencies like this:
<?xml version="1.0" encoding="UTF-8"?><beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-3.0.xsd"> <bean id="mainModule"> <property name="moduleA"> <ref bean="moduleA"/> </property> <property name="moduleB"> <ref bean="moduleB"/> </property> </bean> <bean id="moduleA"></bean> <bean id="moduleB"></bean></beans>
This is our program demonstrating the assembly of a standard BeanFactory container (one of the implementations of Spring IOC containers) to the above configuration file:
class MainModule { private DependModuleA moduleA; private DependModuleB moduleB; public DependModuleA getModuleA() { return moduleA; } public void setModuleA(DependModuleA moduleA) { this.moduleA = moduleA; } public DependModuleB getModuleB() { return moduleB; } public void setModuleB(DependModuleB moduleB) { this.moduleB = moduleB; }}interface DependModuleA { public void funcFromModuleA();}interface DependModuleB { public void funcFromModuleB();}class DependModuleAImpl implements DependModuleA { @Override public void funcFromModuleA() { System.out.println("This is func from Module A"); }}class DependModuleBImpl implements DependModuleB { @Override public void funcFromModuleB() { System.out.println("This is func from Module B"); }}public class SimpleIOCDemo { public static void main(String[] args) throws ClassNotFoundException { DefaultListableBeanFactory beanFactory = new DefaultListableBeanFactory(); XmlBeanDefinitionReader reader = new XmlBeanDefinitionReader(beanFactory); reader.loadBeanDefinitions("Beans.xml"); MainModule mainModule = (MainModule) beanFactory.getBean("mainModule"); mainModule.getModuleA().funcFromModuleA(); mainModule.getModuleB().funcFromModuleB(); }}Here our configuration file and JavaBean are loaded and read and parsed. Here the BeanDefinition generation and usage process is hidden in it. This is the general process that actually happens inside the IOC:
public class SimpleIOCDemo { public static void main(String[] args) throws ClassNotFoundException { DefaultListableBeanFactory beanFactory = new DefaultListableBeanFactory(); AbstractBeanDefinition mainModule = new RootBeanDefinition(MainModule.class); AbstractBeanDefinition moduleA = new RootBeanDefinition(DependModuleAImpl.class); AbstractBeanDefinition moduleB = new RootBeanDefinition(DependModuleBImpl.class); beanFactory.registerBeanDefinition("mainModule", mainModule); beanFactory.registerBeanDefinition("moduleA", moduleA); beanFactory.registerBeanDefinition("moduleB", moduleB); MutablePropertyValues propertyValues = new MutablePropertyValues(); propertyValues.add("moduleA", moduleA); propertyValues.add("moduleB", moduleB); mainModule.setPropertyValues(propertyValues); MainModule module = (MainModule) beanFactory.getBean("mainModule"); module.getModuleA().funcFromModuleA(); module.getModuleB().funcFromModuleB(); }}After loading and reading the meta information of the xml, the IOC parsing engine will create the module mentioned in it into a BeanDefinition based on its real type. This BeanDefinition can be regarded as a reflection or proxy process. The purpose is to make the IOC container clear the bean structure of the instance object to be created in the future, and then register these bean structures into the BeanFactory, and then add the dependencies of the main module to the properties of the main module in the form of setter injection (this depends on whether the setter method or the initialization method provided by the main module.) After registering the definition of all beans specified on the "drawings", the BeanFactory has already taken shape. After that, just call the getBean method to produce the beans that meet the requirements. This is the next stage of the process, and we will talk about it later.
After registering the information on the "drawing" of BeanDefinition to the BeanFactory, we can still make changes to the registered BeanDefinition. This is one of the flexible aspects of Spring design for users mentioned earlier. It does not mean that all processes are uncontrollable, but leaves a lot of room for users to play in many places. The specific method is to use the BeanFactory processor BeanFactoryPostProcessor to intervene in the processing of BeanFactory to further rewrite the BeanDefinition part we need to modify. This process corresponds to the "post-processing" process in the process.
Taking one of the common processors: attribute placeholder configuration processor as an example, it is to process the registered BeanFactory after it has been built, so that the contents in the corresponding BeanDefinition attribute are modified to the information in the configuration processor specified configuration file:
DefaultListableBeanFactory beanFactory = new DefaultListableBeanFactory();XmlBeanDefinitionReader reader = new XmlBeanDefinitionReader(beanFactory);reader.loadBeanDefinitions( new ClassPathResource( "Beans.xml"));PropertyPlaceholderConfigurer configurer = new PropertyPlaceholderConfigurer();configurer.setLocation( new ClassPathResource( "about.properties"));configurer.postProcessBeanFactory( beanFactory);
The BeanFactoryPostProcessor will process the BeanFactory. The result is to change some attributes defined in the BeanDefinition to some information at the location of the BeanFactoryPostProcessor.
2. Bean instantiation stage
Under the guidance of the processed "internal drawings" of BeanDefinition, the container can further transform BeanDefifnition into an activated instance object existing in memory through reflection or CGLIB dynamic bytecode production, and then assemble the dependency object specified by BeanDefinition into the newly created instance object through setter injection or initialization injection. Here, the reference of the dependency object is actually assigned to the object attributes that need to be depended on.
But it should be noted here that the instance created is not just a simple instance of bean definition, but a BeanWrapper instance wrapped by Spring. Why should be used to wrap the bean in the BeanWrapper method? Because BeanWrapper provides an interface to access bean properties uniformly. After creating the basic bean framework, the properties in it must be set. The setter method of each bean is different, so it will be very complicated if you set it directly with reflection. Therefore, spring provides this wrapper to simplify the property settings:
BeanWrapper beanWrapper = new BeanWrapperImpl(Class.forName("com.rocking.demo.MainModule"));beanWrapper.setPropertyValue( "moduleA", Class.forName("com.rocking.demo.DepModuleAImpl").newInstance());beanWrapper.setPropertyValue( "moduleB", Class.forName("com.rocking.demo.DepModuleBImpl").newInstance());MainModule mainModule= (MainModule) beanWrapper.getWrappedInstance();mainModule.getModuleA().funcFromA();mainModule.getModuleB().funcFromB();The above process shows that in Spring, you can understand the structure of the instance bean that is wrapped in the future by obtaining the reflective container of the class and making the packaging. Use the unified property setting method setPropertyValue to set properties for the instance of this package. The final bean instance obtained is obtained through getWrappedInstance, and you can find that its attributes have been successfully assigned.
At this time, the bean instance is actually completely usable, but Spring also prepared flexible strategies for us in the instantiation stage to complete the user's intervention in this stage. Similar to the BeanFactoryPostProcessor control BeanDefinition in the container startup stage, during the instantiation stage, Spring provides a BeanPostProcessor processor to operate on the assembled instances to complete possible changes:
Here is an example to illustrate that you define a BeanPostProcessor implementation class, implementing the methods postProcessAfterInitialization and postProcessBeforeInitialization to define operations performed separately after and before bean instance assembly. After the BeanFactory adds this processor, each time the getBean method assembled, the two methods will be called in the bean instance assembled according to the "drawing" (including the dependent instance bean created during the assembly process). These methods can modify these bean instances.
Here is an example like this (MainModule and its dependencies are the same as those previously in this article):
class ModuleC { private String x; public String getX() { return x; } public void setX(String x) { this.x = x; } }class ModulePostProcessor implements BeanPostProcessor{ @Override public Object postProcessAfterInitialization(Object object, String string) throws BeansException { System.out.println(string); if(object instanceof ModuleC) { System.out.println(string); ((ModuleC)object).setX("after"); } return object; } @Override public Object postProcessBeforeInitialization(Object object, String string) throws BeansException { if(object instanceof ModuleC){ ((ModuleC)object).setX("before"); } return object; } } public class VerySimpleIOCKernal { public static void main(String[] args) throws ClassNotFoundException, BeansException, InstantiationException, IllegalAccessException { DefaultListableBeanFactory beanFactory = new DefaultListableBeanFactory(); XmlBeanDefinitionReader reader = new XmlBeanDefinitionReader(beanFactory); reader.loadBeanDefinitions(new ClassPathResource("Beans.xml")); ModulePostProcessor postProcessor = new ModulePostProcessor(); beanFactory.addBeanPostProcessor(postProcessor); MainModule module = (MainModule) beanFactory.getBean("mainModule"); ModuleC moduleC = (ModuleC) beanFactory.getBean("moduleC"); System.out.println(moduleC.getX()); }}This is the dependency configuration file for the bean:
<?xml version="1.0" encoding="UTF-8"?><beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd"> <bean id="mainModule"> <property name="moduleA"> <ref bean="moduleA"/> </property> <property name="moduleB"> <ref bean="moduleB"/> </property> </bean> <bean id="moduleA"> <property name="infoA"> <value>${moduleA.infoA}</value> </property> </bean> <bean id="moduleB"> <property name="infoB"> <value>info of moduleB</value> </property> </bean> <bean id="moduleC"> </bean></beans>From the final result, we can see that each time the getBean instance (including generated by dependencies) obtained by calling the getBean method will be retrieved by the BeanPostProcessor for pre- and post-processing.
In addition to processing assembled beans similar to the above BeanPostProcessor, Spring can also set callback functions for the initialization and destruction process of beans by configuring init-method and destroy-method. These callback functions can also flexibly provide the opportunity to change bean instances.
The entire Spring IOC process is actually essentially the same as the IOC prototype we wrote ourselves, except that the complex design allows the IOC process to provide users with more flexibly and effectively space. In addition, Spring's IOC has also achieved exquisite design in terms of security, container stability, and metadata to bean conversion efficiency, making the foundation of IOC, a Spring container, stable.