背景
我们都知道,Spring拥有两大特性: 控制反转(IOC)和面向切面编程(AOP)
在我博客之前的文章里有提到过如何去使用AOP,今天这篇文章则主要是简单实现一个通过注解实现的IOC容器。
主要的实现思想为反射,如果不懂反射的话可以先去本站看一下反射的文章。
基础:通过方法获取Bean实例
如上图所示,获取一个Bean的实例首先需要被注册到容器,否则是不能被获取到的,注册到容器的意思就是将这个类的信息存到容器管理处,等到需要使用这个类的时候不再去new一个实例,而是直接向容器索取,容器如果查询到对应的类已经注册了,那么会根据注册信息获取类返回,具体获取方法后文详细说明,具体代码如下:
/**
* 通过名称获取对应Bean实例
* @param beanName Bean的名称
* @return 对应实例
*/
@SuppressWarnings("unchecked")
public static<T> T getBean(String beanName) {
// 先查有没有注册
BeanDefinition beanDefinition = BeanDefinitionRegisterFactory.getBeanDefinition(beanName);
// 没有注册,直接返回
if (Objects.isNull(beanDefinition)) {
return null;
}
// 调用实例工厂去找一个 返回
return (T) SingletonBeanRegisterFactory.getBean(beanName);
}
}
注册与注册工厂
上文已经说到,获取实例需要注册,注册的方式是通过工厂模式构建一个新的BeanDefinition并缓存到工厂内部的一个线程安全的HashMap中,即为完成注册,整个注册流程使用了设计模式中的工厂模式,因此被称为注册工厂。
private static final Map<String, BeanDefinition> beanDefinitionMap =
new ConcurrentHashMap<>();
/**
* 注册一个Bean信息
* @param beanName Bean名称
* @param beanClass Bean字节码
* @param lazyLoad 是否懒加载
* @param singleton 是否单例
*/
public static void registerBeanDefinition(String beanName, Class<?> beanClass, boolean lazyLoad, boolean singleton) {
BeanDefinition beanDefinition = new BeanDefinition(beanClass, beanName);
beanDefinition.setLazyLoad(lazyLoad);
beanDefinition.setSingleton(singleton);
beanDefinitionMap.put(beanName,beanDefinition);
}
这样完成了注册的流程。
通过特定注解实现对象注册
我们实际开发过程中每一个单例类都需要手动调用工厂去注册一遍,显得非常笨拙,因此可以使用注解注册,只需要在需要注册的类上加上@Component
注解,在程序运行开始的时候,调用扫描注册方法,扫描主包下的所有带有该注解的类,通过反射去获取类字节码信息,通过注解去获取参数信息,再对这些类执行上面代码的注册方法,即可完成所有类的注册,这样就可以更优雅了。
于是,我们的流程就变成了:
看看实现代码:
/**
* 注册Bean 扫描指定包及其子包下的所有类,将带有Component注解的类进行注册
* @param packageName 报名
*/
public static void registerBeansWithComponentScan(String packageName) {
// 先进行扫描获得带有Component注解的类字节码
List<Class<?>> classes = scanAnnotation(packageName, Component.class);
for (var clazz : classes) {
// 获得注解内信息
Component clazzAnnotation = clazz.getAnnotation(Component.class);
String beanName = clazzAnnotation.beanName();
// 对没有注明名字的Bean,取其字节码名称为Bean名称
if (Objects.isNull(beanName) || beanName.isEmpty()) {
beanName = clazz.getName();
}
// 执行注册
BeanDefinitionRegisterFactory.registerBeanDefinition(beanName, clazz,
clazzAnnotation.lazyLoad(), clazzAnnotation.singleton());
}
}
/**
* 扫描指定包下的带有指定注解的类
* @param packageName 包名
* @param annotation 要扫描的注解名
* @return 带有对应注解的类字节码列表
*/
private static List<Class<?>> scanAnnotation(String packageName, Class<? extends Annotation> annotation) {
// 使用反射获取对应字节码类
Reflections reflections = new Reflections(packageName);
Set<Class<?>> typesAnnotatedWith = reflections.getTypesAnnotatedWith(annotation);
return typesAnnotatedWith.stream().toList();
}
这样实现后,我们只需要main
函数里调用一下扫描方法,指定包扫描路径,即可完成类的注册。
实例化工厂
相对于单例注册,实例化的操作就比较简单了,只需要根据BeanDefinition
里提供的信息(字节码对象)通过反射获取相应构造方法进行调用即可获得对应实例,检测是否注册时是否单例,如果是单例,那么将其放进缓存池,下次使用直接从缓存池获取即可(缓存池也为一个线程安全的HashMap)。
如果注册注解时使用的信息不是单例的 那么就直接new一个对象返回就好啦。
看看实现代码:
/**
* 单例注册工厂(实例化Bean注册信息)
* @author Polister
*/
public class SingletonBeanRegisterFactory {
// 实例化的缓存池
private static final Map<String, Object> beanList = new ConcurrentHashMap<>();
/**
* 创建Bean操作
* @param beanDefinition Bean注册信息
* @return 创建的实例
*/
public static Object createBean(BeanDefinition beanDefinition) {
// 执行创建
Object bean = doCreate(beanDefinition);
// 如果是单例Bean,放入单例池
if (beanDefinition.isSingleton())
beanList.put(beanDefinition.getBeanName(), bean);
// 进行类内注解注入
BeanInjectFactory.injectWithAutoWired(bean);
return bean;
}
/**
* 实例化Bean的创建周期
* @param beanDefinition Bean的注册信息
* @return Bean实例
*/
private static Object doCreate(BeanDefinition beanDefinition) {
// 没有对应信息,无法执行创建
if (Objects.isNull(beanDefinition) ||
Objects.isNull(beanDefinition.getBeanClass())) {
throw new CreateBeanException("找不到定义的Bean信息");
}
Object instance;
try {
// 通过反射获取对应构造方法进行实例创建
instance = beanDefinition.getBeanClass()
.getConstructor().newInstance();
} catch (Exception e) {
throw new CreateBeanException("创建实体Bean失败" + beanDefinition);
}
return instance;
}
注入工厂
单例对象实例化之后,就会被加入到缓存池里,那么如果这些类里面所定义的字段里有已经交由容器管理的类,那么在构造的时候需要再使用getBean
方法获取,这样子不便于开发,因此在代码编写的时候,只需要将已经交由容器管理的类写入字段内,并加上@AutoWired
注解,容器创建该类实例之后,会对该类的字段进行一次扫描,如果字段中带有@AutoWired
注解,那么会对该字段所属类名称(默认是字节码名称)进行一次getBean
操作,如果时已经注册的单例类,那么会直接进行注入,使用的时候无需进行getBean
操作,直接使用即可,这就是类的自动注入。
/**
* 对加了注解的类进行注入
* @param obj 实例
*/
protected static void injectWithAutoWired(Object obj) {
Class<?> clazz = obj.getClass();
Field[] fields = clazz.getDeclaredFields();
// 遍历参数 查询是否有注入注解
for (Field field : fields) {
AutoWired annotation = field.getAnnotation(AutoWired.class);
if (Objects.nonNull(annotation)) {
try {
// 对其进行注入
field.setAccessible(true);
// 获取注解名称
String beanName = annotation.beanName();
// 注解没有加名称的话,用字节码当名称
if (Objects.isNull(beanName) || beanName.isEmpty()) {
beanName = field.getType().getName();
}
// 获取是否有这个Bean
Object bean = BeanUtils.getBean(beanName);
// 没有 不注入了
if (Objects.isNull(bean)) {
continue;
}
// 判断获取到的Bean是否是目标的子类或者实现
if (!bean.getClass().isAssignableFrom(field.getType())) {
throw new InjectBeanException("注入类与注册实例类型不同!");
}
// 注入操作
field.set(obj, bean);
} catch (IllegalAccessException e) {
throw new InjectBeanException("注入Bean失败");
}
}
}
}
懒汉模式与饿汉模式
完成了基本操作后,一旦程序启动,我们可以通过包扫描向容器注册所有带有Component注解的类,但是他们什么时候实例化呢,我们可以在程序启动的时候就对他们进行实例化,运行的时候就无需再进行实例化了,这便是饿汉模式,优点是所有实例初始化之后程序运行相对稳定,缺点也非常明显,那就是启动程序的时候要对所有注册类进行实例化,导致启动时间稍长,这种模式下,可以通过注册注解的lazyLoad
标签对其进行懒加载,使其不在程序启动的时候实例化。
而另外一种方式是启动的时候只会向容器注册相应的类,不会将其实例化,等到getBean操作发生后,即需要使用对应的实例后,才会触发实例化,这种方式称为懒汉模式
至此,一个简单的IOC管理容器就已经完成实现,同时也解决了循环依赖的问题。
总结
这篇文章其实是从我的课程设计报告中摘录出来的,因为课程设计有简单的需求,那么干脆自己动手去试试,也算是对反射知识的一点应用,同时也加深了对IOC的理解(这也是为啥这篇文章语言比较正式
至于为啥不用三级缓存,那肯定是没有多线程生命周期的需求啊,单线程的循环依赖解决起来也比较简单(其实是懒,逃
多读读底层代码,领悟到的东西确实不太一样~
不知道下一次更新又是啥时候了(捂脸
噢,完整代码稍后传到我的Github上,大家感兴趣可以来看看!~