Spring 循环依赖实践测试
前言
你可能会有如下问题:
1、想看Spring源码,但是不知道应当如何入手去看,对整个Bean的流程没有概念,碰到相关问题也没有头绪如何下手
2、看过几遍源码,没办法彻底理解,没什么感觉,没过一阵子又忘了
本文将结合实际问题,由问题引出源码,并在解释时会尽量以图表的形式让你一步一步彻底理解Spring Bean的IOC、DI、生命周期、作用域等。
先看一个循环依赖问题
现象
循环依赖其实就是循环引用,也就是两个或则两个以上的bean互相持有对方,最终形成闭环。比如A依赖于B,B依赖于C,C又依赖于A。如下图:
如何理解“依赖”呢,在Spring中有:
- 构造器循环依赖
- field属性注入循环依赖
直接上代码:
构造器循环依赖
@Service
public class A {
public A(B b) { }
}
@Service
public class B {
public B(C c) {
}
}
@Service
public class C {
public C(A a) { }
}
结果:项目启动失败,发现了一个cycle。
2.field属性注入循环依赖
@Service
public class A1 {
@Autowired
private B1 b1;
}
@Service
public class B1 {
@Autowired
public C1 c1;
}
@Service
public class C1 {
@Autowired public A1 a1;
}
结果:项目启动成功
3.field属性注入循环依赖(prototype)
@Service
@Scope("prototype")
public class A1 {
@Autowired
private B1 b1;
}
@Service
@Scope("prototype")
public class B1 {
@Autowired
public C1 c1;
}
@Service
@Scope("prototype")
public class C1 {
@Autowired public A1 a1;
}
结果:项目启动失败,发现了一个cycle。
现象总结:同样对于循环依赖的场景,构造器注入和prototype类型的属性注入都会初始化Bean失败。因为@Service默认是单例的,所以单例的属性注入是可以成功的。
分析原因
分析原因也就是在发现SpringIOC的过程,如果对源码不感兴趣可以关注每段源码分析之后的总结和循环依赖问题的分析即可。
@Scope("prototype")
注解,可以通知Spring把被注解的Bean变成多例,如下所示:import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMethod; import org.springframework.web.bind.annotation.RestController; @RestController @RequestMapping(value = "/testScope") public class TestScope { private String name; @RequestMapping(value = "/{username}",method = RequestMethod.GET) public void userProfile(@PathVariable("username") String username) { name = username; try { for(int i = 0; i < 100; i++) { System.out.println(Thread.currentThread().getId() + "name:" + name); Thread.sleep(2000); } } catch (Exception e) { e.printStackTrace(); } return; } }
分别发送请求http://localhost:8043/testScope/aaa
和http://localhost:8043/testScope/bbb
,控制台输出:
34name:aaa 34name:aaa 35name:bbb 34name:bbb
(34和35是两个线程的ID,每次运行都可能不同,但是两个请求使用的线程的ID肯定不一样,可以用来区分两个请求。)可以看到第二个请求bbb开始后,将name的内容改为了“bbb”,第一个请求的name也从“aaa”改为了“bbb”。要想避免这种情况,可以使用@Scope("prototype")
,注解加在TestScope这个类上。加完注解后重复上面的请求,发现第一个请求一直输出“aaa”,第二个请求一直输出“bbb”,成功。
Spring作为一个优秀的、用途广、发展时间长的框架,一定有成熟的解决办法。经过一番搜索,我们发现,注解@Scope("prototype")
(这个注解实际上也可以写成@Scope(value = ConfigurableBeanFactory.SCOPE_PROTOTYPE
,使用常量比手打字符串不容易出错)还有很多用法。
首先value就分为四类:
- ConfigurableBeanFactory.SCOPE_PROTOTYPE,即“prototype”
- ConfigurableBeanFactory.SCOPE_SINGLETON,即“singleton”
- WebApplicationContext.SCOPE_REQUEST,即“request”
- WebApplicationContext.SCOPE_SESSION,即“session”
他们的含义是:
- singleton和prototype分别代表单例和多例;
- request表示请求,即在一次http请求中,被注解的Bean都是同一个Bean,不同的请求是不同的Bean;
- session表示会话,即在同一个会话中,被注解的Bean都是使用的同一个Bean,不同的会话使用不同的Bean。
使用session和request产生了一个新问题,生成controller的时候需要service作为controller的成员,但是service只在收到请求(可能是request也可能是session)时才会被实例化,controller拿不到service实例。为了解决这个问题,@Scope
注解添加了一个proxyMode的属性,有两个值ScopedProxyMode.INTERFACES
和ScopedProxyMode.TARGET_CLASS
,前一个表示表示Service是一个接口,后一个表示Service是一个类。