絕對讓你大開眼界的Spring依賴注入問題
環境:SpringBoot3.2.5
1. 簡介
本篇文章旨在深入探討將原型(Prototype)作用域的bean注入到單例(Singleton)作用域bean中的多種策略,并詳細剖析每種方法的優缺點。在Spring框架中,默認情況下bean是單例的,這意味著它們在應用的生命周期內僅被創建一次并共享。然而,當需要將原型bean(每次請求都創建一個新實例)注入到單例bean中時,就會出現作用域注入問題。如下示例:
@Configuration
public class AppConfig {
@Bean
@Scope(BeanDefinition.SCOPE_PROTOTYPE)
public PrototypeBean prototypeBean() {
return new PrototypeBean() ;
}
@Bean
public SingletonBean singletonBean() {
return new SingletonBean() ;
}
}
在該配置類中,將PrototypeBean定義為原型bean,SingletonBean定義為單例,該單例bean的定義如下:
@Component
public class SingletonBean {
@Resource
private PrototypeBean prototypeBean ;
public void test() {
System.out.println(this.prototypeBean) ;
}
}
在這通過@Resource注解,注入原型bean。接下來我們通過如下的代碼進行測試:
ConfigurableApplicationContext context = SpringApplication.run(App.class, args) ;
SingletonBean bean = context.getBean(SingletonBean.class) ;
bean.test() ;
bean.test() ;
bean.test() ;
以上調用三次test方法,我們期望的是每次調用打印的prototypeBean屬性都不是同一個,結果如下:
com.pack.PrototypeBean@69fb6037
com.pack.PrototypeBean@69fb6037
com.pack.PrototypeBean@69fb6037
三次打印都是同一個。這種不同作用域的注入該如何解決呢?接下來我將介紹8種方式解決。
2. 解決辦法
2.1 單例bean實現ApplicationContextAware
public class SingletonBean implements ApplicationContextAware {
private ApplicationContext context;
public void test() {
System.out.println(this.context.getBean(PrototypeBean.class)) ;
}
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
this.context = applicationContext ;
}
}
每調用test方法時都會通過ApplicationContext對象從容器中獲取新實例。
缺點:
- 這種方法有個嚴重的缺點。它違背了控制權倒置的原則,因為我們直接向容器請求依賴關系。
- 在SingletonBean類中通過applicationContext獲取原型 Bean。這意味著將代碼與 Spring 框架耦合。
除了這2缺點,用起來感覺還行啊
2.2 通過jakarta的Provider注入
public class SingletonBean {
@Resource
private jakarta.inject.Provider<PrototypeBean> provider ;
public void test() {
System.out.println(provider.get()) ;
}
}
這里通過jakarta.inject包中的Provider接口注入原型bean。每次調用get方法都會從容器中再次獲取一個新的實例對象。
2.3 借助Spring提供的ObjectProvider接口
public class SingletonBean {
@Resource
private ObjectProvider<PrototypeBean> objectProvider ;
public void test() {
System.out.println(objectProvider.getIfAvailable()) ;
}
}
直接注入ObjectProvider接口對象,該接口提供了豐富的方法,不僅僅可以獲取單個對象,也可以獲取多個對象(集合),包括如下可用方法:
圖片
2.4 借助Spring的ObjectFactory接口
public class SingletonBean {
@Resource
private ObjectFactory<PrototypeBean> objectFactory ;
public void test() {
System.out.println(objectFactory.getObject()) ;
}
}
該接口與上面ObjectProvider接口挺像,但是ObjectFactory是函數式接口只有一個getObject方法,并且只能返回一個對象。
2.5 使用@Lookup注解
public class SingletonBean {
public void test() {
System.out.println(get()) ;
}
@Lookup
protected PrototypeBean get() {
return null ;
}
}
Spring 會將SingletonBean創建為代理對象并覆蓋使用 @Lookup 注解的 get() 方法。每當我們調用get() 方法時,它會返回一個新的 PrototypeBean 實例。
2.6 注冊Function類型的Bean
@Bean
public <T> Function<Class<T>, T> targetBean(ApplicationContext context) {
return t -> context.getBean(t) ;
}
這里注冊了Function類型的bean對象,而具體會是什么類型完全由使用的地方決定。
public class SingletonBean {
@Resource
private Function<Class<?>, PrototypeBean> targetBean ;
public void test() {
System.out.println(targetBean.apply(PrototypeBean.class)) ;
}
}
通過定義Function類型的bean,這里我們就可以不限制具體是那個類型的原型bean,這就非常的通用了。
2.7 作用域代理
@Component
@Scope(value = BeanDefinition.SCOPE_PROTOTYPE, proxyMode = ScopedProxyMode.TARGET_CLASS)
public class PrototypeBean {
}
在@Scope注解上配置proxyMode=TARGET_CLASS這樣配置以后,容器將會為PrototypeBean創建代理對象。也是能夠保證每次獲取的都是新的實例對象。
2.8 使用@Lazy注解
public class SingletonBean {
@Resource
@Lazy
private PrototypeBean prototypeBean ;
}
通過@Lazy注解后,這里注入的PrototypeBean將會是一個代理對象,這樣也可以保證每次使用時都是新的實例對象。