Featured image of post Java工程师 SpringIoC编码实现I

Java工程师 SpringIoC编码实现I

🌏Java工程师 SpringIoC原理实现 🎯 这篇文章用于记录 对SpringIoC的原理实现实践 包括 1️⃣原理分析 分析在使用Spring框架的时候 IoC是如何为我们工作的 分析IoC的实现原理 2️⃣流程分析 分析IoC容器的工作流程 3️⃣过程实现 编码模拟实现IoC容器

🎄概述

🌈这篇文章用来记录对Spring IOC原理的实现 内容包括 1️⃣原理分析 2️⃣过程实现 3️⃣实现流程回顾 如果不想一头雾水流于表面地使用框架 就必须了解其实现原理 深入理解它们

🌈参考列表

https://mp.weixin.qq.com/s/F_ISmJwNuH1I3l6IXua2SQ

https://zhuanlan.zhihu.com/p/130864540

🎄原理分析

🍭概述

IoC是指Inversion of Control(控制反转)那何为控制反转呢?为什么称为控制反转呢?简单来说,控制反转就是把对象的创建和管理 Java对象的过程转移给了一个IoC容器,而不再手动new对象去使用 刚开始接触到这样的概念 我是感到很懵逼的 其实现在看来 其实并不神秘 因为一切都是JavaSE基础的运用!

IoC是一种思想 Spring中使用依赖注入来实现这种思想 暂且将Spring中使用IoC思想的Java类对象称为IoC容器

Spring IoC容器的实现使用到了JavaSE中的哪些知识呢?

  • 基本的面向对象语法和思想 封装 继承 多态
  • IO IO读取配置文件
  • 集合框架 Spring的IoC容器主要使用集合框架来管理对象的依赖关系和组装对象 比如,使用Map集合类来保存和获取Bean对象
  • 反射 IoC容器通过反射机制实现对象的实例化、属性注入和方法调用等功能
  • 注解 IoC中广泛使用注解来配置和管理对象的生命周期和依赖关系信息

🎯再次强调 IoC容器其实就是Spring框架管理Java对象的容器 更具体来说 这个容器也是一个Java类对象 但IoC的描述总给人高高在上的感觉

从现在开始 要有一个主动的意识: 在使用Spring框架的时候 Spring会用一个Java容器类对象来创建和管理所有的Java对象 特殊的是这个容器类对象的体量非常大 因为它几乎会管理应用程序中所有的Java对象 但我们在使用SpringBoot框架的时候感受不到这个为我们创建和管理Java对象的容器的存在!因为封装度太高了!

🍭初始使用IoC容器的印象

在IoC出现之前 我们在JavaWeb的开发中 所有的mapper、Service、Controller(在JavaWeb其实就是Servlet)在使用的时候都是手动创建的 程序员要使用哪个类 就自己new一个这个类的对象 然后使用 如下商品的Servlet代码:

/**
@author Liu Xianmeng
@createTime 2022/7/31 11:47
@instruction 商品的Servlet类
*/
@WebServlet("/mng/Commodity")
public class CommodityCtrller extends BasicCtrller {
    private CommodityServ fs = new CommodityServImpl(); // ⚡手动创建商品的Service类
    private CommodityDao fd = new CommodityDao(); // ⚡手动创建商品的Dao层类
    
    // 在成员函数中使用自己new的对象
}

在IoC出现以后 我们使用Spring提供的IoC技术 🎯将需要使用的类的创建和管理交给容器 只需要用注解声明自己要使用的Bean IoC容器会把这个Bean注入到响应的类中 程序员在类中直接使用已经生命过的Bean 而不必再关心Bean的创建和管理 这就是控制反转的思想 如下Controller代码:

/**
@author Liu Xianmeng
@createTime 2023/1/20 11:11
@instruction
*/
@Controller
@RequestMapping("/XMall/commodity")
public class CommodityController {
    👻@Autowire // 🎯使用注解将CommodityMapper对象装配给CommodityController
    private CommodityMapper cm; 
    @Autowire // 🎯使用注解将RedisTemplate对象装配给CommodityController
    private RedisTemplate redisTemplate;
    
    // 在成员函数中直接使用上面@Autowire所标注的Java对象 也就是cm和redisTemplate
    // 注意 我们在使用的过程中并没有去new上面的两个对象
}

IoC容器实现对Bean进行创建和管理 上面的👻@Autowire注解起到了关键的作用 其实这就是IoC容器给对象注入容器的一种形式(另一种形式是通过XML配置文件进行配置 后面会讲)

