深圳幻海软件技术有限公司 欢迎您!

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

2023-02-28

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

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

测试原型

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

HelloService.java

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();
  }
}
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.
  • 21.

HelloController.java 代码如下:

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();
  }
}
  • 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.

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

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

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

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

原因分析

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

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

在我们这里 HelloService​ 类是原型的,所以在创建 HelloController Bean​ 的时候,会创建一个 HelloService​ 的 Bean​ 赋值到 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,而是在真正需要的时候再从容器中获取。如下所示:

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);
  }
}
  • 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.

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

解法三

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

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

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;
  }
}
  • 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.

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

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

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

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

总结

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