Spring 的 Bean 明明設(shè)置了 Scope 為 Prototype,為什么還是只能獲取到單例對象?
Spring? 作為當(dāng)下最火熱的Java? 框架,相信很多小伙伴都在使用,對于 Spring? 中的 Bean? 我們都知道默認(rèn)是單例的,意思是說在整個(gè) Spring 容器里面只存在一個(gè)實(shí)例,在需要的地方直接通過依賴注入或者從容器中直接獲取,就可以直接使用。
測試原型
對于有些場景,我們可能需要對應(yīng)的 Bean? 是原型的,所謂原型就是希望每次在使用的時(shí)候獲取到的是一個(gè)新的對象實(shí)例,而不是單例的,這種情況下很多小伙伴肯定會(huì)說,那還不簡單,只要在對應(yīng)的類上面加上 @scope? 注解,將 value? 設(shè)置成 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?,并將值設(shè)置為 SCOPE_PROTOTYPE?,表示是原型類,在 HelloController? 類中我們調(diào)用 HelloService? 的 sayHello? 方式,其中返回了當(dāng)前實(shí)例的 hashcode。
我們通過訪問 http://127.0.0.1:8080/hello 來獲取返回值,如果說每次獲取到的值都不一樣,那就說明我們上面的代碼是沒有問題的,每次在獲取的時(shí)候都會(huì)使用一個(gè)新的 HelloService 實(shí)例。
然而在阿粉的電腦上,無論刷新瀏覽器多少次,最后的結(jié)果卻沒有發(fā)生任何變化,換句話說這里引用到的 HelloService 始終就是一個(gè),并沒有原型的效果。
那么問題來了,我們明明給 HelloService 類增加了原型注解,為什么這里沒有效果呢?
原因分析
我們這樣思考一下,首先我們通過瀏覽器訪問接口的時(shí)候,訪問到的是 HelloController? 類中的方法,那么 HelloController? 由于我們沒有增加 Scope? 的原型注解,所以肯定是單例的,那么單例的 HelloController? 中的 HelloService 屬性是什么怎么賦值的呢?
那自然是 Spring? 在 HelloController? 初始化的時(shí)候,通過依賴注入幫我們賦值的。Spring? 注入依賴的賦值邏輯簡單來說就是創(chuàng)建 Bean? 的時(shí)候如果發(fā)現(xiàn)有依賴注入,則會(huì)在容器中獲取或者創(chuàng)建一個(gè)依賴 Bean?,此時(shí)對應(yīng)屬性的 Bean? 是單例的,則容器中只會(huì)創(chuàng)建一個(gè),如果對應(yīng)的 Bean? 是原型,那么每次都會(huì)創(chuàng)建一個(gè)新的 Bean?,然后將創(chuàng)建的 Bean 賦值給對應(yīng)的屬性。
在我們這里 HelloService? 類是原型的,所以在創(chuàng)建 HelloController Bean? 的時(shí)候,會(huì)創(chuàng)建一個(gè) HelloService? 的 Bean? 賦值到 service? 屬性上;到這里都沒有問題,但是因?yàn)槲覀?nbsp;HelloController Bean? 是單例的,初始化的動(dòng)作在整個(gè)生命周期中只會(huì)發(fā)生一次,所以即使 HelloService 類是原因的,也只會(huì)被依賴注入一次,因此我們上面的這種寫入是達(dá)不到我們需要的效果的。
解法
解法一
寫到這里有的小伙伴就會(huì)想到,那如果我把 HelloController? 類也設(shè)置成原型呢?這樣不就可以了么。給 HelloController? 增加上注解 @Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE)? 重啟過后我們重新訪問 http://127.0.0.1:8080/hello ,發(fā)現(xiàn)確實(shí)是可以的。也很好理解,因?yàn)榇藭r(shí) HelloController? 是原型的,所以每次訪問都會(huì)創(chuàng)建一個(gè)新的實(shí)例,初始化的過程中會(huì)被依賴注入新的 HelloService 實(shí)例。
但是不得不說,這種解法很不優(yōu)雅,把 Controller 類設(shè)置成原型,并不友好,所以這里我們不推薦這種解法。
解法二
除了將 HelloController? 設(shè)置成原型,我們還有其他的解法,上面我們提到 HelloController? 在初始化的時(shí)候會(huì)依賴注入 HelloService?,那我們是不是可以換一個(gè)方式,讓 HelloController? 創(chuàng)建的時(shí)候不依賴注入 HelloService,而是在真正需要的時(shí)候再從容器中獲取。如下所示:
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);
}
}
通過測試這種方式也是可以的,每次從容器中重新獲取的時(shí)候都是重新創(chuàng)建一個(gè)新的實(shí)例。
解法三
上面解法二還是比較常規(guī)的,除了解法二之外還有一個(gè)解法,那就是使用 Lookup? 注解,根據(jù) Spring 的官方文檔,我們可以看到下面的內(nèi)容。
簡單來說就是通過使用 Lookup? 注解的方法,可以被容器覆蓋,然后通過 BeanFactory 返回指定類型的一個(gè)類實(shí)例,可以在單例類中使用獲取到一個(gè)原型類,示例如下:
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;
}
}
寫法跟我們解法二比較相似,只不過不是我們顯示的通過容器中獲取一個(gè)原型 Bean? 實(shí)例,而是通過 Lookup? 的注解,讓容器來幫我們覆蓋對應(yīng)的方法,返回一個(gè)原型實(shí)例對象。這里我們的 getService? 方法里面可以直接返回一個(gè) null,因?yàn)檫@里面的代碼是不會(huì)被執(zhí)行到的。
我們打個(gè)斷點(diǎn)調(diào)試,會(huì)發(fā)現(xiàn)通過 Lookup? 注解的方法最終后走到org.springframework.beans.factory.support.CglibSubclassingInstantiationStrategy.LookupOverrideMethodInterceptor#intercept 這里。
這里我們可以看到,動(dòng)態(tài)從容器中獲取實(shí)例。不過需要注意一點(diǎn),那就是我們通過 Lookup? 注解的方法是有要求的,因?yàn)槭切枰恢貙?,所以針對這個(gè)方法我們只能使用下面的這種定時(shí)定義,必須是 public? 或者 protected,可以是抽象方法,而且方法不能有參數(shù)。
<public|protected> [abstract] <return-type> theMethodName(no-arguments);
總結(jié)
今天阿粉通過幾個(gè)例子,給大家介紹了一下如何在單例類中獲取原型類的實(shí)例,提供了三種解法,其中解法一不推薦,解法二和解法三異曲同工,感興趣的小伙伴可以自己嘗試一下。