🎄概述
🌈这篇文章用来记录对
Spring IOC
原理的实现 内容包括 1️⃣原理分析 2️⃣过程实现 3️⃣实现流程回顾 如果不想一头雾水流于表面地使用框架 就必须了解其实现原理 深入理解它们🌈参考列表
🎄原理分析
🍭概述
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对象的容器 只不过它是最高层的抽象接口 它的具体实现有AnnotationConfigApplicationContext
和ClassPathXMLApplicationContext
等 其实从名字就可以看出来AnnotationConfigApplicationContext
实现了用注解配置应用上下文 这个类应该是存在我上面举过的反射实现代码的
🎄模拟实现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容器 这一篇内容已经很多了 后面剩下的内容将在下一篇叙述~