SpringBoot(6)— Bean懶加載@Lazy和循環依賴處理


==========================Bean懶加載@Lazy介紹==================================

一、問題介紹

  Spring在啟動時,默認會立即將單實例bean進行實例化,並加載到Spring容器中。也就是說,單實例bean默認在Spring容器啟動的時候創建對象,並將對象加載到Spring容器中。如果我們需要對某個bean進行延遲加載(延遲到在第一次調用的時候實例化),我們該如何處理呢?此時,就需要使用到@Lazy注解了。

 

二、如何配置懶加載

1、在xml配置中

<beans ... default-lazy-init="true"> //全局配置
<bean ...  lazy-init="true" /> //指定bean配置

2、在JavaConfig配置中

//全局配置
@Configuration
@Lazy
public class AppConfig {}

//指定bean配置
@Configuration
public class AppConfig{
    @Bean
    @Lazy
    public LazyBean lazyBean(){
        return new LazyBean();
    }
}

3、SpringBoot中指定bean的懶加載,可以在對應的類上直接使用@Lazy

//指定bean配置
@Component @Lazy
public class LazyBean { public LazyBean() { System.out.println("LazyBean should be lazzzzyyyyyy!!!"); } public void doSomething() {} }

  那么SpringBoot中如何全局配置懶加載呢?

  通過在stackoverflow上查找, 發現的答案是, 在啟動類SpringbootApplication上加上@Lazy注解即可. 原來注解@SpringBootApplication是@Configuration, @EnableAutoConfiguration和@ComponentScan注解的合體.

  而這個SpringbootApplication本身就是個配置類, 所以在上面加@Lazy注解理論上是可以的.果然是直觀的東西不方便, 方便的東西不直觀.

(1) 錯誤方式一:

//spring boot中聲明bean
@Component
public class LazyBean {
    public LazyBean() {
        System.out.println("LazyBean should be lazzzzyyyyyy!!!");
    }
    public void doSomething() {}
}


//配置類上加注解
@SpringBootApplication
@Lazy
public class SpringbootApplication {
    public static void main(String[] args) {
        ApplicationContext ctx = SpringApplication.run(SpringbootApplication.class, args);
    }

}

  啟動應用, 發現輸出了

LazyBean should be lazzzzyyyyyy!!!

  也就是說配置並沒有生效. 但是so上的回答一般不會是錯的. 那會是哪里出了問題呢?

(2)方式一修正

  不使用@Component, 而是在配置文件中聲明bean:

//@Component
public class LazyBean {
    public LazyBean() {
        System.out.println("LazyBean should be lazzzzyyyyyy!!!");
    }
    public void doSomething() {}
}

//配置類
@SpringBootApplication
@Lazy
public class SpringbootApplication {

   //在配置類中聲明bean
    @Bean
    public LazyBean lazyBean() {
        return new LazyBean();
    }
    public static void main(String[] args) {
        ApplicationContext ctx = SpringApplication.run(SpringbootApplication.class, args);
    }

}

  這種方式實現了懶加載,但是這跟2(在JavaConfig配置中)中的方式是一樣的.

(3)方式二

  spring2.2中引入了一個application.properties中的新屬性.

spring.main.lazy-initialization=true   //指定整個應用的懶加載.

  這種方式不論是@Component聲明的bean,還是@Bean聲明的bean, 都可以實現懶加載.

 

三、@Lazy的屬性

  @Lazy只有一個屬性value,value取值有 true 和 false 兩個,默認值是true

  true 表示使用 延遲加載, false 表示不使用,false 純屬多余,如果不使用,不標注該注解就可以了。

  通過以下示例看看使用注解和不使用注解的區別

  Person 類

public class Person {
    private String name;
    private Integer age;
 
    public Person() {
    }
 
    public Person(String name, Integer age) {
        System.out.println(" 對象被創建了.............");
        this.name = name;
        this.age = age;
    }
 
  // 省略 getter setter 和 toString 方法
}

1、配置類不標注@Lazy注解(不使用延遲加載)

public class LazyConfig {
    @Bean
    public Person person() {
        return new Person("李四", 55);
    }
}

  測試:

    @Test
    public void test5() {
        ApplicationContext ctx = new AnnotationConfigApplicationContext(LazyConfig.class);
    }

  結果:

   結論:我們發現,沒有獲取bean,但是打印了語句,說明對象調用了構造器,那么方法也就被創建了

2、在配置類打上 @Lazy 注解

