Spring 的 Bean 明明设置了 Scope 为 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);
总结
今天阿粉通过几个例子,给大家介绍了一下如何在单例类中获取原型类的实例,提供了三种解法,其中解法一不推荐,解法二和解法三异曲同工,感兴趣的小伙伴可以自己尝试一下。
END
关注 Stephen,一起学习,一起成长。
点“在看”支持下吧
点 阅读原文 可优惠充值话费,流量,视频会员等。