@Scope 註解失效了?咋回事
來源:江南一點雨
scope 屬性,相信大家都知道,一共有六種:
取值 | 含義 | 生效條件 |
---|---|---|
singleton | 表示這個 Bean 是單例的,在 Spring 容器中,只會存在一個例項。 | |
prototype | 多例模式,每次從 Spring 容器中獲取 Bean 的時候,才會建立 Bean 的例項出來。 | |
request | 當有一個新的請求到達的時候,會建立一個 Bean 的例項處理。 | web 環境下生效 |
session | 當有一個新的會話的時候,會建立一個 Bean 的例項出來。 | web 環境下生效 |
application | 這個表示在專案的整個生命週期中,只有一個 Bean。 | web 環境下生效 |
gloablsession | 有點類似於 application,但是這個是在 portlet 環境下使用的。 | web 環境下生效 |
這個用法也很簡單,透過配置就可以設定一個 Bean 是否為單例模式。
1. 問題呈現
今天我要說的不是基礎用法,是另外一個問題,假設我現在有如下兩個 Bean:
@Service
public class UserService {
@Autowired
UserDao userDao;
}
@Repository
public class UserDao {
}
在 UserService 中注入 UserDao,由於兩者都沒有宣告 scope,所以預設都是單例的。
現在,如果我給 UserDao 設定 Scope,如下:
@Repository
@Scope(value = "prototype")
public class UserDao {
}
這個 prototype 表示如果我們從 Spring 容器中多次獲取 UserDao 的例項,拿到的不是同一個例項。
但是!!!
我現在是在 UserService 裡邊注入 UserDao 的,UserService 是單例的,也就是 UserService 只初始化了一次,按理說 UserService 也只跟 Spring 容器要了一次 UserDao,這就導致我們最終從 UserService 中拿到的 UserDao 始終是同一個。
測試方式如下:
AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(JavaConfig.class);
UserService us = ctx.getBean(UserService.class);
UserService us2 = ctx.getBean(UserService.class);
System.out.println(us.userDao == us2.userDao);
最終列印結果為 true。
其實,這個也沒啥問題,因為你確實只跟 Spring 容器只要了一次 UserDao。但是現在如果我的需求就是 UserService 是單例,UserDao 每次都獲取不同的例項呢?閣下該如何應對?
2. 解決方案
Spring 已經考慮到這個問題了,解決方案就是透過代理來實現。
在我們使用 @Scope 註解的時候,該註解還有另外一個屬性 proxyMode,這個屬性的取值有四種,如下:
public enum ScopedProxyMode {
DEFAULT,
NO,
INTERFACES,
TARGET_CLASS
}
DEFAULT:這個是預設值,預設就是 NO,即不使用代理。 NO:不使用代理。 INTERFACES:使用 JDK 動態代理,要求當前 Bean 得有介面。 TARGET_CLASS:使用 CGLIB 動態代理。
可以透過設定 proxyMode 屬性來為 Bean 產生動態代理物件,進而實現 Bean 的多例。
現在我修改 UserDao 上的註解,如下:
@Repository
@Scope(value = "prototype",proxyMode = ScopedProxyMode.TARGET_CLASS)
public class UserDao {
}
此時,再去執行測試:
AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(JavaConfig.class);
UserService us = ctx.getBean(UserService.class);
UserService us2 = ctx.getBean(UserService.class);
System.out.println(us==us2);
System.out.println("us.userDao = " + us.userDao);
System.out.println("us2.userDao = " + us2.userDao);
System.out.println("us.userDao.getClass() = " + us.userDao.getClass());
最終列印結果如下:
可以看到,UserService 是單例,userDao 確實是不同例項了,並且 userDao 是一個 CGLIB 動態代理物件。
那麼,如果是 XML 配置該怎麼配置呢?
<bean class="org.javaboy.demo.p2.UserDao" id="userDao" scope="prototype">
<aop:scoped-proxy/>
</bean>
<bean class="org.javaboy.demo.p2.UserService">
<property name="userDao" ref="userDao"/>
</bean>
這個跟普通的 AOP 配置方式不一樣,不過也很好理解,對照上面的註解配置來理解即可。
3. 原始碼分析
那麼這一切是怎麼實現的呢?
Spring 中提供了專門的工具方法 AnnotationConfigUtils#applyScopedProxyMode
來處理此事:
static BeanDefinitionHolder applyScopedProxyMode(
ScopeMetadata metadata, BeanDefinitionHolder definition, BeanDefinitionRegistry registry) {
ScopedProxyMode scopedProxyMode = metadata.getScopedProxyMode();
if (scopedProxyMode.equals(ScopedProxyMode.NO)) {
return definition;
}
boolean proxyTargetClass = scopedProxyMode.equals(ScopedProxyMode.TARGET_CLASS);
return ScopedProxyCreator.createScopedProxy(definition, registry, proxyTargetClass);
}
從這裡我們可以看到,如果代理模式是 NO/Default 的話,那麼直接返回原本的 definition,否則就要呼叫 ScopedProxyCreator.createScopedProxy
方法去生成代理物件了,這裡還涉及到一個 proxyTargetClass 引數,這個引數是用來判斷是 JDK 動態代理還是 CGLIB 動態代理的,如果設定了 proxyMode = ScopedProxyMode.TARGET_CLASS
那麼 proxyTargetClass 變數就為 true,表示 CGLIB 動態代理,否則就是 JDK 動態代理。
來繼續看 ScopedProxyCreator.createScopedProxy
方法,該方法內部呼叫到了 ScopedProxyUtils#createScopedProxy
方法:
public static BeanDefinitionHolder createScopedProxy(BeanDefinitionHolder definition,
BeanDefinitionRegistry registry, boolean proxyTargetClass) {
String originalBeanName = definition.getBeanName();
BeanDefinition targetDefinition = definition.getBeanDefinition();
String targetBeanName = getTargetBeanName(originalBeanName);
// Create a scoped proxy definition for the original bean name,
// "hiding" the target bean in an internal target definition.
RootBeanDefinition proxyDefinition = new RootBeanDefinition(ScopedProxyFactoryBean.class);
proxyDefinition.setDecoratedDefinition(new BeanDefinitionHolder(targetDefinition, targetBeanName));
proxyDefinition.setOriginatingBeanDefinition(targetDefinition);
proxyDefinition.setSource(definition.getSource());
proxyDefinition.setRole(targetDefinition.getRole());
proxyDefinition.getPropertyValues().add("targetBeanName", targetBeanName);
if (proxyTargetClass) {
targetDefinition.setAttribute(AutoProxyUtils.PRESERVE_TARGET_CLASS_ATTRIBUTE, Boolean.TRUE);
// ScopedProxyFactoryBean's "proxyTargetClass" default is TRUE, so we don't need to set it explicitly here.
}
else {
proxyDefinition.getPropertyValues().add("proxyTargetClass", Boolean.FALSE);
}
// Copy autowire settings from original bean definition.
proxyDefinition.setAutowireCandidate(targetDefinition.isAutowireCandidate());
proxyDefinition.setPrimary(targetDefinition.isPrimary());
if (targetDefinition instanceof AbstractBeanDefinition abd) {
proxyDefinition.copyQualifiersFrom(abd);
}
// The target bean should be ignored in favor of the scoped proxy.
targetDefinition.setAutowireCandidate(false);
targetDefinition.setPrimary(false);
// Register the target bean as separate bean in the factory.
registry.registerBeanDefinition(targetBeanName, targetDefinition);
// Return the scoped proxy definition as primary bean definition
// (potentially an inner bean).
return new BeanDefinitionHolder(proxyDefinition, originalBeanName, definition.getAliases());
}
這個裡邊的程式碼其實沒啥好解釋的,就是建立了一個新的 RootBeanDefinition 物件,變數名就是 proxyDefinition,從這裡也能看出來這就是用來建立代理物件的,然後把之前舊的 BeanDefinition 物件的各個屬性值都複製進去,最後把新的代理的 proxyDefinition 返回。
這裡有一個值得關注的點就是建立 proxyDefinition 的時候,構造方法傳入的引數是 ScopedProxyFactoryBean,意思就是這個 BeanDefinition 將來要產生的物件是 ScopedProxyFactoryBean 的物件,那我們繼續來看 ScopedProxyFactoryBean,從名字上可以看出來這是一個 FactoryBean:
public class ScopedProxyFactoryBean extends ProxyConfig
implements FactoryBean<Object>, BeanFactoryAware, AopInfrastructureBean {
private final SimpleBeanTargetSource scopedTargetSource = new SimpleBeanTargetSource();
@Nullable
private String targetBeanName;
@Nullable
private Object proxy;
public ScopedProxyFactoryBean() {
setProxyTargetClass(true);
}
@Override
public void setBeanFactory(BeanFactory beanFactory) {
this.scopedTargetSource.setBeanFactory(beanFactory);
ProxyFactory pf = new ProxyFactory();
pf.copyFrom(this);
pf.setTargetSource(this.scopedTargetSource);
Class<?> beanType = beanFactory.getType(this.targetBeanName);
if (!isProxyTargetClass() || beanType.isInterface() || Modifier.isPrivate(beanType.getModifiers())) {
pf.setInterfaces(ClassUtils.getAllInterfacesForClass(beanType, cbf.getBeanClassLoader()));
}
ScopedObject scopedObject = new DefaultScopedObject(cbf, this.scopedTargetSource.getTargetBeanName());
pf.addAdvice(new DelegatingIntroductionInterceptor(scopedObject));
pf.addInterface(AopInfrastructureBean.class);
this.proxy = pf.getProxy(cbf.getBeanClassLoader());
}
@Override
public Object getObject() {
return this.proxy;
}
@Override
public Class<?> getObjectType() {
if (this.proxy != null) {
return this.proxy.getClass();
}
return this.scopedTargetSource.getTargetClass();
}
@Override
public boolean isSingleton() {
return true;
}
}
這裡的 getObject 方法返回的就是 proxy 物件,而 proxy 物件是在 setBeanFactory 方法中初始化的(setBeanFactory 方法是在 Bean 初始化之後,屬性填充完畢之後觸發呼叫的)。
setBeanFactory 方法中就是去建立代理物件,設定的 targetSource 就是 scopedTargetSource,這個裡邊封裝了被代理的物件,scopedTargetSource 是一個 SimpleBeanTargetSource 型別的 Bean,SimpleBeanTargetSource 的特點就是每次獲取代理物件的時候,都會重新去呼叫 getTarget 方法,而在 SimpleBeanTargetSource 的 getTarget 方法中就是根據原始的 Bean 名稱去 Spring 容器中查詢 Bean 並返回,也就是說,在這裡代理物件中,被代理的物件實際上就是原始的 Bean,對應上文案例來說,被代理的物件就是 userDao。
另外一個需要關注的點就是新增的攔截器 DelegatingIntroductionInterceptor 了,這是為代理物件增強的內容(setBeanFactory 方法中其他內容都是常規的 AOP 程式碼,我就不多說了,不熟悉的小夥伴可以看看松哥最近錄製的 Spring 原始碼影片哦Spring原始碼應該怎麼學?)。
DelegatingIntroductionInterceptor 攔截器傳入了 scopedObject 作為引數,這個引數實際上就表示了被代理的物件,也就是被代理的物件是一個 ScopedObject。
public class DelegatingIntroductionInterceptor extends IntroductionInfoSupport
implements IntroductionInterceptor {
@Nullable
private Object delegate;
public DelegatingIntroductionInterceptor(Object delegate) {
init(delegate);
}
protected DelegatingIntroductionInterceptor() {
init(this);
}
private void init(Object delegate) {
this.delegate = delegate;
implementInterfacesOnObject(delegate);
suppressInterface(IntroductionInterceptor.class);
suppressInterface(DynamicIntroductionAdvice.class);
}
@Override
@Nullable
public Object invoke(MethodInvocation mi) throws Throwable {
if (isMethodOnIntroducedInterface(mi)) {
Object retVal = AopUtils.invokeJoinpointUsingReflection(this.delegate, mi.getMethod(), mi.getArguments());
if (retVal == this.delegate && mi instanceof ProxyMethodInvocation pmi) {
Object proxy = pmi.getProxy();
if (mi.getMethod().getReturnType().isInstance(proxy)) {
retVal = proxy;
}
}
return retVal;
}
return doProceed(mi);
}
@Nullable
protected Object doProceed(MethodInvocation mi) throws Throwable {
return mi.proceed();
}
}
DelegatingIntroductionInterceptor 實現了 IntroductionInterceptor 介面,這就是典型的引介增強,這個松哥之前也寫過文章專門跟大家講過:Spring 中一個少見的引介增強 IntroductionAdvisor,看過之前的文章這裡的內容應該都能懂。由於是引介增強,所以最終生成的代理物件,既是 UserDao 的例項,也是 ScopedObject 的例項。
4. 小結
經過上面的分析,我們可以得出如下幾個結論:
從 UserService 中多次獲取到的 UserDao,其實也是 ScopedObject 物件。
AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(JavaConfig.class);
UserService us1 = ctx.getBean(UserService.class);
UserService us2 = ctx.getBean(UserService.class);
UserDao userDao1 = us1.getUserDao();
UserDao userDao2 = us2.getUserDao();
ScopedObject scopedObject1 = (ScopedObject) userDao1;
ScopedObject scopedObject2 = (ScopedObject) userDao2;
System.out.println("userDao1 = " + userDao1);
System.out.println("userDao2 = " + userDao2);
System.out.println("scopedObject1 = " + scopedObject1);
System.out.println("scopedObject2 = " + scopedObject2);
上面這段程式碼不會報錯,這就是引介增強。
生成的代理物件本身其實是同一個,因為 UserService 是單例的,畢竟只注入一次 UserDao,但是代理物件中被代理的 Bean 則是會變化的。
表現出來的現象就是第一點中的四個物件,如果去比較其記憶體地址,userDao1、userDao2、scopedObject1 以及 scopedObject2 是同一個記憶體地址,因為是同一個代理物件。
但是被代理的物件則是不同的。DEBUG 之後大家可以看到,前面四個表示代理物件的地址都是同一個,後面被代理的 UserDao 則是不同的物件。
出現這個現象的原因,就是在 ScopedProxyFactoryBean 的 setBeanFactory 方法中,我們設定的 TargetSource 是一個 SimpleBeanTargetSource,這個 TargetSource 的特點就是每次代理的時候,都會去 Spring 容器中查詢 Bean,而由於 UserDao 在 Spring 容器中是多例的,因此 Spring 每次返回的 UserDao 就不是同一個,就實現了 UserDao 的多例:
public class SimpleBeanTargetSource extends AbstractBeanFactoryBasedTargetSource {
@Override
public Object getTarget() throws Exception {
return getBeanFactory().getBean(getTargetBeanName());
}
}
對於第二點的內容,如果小夥伴們還不理解,可以翻看松哥之前的文章:AOP 中被代理的物件是單例的嗎?。
好啦,現在小夥伴們搞明白怎麼回事了吧~
來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/70024923/viewspace-2989082/,如需轉載,請註明出處,否則將追究法律責任。
相關文章
- 【spring 註解】第3篇:@Scope、@Lazy和@Conditional註解Spring
- eclipse環境下lombok中的註解失效 @AllArgsConstructor @Slf4j 註解失效EclipseLombokStruct
- Spring Boot 自定義註解失效Spring Boot
- 【Spring註解驅動開發】使用@Scope註解設定元件的作用域Spring元件
- 今天好多人 phpstrom 編譯器註冊碼失效了,最新可用註冊碼PHP編譯
- WebMagic多執行緒導致註解失效問題Web執行緒
- [獻醜了!] Android AOP註解GoodAtAndroidGo
- Maven依賴scope範圍詳解Maven
- Maven依賴中的scope詳解Maven
- spring動態註冊bean會使AOP失效?SpringBean
- Mata解決了快取何時失效的世紀難題? - Lu快取
- 答讀者問:BeanFactoryPostProcessor 似乎失效了?Bean
- @Transactional 註解下,事務失效的多種場景
- Maven中optional和scope元素的使用,你弄明白了?Maven
- 我發現買不起自己出版的圖書了,這到底是咋回事?
- tarui drop失效,解決配置UI
- maven中的scope標籤類別詳解Maven
- Maven中的dependency的scope作用域詳解Maven
- 【Java8新特性】重複註解與型別註解,你真的學會了嗎?Java型別
- 終於搞懂Spring中Scope為Request和Session的Bean了SpringSessionBean
- Laravel scope用法Laravel
- Spring 快取註解這樣用,太香了!Spring快取
- 【Spring註解驅動開發】你還不會使用@Resource和@Inject註解?那你就out了!!Spring
- height:100%失效解決辦法
- JAVA-註解(2)-自定義註解及反射註解Java反射
- 註解專題(一)Java元註解,內建註解Java
- win10更新密碼失效了怎麼辦 win10更新檔案密碼失效了恢復方法Win10密碼
- kettle資源庫連線失效不見了
- 別問了,我真的不喜歡這個註解!
- Java註解-後設資料、註解分類、內建註解和自定義註解Java
- SpringBoot框架:兩個方法同時呼叫時父方法使內部方法的DataSource註解失效的解決辦法Spring Boot框架
- 【Spring註解】事務註解@TransactionalSpring
- @ResponseBody註解和@RequestBody註解使用
- [Vue] slot詳解,slot、slot-scope和v-slotVue
- alter system set ... scope=... 中的scope的含義是什麼?
- 註解
- Java註解詳解「註解專案實戰」Java
- 我去,為什麼最左字首原則失效了?