mini-spring实现
一、简介
基于bilibili up主“学java的生生”的手写spring教程,实现一个简单的spring框架。mini-spring包含的核心功能有:包扫描,BeanDefinition封装,IOC容器,依赖注入,生命周期管理,按类型/名称获取Bean等功能。后续还会继续基于mini-spring实现mvc。
二、项目框架
mini-spring
├── .idea
├── .mvn
└── src
└── main
├── java
│ └── tech
│ └── insight
│ └── minispring
│ ├── annotation
│ │ ├── Autowired
│ │ ├── Component
│ │ └── PostConstruct
│ ├── entity
│ ├── ApplicationContext
│ ├── BeanDefinition
│ ├── BeanPostProcessor
│ ├── Main
│ └── MyBeanPostProcessor
└── resources
└── application.properties
三、核心代码解析
1.BeanDefinition
package tech.insight.minispring;
import tech.insight.minispring.sub.Autowired;
import tech.insight.minispring.sub.PostConstruct;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.util.Arrays;
import java.util.List;
public class BeanDefinition {
private final String name;
private final Constructor<?> constructor;
private final Method postConstructMethod;
private final List<Field> autowiredFields;
private final Class<?> beanType;
public BeanDefinition(Class<?> type){
this.beanType = type;
Component component = type.getDeclaredAnnotation(Component.class);
this.name = component.name().isEmpty() ? type.getSimpleName() : component.name();
try {
this.constructor = type.getConstructor();
this.postConstructMethod = Arrays.stream(type.getDeclaredMethods())
.filter(m -> m.isAnnotationPresent(PostConstruct.class))
.findFirst()
.orElse(null);
this.autowiredFields = Arrays.stream(type.getDeclaredFields())
.filter(m -> m.isAnnotationPresent(Autowired.class))
.toList();
} catch (NoSuchMethodException e) {
throw new RuntimeException(e);
}
}
public String getName(){
return name;
}
public Constructor<?> getConstructor(){
return constructor;
}
public Method getPostConstructMethod(){
return postConstructMethod;
}
public List<Field> getAutowiredFields(){
return autowiredFields;
}
public Class<?> getBeanType(){
return beanType;
}
}
为什么要定义BeanDefinition,而不是直接定义Bean然后进行实例化?
BeanDefinition存储的是Bean的元信息,用来描述怎么创建一个Bean。BeanDefinition像是配方,而Bean是具体做出来的菜。
使用BeanDefinition有以下优势:
- 延迟实例化(Lazy Init)
如果你直接new,那一启动就得把所有对象都创建出来。而通过BeanDefinition,可以先将Bean的配方存储,在真正需要使用时再创建Bean,这样就能控制实例化的时机。
- 生命周期管理
直接使用new,只能拿到对象本身,无法管理对象的生命周期。因为BeanDefinition记录了Bean的元数据,所以可以在合适的时机执行生命周期方法,比如PostConstruct,PreDestory等。
- 依赖注入
BeanDefinition中保存了类中带有@Autowired的字段,创建Bean时能够通过反射注入对应的依赖对象。
- 代理增强
可以通过BeanDefinition的配置,决定返回一个原始对象还是增强后的代理对象,可以额外实现日志、事务等功能,这也是SpringAOP的基础。
2.ApplicationContext
- 包扫描函数
**
* 包扫描函数
* @param packageName
* @return
*/
public List<Class<?>> scanPackage(String packageName) throws Exception {
List<Class<?>> classList = new ArrayList<>();
// 需要a.b.c
URL resource = this.getClass().getClassLoader().getResource(packageName.replace(".", File.separator));
Path path = Paths.get(resource.toURI());
Files.walkFileTree(path,new SimpleFileVisitor<>(){
@Override
public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {
Path absolutePath = file.toAbsolutePath();
if (absolutePath.toString().endsWith(".class")) {
String replaceStr = absolutePath.toString().replace(File.separator, ".");
int index = replaceStr.indexOf(packageName);
String className = replaceStr.substring(index,replaceStr.length() - ".class".length());
try {
classList.add(Class.forName(className));
} catch (ClassNotFoundException e) {
throw new RuntimeException(e);
}
}
return FileVisitResult.CONTINUE;
}
});
return classList;
resource:URL对象,包所在的文件目录的定位地址
URI:更通用的统一资源标识符,避免URL中的特殊符号产生的起义
Path:NIO引入的新文件系统的抽象,比File更强大,能够把URI转化为操作系统本地文件路径
Files.walkFileTree():递归遍历包目录及其子目录
这一个方法其实是访问者模式的体现:把堆某种结构的访问操作,从结构本身分离出来,交给访问者去完成。在不改变数据结构的前提下,灵活扩展对结构的操作。
在这里文件是被访问者,SimpleFileVisitor子类是访问者,visitFile()是访问者逻辑,Files.walkFileTree是调度器,遍历目录将文件交给visitor处理。
visitFile()的逻辑是将后缀为.class的文件的绝对路径转化为类的全限定类名,然后通过全限定类名使用反射的方式加载对象(并不是实例化对象,只是生成字节码文件,存储对象的元信息),最后加入classList中。classList中存储的是对象的元信息,之后会被封装成BeanDefinition。
- wrapper映射函数:将字节码对象封装为BeanDefinition
protected BeanDefinition wrapper(Class<?> type){
// 1. 用 Class<?> 创建一个 BeanDefinition(即“配方”)
BeanDefinition beanDefinition = new BeanDefinition(type);
// 2. 校验:容器里不能有重复的 beanName
if (beanDefinitionsMap.containsKey(beanDefinition.getName())){
throw new RuntimeException("Duplicate bean name: " + beanDefinition.getName());
}
// 3. 注册:把 BeanDefinition 放进 beanDefinitionsMap(配方容器)
beanDefinitionsMap.put(beanDefinition.getName(), beanDefinition);
// 4. 返回 BeanDefinition(有时候可能还要进一步处理)
return beanDefinition;
}
- createBean&&doCreateBean
protected Object createBean(BeanDefinition beanDefinition) {
String name = beanDefinition.getName();
if (ioc.containsKey(name)){
return ioc.get(name);
}
if (loadingIoc.containsKey(name)){
return loadingIoc.get(name);
}
return doCreateBean(beanDefinition);
}
先查询单例池IOC中是否有bean,如果有说明已经创建过了,直接返回。再检查loadingIoc中是否有正在创建的bean,如果有,先返回半成品用于解决循环依赖。如果两个容器中都没有,则进行doCreateBean()。
private Object doCreateBean(BeanDefinition beanDefinition) {
Constructor constructor = beanDefinition.getConstructor();
Object bean = null;
try {
// 1. 调用构造器,实例化对象,得到一个裸对象
bean = constructor.newInstance();
// 2. 放入临时集合,标记“正在创建”
loadingIoc.put(beanDefinition.getName(), bean);
// 3. 自动注入依赖(相当于 Spring 的依赖注入阶段)
autowiredBean(bean, beanDefinition);
// 4. 初始化(调用初始化方法、执行 Aware 接口、BeanPostProcessor 等)
bean = initializeBean(bean, beanDefinition);
// 5. 创建完成,从 loadingIoc 移除
loadingIoc.remove(beanDefinition.getName());
// 6. 放入单例池 ioc,表示 Bean 已经创建完成
ioc.put(beanDefinition.getName(), bean);
} catch (Exception e){
throw new RuntimeException(e);
}
return bean;
Spring三级缓存解决循环依赖
在Spring的DefaultSingletonBeanRegistry中有三个Map:
1)singletonObjects:相当于ioc单例池,存放已经初始化好的bean。
2)earlySingletonObjects:相当于loadingIoc,存放已实例化但还未初始化完成的bean。
3)singletonFactories:存放ObjectFactory,一个生成bean的工厂对象
Spring三级缓存相较于本文代码的优势是:在循环依赖时,B拿到的A是工厂返回的代理对象,既可以返回原始对象,也能返回增强后的代理对象;而本文的实现是直接返回裸对象(使用BeanDefinition的构造器进行创建对象),在创建对象后无法再进行增强。因此,Spring的三级缓存具有更强大,更灵活的AOP功能。
图论环检测角度解决循环依赖
提供一个另外思考的视角,Spring 解决循环依赖的做法是 【图论环检测】 的经典实践。 依赖关系可以表示为一个有向图,Cat -> Dog -> Cat 的依赖关系可以表示为一个【有向图】 ,Bean是怕【顶点】, 依赖关系是【边】。Spring在createBean时会递归构建依赖图 (深度优先遍历DFS) , 如果发现当前路径中某个Bean正在创建中(即存在“正在创建”的顶点被重复访问),则判定存在环 【环检测】 。简化做法是采用二级缓存来获取 Cat 的引用 【在图中引入一个虚拟顶点】, 将环拆解为链式依赖 ,从而避免了无限递归。当所有依赖注入完成,再将真实顶点替换虚拟顶点。故视频中的loadingIoc缓存是用于存放虚拟顶点进行环检测的集合容器,ioc是存放真实顶点的集合容器。
- autowiredBean 自动注入方法
private void autowiredBean(Object bean, BeanDefinition beanDefinition) throws IllegalAccessException {
for (Field autowiredField : beanDefinition.getAutowiredFields()) {
autowiredField.setAccessible(true);
autowiredField.set(bean,getBean(autowiredField.getType()));
}
}
- BeanPostProcessor
private Object initializeBean(Object bean,BeanDefinition beanDefinition) throws InvocationTargetException, IllegalAccessException {
for (BeanPostProcessor postProcessor : postProcessors) {
postProcessor.beforeInitializeBean(bean,beanDefinition.getName());
}
Method postConstructMethod = beanDefinition.getPostConstructMethod();
if (postConstructMethod != null){
postConstructMethod.invoke(bean);
}
for (BeanPostProcessor postProcessor : postProcessors) {
postProcessor.afterInitializeBean(bean,beanDefinition.getName());
}
return bean;
}
/**
* 将实现BeanPostProcessor接口的Bean实例化并放入postProcessor列表
*/
private void initBeanPostProcessor() {
beanDefinitionsMap.values().stream().
filter(bd ->BeanPostProcessor.class.isAssignableFrom(bd.getBeanType()))
.map(this::createBean).map((bean) -> (BeanPostProcessor)bean)
.forEach(postProcessors::add);
}
@PostConstruct:在依赖注入完成后,Bean可用前的初始化钩子。
PostProcessor:处理器。所有实现BeanPostProcessor接口的Bean都会被注册成为一个处理器。所有postProcessors列表中的处理器都可以参与生命周期的增强。在创建新的bean时调用initializeBean方法,执行处理器中的逻辑:
先执行 MyBeanLogger.beforeInitializeBean()
再执行 @PostConstruct
方法(如果有)
最后执行 MyBeanLogger.afterInitializeBean()
所以postProcessor的作用就是:定义一系列处理器,在自动注入完成后,对半成品bean进行生命周期的增强
- getBean
/**
* 通过名字获取对象
* @param name
* @return
*/
public Object getBean(String name) {
if (name == null) {
return null;
}
Object bean = ioc.get(name);
//如果bean不为空,直接返回
if (bean != null){
return bean;
}
//如果map中有但还未初始化,则将其初始化后返回
//这样做是为了确保map中存在beanDefinition但还未在ioc容器中创建的对象也被创建
if (beanDefinitionsMap.containsKey(name)){
return createBean(beanDefinitionsMap.get(name));
}
//ioc中确实没有
return null;
}
/**
* 通过类型获取对象
* @param beanType
* @return
* @param <T>
*/
public <T> T getBean(Class<T> beanType){
String beanName = this.beanDefinitionsMap.values().stream()
.filter(bd -> beanType.isAssignableFrom(bd.getBeanType()))
.map(BeanDefinition::getName)
.findFirst()
.orElse(null);
return (T)getBean(beanName);
}
/**
*
* @param beanType
* @return
* @param <T>
*/
public <T> List<T> getBeans(Class<T> beanType){
return this.beanDefinitionsMap.values().stream()
.filter(bd -> beanType.isAssignableFrom(bd.getBeanType()))
.map(BeanDefinition::getName)
.map(this::getBean)
.map(bean -> (T)bean)
.toList();
}
Spring在通过类型获取bean时,为什么需要通过遍历IoC容器获取,而不是通过维护一个Map<Class,Bean>来获取?
因为要支持多态、代理、多实现的场景。如果传入一个接口或一个父类,其具有多个实现,就不能保证Class和Bean的一一映射关系,无法准确获取bean。因此在通过名称获取时依靠beanNname -> Bean的唯一映射,通过Map来获取;而在通过类型获取时,需要遍历ioc容器获取bean,再匹配类型,以支持多态、代理、多实现的场景。