Spring 循环依赖实践测试

前言

你可能会有如下问题:

1、想看Spring源码,但是不知道应当如何入手去看,对整个Bean的流程没有概念,碰到相关问题也没有头绪如何下手

2、看过几遍源码,没办法彻底理解,没什么感觉,没过一阵子又忘了

本文将结合实际问题,由问题引出源码,并在解释时会尽量以图表的形式让你一步一步彻底理解Spring Bean的IOC、DI、生命周期、作用域等。

先看一个循环依赖问题

现象

循环依赖其实就是循环引用,也就是两个或则两个以上的bean互相持有对方,最终形成闭环。比如A依赖于B,B依赖于C,C又依赖于A。如下图:

 
image

如何理解“依赖”呢,在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。

 
image

现象总结:同样对于循环依赖的场景,构造器注入和prototype类型的属性注入都会初始化Bean失败。因为@Service默认是单例的,所以单例的属性注入是可以成功的。

分析原因

分析原因也就是在发现SpringIOC的过程,如果对源码不感兴趣可以关注每段源码分析之后的总结和循环依赖问题的分析即可。

这里我们稍微了解下@Scope的用法:
在使用了Spring的web工程中,除非特殊情况,我们都会选择使用Spring的IOC功能来管理Bean,而不是用到时去new一个。Spring管理的Bean默认是单例的(即Spring创建好Bean,需要时就拿来用,而不是每次用到时都去new,又快性能又好),但有时候单例并不满足要求(比如Bean中不全是方法,有成员,使用单例会有线程安全问题,可以搜索线程安全与线程不安全的相关文章),你上网可以很容易找到解决办法,即使用@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/aaahttp://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.INTERFACESScopedProxyMode.TARGET_CLASS,前一个表示表示Service是一个接口,后一个表示Service是一个类。

 

 

posted @ 2022-07-20 10:28  胡小华  阅读(111)  评论(0编辑  收藏  举报