IoC在应用程序启动的时候扫描指定的包package下的所有类 这里我们假定所有的包都会扫描 当扫描到上面的👻@Autowire注解的时候 它就告诉Spring说: CommodityController类中有一个private CommodityMapper cm;属性被“👻我”标注了 你需要把“👻我”标注的CommodityMapper 类对象注入到CommodityController组件中 然后IoC容器把已经注册的CommodityMapper对象的引用赋给了cm变量 从而在CommodityController类的成员函数中我们就可以直接使用cm变量了 下面的代码可以作为具体实现

// 假定IoC容器扫描到了CommodityController这个类并通过反射创建了这个类的对象instance
Object instance = clazz.getDeclaredConstructor().newInstance();
// 接下来获取CommodityController的所有变量
for (Field declaredField : clazz.getDeclaredFields()) {
    // 如果当前的字段被Autowired注解修饰
    if (declaredField.isAnnotationPresent(Autowired.class)) {
        // 获取变量的名字(比如上面的CommodityMapper变量)
        String name = declaredField.getName(); // 这个名字默认是类名首字母小写(比如上面的CommodityMapper变量对应的名字就是commodityMapper)
        // 通过getBean方法来获取要组装对象 将该对象的引用赋给当前变量
        Object bean = getBean(name); // getBean方法通过IoC容器的beanDefinitionMap来获取Bean(这里先不展开)
        declaredField.setAccessible(true); // 因为属性是pirvate 需要暴破
        declaredField.set(instance, bean); // 把获取到的对象的引用赋给CommodityMapper变量 从而完成依赖注入✅ 这个时候cm就指向了一个由IoC容器创建好的CommodityMapper对象 这个对象在内存的堆上
    }
}

同时我们也可以注意到CommodityController@Controller注解修饰了,这就意味着CommodityController也会被创建并放入IoC容器中进行管理 并且它的@RequestMapping(value = "/XMall/commodity")注解的value路径也会被解析 交给另一个Bean来管理 以便我们在前端发起路径/XMall/commodity请求的时候 这个请求会被映射到CommodityController类对象来进行处理

由此可见 注解的主要作用就是标注 IoC容器在扫描组件的时候 通过反射检测到注解 从而执行需要的操作(创建Bean、注入Bean等)

🍭揭开框架的神秘面纱

大家有没有注意到上一节我举例的JavaWeb开发中用到的这段代码:

/**
@author Liu Xianmeng
@createTime 2022/7/31 11:47
@instruction 商品的Servlet类
*/
@WebServlet("/mng/Commodity")
public class CommodityCtrller extends BasicCtrller {
    private CommodityServ fs = new CommodityServImpl(); // ⚡手动创建商品的Service类
    private CommodityDao fd = new CommodityDao(); // ⚡手动创建商品的Dao层类
    
    // 在成员函数中使用自己new的对象
}

其中有一个注解@WebServlet("/mng/Commodity")

我在写完上一节的分析后似乎又发现了一个世界 此前我并没有注意到的世界 那就是这个注解的作用似乎和IoC容器的依赖注入很相似!是的 Spring容器出现之前,也曾存在其他的容器,曾经风靡全球!

那么 谁来扫描这个注解?web容器 很显然 web容器也是通过反射和Java集合来创建和管理Java对象的

所以到了现在 我才看到JavaSE基础知识才是背后的大魔王!所有的框架的谜团似乎都解开了!

🍭IoC容器具体是谁?

我用Idea导出ApplicationContext类图 往高层抽象来说BeanFactory就是Spring用来管理Java对象的容器 只不过它是最高层的抽象接口 它的具体实现有AnnotationConfigApplicationContextClassPathXMLApplicationContext等 其实从名字就可以看出来AnnotationConfigApplicationContext实现了用注解配置应用上下文 这个类应该是存在我上面举过的反射实现代码的

IoC容器的类图

🎄模拟实现IoC容器

IoC的实现可以有两种方式,一是实现一个模拟ClassPathXMLApplicationContext的IoC容器,二是实现一个模拟AnnotationConfigApplicationContext的IoC容器 接下来我就开始模拟AnnotationConfigApplicationContext实现IoC容器

🍭IoC整体框架

下面的BigBigMengAnnotationApplicationContext类是我模式实现的IoC容器

package bbm.com.ioc;

import java.util.concurrent.ConcurrentHashMap;

/**
@author Liu Xianmeng
@createTime 2023/9/14 17:27
@instruction IoC容器类 这个类就是要模拟实现额IoC容器类
            下面将BigBigMengAnnotationApplicationContext的类名简称为 "IoC容器"

            【说明】在注释中如果出现前文未出现的名词 (如对于 private Class configClass; 的注释中出现singletonObjects)
                   可以在后文中找到
*/

@SuppressWarnings({"all"})
public class BigBigMengAnnotationApplicationContext { // IoC容器

