1. The essence of IOC
Map container: the essence of IOC container is a map
ApplicationContext context: holds a reference to BeanFactory
Bean factory factory: responsible for obtaining beans from containers
BeanDefinitionReader parser: responsible for parsing all configuration files
Definition meta information configuration: xml, yml, annotation and properties. In order to adapt to different configuration files, Spring uses the top-level design BeanDefinition to save the configuration information.
Bean instance: Reflection instantiation bean. The source of this bean may be a native bean or a proxy bean. No matter what kind of bean, it will be decorated by the BeanWrapper (decorator mode), which holds the reference of the bean.
BeanWrapper decorator: it is cached in the IOC container and holds the reference of the Bean
Call the getBean() method through ApplicationContext to obtain various beanfactories. Read the configuration file information through the BeanDefinitionReader and create the Bean according to the BeanDefinition. The obtained Bean, whether it is a native object or a proxy object, is handed over to the BeanWrapper for decoration. Therefore, the BeanWrapper object is obtained by the getBean() method.
2. From Servlet to ApplicationContext
In the previous article, a simple Spring framework process was implemented based on MyDispatchServlet:
- Read configuration file
- Scan files according to package address
- Initialize the IOC container, instantiate the Bean, and load it into the IOC
- Dependency injection
- Match the URL and method and store it in HandlerMapping
Steps 1.4 are both IOC and DI operations, which need to be transformed into ApplicationContext.
The new steps are:
- Read the configuration file and scan the files under the corresponding package
- Get all configuration information
- Save all configuration information
- Load all non deferred beans
The method of loading beans is the core method of Spring framework, get Bean method. The steps are as follows:
- Get configuration information according to BeanName
- Instantiate Instance
- Encapsulate an Instance into a BeanWrapper
- Dependency injection on Instance
- Load Instance into IOC
3. Combination with Spring source code
In order to realize the function, some analysis needs to be carried out in combination with the Spring source code.
BeanFactory is the core. It has a key method, getBean.
ApplicationContext class and DefaultListableBeanFactory class inherit BeanFactory.
The function of DefaultListableBeanFactory is to save all configuration information: beanDefinitionMap, three-level cache (i.e. the ultimate cache of IOC): factoryBeanInstanceCache, class backup of all beans: factoryBeanObjectCache, and implement the get Bean method
ApplicationContext holds a reference to DefaultListableBeanFactory, so you can complete the method by using some data of DefaultListableBeanFactory.
BeanDefinitionReader class is specially used to read configuration files and scan files under packages.
BeanDefinition is used to save the name of the bean: factoryBeanName, and the full class name of the bean: beanClassName
BeanWrapper is used to encapsulate the instantiated bean and finally save it to IOC, including instance object: wrappedInstance and class object: wrappedClass.
Knowing these main classes, you can start coding.
4. Implementation process
4.1. ApplicationContext
4.1. 1. Object factory
/** * Create factory for object */ public interface MyBeanFactory { Object getBean(Class beanClass); Object getBean(String beanName); }
4.1. 2. Read the configuration file and scan the package
In the constructor of ApplicationContext, perform the creation of BeanDefinitionReader to load the configuration file
public class MyApplicationContext implements MyBeanFactory { //Hold reference private MyDefaultListableBeanFactory registry = new MyDefaultListableBeanFactory(); //Profile reader private MyBeanDefinitionReader reader = null; public MyApplicationContext(String... configLocations) { //1. Load profile reader = new MyBeanDefinitionReader(configLocations); } }
The BeanDefinitionReader class has Properties and registrybeanclases. These two containers do not need to be repeated. They have appeared in the previous article
public class MyBeanDefinitionReader { //Profile information private Properties contextConfig = new Properties(); //Register file name collection private List<String> registryBeanClasses = new ArrayList<String>(); public MyBeanDefinitionReader(String... locations) { //1. Load profile doLoadConfig(locations[0]); //2. Scan related classes doScanner(contextConfig.getProperty("scanPackage")); } }
The implementation of these two methods is no different from that in the previous article.
private void doLoadConfig(String configLocation) { InputStream stream = this.getClass().getClassLoader().getResourceAsStream(configLocation.replaceAll("classpath:", "")); try { contextConfig.load(stream); } catch (IOException e) { e.printStackTrace(); } finally { if (stream != null) { try { stream.close(); } catch (IOException e) { e.printStackTrace(); } } } } private void doScanner(String scanPackage) { URL url = this.getClass().getClassLoader().getResource("/" + scanPackage.replaceAll("\\.", "/")); File aFile = new File(url.getFile()); for (File file : aFile.listFiles()) { String fileName = file.getName(); if (file.isDirectory()) { doScanner(scanPackage + "." + fileName); } if (!fileName.endsWith(".class")) { continue; } String className = scanPackage + "." + fileName.replaceAll(".class", ""); registryBeanClasses.add(className); } }
4.1. 3. Get all configuration information
First, you need to create a configuration class BeanDefinition, which has
bean Name: factoryBeanName
The full class name of the bean: beanClassName
Whether to delay loading: getLazyInit()
public class MyBeanDefinition { //beanName private String factoryBeanName; //Full class name private String beanClassName; public String getFactoryBeanName() { return factoryBeanName; } public void setFactoryBeanName(String factoryBeanName) { this.factoryBeanName = factoryBeanName; } public String getBeanClassName() { return beanClassName; } public void setBeanClassName(String beanClassName) { this.beanClassName = beanClassName; } //Whether to delay loading public boolean isLazyInit() { return false; } }
In the ApplicationContext file, add a step:
public class MyApplicationContext implements MyBeanFactory { //Hold reference private MyDefaultListableBeanFactory registry = new MyDefaultListableBeanFactory(); //Profile reader private MyBeanDefinitionReader reader = null; public MyApplicationContext(String... configLocations) { //1. Load profile reader = new MyBeanDefinitionReader(configLocations); try { //2. Parse the configuration file and encapsulate the configuration information into BeanDefinition objects List<MyBeanDefinition> beanDefinitions = reader.loadBeanDefinitions(); } catch (Exception e) { e.printStackTrace(); } }
It can be found that the method loadBeanDefinition here is similar to the instantiation process in the previous chapter, except that the bean name and full bean name of the bean object are saved this time.
public List<MyBeanDefinition> loadBeanDefinitions() { List<MyBeanDefinition> result = new ArrayList<MyBeanDefinition>(); try { for (String beanName : registryBeanClasses) { Class<?> aClass = Class.forName(beanName); if (aClass.isInterface()) { continue; } result.add(createBeanDefinition(toLowerFirstCase(aClass.getSimpleName()), aClass.getName())); for (Class<?> anInterface : aClass.getInterfaces()) { result.add(createBeanDefinition(anInterface.getName(), aClass.getName())); } } } catch (Exception e) { e.printStackTrace(); } return result; } private MyBeanDefinition createBeanDefinition(String factoryBeanName, String beanClassName) { MyBeanDefinition beanDefinition = new MyBeanDefinition(); beanDefinition.setFactoryBeanName(factoryBeanName); beanDefinition.setBeanClassName(beanClassName); return beanDefinition; } private String toLowerFirstCase(String simpleName) { char[] chars = simpleName.toCharArray(); chars[0] += 32; return String.valueOf(chars); }
4.1. 4. Save configuration information to DefaultListableBeanDefinition
Continue adding steps in ApplicationContext:
public class MyApplicationContext implements MyBeanFactory { //Hold reference private MyDefaultListableBeanFactory registry = new MyDefaultListableBeanFactory(); //Profile reader private MyBeanDefinitionReader reader = null; public MyApplicationContext(String... configLocations) { //1. Load profile reader = new MyBeanDefinitionReader(configLocations); try { //2. Parse the configuration file and encapsulate the configuration information into BeanDefinition objects List<MyBeanDefinition> beanDefinitions = reader.loadBeanDefinitions(); //3. Cache all configuration information this.registry.doRegistryBeanDefinition(beanDefinitions); } catch (Exception e) { e.printStackTrace(); } } }
The method doRegistryBeanDefinition is simply understood to register the configuration information, that is, transfer the BeanDefinition list to the BeanDefinitionMap, with the key as factoryBeanName and the value as BeanDefinition.
public class MyDefaultListableBeanFactory implements MyBeanFactory { //Save all configuration information public Map<String, MyBeanDefinition> beanDefinitionMap = new HashMap<String, MyBeanDefinition>(); //The third level cache is also the ultimate cache private Map<String, MyBeanWrapper> factoryBeanInstanceCache = new HashMap<String, MyBeanWrapper>(); private Map<String, Object> factoryBeanObjectCache = new HashMap<String, Object>(); public void doRegistryBeanDefinition(List<MyBeanDefinition> beanDefinitions) throws Exception { for (MyBeanDefinition beanDefinition : beanDefinitions) { if (beanDefinitionMap.containsKey(beanDefinition.getFactoryBeanName())) { throw new Exception("Already exists" + beanDefinition.getFactoryBeanName()); } beanDefinitionMap.put(beanDefinition.getFactoryBeanName(), beanDefinition); } } }
4.1. 5. Load non delayed Bean
In ApplicationContext, continue to add steps:
public class MyApplicationContext implements MyBeanFactory { //Hold reference private MyDefaultListableBeanFactory registry = new MyDefaultListableBeanFactory(); //Profile reader private MyBeanDefinitionReader reader = null; public MyApplicationContext(String... configLocations) { //1. Load profile reader = new MyBeanDefinitionReader(configLocations); try { //2. Parse the configuration file and encapsulate the configuration information into BeanDefinition objects List<MyBeanDefinition> beanDefinitions = reader.loadBeanDefinitions(); //3. Cache all configuration information this.registry.doRegistryBeanDefinition(beanDefinitions); //4. Load all non deferred loaded bean s doLoadInstance(); } catch (Exception e) { e.printStackTrace(); } } }
According to the configuration file information obtained from the DefaultListableBeanDefinition, the bean can be loaded in a circular manner, and the method used is also getBean. The getBean here can use the ApplicationContext method because it inherits the BeanFactory.
However, DefaultListableBeanDefinition also inherits BeanFactory, which also contains three-level cache and Bean instance saving, and ApplicationContext holds a reference to DefaultListableBeanDefinition. Therefore, getBean in Spring source code is implemented by DefaultListableBeanDefinition, and ApplicationContext only needs to reference its method.
private void doLoadInstance() { //Loop call getBean method for (Map.Entry<String, MyBeanDefinition> entry : this.registry.beanDefinitionMap.entrySet()) { String beanName = entry.getKey(); //Whether to delay loading. If not, load immediately if (!entry.getValue().isLazyInit()) { getBean(beanName); } } }
4.2. DefaultListableBeanDefinition
In fact, only the implementation of getBean is left in the DefaultListableBeanDefinition part.
Write according to the above steps
4.2. 1. Get configuration information
Steps for DefaultListableBeanDefinition class:
public class MyDefaultListableBeanFactory implements MyBeanFactory { //Save all configuration information public Map<String, MyBeanDefinition> beanDefinitionMap = new HashMap<String, MyBeanDefinition>(); //The third level cache is also the ultimate cache private Map<String, MyBeanWrapper> factoryBeanInstanceCache = new HashMap<String, MyBeanWrapper>(); private Map<String, Object> factoryBeanObjectCache = new HashMap<String, Object>(); @Override public Object getBean(String beanName) { //1. Get the corresponding beanDefinition configuration information first MyBeanDefinition beanDefinition = this.beanDefinitionMap.get(beanName); return null; } }
4.2. 2. Instantiation class
To add the DefaultListableBeanDefinition class:
public class MyDefaultListableBeanFactory implements MyBeanFactory { //Save all configuration information public Map<String, MyBeanDefinition> beanDefinitionMap = new HashMap<String, MyBeanDefinition>(); //The third level cache is also the ultimate cache private Map<String, MyBeanWrapper> factoryBeanInstanceCache = new HashMap<String, MyBeanWrapper>(); private Map<String, Object> factoryBeanObjectCache = new HashMap<String, Object>(); @Override public Object getBean(String beanName) { //1. Get the corresponding beanDefinition configuration information first MyBeanDefinition beanDefinition = this.beanDefinitionMap.get(beanName); //2. Reflect instanced objects Object instance = instantiateBean(beanName, beanDefinition); return null; } }
Method instantiateBean calls reflection to instantiate, and saves the instantiated content to factoryBeanObjectCache
private Object instantiateBean(String beanName, MyBeanDefinition beanDefinition) { String beanClassName = beanDefinition.getBeanClassName(); Object instance = null; try { Class<?> aClass = Class.forName(beanClassName); instance = aClass.newInstance(); //If it is a proxy object, the AOP proxy is triggered this.factoryBeanObjectCache.put(beanName, instance); } catch (Exception e) { e.printStackTrace(); } return instance; }
4.2. 3. Encapsulating BeanWrapper
To add the DefaultListableBeanDefinition class:
public class MyDefaultListableBeanFactory implements MyBeanFactory { //Save all configuration information public Map<String, MyBeanDefinition> beanDefinitionMap = new HashMap<String, MyBeanDefinition>(); //The third level cache is also the ultimate cache private Map<String, MyBeanWrapper> factoryBeanInstanceCache = new HashMap<String, MyBeanWrapper>(); private Map<String, Object> factoryBeanObjectCache = new HashMap<String, Object>(); @Override public Object getBean(String beanName) { //1. Get the corresponding beanDefinition configuration information first MyBeanDefinition beanDefinition = this.beanDefinitionMap.get(beanName); //2. Reflect instanced objects Object instance = instantiateBean(beanName, beanDefinition); //3. Encapsulate the returned object as a BeanWrapper MyBeanWrapper beanWrapper = new MyBeanWrapper(instance); return null; } }
To create a BeanWrapper, two properties are required
Instantiated class: wrappedInstance
Class of instantiated class: wrappedClass
public class MyBeanWrapper { private Object wrappedInstance; private Class<?> wrappedClass; public MyBeanWrapper(Object instance) { this.wrappedInstance = instance; this.wrappedClass = instance.getClass(); } public Object getWrappedInstance() { return this.wrappedInstance; } public Class<?> getWrappedClass() { return this.wrappedClass; } }
Just initialize the constructor
4.2. 4. Dependency injection
To add the DefaultListableBeanDefinition class:
public class MyDefaultListableBeanFactory implements MyBeanFactory { //Save all configuration information public Map<String, MyBeanDefinition> beanDefinitionMap = new HashMap<String, MyBeanDefinition>(); //The third level cache is also the ultimate cache private Map<String, MyBeanWrapper> factoryBeanInstanceCache = new HashMap<String, MyBeanWrapper>(); private Map<String, Object> factoryBeanObjectCache = new HashMap<String, Object>(); @Override public Object getBean(String beanName) { //1. Get the corresponding beanDefinition configuration information first MyBeanDefinition beanDefinition = this.beanDefinitionMap.get(beanName); //2. Reflect instanced objects Object instance = instantiateBean(beanName, beanDefinition); //3. Encapsulate the returned object as a BeanWrapper MyBeanWrapper beanWrapper = new MyBeanWrapper(instance); //4. Perform dependency injection populateBean(beanName, beanDefinition, beanWrapper); return null; } }
The method populateBean generates beans. This method is not very different from the previous dependency injection, so there is no need to repeat it
private void populateBean(String beanName, MyBeanDefinition beanDefinition, MyBeanWrapper beanWrapper) { Object instance = beanWrapper.getWrappedInstance(); Class<?> clazz = instance.getClass(); if (!(clazz.isAnnotationPresent(MyController.class) || clazz.isAnnotationPresent(MyService.class))) { return; } for (Field field : clazz.getDeclaredFields()) { if (!field.isAnnotationPresent(MyAutowired.class)) { continue; } MyAutowired autowired = field.getAnnotation(MyAutowired.class); String autowiredBeanName = field.getType().getName(); if (!"".equals(autowired.value())) { autowiredBeanName = autowired.value().trim(); } field.setAccessible(true); try { if (this.factoryBeanInstanceCache.get(autowiredBeanName) == null) { continue; } field.set(instance, this.factoryBeanInstanceCache.get(autowiredBeanName).getWrappedInstance()); } catch (IllegalAccessException e) { e.printStackTrace(); } } }
4.2. 5. IOC mount
To add the DefaultListableBeanDefinition class:
public class MyDefaultListableBeanFactory implements MyBeanFactory { //Save all configuration information public Map<String, MyBeanDefinition> beanDefinitionMap = new HashMap<String, MyBeanDefinition>(); //The third level cache is also the ultimate cache private Map<String, MyBeanWrapper> factoryBeanInstanceCache = new HashMap<String, MyBeanWrapper>(); private Map<String, Object> factoryBeanObjectCache = new HashMap<String, Object>(); @Override public Object getBean(String beanName) { //1. Get the corresponding beanDefinition configuration information first MyBeanDefinition beanDefinition = this.beanDefinitionMap.get(beanName); //2. Reflect instanced objects Object instance = instantiateBean(beanName, beanDefinition); //3. Encapsulate the returned object as a BeanWrapper MyBeanWrapper beanWrapper = new MyBeanWrapper(instance); //4. Perform dependency injection populateBean(beanName, beanDefinition, beanWrapper); //5. Save to IOC container this.factoryBeanInstanceCache.put(beanName, beanWrapper); return beanWrapper.getWrappedInstance(); } }
The step here is to put the final encapsulation result into the IOC, that is, the three-level cache factoryBeanInstanceCache.
4.3. Part of DispatchServlet
After completing the construction of ApplicationContext, you can remove those unnecessary steps and replace them with ApplicationContext.
public class MyDispatchServlet extends HttpServlet { private Map<String, Method> handlerMapping = new HashMap<String, Method>(); private MyApplicationContext myApplicationContext = null; @Override protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { this.doPost(req, resp); } @Override protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { try { doDispatch(req, resp); } catch (Exception e) { e.printStackTrace(); } } @Override public void init(ServletConfig config) throws ServletException { myApplicationContext = new MyApplicationContext(config.getInitParameter("contextConfigLocation")); doHandlerMapping(); } }
Without the previous IOC container, many judgments will lose their basis, so you need to get the content about the container from the ApplicationContext
Increase the method getBeanDefinitionCount to obtain the length of the configuration file
Add the method getBeanDefinitionNames to obtain the key of the configuration file, that is, the name of the bean
public class MyApplicationContext implements MyBeanFactory { //Hold reference private MyDefaultListableBeanFactory registry = new MyDefaultListableBeanFactory(); //Profile reader private MyBeanDefinitionReader reader = null; public MyApplicationContext(String... configLocations) { //1. Load profile reader = new MyBeanDefinitionReader(configLocations); try { //2. Parse the configuration file and encapsulate the configuration information into BeanDefinition objects List<MyBeanDefinition> beanDefinitions = reader.loadBeanDefinitions(); //3. Cache all configuration information this.registry.doRegistryBeanDefinition(beanDefinitions); //4. Load all non deferred loaded bean s doLoadInstance(); } catch (Exception e) { e.printStackTrace(); } } private void doLoadInstance() { //Loop call getBean method for (Map.Entry<String, MyBeanDefinition> entry : this.registry.beanDefinitionMap.entrySet()) { String beanName = entry.getKey(); //Whether to delay loading. If not, load immediately if (!entry.getValue().isLazyInit()) { getBean(beanName); } } } @Override public Object getBean(Class beanClass) { return this.getBean(beanClass.getName()); } @Override public Object getBean(String beanName) { return this.registry.getBean(beanName); } public int getBeanDefinitionCount() { return this.registry.beanDefinitionMap.size(); } public String[] getBeanDefinitionNames() { return this.registry.beanDefinitionMap.keySet().toArray(new String[0]); } }
So the rest of the judgment has a basis
public class MyDispatchServlet extends HttpServlet { private Map<String, Method> handlerMapping = new HashMap<String, Method>(); private MyApplicationContext myApplicationContext = null; @Override protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { this.doPost(req, resp); } @Override protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { try { doDispatch(req, resp); } catch (Exception e) { e.printStackTrace(); } } @Override public void init(ServletConfig config) throws ServletException { myApplicationContext = new MyApplicationContext(config.getInitParameter("contextConfigLocation")); doHandlerMapping(); } private void doDispatch(HttpServletRequest req, HttpServletResponse resp) throws InvocationTargetException, IllegalAccessException, IOException { String url = req.getRequestURI(); String contextPath = req.getContextPath(); url = url.replaceAll(contextPath, "").replaceAll("/+", "/"); if (!this.handlerMapping.containsKey(url)) { resp.getWriter().write("404 not found"); return; } Method method = handlerMapping.get(url); Map<String, Integer> paramMapping = new HashMap<String, Integer>(); Annotation[][] parameterAnnotations = method.getParameterAnnotations(); for (int i = 0; i < parameterAnnotations.length; i++) { for (Annotation annotation : parameterAnnotations[i]) { if (annotation instanceof MyRequestParam) { String value = ((MyRequestParam) annotation).value(); if (!"".equals(value)) { paramMapping.put(value, i); } } } } Class<?>[] parameterTypes = method.getParameterTypes(); for (int i = 0; i < parameterTypes.length; i++) { Class<?> parameterType = parameterTypes[i]; if (HttpServletRequest.class == parameterType || HttpServletResponse.class == parameterType) { paramMapping.put(parameterType.getName(), i); } } Object[] paramValues = new Object[paramMapping.size()]; Map<String, String[]> params = req.getParameterMap(); for (Map.Entry<String, String[]> param : params.entrySet()) { String value = Arrays.toString(param.getValue()) .replaceAll("\\[|\\]", "") .replaceAll("\\s", ""); if (!paramMapping.containsKey(param.getKey())) { continue; } paramValues[paramMapping.get(param.getKey())] = value; } if (paramMapping.containsKey(HttpServletRequest.class.getName())) { int index = paramMapping.get(HttpServletRequest.class.getName()); paramValues[index] = req; } if (paramMapping.containsKey(HttpServletResponse.class.getName())) { int index = paramMapping.get(HttpServletResponse.class.getName()); paramValues[index] = resp; } String beanName = toLowerFirstCase(method.getDeclaringClass().getSimpleName()); Object obj = myApplicationContext.getBean(beanName); method.invoke(obj, paramValues); } private void doHandlerMapping() { if (myApplicationContext.getBeanDefinitionCount() == 0) { return; } try { for (String beanName : myApplicationContext.getBeanDefinitionNames()) { Object instance = myApplicationContext.getBean(beanName); Class aClass = instance.getClass(); if (!aClass.isAnnotationPresent(MyController.class)) { continue; } String url = ""; if (aClass.isAnnotationPresent(MyRequestMapping.class)) { MyRequestMapping myRequestMapping = (MyRequestMapping) aClass.getAnnotation(MyRequestMapping.class); url = "/" + myRequestMapping.value(); } for (Method method : aClass.getMethods()) { if (!method.isAnnotationPresent(MyRequestMapping.class)) { continue; } MyRequestMapping myRequestMapping = (MyRequestMapping) method.getAnnotation(MyRequestMapping.class); handlerMapping.put((url + "/" + myRequestMapping.value()).replaceAll("/+", "/"), method); } } } catch (Exception e) { e.printStackTrace(); } } private String toLowerFirstCase(String fileName) { char[] chars = fileName.toCharArray(); chars[0] += 32; return String.valueOf(chars); } }
When all is complete, execute and access: http://localhost:8080/user/name?name=lily