給學妹看的SpringIOC 面試題(下)
之前上篇跟學弟學妹講了一下SpringIOC的啟動流程,今天接著給學妹聊聊DI—Dependency Injection(依賴注入)
什么是依賴注入?
依賴注入(DI)是一個過程,通過該過程,對象只能通過構造函數參數,工廠方法的參數或在構造或創建對象實例后在對象實例上設置的屬性來定義其依賴關系(即,與它們一起工作的其他對象)。從工廠方法返回。
然后,容器在創建 bean 時注入那些依賴項。從根本上講,此過程是通過使用類的直接構造或服務定位器模式來自己控制其依賴關系的實例化或位置的 Bean 本身的逆過程(因此稱為 Control Inversion)。
使用 DI 原理,代碼更簡潔,當為對象提供依賴項時,去耦會更有效。該對象不查找其依賴項,也不知道依賴項的位置或類。結果,您的類變得更易于測試,尤其是當依賴項依賴于接口或抽象 Base Class 時,它們允許在單元測試中使用存根或模擬實現。
-----------以上解釋來源Spring官方文檔
說白了依賴注入只是把bean添加到IOC容器的一種方式。
從依賴注入的方式來說整體可以分為兩大類來處理,一種是手動方式,一種是自動方式。
手動方式:
- XML 資源配置元信息(比較常見)
- Java 注解配置元信息 (比較常見)
- API 配置元信息(不太常用)
自動方式:
- Autowiring
依賴注入的方式有上面的兩種,但是也可按注入的類型來區分:
- Setter注入
- 構造器注入
- 接口注入
- 方法注入
聊到依賴注入那么首先需要先聊聊 Autowiring Modes自動綁定模式
Spring的官方文檔中對Autowiring Modes解釋是:
Spring 容器可以自動裝配協作 bean 之間的關系。通過檢查 ApplicationContext 的內容,您可以讓 Spring 自動為您的 bean 解析協作者(其他 bean)
同時也提出了4種自動裝配模式
- no:(默認)無自動裝配。Bean 引用必須由ref元素定義。對于大型部署,建議不要更改默認設置,因為明確指定協作者可以提供更好的控制和清晰度。在某種程度上,它記錄了系統的結構。
- byName:按屬性名稱自動布線。Spring 尋找與需要自動裝配的屬性同名的 bean。例如,如果一個 bean 定義被設置為按名稱自動裝配,并且包含一個master屬性(即,它具有setMaster(..)方法),那么 Spring 將查找一個名為master的 bean 定義并使用它來設置屬性。
- byType:如果容器中恰好存在一個該屬性類型的 bean,則使該屬性自動裝配。如果存在多個錯誤,則會引發致命異常,這表明您可能不對該 bean 使用byType自動裝配。如果沒有匹配的 bean,則什么也不會發生(未設置該屬性)。
- constructor:類似于byType,但適用于構造函數參數。如果容器中不存在構造函數參數類型的一個 bean,則將引發致命錯誤。
雖然官方文檔提出了Autowiring自動綁定方式,但是在我們的真實的業務場景中,相對來說是用的比較少的,因為它有一定的局限性,而且Spring官方文檔中也列出了其中的不足點。
自動裝配的局限性和缺點(官方文檔鏈接)
- property和constructor-arg設置中的顯式依賴項始終會覆蓋自動裝配。您不能自動連接簡單屬性,例如基元,Strings和Classes(以及此類簡單屬性的數組)。此限制是設計使然 PS:針對這種情況可以通過另外的一種方式@value等進行轉化來處理這個場景。
- 自動裝配不如顯式接線精確。盡管如前所述,Spring 還是小心避免在可能產生意外結果的模棱兩可的情況下進行猜測。SpringManagement 的對象之間的關系不再明確記錄。
- 容器內的多個 bean 定義可能與要自動裝配的 setter 方法或構造函數參數指定的類型匹配。對于數組,集合或Map實例,這不一定是問題。但是,對于需要單個值的依賴項,不會任意解決此歧義。如果沒有唯一的 bean 定義可用,則引發異常。
說完這么多文檔的基礎知識,那么接下來就是開始demo測試環節,來加深理解一下上面的說的那么多到底是個啥。
Setter
先從注入的類型先分析怎么樣的一種方式叫Setter方式注入
- /構建一個測試Service
- public class SetterServiceInjection {
- public void testMethod(String param) {
- System.out.println(param);
- }
- }
- public class SetterServiceInjectionTest {
- private SetterServiceInjection setterServiceInjection;
- // Setter方式注入
- public void setSetterServiceInjection(SetterServiceInjection setterServiceInjection) {
- this.setterServiceInjection = setterServiceInjection;
- }
- public void testMethod(){
- setterServiceInjection.testMethod("Setter方式注入");
- }
- // 測試啟動demo
- public static void main(String[] args) {
- ApplicationContext applicationContext = new ClassPathXmlApplicationContext("classpath:applicationContext.xml");
- //獲取IOC容器中的bean
- SetterServiceInjectionTest serviceInjectionTest = (SetterServiceInjectionTest) applicationContext.getBean("setterServiceInjectionTest");
- serviceInjectionTest.testMethod();
- // 結果打印:
- // Setter方式注入
- }
- }
xml文件配置
- <bean id="setterServiceInjection" class="com.ao.bing.demo.spring.ioc.SetterServiceInjection"/>
- <!--Setter方式-->
- <bean id="setterServiceInjectionTest" class="com.ao.bing.demo.spring.ioc.SetterServiceInjectionTest">
- <property name="setterServiceInjection" ref="setterServiceInjection"/>
- </bean>
上面是很常見的一種注入方式,而且這種方式常見于去寫一些配置文件、插件二方包、或者注入數據源信息等。
當然Setter不是僅僅只是這一種使用方式,還可以注入對象,或者說注入一些集合信息等等。
構造器注入
在代碼的實現上面構造器和Setter方式是很相似的。還是按照上面的代碼改造一下如下所示
- private final SetterServiceInjection setterServiceInjection;
- // Setter方式注入
- // public void setSetterServiceInjection(SetterServiceInjection setterServiceInjection) {
- // this.setterServiceInjection = setterServiceInjection;
- // }
- public void testMethod(){
- setterServiceInjection.testMethod("構造器方式注入");
- }
- //構造器注入
- public SetterServiceInjectionTest(SetterServiceInjection setterServiceInjection){
- this.setterServiceInjection = setterServiceInjection;
- }
- <context:component-scan base-package="com.ao.bing.demo"/>
- <bean id="setterServiceInjection" class="com.ao.bing.demo.spring.ioc.SetterServiceInjection"/>
- <!--Setter方式-->
- <!-- <bean id="setterServiceInjectionTest" class="com.ao.bing.demo.spring.ioc.SetterServiceInjectionTest">-->
- <!-- <property name="setterServiceInjection" ref="setterServiceInjection"/>-->
- <!-- </bean>-->
- <bean id="setterServiceInjectionTest" class="com.ao.bing.demo.spring.ioc.SetterServiceInjectionTest">
- <constructor-arg index="0" ref="setterServiceInjection"/>
- </bean>
既然兩個代碼這么相似,為什么Spring官方還需要推薦使用這種方式呢?和Setter方式區別又是啥?
推薦原因:從定義的屬性來說添加了final修飾說明我們注入的依賴不能再變動。其次從XML的配置bean的屬性來說,當需要實例化setterServiceInjectionTest這個類的時候已經實現了有參構造函數,那么就不會再使用默認的構造函數,同時針對傳入的參數需要確保有這種類型的值,否則就會報錯,所以這樣就保證了依賴不會為空最后因為構造器傳入的參數是確定有值的,那就意味著構造屬性是已經完全初始化的狀態,所以這也就避免了后面需要分析的循環依賴的問題。
區別
- 在Setter注入,可以將依賴項部分注入,構造方法注入不能部分注入
- 使用setter注入不能保證類的所有的屬性都注入進來。
- 在類對象相互依賴的時候可以通過Setter方式解決循環依賴問題。
接口回調注入
提供Spring中獲取容器本身的一些功能資源,就是通過實現一系列Spring Aware接口來實現具體的功能。
- BeanFactoryAware:獲取 IoC 容器 - BeanFactory
- ApplicationContextAware:獲取 Spring 應用上下文 - ApplicationContext 對象
- EnvironmentAware:獲取 Environment 對象
- ResourceLoaderAware:獲取資源加載器 對象 - ResourceLoader
- BeanClassLoaderAware:獲取加載當前 Bean Class 的 ClassLoader
- BeanNameAware:獲取當前 Bean 的名稱
- MessageSourceAware:獲取 MessageSource 對象,用于 Spring 國際化
- ApplicationEventPublisherAware:獲取 ApplicationEventPublishAware 對象,用于 Spring 事件
- EmbeddedValueResolverAware:獲取 StringValueResolver 對象,用于占位符處理
上面的接口回調實現方式也比較簡單,基本所有的bean都能實現Aware接口,但是實現Aware接口也有一定的局限性,不能進行擴展只能是進行內嵌,所以理解這就是一種內建的回調方式。
以ApplicationContextAware實現代碼為例如下圖所示
- @Component
- public class SetterServiceInjectionTest implements ApplicationContextAware {
- // @Autowired
- // private SetterServiceInjection setterServiceInjection;
- private ApplicationContext applicationContext;
- public void testMethod() {
- SetterServiceInjection setterServiceInjection = (SetterServiceInjection) applicationContext.getBean("setterServiceInjection");
- setterServiceInjection.testMethod("接口回調");
- }
- public static void main(String[] args) {
- ApplicationContext applicationContext = new ClassPathXmlApplicationContext("classpath:applicationContext.xml");
- //獲取IOC容器中的bean
- SetterServiceInjectionTest serviceInjectionTest = (SetterServiceInjectionTest) applicationContext.getBean("setterServiceInjectionTest");
- serviceInjectionTest.testMethod();
- }
- // 獲取上下文
- @Override
- public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
- this.applicationContext = applicationContext;
- }
- }
方法注入
方法注入實現方式可以分為四種:
- @Autowired:是Spring自帶的注解,依照類型進行裝配。
- @Bean:產生一個Bean對象,然后這個Bean對象交給Spring管理。
- @Resource:@Resource`是JavaEE的標準,Spring對它是兼容性的支持,依照名稱進行裝配。
- @Inject(不常見):jsr330中的規范。
以常見的Autowired為例
- @Autowired
- private SetterServiceInjection setterServiceInjection;
- public void testMethod(){
- setterServiceInjection.testMethod("方法注入");
- }
- public static void main(String[] args) {
- ApplicationContext applicationContext = new ClassPathXmlApplicationContext("classpath:applicationContext.xml");
- //獲取IOC容器中的bean
- SetterServiceInjectionTest serviceInjectionTest = (SetterServiceInjectionTest) applicationContext.getBean("setterServiceInjectionTest");
- serviceInjectionTest.testMethod();
- }
從上面的代碼中并不需要再寫一些構造方法,也不用配置相關XML文件只要簡單的加上@Autowired一個注解就能完成bean的相互關聯。
所以方法注入可以理解不用關心方法名稱也不用關心方法類型,只要方法上面在參數里面有相關的依賴類型同時加上@Autowired或者 @Resource 就能相關聯上。
類型選擇
上面介紹了這么多類型,那么應該怎么合理的選擇哪個依賴的注入類型呢?
- 構造器注入:強制依賴類型,低依賴。
- Setter 方法注入:非很強的強制依賴類型(無依賴順序),多依賴。
- 方法注入:常用于聲明類
- 接口回調注入:業務中常用于寫一些主鍵啥的。
合理的選擇注入類型能減少業務開發環境中的很多的問題。
在真實的業務場景中還會遇到另外的一個問題,就是多個類型相同的bean注冊到Spring容器中,那么僅僅使用上面的幾種方式Spring框架則會拋出NoUniqueBeanDefinitionException異常,所以為了解決上述的問題Spring提出了一個新的注解**@Qualifier**來指定哪一個bean或者實現bean的邏輯分組,其用法也相對來說比較加單
- public class QualifierDemo {
- @Autowired
- private List<Demo> demos; // 1 ,2,3,4 全部都有
- @Autowired
- @Qualifier
- private List<Demo> demosQualifier; // 只有 3,4
- @Autowired
- @Qualifier("demo2")
- private Demo demo1; // 只有2
- @Bean
- public Demo demo1() {
- return new Demo(1);
- }
- @Bean
- public Demo demo2() {
- return new Demo(2);
- }
- @Bean
- @Qualifier // 進行邏輯分組
- public Demo demo3() {
- return new Demo(3);
- }
- @Bean
- @Qualifier // 進行邏輯分組
- public Demo demo4() {
- return new Demo(4);
- }
- @Data
- public class Demo {
- private Integer id;
- public Demo (Integer id){
- this.id =id;
- }
- }
- }
通過上面的代碼就能很明確的知道沒有使用Qualifier注解的默認就是加載了所有的,使用了Qualifier注解的demosQualifier的里面只有 demo3 和 demo4兩個,同樣也可以指定使用那么bean如demo1所示。
當然這里只介紹了Qualifier的簡單實用,在Spring的官方文檔中還有一種用法就是實現Qualifier擴展用法,自定義注解,了解Spring Cloud 的同學可以去看看@LoadBalanced這個注解。用法如下
- @Target({ElementType.FIELD, ElementType.METHOD})
- @Retention(RetentionPolicy.RUNTIME)
- @Inherited
- @Documented
- @Qualifier
- public @interface DemoGroup {
- }
Spring依賴注入差不多就跟大家聊完了,當然后一些其他的一些比較少見的就不跟大家細聊了,比如說延遲依賴注入感興趣的可以小伙伴可以再去看下,推薦是使用ObjectProvider方式來處理。
總結
Spring的依賴注入用一句話來說解耦對象之間的依賴關系,通過xml方式或者注解的方式來靈活管理依賴。
看這中框架性的東西推薦大家可以去看看官方文檔,如果看不懂的英文的可以去找找中文翻譯過的,來加深自己的理解。(中文官方文檔鏈接)。
接下來剖析一下Spring中的3層緩存怎么去解決的循環依賴。
為了加深理解還給大家整理了一下幾個面試題。
構造器注入和 Setter 注入有啥區別?更推薦什么方式?
答案已經在文中構造器的解釋中給說出來了
怎么解決多個類型相同的bean注冊到Spring容器的使用問題?
可以使用Qualifier注解來實現
參考文檔:中文官方文檔、《小馬哥核心編程》。
最近在搞的面試版PDF真的覺得還挺有意思的,等搞出來了,應該可以讓大家面試前突擊突擊,對了面試視頻籌劃中了,這次準備用不同的風格演繹,下個月肯定能出來。
我是敖丙,你知道的越多,你不知道的越多,我們下期見。