切換語言為:簡體
透過 Lazy 註解解決 Spring 迴圈依賴原理解析

透過 Lazy 註解解決 Spring 迴圈依賴原理解析

  • 爱糖宝
  • 2024-07-05
  • 2167
  • 0
  • 0

Spring 迴圈依賴一般包含 構造器注入迴圈依賴 和欄位注入(setter方式)迴圈依賴, 欄位注入迴圈依賴,Spring 官方透過三層快取解決。而今天分享的重點是:Spring 是如何解決構造器注入產生的迴圈依賴問題?

申明:本文原始碼 基於 springboot-2.7.0 、spring-5.3.20 和 JDK11

注意:從 springboot-2.7.0 開始,不再推薦 使用 /META-INF/spring.factories 檔案所指定的配置類去載入Bean。

背景

前段時間,因部門同事遇到一個 Spring 迴圈依賴的問題,IDEA 錯誤資訊如下:

  ***************************
  APPLICATION FAILED TO START
  ***************************

  Description:

  The dependencies of some of the beans in the application context form a cycle:

  ┌─────┐
  |  orderService defined in file [./target/classes/cn/xxx/spring/OrderService.class]
  ↑     ↓
  |  userService defined in file [./target/classes/cn/xxx/spring/UserService.class]
  └─────┘

  Action:

  Relying upon circular references is discouraged and they are prohibited by default. Update your application to remove the dependency cycle between beans. As a last resort, it may be possible to break the cycle automatically by setting spring.main.allow-circular-references to true.

錯誤資訊大體意思是:不鼓勵依賴迴圈引用,預設情況下是禁止的。可以透過修改程式碼,刪除 bean 之間的依賴迴圈。或者透過將 spring.main.allow-circular-references 設定為 true 來自動中斷迴圈。

鑑於自己曾經也遇到過這個問題,因此把曾經整理的雲筆記結合原始碼輸出此文,希望幫助到同樣遇坑的小夥伴。

什麼是迴圈依賴

迴圈依賴是指:物件例項之間依賴關係構成一個閉環,通常分為:單個物件的自我依賴、兩個物件的相互依賴、多個物件依賴成環。迴圈依賴抽象如下圖:

透過 Lazy 註解解決 Spring 迴圈依賴原理解析

單個物件的自我依賴

// 變數依賴注入迴圈依賴,伺服器啟動會報錯
@Component
public class UserService {
  @Autowired
  private UserService userService;
}

執行結果

***************************
APPLICATION FAILED TO START
***************************

Description:

The dependencies of some of the beans in the application context form a cycle:

┌──->──┐
|  userService2 (field cn.yuanjava.service.UserService2 cn.yuanjava.service.UserService2.userService2)
└──<-──┘

// 欄位依賴注入迴圈依賴
@Component
public class OrderService {
  @Autowired
  private OrderService orderService;
}

// 構造器注入迴圈依賴
@Component
public class OrderService {

  private final OrderService orderService;

  public OrderService(){
    this.OrderService = orderService;
  }
}

Error starting ApplicationContext. To display the conditions report re-run your application with 'debug' enabled.
 ERROR 3846 --- [           main] o.s.b.d.LoggingFailureAnalysisReporter   :

  ***************************
  APPLICATION FAILED TO START
  ***************************

  Description:
  The dependencies of some of the beans in the application context form a cycle:

  ┌─────┐
  |  orderService (field private cn.yuanjava.service.UserService cn.yuanjava.service.OrderService.userService)
  ↑     ↓
  |  userService (field private cn.yuanjava.service.OrderService cn.yuanjava.service.UserService.orderService)
  └─────┘

  Action:
  Relying upon circular references is discouraged and they are prohibited by default. Update your application to remove the dependency cycle between beans. As a last resort, it may be possible to break the cycle automatically by setting spring.main.allow-circular-references to true.

產生了這種迴圈依賴,說明程式碼真的很low,要自我檢討。

兩個物件的相互依賴

從上文 OrderService 和 UserService 兩個類的程式碼可以看出,在初始化 OrderService 類時,需要依賴 UserService,而 UserService 類未例項化,因此需要例項化 UserService 類,但是在初始化 UserService 類時發現它又依賴 OrderService 類,因此就產生了迴圈依賴,依賴關係可以抽象成下圖:

透過 Lazy 註解解決 Spring 迴圈依賴原理解析

// 欄位依賴注入迴圈依賴,伺服器能正常run
@Component
public class OrderService {
  @Autowired
  private UserService userService;
}

