@Scope 註解失效了?咋回事

張哥說技術發表於2023-10-16

來源:江南一點雨


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

最終列印結果如下:

@Scope 註解失效了?咋回事

可以看到,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>, BeanFactoryAwareAopInfrastructureBean 
{
 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. 小結

經過上面的分析,我們可以得出如下幾個結論:

  1. 從 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);

上面這段程式碼不會報錯,這就是引介增強。

  1. 生成的代理物件本身其實是同一個,因為 UserService 是單例的,畢竟只注入一次 UserDao,但是代理物件中被代理的 Bean 則是會變化的。

表現出來的現象就是第一點中的四個物件,如果去比較其記憶體地址,userDao1、userDao2、scopedObject1 以及 scopedObject2 是同一個記憶體地址,因為是同一個代理物件。

但是被代理的物件則是不同的。DEBUG 之後大家可以看到,前面四個表示代理物件的地址都是同一個,後面被代理的 UserDao 則是不同的物件。

@Scope 註解失效了?咋回事

出現這個現象的原因,就是在 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/,如需轉載,請註明出處,否則將追究法律責任。

相關文章