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

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

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

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

简单描述一下上面的代码,其中 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);
}
}

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

解法三

上面解法二还是比较常规的,除了解法二之外还有一个解法,那就是使用 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;
}
}

写法跟我们解法二比较相似,只不过不是我们显示的通过容器中获取一个原型 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极客技术
相关推荐

2022-05-27 08:25:55

容器Spring

2020-10-29 09:19:11

索引查询存储

2022-06-23 10:47:57

Spring容器工具

2022-08-04 08:22:49

MySQL索引

2023-10-08 10:14:12

2017-04-17 11:50:13

51CTO 学院

2024-01-05 08:38:20

SpringBeanScope

2021-04-29 07:18:21

Spring IOC容器单例

2021-05-08 08:55:54

CPUIBMIntel

2022-05-26 09:24:09

volatile懒汉模式

2021-03-08 08:40:25

Spring Bean 创建单例对象

2011-03-18 09:27:00

Spring

2021-07-05 08:43:46

Spring Beanscope作用域

2009-06-17 17:20:14

BeanFactorySpring

2021-09-13 10:03:54

蓝牙连接蓝牙蓝牙设备

2023-01-13 07:41:20

BeanSpring容器

2024-05-28 07:55:31

SpringBean用域

2022-09-15 15:04:05

datamethods

2021-06-03 07:55:12

技术

2023-09-05 08:23:56

SpringScope方法
点赞
收藏

51CTO技术栈公众号