前言
IoC
(Inversion of Control,控制反转)将对象的生成、设置等行为交给容器来完成,而不是由开发者自己编写 new, setXxx 等语句控制。其中最常见的实现方式是 Spring 的依赖注入(Dependency Injection, DI),如有个类 A, 其中一个字段是 B(带有注解 @Resource 或 @AutoWired
),那么容器会自动先查找一个 B 再注入到 A 的这个字段中(查找过程中可能会发现 B 也需要注入一个 C, 这些容器都会帮你完成)。
通常控制反转和依赖注入可以等同起来,我这里用 IoC 这个名称是因为它看起来比较好看——以上是我随便说的,说错了请指正……
于是我们可以自己实现一个 IoC 容器吗?虽然 Spring 已经做得足够好了,但我就是闲得蛋疼(并不闲,每天加班到九点)特别想自己试试怎么实现可以吗?——当然可以,毕竟我还是一个爱学习的好孩子。
接口定义
首先,我们定义一个上下文 Context
—— 这个词真是不好理解,哪里都有上下文的概念,比如 安卓开发,比如…… 总之就是很常见,这里可以理解为就是我们要的那个容器。
学了这么多年 Java, 直觉告诉我,它应该要是一个接口,于是我们的第一行代码出来了:
public interface Context{}
万事开头难,开了头就好办了。
容器应该有什么功能呢?
读者:你傻啊,当然是刚刚说的自动注入啊!
No, No, No, 这个是容器自己内部的功能,并不是暴露给开发者用的功能,因此不应该出现在接口中。我们要用到的其实就是从容器中获取 Bean 对象就行了:
<T> T getBean(Class<T> clazz);
使用场景
下面我们先写一下我们希望怎么使用这个 IoC 容器吧:
public interface IUserDao{ //... } @Bean //means this class should register to the IoC container public class UserDao implements IUserDao{ //implement } @Bean public class UserService{ @Bean //means this field should injected private IUserDao userDao; //...service method } public class App{ public static void main(String[] args){ Context context = new ClasspathContext(); //will auto scan UserService userService = context.getBean(UserService.class); //... } }
毕竟都是用 Spring 用习惯了,我们希望先写一个带注解的 Dao,表示它可以注册到容器中;再写一个 Service, 其中一个字段是 Dao 也带一个注解,表示需要自动注入;主程序中从容器中获取 Service,然后干需要干的事。
嗯,这里我们反复提到了注册到容器,看来 Context 还需要一个方法
void register(Object bean);
OK, 为了达到这个目的,我们需要扫描所有带注解的类,如果它需要注册到容器中,那么我们就生成它的一个实例然后加入到容器中;如果它包含需要注入的字段,那么就从容器中寻找一个合适的 Bean 注入进去。
思路就是这样,听起来是不是特别简单~~
聪明的你一定会想到这肯定需要用到反射吧~~~
答对咯,我们一步步来:
注解
注解定义
先定义一下注解吧,最初我想的是,可以用两个注解,一个叫做 @Out 写在类上的,表示这个类可以给其他类使用(需要注册到容器中);另一个叫做 @In, 写在字段上,表示需要从容器中取,注入到字段上。不过,后来我们其实用一个注解就行,干脆就叫做 @Bean 可以写在类上或字段上。不过最后我又发现,其实我们可以使用 Java 自带那个 @Resource 啊——额,好吧,那就 @Resource 吧,然而我都写完了 @Bean 了,干脆两个都支持吧~~
@Target({ElementType.TYPE, ElementType.FIELD, ElementType.METHOD, ElementType.PARAMETER}) @Retention(RetentionPolicy.RUNTIME) //必须是 RUNTIME 这样我们才能在代码中获取到 public @interface Bean { String value() default ""; }
注解处理器
因为要扫描带注解的类嘛,那么还需要定义一个注解处理器,其中有一个 autoScan 的方法:
public interface IAnnotationProcessor{ void autoScan(Context context, String... basePackage); }
然后我们实现一个 Context 在构造时调用这个处理器的 autoScan 方法,那就万事大吉啦!
public class ClasspathContext implements Context{ private Map<class ,Object> clazzBeanMap = new ConcurrentHashMap<>(); public ClasspathContext(String... scanPackages){ new SimpleAnnotationProcessor().autoScan(this, scanPackages); } public <T> T getBean(Class<T> clazz){ return clazzBeanMap.get(clazz); } public void register(Object bean){ clazzBeanMap.put(bean.getClass(),bean); } }
看起来工作都集中在注解处理器中了呢:
- – 第一步,怎样扫描类
- – 第二步,怎样把需要的 Bean 注册到容器中
- – 第三步,怎样把需要自动注入的字段填补上
扫描类
就是获取所有的类,最好只获取带我们需要的注解的那些。很遗憾,貌似无法一步到位,一下子拿到那些带注解的类的 Class
对象,不过我们可以曲线救国。
先使用 ClassLoader
获取类路径中所有包含 class 文件的资源,使用 ClassLoader.getResources
可以返回一个 URL 集合,然后遍历所有 class 资源,获取类路径下所有的类名。
有了类名,我们即可以通过 Class.forName("xxx.xxx.Xxx")
得到 class 对象,得到 class 对象后,才能使用反射获取我们关心的信息。
注册 Bean
有了 class 对象,通过反射,可以知道它是否包含注解 @Bean/@Resource 如果包含,那就通过 `Class.newInstance` 生成一个实例,注册到容器中——等一下!它的字段怎么办?——别急,下一步我们再注入字段,反正现在容器还没就绪,字段暂时是空也不会有事的。
自动注入
嘿嘿嘿,虽然刚开始是这样描述的:
在查找 A 时发现它有个字段 B 需要注入,于是我们再去查找 B, 在构造 B 时发现还依赖一个 C, 于是我们去构造 C.
但其实我们换个思路,一下子把所有 Bean 都实例出来加入到容器中(第二步)(虽然现在这些 bean 可能是“半成品”),然后再注入每个 Bean 的每个字段。那就简单了,遍历每个 Bean 的所有字段,要是包含注解 @Bean/@Resouece 注解,就从容器里取一个填上就 OK 啦,这样下来容器里所有 Bean 都是完整可用的“成品”了。
public void autoScan(Context context, String... scanPackages) { Set<string> classNames = new HashSet<>(); classNames.addAll(AnnotationUtil.getClassNames(scanPackages)); // 构造 Bean for (String className : classNames) { registerClass(context, className); } // 注入需要的字段 for (Map.Entry<class , Object> entry : context.getClazzBeanMap().entrySet()) { Object obj = entry.getValue(); injectFiled(context, obj); injectMethod(context, obj); } protected void registerClass(Context context, String className){ try{ Class<?> aClass = Class.forName(className); Object bean = aClass.newInstance(); //具体处理请看下方链接中完整代码 context.register(bean); }catch (Exception ignore){} } protected void injectFiled(Context context, Object bean){ try{ Class<?> objClass = object.getClass(); for(Field field: objClass.getDeclaredFields()){ Class<?> type = field.getType(); Object fieldValue = context.getBean(type); field.set(bean, filedValue); } }catch (Exception ignore){} } protected void injectMethod(Context context, Object object) { //... } }
以上仅是粗略的表述,还有许多细节由于篇幅原因没有完全展开,如果你有兴趣,可以在 https://github.com/YouthLin/mini-framework/tree/master/mini-ioc 查看完整代码~
局限性
由于我们只是一个 Mini Ioc 容器,所以现在的 Bean 对象必须要有无参构造器、并且暂时只支持字段注入,其实要支持 setter 注入也挺简单的,获取方法参数类型,然后从容器中找出参数,调用方法就行啦。同时没有 xml 配置文件,只有注解。
示例
如果你有兴趣看看到底能不能跑起来,可以通过 Maven 坐标引用:
com.youthlin mini-ioc 1.0.1
或者在 https://github.com/YouthLin/examples/tree/master/example-my-ioc 能看到示例代码~
鸣谢
本项目参考了厦大学长的项目: https://github.com/Vino007/mini-ioc
下期预告
写完发现这个容器根本用不上啊,现在大都是写 Web 项目,该怎么集成到 Java Web 中呢?
Spring MVC 是一个很赞的 MVC 框架,不需要写原生的 Servlet+JSP 了,直接在 Controller 中处理页面传过来的参数,调用 Service 层方法,往页面上添加属性,返回页面名称就能渲染出结果页面。那么我们的 Mini IoC 能不能也支持 Web 项目呢?
——请看下集:自己实现一个 Mini MVC 框架
声明
- 本作品采用署名-非商业性使用-相同方式共享 4.0 国际许可协议进行许可。除非特别注明, 霖博客文章均为原创。
- 转载请保留本文(《自己实现一个 Mini IoC 容器》)链接地址: https://youthlin.com/?p=1512
- 订阅本站:https://youthlin.com/feed/
“自己实现一个 Mini IoC 容器”上的2条回复
专业啊!完全让我懵逼了
不敢入坑JAVA……