    /**
     * IoC容器持有一个配置类的Class对象
     *
     * 这个类的定义如下:
     * @ComponentScan(value = "IoC容器要扫描的包名")
     * public class BigBigMengSpringConfig {}
     *
     * [IoC容器注入Bean]
     * 这个类的@ComponentScan注解的value指定IoC容器要扫描的包 IoC容器会使用反射获取这个类的@ComponentScan注解
     * 然后将 value = "IoC容器要扫描的包名" -> 包名取出 然后扫描包中所有的Class对象
     * 如果扫描到到的Class对象有类似@Component、@Bean、@Mapper、@Service、@Controller等标识这个Class对象是一个
     * 组件的注解修饰 就创建一个此Class对象对应的Java类对象 并放入singletonObjects
     *
     * [IoC容器注入Bean的依赖Bean]
     * 在扫描到一个Class对象后 不仅要创建该Class对象对应的Java类对象 还需要扫描其所有的属性 如果其某个属性A有
     *  @Autowire @Resource等标识 则该属性A应该被注入到该当前创建的Java对象 如果singletonObjects已经存在该属性A对象
     * 则直接从singletonObjects中取出相应的Java对象并将引用赋值给该属性A 如果singletonObjects还不存在该属性A对象
     * 则创建对应的A属性Java对象放入singletonObjects 并将此Java对象的引用赋值给属性A 这样就完成了属性的依赖注入
     *
     * 直到配置类指定的包中的所有的Class对象都被创建并放入singletonObjects 整个包扫描的过程就完成了
     */
    private Class configClass;

    /**
     * 定义属性BeanDefinitionMap -> 存放BeanDefinition对象
     *
     * IoC容器中有一个变量域/也称为属性 叫beanDefinitionMap 这个map用来保存Bean的定义信息
     * 为什么要存储Bean的定义信息?因为在创建Bean的时候要用到
     * 也就是说IoC容器会先扫描1遍配置类指定的包获取所有Bean的定义信息 
     * 然后遍历beanDefinitionMap创建所有的Bean放入singletonObjects
     *
     * beanDefinitionMap是ConcurrentHashMap的一个对象 键是Bean的名字 值是Bean的定义信息
     * Bean的定义信息由BeanDefinition类的对象来充当
     */
    private ConcurrentHashMap<String, BeanDefinition> beanDefinitionMap = new ConcurrentHashMap<>();

    /**
     * 定义属性SingletonObjects -> 存放单例对象
     *
     * singletonObjects就是我上面提到的存放单例对象的变量 在上文已经出现过
     * 它也是一个ConcurrentHashMap对象 键是Bean的名字 值是Bean对象本身
     */
    private ConcurrentHashMap<String, Object> singletonObjects = new ConcurrentHashMap<>();

    /**
     * 接下来是IoC容器的构造器
     * 
     * 构造器中完成两个任务
     * (1)初始化Beans的定义信息beanDefinitionMap
     * (2)遍历Beans的定义信息beanDefinitionMap初始化单例池singletonObjects
     *
     * @param configClass 在创建一个IoC容器对象的时候 需要传入配置类的Class对象
     *                    配置类的Class对象 上文已经提到 IoC容器
     */
    public BigBigMengAnnotationApplicationContext(Class configClass) {}

    /**
     * beanDefinitionsByScan方法完成对指定包的扫描 并将Bean信息封装到BeanDefinition对象
     */
    public void beanDefinitionsByScan(Class configClass) {}
    
    /**
     * initialSingletonObjects方法beanDefinitionMap单例池的初始化
     */
    public void initialSingletonObjects(){}

    /**
     * createBean方法完成Bean对象的创建
     *
     * @param beanDefinition    传入Bean的定义信息
     * @return                  返回创建的Bean对象
     */
    private Object createBean(BeanDefinition beanDefinition) {}

    /**
     * getBean 根据传入的Bean的name返回singletonObjects中的bean对象
     * @param   name
     * @return  返回singletonObjects中的bean对象
     */
    public Object getBean(String name) {}
}

🍭补全IoC整体框架中其他类的定义

接下来补全其他要用到的类的定义 代码在整体框架搭建完毕后再开始写 这样思路会很清晰 目前IoC包下还有两个类是需要补充定义的 即1️⃣ BeanDefinition Bean的信息定义类 2️⃣ 配置类 BigBigMengConfig Bean的配置类 指定要装配到IoC容器的Bean(或者说是指定要扫描的包)

1️⃣BeanDefinition类的代码如下

package bbm.com.ioc;

/**
@author Liu Xianmeng
@createTime 2023/9/14 18:24
@instruction 这是BeanDefinition类
*/

@SuppressWarnings({"all"})
public class BeanDefinition {

    /****** 目前 仅需要这两个属性 如果模拟实现的功能更加复杂 可以进行拓展 ******/