@Component
public class UserService {
  @Autowired
  private OrderService orderService;
}


// 構造器注入迴圈依賴,伺服器啟動會報錯
@Component
public class OrderService {
  private final UserService userService;
  public OrderService(UserService userService){
    this.userService = userService;
  }
}

@Component
public class UserService {

  private final OrderService orderService;
  public UserService(OrderService orderService){
    this.orderService = orderService;
  }
}

多個物件的依賴成環

// 變數依賴注入迴圈依賴,伺服器啟動會報錯
@Component
public class UserService {
  @Autowired
  private OrderService orderService;
}

@Component
public class OrderService {
  @Autowired
  private GoodsService goodsService;
}

@Component
public class GoodsService {
  @Autowired
  private UserService userService;
}

// 構造器注入迴圈依賴,伺服器啟動會報錯
@Component
public class OrderService {
  private final UserService userService;
  public OrderService(UserService userService){
    this.userService = userService;
  }
}

@Component
public class UserService {

  private final GoodsService goodsService;
  public UserService(GoodsService goodsService){
    this.goodsService = goodsService;
  }
}

@Component
public class GoodsService {

  private final OrderService orderService;
  public GoodsService(OrderService orderService){
    this.orderService = orderService;
  }
}

這種迴圈依賴比較隱蔽,多個物件依賴,最終成環。

Error starting ApplicationContext. To display the conditions report re-run your application with 'debug' enabled.
ERROR 33185 --- [           main] o.s.b.d.LoggingFailureAnalysisReporter   :

***************************
APPLICATION FAILED TO START
***************************

Description:

The dependencies of some of the beans in the application context form a cycle:

┌─────┐
|  goodsService (field private cn.yuanjava.service.UserService cn.yuanjava.service.GoodsService.userService)
↑     ↓
|  userService (field private cn.yuanjava.service.OrderService cn.yuanjava.service.UserService.orderService)
↑     ↓
|  orderService (field private cn.yuanjava.service.GoodsService cn.yuanjava.service.OrderService.goodsService)
└─────┘


Action:

Relying upon circular references is discouraged and they are prohibited by default. Update your application to remove the dependency cycle between beans. As a last resort, it may be possible to break the cycle automatically by setting spring.main.allow-circular-references to true.

@Service
public class UserService {
    private OrderService orderService;
    public UserService(OrderService orderService) {
        this.orderService = orderService;
    }
}

@Service
public class OrderService {
    private UserService userService;

    public OrderService(UserService userService) {
        this.userService = userService;
    }
}


Error starting ApplicationContext. To display the conditions report re-run your application with 'debug' enabled.
2023-02-19 14:36:13.133 ERROR 15885 --- [           main] o.s.b.d.LoggingFailureAnalysisReporter   :

***************************
APPLICATION FAILED TO START
***************************

Description:

The dependencies of some of the beans in the application context form a cycle:

┌─────┐
|  orderService defined in file [/Users/weiki/Desktop/workspace/yuan/yuanjava/build/classes/java/main/cn/yuanjava/service/OrderService.class]
↑     ↓
|  userService defined in file [/Users/weiki/Desktop/workspace/yuan/yuanjava/build/classes/java/main/cn/yuanjava/service/UserService.class]
└─────┘

Action:

Relying upon circular references is discouraged and they are prohibited by default. Update your application to remove the dependency cycle between beans. As a last resort, it may be possible to break the cycle automatically by setting spring.main.allow-circular-references to true.

如何解決迴圈依賴

3.1 修改程式碼

既然迴圈依賴是程式碼編寫帶來的,最徹底的方案是把出現迴圈依賴的程式碼重構,但是,重構程式碼的範圍可能不可控,因此,對於測試等存在一定的迴歸成本,這是一種代價稍微大點的方案。

不過,程式碼出現迴圈依賴,在一定意義上(不是絕對哦)預示了 code smell:為什麼會存在迴圈依賴?程式碼抽象是否合理?程式碼設計是否違背了 SOLID 原則?

3.2 使用欄位依賴注入

曾經很長一段時間(Spring 3.0 以前的版本),欄位依賴是比較主流的一種程式設計方式,因為這種方式編寫方便簡潔,而且 Spring 也利用三層快取解決了迴圈依賴問題,但後面因 Spring 不推薦欄位依賴注入方式,並且在 github上也可以發現大部分的開源軟體也不採用這種方式了,所以該方案也僅供參考不推薦,改造程式碼如下:

@Component
public class OrderService {
    @Autowired
    private UserService userService;

    public User getUser(){
        return userService.getUser();
    }
}

@Component
public class UserService {
  @Autowired
  private OrderService orderService;

  public Order getOrder(){
    return orderService.getOrder();
  }
}

3.2 使用 @Lazy 註解

@Lazy 是 spring 3.0 提供的一個註解,用來表示是否要延遲初始化 bean,首先看下 @Lazy註解的原始碼:

/**
 * Indicates whether a bean is to be lazily initialized.
 *
 * <p>May be used on any class directly or indirectly annotated with {@link
 * org.springframework.stereotype.Component @Component} or on methods annotated with
 * {@link Bean @Bean}.
 *
 * <p>If this annotation is not present on a {@code @Component} or {@code @Bean} definition,
 * eager initialization will occur. If present and set to {@code true}, the {@code @Bean} or
 * {@code @Component} will not be initialized until referenced by another bean or explicitly
 * retrieved from the enclosing {@link org.springframework.beans.factory.BeanFactory
 * BeanFactory}. If present and set to {@code false}, the bean will be instantiated on
 * startup by bean factories that perform eager initialization of singletons.
 *
 * <p>If Lazy is present on a {@link Configuration @Configuration} class, this
 * indicates that all {@code @Bean} methods within that {@code @Configuration}
 * should be lazily initialized. If {@code @Lazy} is present and false on a {@code @Bean}
 * method within a {@code @Lazy}-annotated {@code @Configuration} class, this indicates
 * overriding the 'default lazy' behavior and that the bean should be eagerly initialized.
 *
 * <p>In addition to its role for component initialization, this annotation may also be placed
 * on injection points marked with {@link org.springframework.beans.factory.annotation.Autowired}
 * or {@link javax.inject.Inject}: In that context, it leads to the creation of a
 * lazy-resolution proxy for all affected dependencies, as an alternative to using
 * {@link org.springframework.beans.factory.ObjectFactory} or {@link javax.inject.Provider}.
 * Please note that such a lazy-resolution proxy will always be injected; if the target
 * dependency does not exist, you will only be able to find out through an exception on
 * invocation. As a consequence, such an injection point results in unintuitive behavior
 * for optional dependencies. For a programmatic equivalent, allowing for lazy references
 * with more sophistication, consider {@link org.springframework.beans.factory.ObjectProvider}.
 *
 * @author Chris Beams
 * @author Juergen Hoeller
 * @since 3.0
 * @see Primary
 * @see Bean
 * @see Configuration
 * @see org.springframework.stereotype.Component
 */