public class LazyConfig {
    @Lazy
    @Bean
    public Person person() {
        return new Person("李四", 55);
    }
}

  結果:

  結論:我們發現,沒有獲取bean,沒有打印了語句,說明對象沒有調用構造器,那么方法就沒有被創建了

注意:

  1、@Lazy(value = false) 或者 @Lazy(false) 那么對象會在初始化的時候被創建,相當於沒有使用@Lazy注解,@Lazy注解默認值為true

  2、@Lazy注解的作用主要是減少springIOC容器啟動的加載時間

  3、當出現循環依賴的時候,也可以添加@Lazy

  4、雖然 懶加載可以提升應用的啟動速度, 但是不利於盡早的發現錯誤, 對於HTTP請求, 首次訪問的響應時間也會增長.

 

 

 

===========================Spring中循環的循環依賴============================

一、什么是循環依賴

  一般場景是一個Bean A依賴Bean B,而Bean B也依賴Bean A.
  Bean A → Bean B → Bean A

  當然我們也可以添加更多的依賴層次,比如:
  Bean A → Bean B → Bean C → Bean D → Bean E → Bean A

 

二、Spring中的循環依賴

  當Spring上下文在加載所有的bean時,他會嘗試按照他們他們關聯關系的順序進行創建。比如,如果不存在循環依賴時,例如:
Bean A → Bean B → Bean C
  Spring會先創建Bean C,再創建Bean B(並將Bean C注入到Bean B中),最后再創建Bean A(並將Bean B注入到Bean A中)。
但是,如果我們存在循環依賴,Spring上下文不知道應該先創建哪個Bean,因為它們依賴於彼此。在這種情況下,Spring會在加載上下文時,拋出一個BeanCurrentlyInCreationException。

  當我們使用構造方法進行注入時,也會遇到這種情況,因為JVM虛擬機在對類進行實例化的時候,需先實例化構造器的參數,而由於循環引用這個參數無法提前實例化,故只能拋出錯誤。如果您使用其它類型的注入,你應該不會遇到這個問題。因為它是在需要時才會被注入,而不是上下文加載被要求注入。

 

三、示例

  我們定義兩個Bean並且互相依賴(通過構造函數注入)。

@Component
public class CircularDependencyA {
 
    private CircularDependencyB circB;
 
    @Autowired
    public CircularDependencyA(CircularDependencyB circB) {
        this.circB = circB;
    }
}
@Component
public class CircularDependencyB {
 
    private CircularDependencyA circA;
 
    @Autowired
    public CircularDependencyB(CircularDependencyA circA) {
        this.circA = circA;
    }
}

  現在,我們寫一個測試配置類,姑且稱之為TestConfig,指定基本包掃描。假設我們的Bean在包“com.baeldung.circulardependency”中定義:

@Configuration
@ComponentScan(basePackages = { "com.baeldung.circulardependency" })
public class TestConfig {
}

  最后,我們可以寫一個JUnit測試,以檢查循環依賴。該測試方法體可以是空的,因為循環依賴將上下文加載期間被檢測到。

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes = { TestConfig.class })
public class CircularDependencyTest {
 
    @Test
    public void givenCircularDependency_whenConstructorInjection_thenItFails() {
        // Empty test; we just want the context to load
    }
}

  如果您運行這個測試,你會得到以下異常:

BeanCurrentlyInCreationException: Error creating bean with name 'circularDependencyA':
Requested bean is currently in creation: Is there an unresolvable circular reference?

 

四、解決辦法

  我們將使用一些最流行的方式來處理這個問題。

1、重新設計

  當你有一個循環依賴,很可能你有一個設計問題並且各責任沒有得到很好的分離。你應該盡量正確地重新設計組件,以便它們的層次是精心設計的,也沒有必要循環依賴。

  如果不能重新設計組件(可能有很多的原因:遺留代碼,已經被測試並不能修改代碼,沒有足夠的時間或資源來完全重新設計......),但有一些變通方法來解決這個問題。

2、使用@Lazy

  解決Spring 循環依賴的一個簡單方法就是對一個Bean使用延時加載。也就是說:這個Bean並沒有完全的初始化完,實際上他注入的是一個代理,只有當他首次被使用的時候才會被完全的初始化。

  我們對CircularDependencyA 進行修改,結果如下:

@Component
public class CircularDependencyA {
 
    private CircularDependencyB circB;
 
    @Autowired
    public CircularDependencyA(@Lazy CircularDependencyB circB) {
        this.circB = circB;
    }
}

  如果你現在運行測試,你會發現之前的錯誤不存在了。