    /**
     * 指定Bean是单例还是多例 singleton | prototype
     *
     * 当IoC扫描包并遇到一个Class对象的时候 它会检查这个类是否有@Scope注解修饰
     * 如果没有@Scope注解修饰则默认将这个Bean的定义信息的scope值为singleton
     * 如果有@Scope注解修饰 则看@Scope指定的值
     *
     * @Scope注解的定义如下:
     *
     * @Target(ElementType.TYPE) // 表示@Scope用于修饰类
     * // 指定保留策略 RUNTIME标识运行的时候可以通过反射检测到该注解
     * @Retention(RetentionPolicy.RUNTIME) 
     * public @interface Scope {
     *     // 通过value可以指定 singleton prototype
     *     String value() default "singleton"; // 默认指定单例
     * }
     */
    private String scope;

    /**
     * IoC容器初始化的时候会第一遍扫描容器
     * 
     * 扫描到Class对象的时候 直接将其引用赋给该属性
     */
    private Class clazz;  // Bean的Class对象

    public String getScope() {
        return scope;
    }
    public void setScope(String scope) {
        this.scope = scope;
    }
    public Class getClazz() {
        return clazz;
    }
    public void setClazz(Class clazz) {
        this.clazz = clazz;
    }
    @Override
    public String toString() {
        return "BeanDefinition{" +
            "scope='" + scope + '\'' +
            ", clazz=" + clazz +
            '}';
    }
}

2️⃣ 配置类 BigBigMengConfig

指定IoC容器需要管理的Beans IoC容器在初始化的时候会将BigBigMengConfig类指定的Beans 进行创建并放到singletonObjects变量中

package bbm.com.ioc;

import bbm.com.annotation.ComponentScan;

/**
@author Liu Xianmeng
@createTime 2023/9/14 22:43
@instruction BigBigMengConfig配置类的@ComponentScan注解指定了
             IoC容器应该要扫描的包 该包下的所有被注解标识的Beans
             都会被IoC容器进行创建和管理
*/
@ComponentScan(values = "bbm.com.component")
public class BigBigMengConfig {
}

在配置类BigBigMengConfig中使用到了@ComponentScan注解 其定义如下

package bbm.com.annotation;

import java.lang.annotation.*;

/**
@author Liu Xianmeng
@createTime 2023/9/14 22:44
@instruction 这是@ComponentScan注解 这个注解用来指定IoC要扫描的包
             从而在包下发现Beans并装到IoC容器中
*/


/*
@Retention(RetentionPolicy.RUNTIME)是Java中的一个注解元注解
它指定了被注解的元素在运行时可以通过反射被访问到
具体来说 @Retention(RetentionPolicy.RUNTIME)表示该注解在运行时保留
因此可以在运行时通过反射API获取到被注解元素的信息

Java中的注解有不同的保留策略 包括源代码(RetentionPolicy.SOURCE)
编译时(RetentionPolicy.CLASS)和运行时(RetentionPolicy.RUNTIME)
其中 @Retention注解用来指定注解的保留策略 默认为RetentionPolicy.CLASS 即在编译时保留注解信息
但在运行时无法通过反射获取 而设置为RetentionPolicy.RUNTIME之后 注解将在运行时保留
意味着我们可以在程序运行期间动态地获取和使用注解信息

通常情况下 如果我们需要在运行时使用反射来处理注解信息 比如自定义注解处理器
那么就需要将注解的保留策略设置为RetentionPolicy.RUNTIME 以便在运行时能够获取注解信息并进行相应的处理

[如果在定义注解时没有指定@Retention注解的保留策略呢?]
这时默认情况下将使用RetentionPolicy.CLASS作为保留策略 这意味着该注解在编译时会被保留
但在运行时无法通过反射获取到注解信息
 */
@Retention(RetentionPolicy.RUNTIME)

/*
该注解用于指示该注解可以应用于类 接口或枚举类型的声明
@Target(ElementType.TYPE)表示该注解只能应用于类 接口和枚举类型的声明上 不能应用于其他类型的声明上

还有其他的指定作用范围 如下
@Target(ElementType.FIELD) 应用于字段(成员变量)上
@Target(ElementType.METHOD) 应用于方法上
@Target(ElementType.PARAMETER) 应用于方法参数上
@Target(ElementType.CONSTRUCTOR) 应用于构造方法上
@Target(ElementType.LOCAL_VARIABLE) 应用于局部变量上
@Target(ElementType.ANNOTATION_TYPE) 应用于注解类型

指定注解作用范围的意义是什么?提供更明确的编程规范和约束
 */
@Target(ElementType.TYPE)
public @interface ComponentScan {
    String value() default ""; // 通过value指定要扫描的包
}

🌴到此为止 IoC整体框架就搭建出来了 其中最核心的类就是BigBigMengAnnotationApplicationContext 它就是我将模拟实现的IoC容器 这一篇内容已经很多了 后面剩下的内容将在下一篇叙述~