==========================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,所以沒有必要循環依賴:他們通常是可以提高設計的一種症狀。 但是,如果你在你的項目中確實是需要有循環依賴,那么你可以遵循一些這里提出的解決方法。