3、使用Setter/Field注入

  其中最流行的解決方法,就是Spring文檔中建議,使用setter注入。
  簡單地說,你對你須要注入的bean是使用setter注入(或字段注入),而不是構造函數注入。通過這種方式創建Bean,實際上它此時的依賴並沒有被注入,只有在你須要的時候他才會被注入進來。

  讓我們開始動手干吧。我們將在CircularDependencyB 中添加另一個屬性,並將我們兩個Class Bean從構造方法注入改為setter方法注入:

@Component
public class CircularDependencyA {
 
    private CircularDependencyB circB;
 
    @Autowired
    public void setCircB(CircularDependencyB circB) {
        this.circB = circB;
    }
 
    public CircularDependencyB getCircB() {
        return circB;
    }
}
@Component
public class CircularDependencyB {
 
    private CircularDependencyA circA;
 
    private String message = "Hi!";
 
    @Autowired
    public void setCircA(CircularDependencyA circA) {
        this.circA = circA;
    }
 
    public String getMessage() {
        return message;
    }
}

  現在,我們對修改后的代碼進單元測試:

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes = { TestConfig.class })
public class CircularDependencyTest {
 
    @Autowired
    ApplicationContext context;
 
    @Bean
    public CircularDependencyA getCircularDependencyA() {
        return new CircularDependencyA();
    }
 
    @Bean
    public CircularDependencyB getCircularDependencyB() {
        return new CircularDependencyB();
    }
 
    @Test
    public void givenCircularDependency_whenSetterInjection_thenItWorks() {
        CircularDependencyA circA = context.getBean(CircularDependencyA.class);
 
        Assert.assertEquals("Hi!", circA.getCircB().getMessage());
    }
}
  下面對上面看到的注解進行說明:
  @Bean:在Spring框架中,標志着他被創建一個Bean並交給Spring管理
  @Test:測試將得到從Spring上下文中獲取CircularDependencyA bean並斷言CircularDependencyB已被正確注入,並檢查該屬性的值。

4、使用@PostConstruct

  打破循環的另一種方式是,在要注入的屬性(該屬性是一個bean)上使用 @Autowired ,並使用@PostConstruct 標注在另一個方法,且該方法里設置對其他的依賴。

  我們的Bean將修改成下面的代碼:

@Component
public class CircularDependencyA {
 
    @Autowired
    private CircularDependencyB circB;
 
    @PostConstruct
    public void init() {
        circB.setCircA(this);
    }
 
    public CircularDependencyB getCircB() {
        return circB;
    }
}
@Component
public class CircularDependencyB {
 
    private CircularDependencyA circA;
     
    private String message = "Hi!";
 
    public void setCircA(CircularDependencyA circA) {
        this.circA = circA;
    }
     
    public String getMessage() {
        return message;
    }
}

  現在我們運行我們修改后的代碼,發現並沒有拋出異常,並且依賴正確注入進來。

5、實現ApplicationContextAware and InitializingBean接口

  如果一個Bean實現了ApplicationContextAware,該Bean可以訪問Spring上下文,並可以從那里獲取到其他的bean。實現InitializingBean接口,表明這個bean在所有的屬性設置完后做一些后置處理操作(調用的順序為init-method后調用);在這種情況下,我們需要手動設置依賴。

@Component
public class CircularDependencyA implements ApplicationContextAware, InitializingBean {
 
    private CircularDependencyB circB;
 
    private ApplicationContext context;
 
    public CircularDependencyB getCircB() {
        return circB;
    }
 
    @Override
    public void afterPropertiesSet() throws Exception {
        circB = context.getBean(CircularDependencyB.class);
    }
 
    @Override
    public void setApplicationContext(final ApplicationContext ctx) throws BeansException {
        context = ctx;
    }
}
public class CircularDependencyB {
 
    private CircularDependencyA circA;
 
    private String message = "Hi!";
 
    @Autowired
    public void setCircA(CircularDependencyA circA) {
        this.circA = circA;
    }
 
    public String getMessage() {
        return message;
    }
}

  同樣,我們可以運行之前的測試,看看有沒有異常拋出,程序結果是否是我們所期望的那樣。

 

五、總結

  有很多種方法來應對Spring的循環依賴。但考慮的第一件事就是重新設計你的bean,所以沒有必要循環依賴:他們通常是可以提高設計的一種症狀。 但是,如果你在你的項目中確實是需要有循環依賴,那么你可以遵循一些這里提出的解決方法。

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM