自定义实现IOC思路拆解
近日在理解IOC与DI,看了很多概念,也写过很多小练习,但总觉得只是认识,不甚熟悉,蓦然回首,偶尔还能发出“大兄dei,您哪位?”的感慨。在前辈的指导下,自己写了个玩具性质的IOC容器,逐步的摸索过程让很多浮于水上的知识点瞬间有了一花一世界的立体感。特此记录,希望对你的学习也能有所帮助。
思路
- 明确IOC思想——明确需求,知道需要实现的初步效果
- 确定实现方式——拆解需求,逐功能点实现初步效果
- 设定完善点——发现并确立需要完善的关键问题
- 拆解并实现问题——解决问题
- 寻找参照,发现问题——头脑风暴,提升解决问题的能力
一、初步实现"IOC思想"
IOC(Inersion of Control):控制反转
借助于“第三方”实现具有依赖关系的对象之间的解耦
深度理解

软件系统没有引入IOC容器前(如图1):对象直接相互依赖,若对象A进行初始化,则必须主动去创建对象B或使用已创建的对象B,这个创建对象或者使用对象的过程,控制器在Coder手中。

软件系统引入IOC容器后(如图2):对象间失去直接联系,若对象A进行初始化,IoC容器会主动创建一个对象B注入到对象A需要的地方。
对象A获得依赖对象B的过程,由Coder的主动行为,变成了被动行为,控制权颠倒,即控制反转。
文字参考“浅谈IOC”,若是觉得还是很抽象,可参考原文
实现效果
通过对IOC思想的理解,我们可以得出,IOC容器需要实现关于对象的创建与存取功能。
我们应该实现以下3个接口:
/**
* 注册一个Class到IoC容器中
* @param clasz
* @throws Exception
*/
void registerBean(Class<?> clasz) throws Exception;
/**
* 初始化装配
*/
void initAutoWired();
/**
* 获取bean——根据Class获取Bean
* @param clasz
*/
<T> T getBean(Class<T> clasz);
创建对象
Constructor:构造方法
创建对象:
T newInstance(Object... initargs)
如果使用空参构造方法创建对象,操作可以简化:Class对象的newInstance()方法
String name = clasz.getName();
//创建对象
Constructor constructor = clasz.getConstructor();
Object obj = constructor.newInstance(null);
初始化装配
存储对象——创建一个Map容器,存储对象
//存储所有的对象(类名:该类的实例化对象)
private final Map<String ,Object> beanNameMap;
获取对象
通过类名,从Map容器中获取对象
啊。。。就这?
不,这只是开始
注意,我们的根本目的是学习和思考
?!
二、完善实现"IOC思想"
发现问题
1、我们通过类名实例化对象,那么类名从何而来?如何不通过Coder自动化获取类名? 2、我们仅仅创建对象,并不是完整的实例化对象,成员属性均未赋值,如何赋值?赋什么值? 3、若类之间存在聚合、组合等关系,那么如何实现注入?
解决问题
Q1——自动获取所有类名
思路拆解
我们可以扫描包下的所有类,找到需要实例化的实体类,获取他们的类名 关键点:
1.扫描存放实体类的包 2.筛选出需要实例化的实体类 3.获取实体类的类名,并存放至容器
关键点1有很多种方式实现,只是输出结果要成为关键点2的实现依据,
即,通过什么判断筛选实体类?——注解
实现步骤
确定了通过注解筛选实体类,那么对应着扫描实体包下该注解标注的实体类,获取类名,存放至容器
1.自定义注解类——标志这个类需要加入bean管理 2.通过反射扫描对应的包——导入reflections包 3.获取标注了刚刚自定义注解类的实体类
关于reflections包若是不太熟悉,可参考”java非常好用的反射框架Reflections“
实现
- 自定义注解类
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface MyClassAnno {
//通过该值获取类(类全名)
String name();
}
- maven导入reflections包
<dependency>
<groupId>org.reflections</groupId>
<artifactId>reflections</artifactId>
<version>0.10.2</version>
</dependency>
- 扫描包并获取实体类
//2. 反射扫描对应的包下的所有文件
Reflections reflection = new Reflections(packageName);
//3.获取标注定义类注解的类
Set<Class<?>> classes = reflection.getTypesAnnotatedWith(MyClassAnno.class, true);
Q2——实例化对象
思路拆解
在考虑成员属性均为基本数据类型的条件下,我们可以在定义类的时候直接赋值,这是一种解决方式,简单粗暴,有很多缺点,关键是它不满足我们学习的初衷。 借鉴上一个问题,我们可以通过注解进行初始化赋值,而且注解可以对成员属性进行筛选,并且可以设置多个值,多种值,可扩展性更强,远比直接赋值高级。 关键点:
1.获取需要初始化的成员属性 2.判断该成员属性的基本数据类型 3.为该成员属性赋值
通过获取类中自定义属性的注解获取需要的成员属性,并利用注解进行赋值
实现步骤
通过注解获取属性,获取属性的数据类型,并为属性赋值
1.自定义注解类——标志这个属性需要初始化赋值,并在实体类的成员属性上设置应赋的值 2.获取刚刚自定义注解类标注的属性,get该属性的数据类型 3.赋值
实现
- 自定义注解类
/**
* 属性的注解
*/
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface MyFieldAnno {
//通过该值设置/获取类的属性
String value();
}
- 实体类
@MyClassAnno(name="student")
public class StudentImpl implements Person {
@MyFieldAnno("2")
private int id;
@MyFieldAnno("Student-Jing")
private String name;
}
- 遍历实体类并为其赋值
for (Class<?> aClass : classes) {
try {
...
//注册对象
...
//获取该对象的所有属性
Field[] fields = aClass.getDeclaredFields();
//获取该属性的所有注解
Annotation[] declaredAnnotations = field.getDeclaredAnnotations();
if(annotation.annotationType().toString().equals("interface it.lucifer.annotate.MyFieldAnno")){
//获取注解——基本数据类型
MyFieldAnno fieldAnno = field.getAnnotation(MyFieldAnno.class);
//获取属性值的数据类型的字符串
String fieldType = field.getGenericType().toString();
//将注解的值赋值给该属性
if(fieldType.equals("class java.lang.Integer") || fieldType.equals("int")){
field.set(obj,Integer.valueOf(fieldAnno.value()));
}else if(fieldType.equals("class java.lang.Double") || fieldType.equals("double")){
field.set(obj,Double.valueOf(fieldAnno.value()));
...
}
...
}
} catch (Exception e) {
e.printStackTrace();
}
}
Q3——对象属性注入
思路拆解
对于存在关联关系的两个类如何实现对象的注入? 假设A类的对象包含有B类对象,若先实例化生成B类的对象,则在实例化A类对象时,可将B类对象直接注入,但若先实例化A类对象呢? 关键点:
1.通过A类成员属性找到B类的实例化对象 2.在保证程序运行成功的条件下,实现对象的注入
我们可以先对类进行基本初始化,即保留其他类的对象属性暂不赋值,待所有对象基本初始化结束后,再针对这些属性一一赋值,而寻找对象的方式我们依旧可以通过添加注解进行
实现步骤
确定了通过注解筛选实体类,那么对应着扫描实体包下该注解标注的实体类,获取类名,存放至容器
1.自定义注解类——标志这个成员属性需要注入其他类的对象 2.定义两个容器,第一个容器存放成员类的类名注解值与类名,第二个容器存放待注入属性与对应对象 3.实现对象注入
实现
- 自定义注解类
/**
* 类属性的注解——便于实现依赖注入
*/
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface ClassFieldAnno {
String value();
}
- 实体类(Student)
@MyClassAnno(name="student")
public class StudentImpl implements Person {
...
@ClassFieldAnno("teacher2")
private TeacherImpl teacher;
...
}
- 实体类(Teacher)
@MyClassAnno(name = "teacher2")
public class TeacherImpl implements Person {
@MyFieldAnno("Teacher-Yan")
private String name;
@MyFieldAnno("25")
private int age;
@MyFieldAnno("Java")
private String course;
}
- 创建容器
//存储bean和name的映射关系
private final Map<String,String> beanClassKeys;
//存储未初始化属性
private final Map<Field,Object> beanField;
- 将待注入属性存入beanField容器
if(annotation.annotationType().equals(ClassFieldAnno.class)){
//将待注入属性放入容器
beanField.put(field,obj);}
- 注入对象
for(Map.Entry<Field,Object> entry : beanField.entrySet()){
try {
//获取注解——依赖类
ClassFieldAnno classFieldAnno =
entry.getKey().getAnnotation(ClassFieldAnno.class);
//获取注解的值——对象的标签名
String classFileName = classFieldAnno.value();
//查找对应类名的对象
Object objBean = beanNameMap.get(beanClassKeys.get(classFileName));
//注入对象
entry.getKey().set(entry.getValue(),objBean);
} catch (IllegalAccessException e) {
e.printStackTrace();
}
}
emmm~
好像和之前看的不一样
不一样就对了,我自己写的
解决问题的方式与实现的效果有很多种
能达到自己的目的即可
三、深入理解"控制反转"
追根溯源:为了降低软件系统的耦合性,结合软件设计原则(依赖倒置与单一职责原则),Michael Mattson提出了IOC的概念,这是一种设计代码的思路,为了实现这个解耦的思路,实现了DI(依赖注入),这是一种设计代码模式。
所以,IOC 不是一种技术,只是一种思想,一个重要的面向对象编程的法则,它能指导我们如何设计出松耦合、更优良的程序
IOC和DI由什么关系呢?其实它们是同一个概念的不同角度描述,由于控制反转概念比较含糊(可能只是理解为容器控制对象这一个层面,很难让人想到谁来维护对象关系),所以2004年大师级人物Martin Fowler又给出了一个新的名字:“依赖注入”,相对IOC 而言,“依赖注入”明确描述了“被注入对象依赖IOC容器配置依赖对象”。
IOC
传统应用程序都是由我们在类内部主动创建依赖对象,从而导致类与类之间高耦合,难于测试;有了IOC容器后,把创建和查找依赖对象的控制权交给了容器,由容器进行注入组合对象,所以对象与对象之间是 松散耦合,这样也方便测试,利于功能复用,更重要的是使得程序的整个体系结构变得非常灵活。
其实IOC对编程带来的最大改变不是从代码上,而是从思想上,发生了“主从换位”的变化。应用程序原本是老大,要获取什么资源都是主动出击,但是在IOC/DI思想中,应用程序就变成被动的了,被动的等待IOC容器来创建并注入它所需要的资源了。
即由IOC容器帮对象找相应的依赖对象并注入,而不是由对象主动去找。
DI
DI—Dependency Injection,即“依赖注入”:组件之间依赖关系由容器在运行期决定,形象的说,即由容器动态的将某个依赖关系注入到组件之中。
依赖注入的目的并非为软件系统带来更多功能,而是为了提升组件重用的频率,并为系统搭建一个灵活、可扩展的平台。
通过依赖注入机制,我们只需要通过简单的配置,而无需任何代码就可指定目标需要的资源,完成自身的业务逻辑,而不需要关心具体的资源来自何处,由谁实现。
理解DI的关键是:“谁依赖谁,为什么需要依赖,谁注入谁,注入了什么”,那我们来深入分析一下:
1.谁依赖于谁:当然是应用程序依赖于IOC容器; 2.为什么需要依赖:应用程序需要IOC容器来提供对象需要的外部资源; 3.谁注入谁:很明显是IOC容器注入应用程序某个对象,应用程序依赖的对象; 4.注入了什么:就是注入某个对象所需要的外部资源(包括对象、资源、常量数据)。
文字参考“深度理解依赖注入(Dependence Injection)–EagleFish(邢瑜琨)”、“依赖注入和控制反转的理解”若是存在疑问,可参考原文
a~~~
再看Spring感觉瞬间明了了很多
此文章为学习总结,仅代表个人愚见
文中存在不当之处难免
权当抛砖引玉,敬听评说
望不吝赐教
希望我的思路拆解对同样待启航的你能有所帮助
希君生羽翼
一化北溟鱼