声明
本篇文章除部分引用外,均为原创内容,如有雷同纯属巧合,引用转载请附上原文链接与声明。
阅读条件
读本篇文章需掌握java基础知识,对Spring IOC有清楚的认识,掌握注解的使用方式,灵活运用反射
注意
本文若包含部分下载内容,本着一站式阅读的想法,本站提供其对应软件的直接下载方式,但是由于带宽原因下载缓慢是必然的,建立读者去相关官网进行下载,若某些软件禁止三方传播,请在主页上通过联系作者的方式将相关项目进行取消。
相关文章
文章大纲
- 注解定义
- 注解处理&实现容器
- 测试结果
简述
项目github地址传送门点此 ,该篇文章对应的代码在ioc-1.0分支上
该篇文章将详细阐述如何实现一个简易的IOC框架,创造一个类似于Spring IOC类似的框架,提供其依赖注入能力。
该自研IOC框架具有以下特征。
- 支持.Class文件的类资源识别和加载功能,暂时未实现jar包资源和网络资源识别
- 提供 @Controller @Service @Repository @Component 注解的bean定义
- 提供 @Autowired 注解实现注入能力,但仅支持通过反射并调用Set方法方式注入,且要求注入目标类的构造函数均为无参构造函数
- 提供基础的容器管理能力
- 不支持识别jar包内.class文件资源,不识别定义的网络class资源,即仅支持本地启动和识别和加载.class
- 不支持构造函数注入
- 装配时不考虑并发问题
- 仅支持单例模式
注解定义
定义@SpringBootApplication注解,用于标记在启动类上,并获取扫描包路径。该注解中存在唯一的方法,用于指定需要扫描包的位置,如果不指定,则将默认使用该注解标记类的包路径。该注解起到的作用与Spring中的@ComponentScan注解效果类似
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface SpringBootApplication {
/**
* 扫描的包位置
*
* @return
*/
String value() default "";
}
定义@Controller注解,用于标记Controller层对象,和Spring中的@Controller注解定位相同
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface Controller {
}
定义@Service注解,用于标记Service层对象,和Spring中的@Service注解定位相同
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface Service {
}
定义@Repository注解,用于标记Repository层对象,和Spring中的@Repository注解定位相同
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface Repository {
}
定义@Autowired注解,用于标记自动注入的字段,仅支持对类成员变量进行注入,和Spring中的@Autowired注解定位相同
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Autowired {
}
注解处理&实现容器
识别@SpringBootApplication。首先创建SpringApplication类,定位与Spring中的SpringApplication相同,可以看到有抽取basePackage的逻辑代码,如果启动类上的@SpringBootApplication未设置值,则使用启动类的包位置作为扫描路径,反之使用设置的值
public class SpringApplication {
public static void run(Class<?> appClass, String... args) {
SpringBootApplication annotation = appClass.getAnnotation(SpringBootApplication.class);
String basePackage = Strings.isNullOrEmpty(annotation.value()) ? appClass.getPackageName() : annotation.value();
if (Strings.isNullOrEmpty(basePackage)) {
basePackage = appClass.getPackageName();
}
BeanContainer.scanBean(basePackage);
BeanContainer.doIoc();
}
}
识别@Controller,@Service,@Repositiry标记的类资源。首先创建容器内BeanContainer,这里主要关注scanBean方法,extractClassPath方法,needLoad方法。主要经过了以下几个流程:
- 首先scanBean方法接收basePackage作为根路径,并通过ClassLoader得到basePackage的Resource对象
- 通过Resource对象可以定位到basePackage在其实际运行环境上的文件系统路径,从而定位到相关的子文件和子文件夹目录
- 通过ClassLoader将其子文件加载为class对象,并且在needLoad方法中判断该class是否带有@Controller.@Service.@Repository注解,有的话,则将其class对象装载到容器中
- 对上述步骤得到的class对象通过反射获取其对象实例,然后放入container容器中以供后续使用
完成了上述流程即完成了basePackage下的满足条件的class装载为bean的过程,此时的bean并没有进行依赖注入的,仅仅是刚生成的对象。
public class BeanContainer {
private static Map<Class<?>, Object> container = new ConcurrentHashMap<>();
private static List<Class<? extends Annotation>> beanAnnotations = Arrays.asList(Controller.class, Service.class, Repository.class);
private static Set<Class<?>> getClasses() {
// 省略实现
}
public static <T> T getBean(Class<T> clazz) {
// 省略实现
}
public static void addBean(Class<?> clazz, Object bean) {
// 省略实现
}
public static Object remove(Class<?> clazz) {
// 省略实现
}
public static void doIoc() {
// 省略实现
}
/**
* 扫描指定包名下的所有bean
* @param basePackage
*/
public static void scanBean(String basePackage) {
URL resource = Thread.currentThread().getContextClassLoader().getResource(basePackage.replace(".", "/"));
if (Objects.isNull(resource)) {
throw new RuntimeException("con't locate the package :" + basePackage);
}
Set<Class<?>> classes = new HashSet<>();
// 这里简化实现只处理.class结尾的类文件,可以进行扩展,支持jar包,网络加载等.
// 通过 resource.getProtocol() 来判断
extractClassPath(basePackage, resource.getPath(), classes);
for (Class<?> clazz : classes) {
try {
container.put(clazz, clazz.getConstructor().newInstance());
} catch (InstantiationException | IllegalAccessException | InvocationTargetException | NoSuchMethodException e) {
log.error("can't invoke the constructor of [{}]", clazz.getName());
throw new RuntimeException(e);
}
}
}
/**
* 扫描basePackage下所有.class文件,若该类被相关注解标记,则进行装载
* @param basePackage
* @param path
* @param classes
*/
private static void extractClassPath(String basePackage, String path, Set<Class<?>> classes) {
File file = new File(path);
File[] files = file.listFiles();
if (Objects.isNull(files) || files.length == 0) {
return;
}
for (File f : files) {
if (f.isDirectory()) {
extractClassPath(basePackage, f.getAbsolutePath(), classes);
} else if (f.getName().endsWith(".class")) {
try {
String filePath = f.getAbsolutePath().replace(File.separator, ".");
String className = filePath.substring(filePath.indexOf(basePackage));
className = className.substring(0, className.indexOf(".class"));
Class<?> aClass = Thread.currentThread().getContextClassLoader().loadClass(className);
if (needLoad(aClass)) {
classes.add(aClass);
}
} catch (ClassNotFoundException e) {
log.error("can't find class");
throw new RuntimeException(e);
}
}
}
}
private static boolean needLoad(Class<?> clazz) {
for (Class<? extends Annotation> cla : beanAnnotations) {
if (clazz.isAnnotationPresent(cla)) {
return true;
}
}
return false;
}
}
识别@Autowired注解,该注解的作用即通过该注解达到自动注入的能力,这里主要关注BeanContainer中的doIoc方法,该方法经过了以下几个流程
- 对容器所有受管理的bean,获取其所有字段
- 对每一个字段,判断是否存在@Autowired注解,如果存在则通过getBean的方式获取其bean实例
- getBean方法兼容了若声明的字段类型是接口或者抽象类,可以动态去从容器中获取其实现类或者子类的bean实例进行注入
此时,通过上述流程,即对容器中的bean实例进行了注入相关依赖。此时容器的bean实例为完整的实例,可以对外提供服务。
public class BeanContainer {
private static Map<Class<?>, Object> container = new ConcurrentHashMap<>();
private static List<Class<? extends Annotation>> beanAnnotations = Arrays.asList(Controller.class, Service.class, Repository.class);
private static Set<Class<?>> getClasses() {
// 省略实现
}
/**
* 从容器中获取受管理的bean,这里需要注意的是需要支持对于接口类型注入其实现类
* @param clazz
* @param <T>
* @return
*/
public static <T> T getBean(Class<T> clazz) {
T bean = (T) container.get(clazz);
if (Objects.isNull(bean)) {
for (Class<?> cla : getClasses()) {
if (clazz.isAssignableFrom(cla) && !clazz.equals(cla)) {
bean = (T) getBean(cla);
break;
}
}
}
return bean;
}
public static void addBean(Class<?> clazz, Object bean) {
// 省略实现
}
public static Object remove(Class<?> clazz) {
// 省略实现
}
/**
* 根据 {@link tech.chenx.core.annotation.Autowired} 标记进行set装配
*/
public static void doIoc() {
// 根据@Autowired进行装配
for (Map.Entry<Class<?>, Object> entry : container.entrySet()) {
Class<?> clazz = entry.getKey();
Object o = entry.getValue();
Field[] fields = clazz.getDeclaredFields();
for (Field field : fields) {
if (field.isAnnotationPresent(Autowired.class)) {
Object bean = getBean(field.getType());
field.setAccessible(true);
try {
field.set(o, bean);
} catch (IllegalAccessException e) {
log.error("inject by set fail.", e);
throw new RuntimeException(e);
}
}
}
}
}
public static void scanBean(String basePackage) {
// 省略实现
}
private static void extractClassPath(String basePackage, String path, Set<Class<?>> classes) {
// 省略实现
}
private static boolean needLoad(Class<?> clazz) {
// 省略实现
}
}
测试结果
测试项目源码请查看项目源码中的实现,这里直接贴图展示依赖情况
以下是debug的测试结果,可以看到容器中保存了声明的bean实例,并在ExampleController中注入了依赖的service实现和repository实现,service声明的接口类,但实际上注入的service实现类,表明getBean方法正确。此时ioc容器的大部分功能已完成