# simple_spring **Repository Path**: yudanx/simple_spring ## Basic Information - **Project Name**: simple_spring - **Description**: 以一个基础的项目结构,从零出发,手写一个麻雀虽小但五脏俱全的简单Spring框架。 Start Time Of Project: 2023-04-17 - **Primary Language**: Unknown - **License**: MulanPSL-2.0 - **Default Branch**: master - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 0 - **Forks**: 0 - **Created**: 2023-04-17 - **Last Updated**: 2023-05-09 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README ## 1. 自定义实现Bean容器 ### 1.1、自定义简单Bean容器 在最初版本中,通过一个简单的 Map 去实现简单容器,利用 Map 的特性并且使用 ConcurrentHashMap 避免并发问题的出现,从而实现 Bean 对象的定义、注册和获取。 1. 通过 Map 定义 Bean 容器 ```java public class BeanDefinition { private Object bean; public BeanDefinition(Object object) { this.bean = object; } public Object getBean() { return bean; } } ``` 2. 自定义 BeanFactory 工厂类实现 Bean 对象的注册和获取 ```java public class BeanFactory { private Map beanDefinitionMap = new ConcurrentHashMap<>(); /** * @description 通过bean名称获取bean * @author xbaozi * @date 2023/4/17 18:47 * @param name bean名称 **/ public Object getBean(String name) { return beanDefinitionMap.get(name).getBean(); } /** * @description 注册bean * @author xbaozi * @date 2023/4/17 18:47 * @param name bean名称 * @param beanDefinition bean对象 **/ public void registerBeanDefinition(String name, BeanDefinition beanDefinition) { beanDefinitionMap.put(name, beanDefinition); } } ``` ### 1.2、完善Bean容器 在前面自定义简单Bean容器的时候,是通过一个简单的map进行实现的。但是真实情况应该**通过Bean容器创建Bean对象**,而不是在调用时传递一个完成了实例化的Bean对象。 还需要考虑单例Bean对象问题,当第二次获取对象时应该从内存中获取,避免生成过多相同的对象。 此外还需要考虑整体的扩展性,因此需要使用设计模式来开发容器的功能结构,按照类的功能拆分出不同的接口和实现类。 ![bean uml](.README_images/完善Bean_UML.png) #### 测试代码 下述代码运行解析如下(Debug结果): 1. 初始化BeanFactory 2. 注册bean 1. 只在 `DefaultListableBeanFactory` 中的 `beanDefinitionMap` 存放类信息; 3. 第一次获取bean 1. 先从 `DefaultSingletonBeanRegistry` 中的 `singletonObjects` 查看是否有创建过 bean 单例; 2. 如果存在直接返回; 3. 如果不存在先从 `DefaultListableBeanFactory` 中的 `beanDefinitionMap` 中获取到该 bean 的类信息; 4. 之后调用 `AbstractAutowireCapableBeanFactory` 中实现的 `createBean` 方法创建 bean 单例并放入 `DefaultSingletonBeanRegistry` 中的 `singletonObjects`; 5. 返回创建之后的 bean 单例; 4. 第二次获取bean 1. 先从`DefaultSingletonBeanRegistry` 中的 `singletonObjects` 查看是否有创建过 bean 单例; 2. 因为是第一次已经往 `singletonObjects` 中写入 bean 单例,因此能够直接获取到 bean 单例; 3. 返回获取到的 bean 单例; 5. 判断是否单例 1. 因为第二次获取到的 bean 对象是第一次创建的成功时放入内存 `singletonObjects` 中的对象,因此两者是同一个对象,为单例。 ```java public class ApiTest { /** * @description 测试完善后的bean容器 * @author xbaozi * @date 2023/4/20 15:46 **/ @Test public void testBeanFactory() { // 初始化 BeanFactory DefaultListableBeanFactory beanFactory = new DefaultListableBeanFactory(); // 注册bean BeanDefinition beanDefinition = new BeanDefinition(UserService.class); beanFactory.registerBeanDefinition("userService", beanDefinition); // 第一次获取bean UserService userService = (UserService) beanFactory.getBean("userService"); userService.queryUserInfo(); // 第二次获取bean from Singleton UserService userServiceSingleton = (UserService) beanFactory.getBean("userService"); userServiceSingleton.queryUserInfo(); // 判断两个bean System.out.println("isSingleton: " + (userService == userServiceSingleton)); } } ``` 运行结果如下: ```text 查询用户信息…… 查询用户信息…… isSingleton: true ``` #### Q&A > Q: 为什么Spring容器中使用的是 ConcurrentHashMap 而不是 HashMap? A: 使用 ConcurrentHashMap 是因为它是**线程安全**的,而且**具有较高的并发性能**。 在多线程环境下,ConcurrentHashMap 能够保证数据的**一致性和正确性**,同时也不会牺牲太多性能。 与此相比,在单线程环境下,ConcurrentHashMap 的性能可能不如 HashMap,但是在多线程环境下, ConcurrentHashMap 能够**避免出现死锁**等问题,提高了程序的稳定性和可靠性。 由于 Spring 容器要管理大量的对象,需要在各个地方频繁地读取和写入对象,并且容器本身也是一个多线程的环境, 所以使用 ConcurrentHashMap 能够有效地提高容器的**并发性能和稳定性**。 > Q: 在Spring 容器中,为什么注册的时候只是注册类信息,直到第一次获取bean的时候才实例化bean放到缓存中,直接注册的时候放到缓存中不是更方便吗? A: 在Spring容器中,注册类信息时并不会立即实例化bean并将其放入缓存中,而是在第一次请求获取bean时才进行实例化并放到缓存中。这是因为如果在注册时就实例化并放入缓存中,可能会导致以下问题: - **性能问题**:如果注册了大量的bean,那么在启动应用程序时需要实例化所有的bean,这将消耗大量时间和资源; - **循环依赖问题**:如果两个bean之间存在循环依赖关系,那么在注册时就会出现无限递归的情况,从而导致应用程序崩溃; - **延迟初始化问题**:有时候我们希望某些bean只在需要时才进行初始化,如果在注册时就实例化这些bean,就无法实现延迟初始化的效果。 ### 1.3、使用CGLIB补充有参构造实例化 在前面完善的时候只考虑到了空参构造的实例化存在进容器中,而忽略了对象的有参实例化,导致在注入只具备有参构造器的对象时报异常。 因此在注入bean时需要考虑类中是否含有带入参信息的构造函数。 解决上述问题主要需要考虑两方面,一方面是如何将构造函数的入参信息合理地传递到实例化中,另一方面是如何将汉以后带入参信息的构造函数的类实例化。 这一共有两种方式可以选择,一种是基于Java本身自带的方法 `DeclaredConstructor`,另一种是使用 `CGLIB` 动态创建 Bean 对象。 在代码中使用的是 CGLIB 实例化策略进行 Bean 实例化,JDK 实例化策略可自行查看 `SimpleInstantiationStrategy`。 ```java /** * @author xbaozi * @version 1.0 * @classname CglibSubclassingInstantiationStrategy * @date 2023-04-20 20:18 * @description Cglib实例化策略 */ public class CglibSubclassingInstantiationStrategy implements InstantiationStrategy { @Override public Object instantiate(BeanDefinition beanDefinition, String beanName, Constructor ctor, Object[] args) throws BeansException { Enhancer enhancer = new Enhancer(); enhancer.setSuperclass(beanDefinition.getBeanClass()); // 设置空的回调 enhancer.setCallback(new NoOp() { @Override public int hashCode() { return super.hashCode(); } }); if (null == ctor) { // 使用无参构造器实例化对象 return enhancer.create(); } // 使用与argumentTypes参数匹配的超类的构造函数和给定的参数实例化对象 return enhancer.create(ctor.getParameterTypes(), args); } } ``` 并且在创建 Bean 创建工厂中调用 CGLIB 策略进行实例化 ```java /** * @author xbaozi * @version 1.0 * @classname AbstractAutowireCapableBeanFactory * @date 2023-04-18 17:40 * @description 实现默认 bean 创建的抽象 bean 工厂父类,用于实现创建对象的具体功能 */ public abstract class AbstractAutowireCapableBeanFactory extends AbstractBeanFactory { private InstantiationStrategy instantiationStrategy = new CglibSubclassingInstantiationStrategy(); @Override protected Object createBean(String beanName, BeanDefinition beanDefinition, Object[] args) throws BeansException { Object bean = null; try { bean = createBeanInstance(beanDefinition, beanName, args); } catch (Exception e) { throw new BeansException("Instantiation of bean failed", e); } registerSingleton(beanName, bean); return bean; } /** * @param beanDefinition bean 定义 * @param beanName bean 名称 * @param args 用显式参数创建 bean 实例时使用的参数 * @return Object * @description 创建 bean 实例 * @author xbaozi * @date 2023/4/20 20:46 **/ protected Object createBeanInstance(BeanDefinition beanDefinition, String beanName, Object[] args) { Constructor constructorToUse = null; Class beanClass = beanDefinition.getBeanClass(); // 获取所有构造函数 Constructor[] declaredConstructors = beanClass.getDeclaredConstructors(); for (Constructor ctor : declaredConstructors) { // 判断构造函数是否合法 if (null != args && ctor.getParameterTypes().length == args.length) { constructorToUse = ctor; break; } } // 实例化 return getInstantiationStrategy().instantiate(beanDefinition, beanName, constructorToUse, args); } public InstantiationStrategy getInstantiationStrategy() { return instantiationStrategy; } } ``` 测试策略代码如下: ```java public class ApiTest { /** * @description 测试使用Cglib实现注入bean * @author xbaozi * @date 2023/4/20 21:32 **/ @Test public void testConstructorBeanFactoryCglib() { // 初始化 BeanFactory DefaultListableBeanFactory beanFactory = new DefaultListableBeanFactory(); // 注册bean对象 BeanDefinition beanDefinition = new BeanDefinition(UserService.class); beanFactory.registerBeanDefinition("userService", beanDefinition); // 获取bean对象 // UserService userServiceNoneConstructor = (UserService) beanFactory.getBean("userService"); UserService userService = (UserService) beanFactory.getBean("userService", "xbaoziplus"); // userServiceNoneConstructor.queryUserInfo(); userService.queryUserInfo(); } } ``` 运行结果如下: ```text 查询用户 [xbaoziplus] 信息…… ``` > 从不断完善、增加需求的过程中可以看出,当代吗结构设计得当时,就可以很容易且方便地扩展不同属性类的职责,而不会因为需求的增加导致类的结构混乱。 ### 1.4、注入属性 在前面完善的时候,虽然考虑到了构造器的问题,但是还忽略了很重要一点,那就是“**类中是否有属性**”。如果类中包含属性,按照惯例,我们就需要在实例化Bean时对属性信息进行填充,这样才是一个完整的对象。 在填充属性信息时,我们需要考虑到属性不只有基本类型,还包括可能没有被实例化的对象属性,这些都需要在创建Bean对象时填充。而这里暂时不对循环依赖这一问题进行考虑,这是后续需要完善的点之一。 值得注意的是属性填充是在**实例化对象策略执行完毕之后**开始执行的,因此得益于之前结构的设计,我们可以直接在 `AbstractAutowireCapableBeanFactory` 类中进行属性填充操作。 这里使用的是Hutool工具包中的 `setFieldValue` 方法进行填充,底层是通过反射进行实现的,读者可自行使用反射进行实现,或引入Hutool的依赖即可。 #### 工程结构 此时的工程结果如下: ![1_4工程结构](.README_images/1_4工程结构.png) 主要变化的有如下类: - 【新增】`BeanReference`:类引用; - 【新增】`PropertyValue`:用于保存单个 bean 属性的信息和值的对象; - 【新增】`PropertyValues`:包含一个或多个 PropertyValue 对象的持有者; - 【修改】`BeanDefinition`:补全 Bean 对象的定义; - 【修改】`AbstractAutowireCapableBeanFactory`:补充对 Bean 对象属性的填充。 #### 核心代码 这里主要展示 `AbstractAutowireCapableBeanFactory` 中填充属性的代码: ```java /** * @description 填充属性值 * @author xbaozi * @date 2023/4/24 16:38 * @param beanName bean 名称 * @param bean bean 对象 * @param beanDefinition bean 定义 **/ protected void applyPropertyValue(String beanName, Object bean, BeanDefinition beanDefinition) { try { PropertyValues propertyValues = beanDefinition.getPropertyValues(); for (PropertyValue propertyValue : propertyValues.getPropertyValues()) { String name = propertyValue.getName(); Object value = propertyValue.getValue(); if (value instanceof BeanReference) { // 如果 A 依赖 B,获取 B 的实例化对象 BeanReference beanReference = (BeanReference) value; value = getBean(beanReference.getBeanName()); } // 属性填充 BeanUtil.setFieldValue(bean, name, value); } } catch (Exception e) { throw new BeansException("Error setting property values: " + beanName); } } ``` #### 注入测试 准备好模拟DAO ```java public class UserDao { private static Map hashMap = new HashMap<>(); static { hashMap.put("10001", "xbaoziplus"); hashMap.put("10002", "陈宝子"); hashMap.put("10003", "爱吃鱼蛋"); } public String queryUserName(String uId) { return hashMap.get(uId); } } ``` 测试实例 ```java @Test public void testPropertyBeanFactory() { // 1.初始化 BeanFactory DefaultListableBeanFactory beanFactory = new DefaultListableBeanFactory(); // 2. UserDao 注册 beanFactory.registerBeanDefinition("userDao", new BeanDefinition(UserDao.class)); // 3. UserService 设置属性[uId、userDao] PropertyValues propertyValues = new PropertyValues(); propertyValues.addPropertyValue(new PropertyValue("uId", "10001")); propertyValues.addPropertyValue(new PropertyValue("userDao",new BeanReference("userDao"))); // 4. UserService 注入bean BeanDefinition beanDefinition = new BeanDefinition(UserService.class, propertyValues); beanFactory.registerBeanDefinition("userService", beanDefinition); // 5. UserService 获取bean UserService userService = (UserService) beanFactory.getBean("userService"); userService.queryUserInfo(); } ``` ## 2、资源加载器解析 ### 2.1、当前问题 通过前面对Spring框架的搭建之后,我们可以通过手动操作完成对 Bean 对象的定义、注册和属性填充,以及获取对象 调用的方法。但在实际开发过程中,让用户通过手动方式创建对象显然是不太实际的,而是要通过 Spring **配置文件**来 简化创建的过程。 ### 2.2、结构设计 既然是需要对配置文件进行解析,那么我们对应的就需要在现有的基础上添加一个资源加载器用于读取 ClassPath、本地文件 和远程云文件的配置内容,这些配置内容中包含了 Bean 对象的描述信息和属性信息。我们需要通过定义的资源加载器 读取 Bean 描述信息解析之后,再将其注册到 Spring Bean 容器中。 主要有所变动的的结构如下: ![](.README_images/结构类图.png) 新增的文件如下: - com.xbaoziplus.simple_spring.beans.factory.`support`.AutowireCapableBeanFactory - com.xbaoziplus.simple_spring.beans.factory.`support`.ConfigurableBeanFactory - com.xbaoziplus.simple_spring.beans.factory.`support`.AbstractBeanDefinitionReader - com.xbaoziplus.simple_spring.beans.factory.`support`.BeanDefinitionReader - com.xbaoziplus.simple_spring.beans.factory.`xml`.XmlBeanDefinitionReader - com.xbaoziplus.simple_spring.`core.io`.ClassPathResource - com.xbaoziplus.simple_spring.`core.io`.DefaultResourceLoader - com.xbaoziplus.simple_spring.`core.io`.FileSystemResource - com.xbaoziplus.simple_spring.`core.io`.Resource - com.xbaoziplus.simple_spring.`core.io`.ResourceLoader - com.xbaoziplus.simple_spring.`core.io`.UrlResource - com.xbaoziplus.simple_spring.`util`.ClassUtils 为了能把 Bean 的定义、注册和初始化交给 Spring.xml 配置化处理,那么就需要实现两大块内容,分别是:资源加载器、xml资源处理类,实现过程主要以对接口 Resource、ResourceLoader 的实现,而另外 BeanDefinitionReader 接口则是对资源的具体使用,将配置信息注册到 Spring 容器中去。 在 Resource 的资源加载器的实现中包括了,ClassPath、系统文件、云配置文件,这三部分与 Spring 源码中的设计和实现保持一致,最终在 DefaultResourceLoader 中做具体的调用。 接口:BeanDefinitionReader、抽象类:AbstractBeanDefinitionReader、实现类:XmlBeanDefinitionReader,这三部分内容主要是合理清晰的处理了资源读取后的注册 Bean 容器操作。接口管定义,抽象类处理非接口功能外的注册Bean组件填充,最终实现类即可只关心具体的业务实现