Spring的Bean明明设置了Scope,为什么还是只能获取到单例对象?
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);
总结
今天阿粉通过几个例子,给大家介绍了一下如何在单例类中获取原型类的实例,提供了三种解法,其中解法一不推荐,解法二和解法三异曲同工,感兴趣的小伙伴可以自己尝试一下。