@Target({ElementType.TYPE, ElementType.METHOD, ElementType.CONSTRUCTOR, ElementType.PARAMETER, ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Lazy {

    /**
     * Whether lazy initialization should occur.
     */
    boolean value() default true;

}

從 @Lazy 註解的原始碼可以總結幾點:

  1. @Lazy 用來標識類是否需要延遲載入

  2. @Lazy 可以作用在類上、方法上、構造器上、方法引數上、成員變數中

  3. @Lazy 作用於類上時,通常與 @Component 及其衍生註解配合使用,@Lazy 註解作用於方法上時,通常與 @Bean 註解配合使用

因此,透過 @Lazy 解決構造器迴圈依賴的程式碼改造如下:

@Component
public class UserService {

  private final OrderService orderService;

  @Lazy
  public UserService(OrderService orderService){
    this.orderService = orderService;
  }
  // 或者
  public UserService(@Lazy OrderService orderService){
    this.orderService = orderService;
  }

  public Order getOrder(){
    return orderService.getOrder();
  }
}

@Lazy 原理剖析

本文使用的是 Springboot 2.7 啟動的,因此整體思路是:Springboot是如何啟動 Spring IOC容器?如何載入 Bean?如何 處理 @Lazy註解?

原始碼檢視足跡可以參考下面的類:

Springboot 啟動類 main() 呼叫 org.springframework.boot.SpringApplication#run()

org.springframework.boot.SpringApplication#refreshContext()

org.springframework.context.support.AbstractApplicationContext#refresh()

org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory#populateBean()

org.springframework.beans.factory.annotation.AutowiredAnnotationBeanPostProcessor#postProcessProperties()

org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory#createBean

org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory#doCreateBean

org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory#createBeanInstance

org.springframework.beans.factory.support.ConstructorResolver#autowireConstructor

org.springframework.beans.factory.support.ConstructorResolver#resolvePreparedArguments

org.springframework.beans.factory.support.ConstructorResolver#resolveAutowiredArgument

org.springframework.beans.factory.config.AutowireCapableBeanFactory#resolveDependency()

這裏摘取了處理構造器依賴的幾個核心方法來解釋@Lazy 如何解決迴圈依賴,因為 UserService類 構造器注入 OrderService 是強依賴關係,因此會經過 AbstractAutowireCapableBeanFactory#createBeanInstance() 中關於構造器邏輯程式碼:

// org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory#createBeanInstance
class AbstractAutowireCapableBeanFactory{
    // Create a new instance for the specified bean, using an appropriate instantiation strategy: factory method, constructor autowiring, or simple instantiation.
    // 使用適當的例項化策略為指定的 bean 建立一個新例項:工廠方法、建構函式自動裝配或簡單例項化。
    protected BeanWrapper createBeanInstance(String beanName, RootBeanDefinition mbd, @Nullable Object[] args) {
      // Candidate constructors for autowiring?
      Constructor<?>[] ctors = determineConstructorsFromBeanPostProcessors(beanClass, beanName);
      if (ctors != null || mbd.getResolvedAutowireMode() == AUTOWIRE_CONSTRUCTOR ||
          mbd.hasConstructorArgumentValues() || !ObjectUtils.isEmpty(args)) {
        return autowireConstructor(beanName, mbd, ctors, args);
      }
       }
}

在 autowireConstructor(beanName, mbd, ctors, args) 方法會呼叫 ConstructorResolver#resolvePreparedArguments(),再進入ConstructorResolver#resolveAutowiredArgument(), 再進入DefaultListableBeanFactory#resolveDependency(), resolveDependency()方法的 getAutowireCandidateResolver().getLazyResolutionProxyIfNecessary 邏輯就是針對Lazy情況進行處理: 判斷構造器引數是有@Lazy註解,有則透過buildLazyResolutionProxy 生成代理物件,無則直接返回beanName。而在buildLazyResolutionProxy()裡會生成 一個TargetSource物件來和代理物件相關聯。部分原始碼如下:

// org.springframework.beans.factory.support.DefaultListableBeanFactory#resolveDependency
public class DefaultListableBeanFactory{
  public Object resolveDependency(DependencyDescriptor descriptor, @Nullable String requestingBeanName,
                                  @Nullable Set<String> autowiredBeanNames, @Nullable TypeConverter typeConverter) throws BeansException {

    // 此處省略部分程式碼
    if (Optional.class == descriptor.getDependencyType()) {
    } else {
       // 處理 Lazy 邏輯
      Object result = getAutowireCandidateResolver().getLazyResolutionProxyIfNecessary(
        descriptor, requestingBeanName);
      if (result == null) {
        result = doResolveDependency(descriptor, requestingBeanName, autowiredBeanNames, typeConverter);
      }
      return result;
    }
  }
}

// org.springframework.context.annotation.ContextAnnotationAutowireCandidateResolver#getLazyResolutionProxyIfNecessary
public class ContextAnnotationAutowireCandidateResolver extends QualifierAnnotationAutowireCandidateResolver {

    @Override
    @Nullable
    public Object getLazyResolutionProxyIfNecessary(DependencyDescriptor descriptor, @Nullable String beanName) {
        // 判斷註解是否有@Lazy,有則透過buildLazyResolutionProxy 生成代理物件,沒有則直接返回beanName
        return (isLazy(descriptor) ? buildLazyResolutionProxy(descriptor, beanName) : null);
    }

  protected boolean isLazy(DependencyDescriptor descriptor) {
    for (Annotation ann : descriptor.getAnnotations()) {
      Lazy lazy = AnnotationUtils.getAnnotation(ann, Lazy.class);
      if (lazy != null && lazy.value()) {
        return true;
      }
    }
    MethodParameter methodParam = descriptor.getMethodParameter();
    if (methodParam != null) {
      Method method = methodParam.getMethod();
      if (method == null || void.class == method.getReturnType()) {
        Lazy lazy = AnnotationUtils.getAnnotation(methodParam.getAnnotatedElement(), Lazy.class);
        if (lazy != null && lazy.value()) {
          return true;
        }
      }
    }
    return false;
  }
}

透過上面核心程式碼的解讀,我們可以知道,構造器(引數)增加 @Lazy 註解後,Spring不會去初始化引數對應類的例項,而是返回它的一個代理物件,解決了迴圈依賴問題,邏輯可以抽象為下圖:

透過 Lazy 註解解決 Spring 迴圈依賴原理解析

儘管迴圈依賴的問題解決了,但是,UserService類 依賴的只是OrderService的一個代理物件。因此,我們自然會好奇:當呼叫orderService.getOrder()時,spring是如何找到 OrderService 的真實物件呢?

從上文知道,注入給UserService類的是一個代理,說起代理就不得不說起Spring AOP機制,它就是透過動態代理實現的(JDK動態代理 和 CGLib動態代理)。 因為OrderService並非介面,因此不能使用 JDK動態代理,只能透過 CGLib進行代理,CGLib原始碼如下:

// org.springframework.aop.framework.CglibAopProxy.DynamicAdvisedInterceptor#intercept
class CglibAopProxy implements AopProxy, Serializable {
    private static class DynamicAdvisedInterceptor implements MethodInterceptor, Serializable {
      public Object intercept(Object proxy, Method method, Object[] args, MethodProxy methodProxy) throws Throwable {
        Object oldProxy = null;
        boolean setProxyContext = false;
        Object target = null;
        TargetSource targetSource = this.advised.getTargetSource();
        try {
          if (this.advised.exposeProxy) {
            // Make invocation available if necessary.
            oldProxy = AopContext.setCurrentProxy(proxy);
            setProxyContext = true;
          }
          // Get as late as possible to minimize the time we "own" the target, in case it comes from a pool...
          // 獲取被代理的物件
          target = targetSource.getTarget();
          Class<?> targetClass = (target != null ? target.getClass() : null);
          List<Object> chain = this.advised.getInterceptorsAndDynamicInterceptionAdvice(method, targetClass);
          Object retVal;
          // Check whether we only have one InvokerInterceptor: that is,
          // no real advice, but just reflective invocation of the target.
          if (chain.isEmpty() && CglibMethodInvocation.isMethodProxyCompatible(method)) {
            // We can skip creating a MethodInvocation: just invoke the target directly.
            // Note that the final invoker must be an InvokerInterceptor, so we know
            // it does nothing but a reflective operation on the target, and no hot
            // swapping or fancy proxying.
            Object[] argsToUse = AopProxyUtils.adaptArgumentsIfNecessary(method, args);
            // 透過反射呼叫被代理物件的方法
            retVal = invokeMethod(target, method, argsToUse, methodProxy);
          }
          else {
            // We need to create a method invocation...
            retVal = new CglibMethodInvocation(proxy, target, method, args, targetClass, chain, methodProxy).proceed();
          }
          retVal = processReturnType(proxy, target, method, retVal);
          return retVal;
        }catch (Exception e){}
      }
    }
}

這裏抽取了CGLib動態代理核心的3步:

// 此處的TargetSource 和 上文 buildLazyResolutionProxy() 構建的TargetSource 關聯
1. TargetSource targetSource = this.advised.getTargetSource();

// 獲取被代理的物件target
2. target = targetSource.getTarget();

// 反射呼叫被代理物件的方法
3. retVal = invokeMethod(target, method, argsToUse, methodProxy);

透過CGLib核心的3步解釋了Spring中代理類是如何與真實物件進行關聯,因此,orderService關聯到真實物件可以抽象成下圖:

透過 Lazy 註解解決 Spring 迴圈依賴原理解析

另外,我們透過3張 IDEA debugger 截圖來佐證下:

透過 Lazy 註解解決 Spring 迴圈依賴原理解析

透過 Lazy 註解解決 Spring 迴圈依賴原理解析

透過 Lazy 註解解決 Spring 迴圈依賴原理解析

總結

  • Spring構造器注入迴圈依賴有3種解決辦法:重構程式碼、欄位依賴注入、@Lazy註解。強烈推薦 @Lazy註解;

  • @Lazy註解 解決思路是:初始化時注入代理物件,真實呼叫時使用Spring AOP動態代理去關聯真實物件,然後透過反射完成呼叫;

  • @Lazy註解 加在構造器上,作用域為構造器所有引數,加在構造器某個引數上,作用域為該引數;

  • @Lazy註解 作用在介面上,使用 JDK動態代理,作用在類上,使用 CGLib動態代理;

0則評論

您的電子郵件等資訊不會被公開,以下所有項目均必填

OK! You can skip this field.