Spring 的 Bean 明明设置了 Scope 为 Prototype,为什么还是只能获取到单例对象?

Spring 作为当下最火热的Java 框架,相信很多小伙伴都在使用,对于 Spring 中的 Bean 我们都知道默认是单例的,意思是说在整个 Spring 容器里面只存在一个实例,在需要的地方直接通过依赖注入或者从容器中直接获取,就可以直接使用。

测试原型

对于有些场景,我们可能需要对应的 Bean 是原型的,所谓原型就是希望每次在使用的时候获取到的是一个新的对象实例,而不是单例的,这种情况下很多小伙伴肯定会说,那还不简单,只要在对应的类上面加上 @scope 注解,将 value 设置成 Prototype 不就行了。如下所示

HelloService.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
package com.example.demo.service;

import org.springframework.beans.factory.config.ConfigurableBeanFactory;
import org.springframework.context.annotation.Scope;
import org.springframework.stereotype.Service;

/**
 * <br>
 * <b>Function:</b><br>
 * <b>Author:</b>@author ziyou<br>
 * <b>Date:</b>2022-07-17 21:20<br>
 * <b>Desc:</b>无<br>
 */
@Service
@Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE)
public class HelloService {

  public String sayHello() {
    return "hello: " + this.hashCode();
  }
}

HelloController.java 代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
package com.example.demo.controller;

import com.example.demo.service.HelloService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Lookup;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

/**
 * <br>
 * <b>Function:</b><br>
 * <b>Author:</b>@author ziyou<br>
 * <b>Date:</b>2022-07-17 15:43<br>
 * <b>Desc:</b>无<br>
 */
@RestController
public class HelloController {

  @Autowired
  private HelloService service;

  @GetMapping(value = "/hello")
  public String hello() {
    return service.sayHello();
  }
}

简单描述一下上面的代码,其中 HelloService 类我们使用了注解 Scope,并将值设置为 SCOPE_PROTOTYPE,表示是原型类,在 HelloController 类中我们调用 HelloServicesayHello 方式,其中返回了当前实例的 hashcode

我们通过访问 http://127.0.0.1:8080/hello 来获取返回值,如果说每次获取到的值都不一样,那就说明我们上面的代码是没有问题的,每次在获取的时候都会使用一个新的 HelloService 实例。

然而在阿粉的电脑上,无论刷新浏览器多少次,最后的结果却没有发生任何变化,换句话说这里引用到的 HelloService 始终就是一个,并没有原型的效果。

那么问题来了,我们明明给 HelloService 类增加了原型注解,为什么这里没有效果呢?

原因分析

我们这样思考一下,首先我们通过浏览器访问接口的时候,访问到的是 HelloController 类中的方法,那么 HelloController 由于我们没有增加 Scope 的原型注解,所以肯定是单例的,那么单例的 HelloController 中的 HelloService 属性是什么怎么赋值的呢?

那自然是 SpringHelloController 初始化的时候,通过依赖注入帮我们赋值的。Spring 注入依赖的赋值逻辑简单来说就是创建 Bean 的时候如果发现有依赖注入,则会在容器中获取或者创建一个依赖 Bean,此时对应属性的 Bean 是单例的,则容器中只会创建一个,如果对应的 Bean 是原型,那么每次都会创建一个新的 Bean,然后将创建的 Bean 赋值给对应的属性。

在我们这里 HelloService 类是原型的,所以在创建 HelloController Bean 的时候,会创建一个 HelloServiceBean 赋值到 service 属性上;到这里都没有问题,但是因为我们 HelloController Bean 是单例的,初始化的动作在整个生命周期中只会发生一次,所以即使 HelloService 类是原因的,也只会被依赖注入一次,因此我们上面的这种写入是达不到我们需要的效果的。

解法

解法一

写到这里有的小伙伴就会想到,那如果我把 HelloController 类也设置成原型呢?这样不就可以了么。给 HelloController 增加上注解 @Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE) 重启过后我们重新访问 http://127.0.0.1:8080/hello ,发现确实是可以的。也很好理解,因为此时 HelloController 是原型的,所以每次访问都会创建一个新的实例,初始化的过程中会被依赖注入新的 HelloService 实例。

但是不得不说,这种解法很不优雅,把 Controller 类设置成原型,并不友好,所以这里我们不推荐这种解法。

解法二

除了将 HelloController 设置成原型,我们还有其他的解法,上面我们提到 HelloController 在初始化的时候会依赖注入 HelloService,那我们是不是可以换一个方式,让 HelloController 创建的时候不依赖注入 HelloService,而是在真正需要的时候再从容器中获取。如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
package com.example.demo.controller;

import com.example.demo.service.HelloService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationContext;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

/**
 * <br>
 * <b>Function:</b><br>
 * <b>Author:</b>@author ziyou<br>
 * <b>Date:</b>2022-07-17 15:43<br>
 * <b>Desc:</b>无<br>
 */
@RestController
public class HelloController {

  @Autowired
  private ApplicationContext applicationContext;

  @GetMapping(value = "/hello")
  public String hello() {
    HelloService service = getService();
    return service.sayHello();
  }

  public HelloService getService() {
    return applicationContext.getBean(HelloService.class);
  }
}

通过测试这种方式也是可以的,每次从容器中重新获取的时候都是重新创建一个新的实例。

解法三

上面解法二还是比较常规的,除了解法二之外还有一个解法,那就是使用 Lookup 注解,根据 Spring 的官方文档,我们可以看到下面的内容。

简单来说就是通过使用 Lookup 注解的方法,可以被容器覆盖,然后通过 BeanFactory 返回指定类型的一个类实例,可以类单例类中使用获取到一个原型类,示例如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
package com.example.demo.controller;

import com.example.demo.service.HelloService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Lookup;
import org.springframework.context.ApplicationContext;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

/**
 * <br>
 * <b>Function:</b><br>
 * <b>Author:</b>@author ziyou<br>
 * <b>Date:</b>2022-07-17 15:43<br>
 * <b>Desc:</b>无<br>
 */
@RestController
public class HelloController {

  @GetMapping(value = "/hello")
  public String hello() {
    HelloService service = getService();
    return service.sayHello();
  }

  @Lookup
  public HelloService getService() {
    return null;
  }
}

写法跟我们解法二比较相似,只不过不是我们显示的通过容器中获取一个原型 Bean 实例,而是通过 Lookup 的注解,让容器来帮我们覆盖对应的方法,返回一个原型实例对象。这里我们的 getService 方法里面可以直接返回一个 null,因为这里面的代码是不会被执行到的。

我们打个断点调试,会发现通过 Lookup 注解的方法最终后走到org.springframework.beans.factory.support.CglibSubclassingInstantiationStrategy.LookupOverrideMethodInterceptor#intercept 这里,

这里我们可以看到,动态从容器中获取实例。不过需要注意一点,那就是我们通过 Lookup 注解的方法是有要求的,因为是需要被重写,所以针对这个方法我们只能使用下面的这种定时定义,必须是 public 或者 protected,可以是抽象方法,而且方法不能有参数。

<public|protected> [abstract] <return-type> theMethodName(no-arguments);

总结

今天阿粉通过几个例子,给大家介绍了一下如何在单例类中获取原型类的实例,提供了三种解法,其中解法一不推荐,解法二和解法三异曲同工,感兴趣的小伙伴可以自己尝试一下。

Java Geek Tech wechat
欢迎订阅 Java 极客技术,这里分享关于 Java 的一切。