从零开始实现一个简易的 Java MVC 框架(三)--实现 IOC
doodle
2018-07-05
4093
6
Spring 中的 IOC
IoC 全称是 Inversion of Control,就是控制反转,他其实不是 spring 独有的特性或者说也不是 java 的特性,他是一种设计思想。而 DI(Dependency Injection),即依赖注入就是 Ioc 的一种实现方式。关于 Ioc 和 DI 的具体定义和优缺点等大家可以自行查找资料了解一下,这里就不详细赘述,总之 spring 的 IoC 功能很大程度上便捷了我们的开发工作。
在实现我们的 Ioc 之前,我们先了解一下 spring 的依赖注入,在 spring 中依赖注入有三种方式,分别是:
- 接口注入 (Interface Injection)
- 设值方法注入 (Setter Injection)
- 构造注入 (Constructor Injection)
@Component
public class ComponentA {
@Autowired // 1. 接口注入
private ComponentB componentB;
@Autowired // 2. 设值方法注入
public void setComponentB(ComponentB componentB) {
this.componentB = componentB;
}
@Autowired // 3. 构造注入
public ComponentA(ComponentB componentB) {
this.componentB = componentB;
}
}
循环依赖注入
如果只是实现依赖注入的话实际上很简单,只要利用 java 的反射原理将对应的属性‘注入’进去就可以了。但是必须要注意一个问题,那就是循环依赖问题。循环依赖就是类之间相互依赖形成了一个循环,比如 A 依赖于 B,同时 B 又依赖于 A,这就形成了相互循环。
// ComponentA
@Component
public class ComponentA {
@Autowired
private ComponentB componentB;
}
// ComponentB
@Component
public class ComponentB {
@Autowired
private ComponentA componentA;
}
那么在 spring 中又是如何解决循环依赖问题的呢,我们大致说一下原理。
如果要创建一个类,先把这个类放进'正在创建池'中,通过反射等创建实例,创建成功的话就把这个实例放入创建池中,并移除'正在创建池'中的这个类。每当实例中有依赖需要注入的话,就从创建池中找对应的实例注入进去,如果没有找到实例,则先创建这个依赖。
利用了这个正在创建的中间状态缓存,让 Bean 的创建的时候即使有依赖还没有实例化,可以先把 Bean 放进这个中间状态,然后跑去创建那个依赖,假如那个依赖的类又依赖与这个 Bean,那么只要在'正在创建池'中再把这个 Bean 拿出来,注入到这个依赖中,就可以保证 Bean 的依赖能够实例化完成。再回头来把这个依赖注入到 Bean 中,那么这个 Bean 也实例化完成了,就把这个 Bean 从'正在创建池'移到'创建完成池'中,就解决了循环依赖问题。
虽然 spring 巧妙的避免了循环依赖问题,但是事实上构造注入是无法避免循环依赖问题的。因为在实例化ComponentA
的构造函数的时候必须得到ComponentB
的实例,但是实例化ComponentB
的构造函数的时候又必须有ComponentA
的实例。这两个 Bean 都不能通过反射实例化然后放到'正在创建池',所以无法解决循环依赖问题,这时候 spring 就会主动抛出BeanCurrentlyInCreationException
异常避免死循环。
* 注意,前面讲的这些都是基于 spring 的单例模式下的,如果是多例模式会有所不同,大家有兴趣可以自行了解。
实现 IOC
现在可以开始实现 IOC 功能了
增加注解
先在 zbw.ioc 包下创建一个 annotation 包,然后再创建一个Autowired
的注解。这个注解的 Target 只有一个ElementType.FIELD
,就是只能注解在属性上。意味着我们目前只实现接口注入的功能。这样可以避免构造注入造成的循环依赖问题无法解决,而且接口注入也是用的最多的方式了。如果想要实现设值方式注入大家可以自己去实现,实现原理几乎都一样。
package com.zbw.ioc.annotation;
import ...
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Autowired {
}
实现 IOC 类
package com.zbw.ioc;
import ...
@Slf4j
public class Ioc {
/**
* Bean 容器
*/
private BeanContainer beanContainer;
public Ioc() {
beanContainer = BeanContainer.getInstance();
}
/**
* 执行 Ioc
*/
public void doIoc() {
for (Class<?> clz : beanContainer.getClasses()) { //遍历 Bean 容器中所有的 Bean
final Object targetBean = beanContainer.getBean(clz);
Field[] fields = clz.getDeclaredFields();
for (Field field : fields) { //遍历 Bean 中的所有属性
if (field.isAnnotationPresent(Autowired.class)) {// 如果该属性被 Autowired 注解,则对其注入
final Class<?> fieldClass = field.getType();
Object fieldValue = getClassInstance(fieldClass);
if (null != fieldValue) {
ClassUtil.setField(field, targetBean, fieldValue);
} else {
throw new RuntimeException("无法注入对应的类,目标类型:" + fieldClass.getName());
}
}
}
}
}
/**
* 根据 Class 获取其实例或者实现类
*/
private Object getClassInstance(final Class<?> clz) {
return Optional
.ofNullable(beanContainer.getBean(clz))
.orElseGet(() -> {
Class<?> implementClass = getImplementClass(clz);
if (null != implementClass) {
return beanContainer.getBean(implementClass);
}
return null;
});
}
/**
* 获取接口的实现类
*/
private Class<?> getImplementClass(final Class<?> interfaceClass) {
return beanContainer.getClassesBySuper(interfaceClass)
.stream()
.findFirst()
.orElse(null);
}
}
在知道 IOC 的原理之后发现其实真的是非常简单,这里用了几十行的代码就实现了 IOC 的功能。
首先在Ioc 类
构造的时候先获取到我们之前已经单例化的 BeanContainer 容器。
然后在doIoc()
方法中就是正式实现 IOC 功能的了。
- 遍历在 BeanContainer 容器的所有 Bean
- 对每个 Bean 的 Field 属性进行遍历
- 如果某个 Field 属性被
Autowired
注解,则调用getClassInstance()
方法对其进行注入 getClassInstance()
会根据 Field 的 Class 尝试从 Bean 容器中获取对应的实例,如果获取到则返回该实例,如果获取不到,则我们认定该 Field 为一个接口,我们就调用getImplementClass()
方法来获取这个接口的实现类 Class,然后再根据这个实现类 Class 在 Bean 容器中获取对应的实现类实例。
测试用例
为了测试我们的 Ioc 和之前写的 BeanContainer 编写正确,我们写一下测试用例测试一下。
先在 pom.xml 添加 junit 的依赖
<properties>
...
<junit.version>4.12</junit.version>
</properties>
<dependencies>
...
<!-- junit -->
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>${junit.version}</version>
<scope>test</scope>
</dependency>
</dependencies>
然后在 test 包下添加DoodleController
、DoodleService
、DoodleServiceImpl
三个类方便测试
// DoodleController
package com.zbw.bean;
@Controller
@Slf4j
public class DoodleController {
@Autowired
private DoodleService doodleService;
public void hello() {
log.info(doodleService.helloWord());
}
}
// DoodleService
package com.zbw.bean;
public interface DoodleService {
String helloWord();
}
// DoodleServiceImpl
package com.zbw.bean;
@Service
public class DoodleServiceImpl implements DoodleService{
@Override
public String helloWord() {
return "hello word";
}
}
再编写IocTest
的测试用例
package com.zbw.ioc;
import ...
@Slf4j
public class IocTest {
@Test
public void doIoc() {
BeanContainer beanContainer = BeanContainer.getInstance();
beanContainer.loadBeans("com.zbw");
new Ioc().doIoc();
DoodleController controller = (DoodleController) beanContainer.getBean(DoodleController.class);
controller.hello();
}
}
看到在DoodleController
中输出了DoodleServiceImpl
的helloWord()
方法里的字符串,说明DoodleController
中的DoodleService
已经成功注入了DoodleServiceImpl
。那么我们的 IOC 的功能也完成了。
- 从零开始实现一个简易的 Java MVC 框架(一)--前言
- 从零开始实现一个简易的 Java MVC 框架(二)--实现 Bean 容器
- 从零开始实现一个简易的 Java MVC 框架(三)--实现 IOC
- 从零开始实现一个简易的 Java MVC 框架(四)--实现 AOP
- 从零开始实现一个简易的 Java MVC 框架(五)--引入 aspectj 实现 AOP 切点
- 从零开始实现一个简易的 Java MVC 框架(六)--加强 AOP 功能
- 从零开始实现一个简易的 Java MVC 框架(七)--实现 MVC
- 从零开始实现一个简易的 Java MVC 框架(八)--制作 Starter
- 从零开始实现一个简易的 Java MVC 框架(九)--优化 MVC 代码
源码地址:doodle