又一次被面試官帶到坑里面了。
面試官:springmvc用過么?
我:用過啊,經常用呢
面試官:springmvc中為什么需要用父子容器?
我:嗯。。。沒聽明白你說的什么。
面試官:就是controller層交給一個spring容器加載,其他的service和dao層交給另外一個spring容器加載,web.xml中有這塊配置,這兩個容器組成了父子容器的關系。
我:哦,原來是這塊啊,我想起來了,我看大家都這么用,所以我也這么用
面試官:有沒有考慮過為什么?
我:我在網上看大家都這么用,所以我也這么用了,具體也不知道為什么,不過用起來還挺順手的
面試官:如果只用一個容器可以么,所有的配置都交給一個spring容器加載?
我:應該不行吧!
面試官:確定不行么?
我:讓我想一會。。。。。我感覺是可以的,也可以正常運行。
面試官:那我們又回到了開頭的問題,為什么要用父子容器呢?
我:我叫你哥好么,別這么玩我了,被你繞暈了?
面試官:好吧,你回去試試看吧,下次再來告訴我,出門右轉,不送!
我:臉色變綠了,灰頭土臉的走了。
回去之后,我好好研究了一番,下次准備再去給面試官一點顏色看看。
主要的問題
-
什么是父子容器?
-
為什么需要用父子容器?
-
父子容器如何使用?
下面我們就來探討探討。
我們先來看一個案例
系統中有2個模塊:module1和module2,兩個模塊是獨立開發的,module2會使用到module1中的一些類,module1會將自己打包為jar提供給module2使用,我們來看一下這2個模塊的代碼。
模塊1
放在module1包中,有3個類
Service1
package com.javacode2018.lesson002.demo17.module1;
import org.springframework.stereotype.Component;
@Component
public class Service1 {
public String m1() {
return "我是module1中的Servce1中的m1方法";
}
}
Service2
package com.javacode2018.lesson002.demo17.module1;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
@Component
public class Service2 {
@Autowired
private com.javacode2018.lesson002.demo17.module1.Service1 service1; //@1
public String m1() { //@2
return this.service1.m1();
}
}
上面2個類,都標注了@Compontent注解,會被spring注冊到容器中。
@1:Service2中需要用到Service1,標注了@Autowired注解,會通過spring容器注入進來
@2:Service2中有個m1方法,內部會調用service的m1方法。
來個spring配置類:Module1Config
package com.javacode2018.lesson002.demo17.module1;
import org.springframework.context.annotation.ComponentScan;
@ComponentScan
public class Module1Config {
}
上面使用了@CompontentScan注解,會自動掃描當前類所在的包中的所有類,將標注有@Compontent注解的類注冊到spring容器,即Service1和Service2會被注冊到spring容器。
再來看模塊2
放在module2包中,也是有3個類,和模塊1中的有點類似。
Service1
模塊2中也定義了一個Service1,內部提供了一個m2方法,如下:
package com.javacode2018.lesson002.demo17.module2;
import org.springframework.stereotype.Component;
@Component
public class Service1 {
public String m2() {
return "我是module2中的Servce1中的m2方法";
}
}
Service3
package com.javacode2018.lesson002.demo17.module2;
import com.javacode2018.lesson002.demo17.module1.Service2;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
@Component
public class Service3 {
//使用模塊2中的Service1
@Autowired
private com.javacode2018.lesson002.demo17.module2.Service1 service1; //@1
//使用模塊1中的Service2
@Autowired
private com.javacode2018.lesson002.demo17.module1.Service2 service2; //@2
public String m1() {
return this.service2.m1();
}
public String m2() {
return this.service1.m2();
}
}
@1:使用module2中的Service1
@2:使用module1中的Service2
先來思考一個問題
上面的這些類使用spring來操作會不會有問題?會有什么問題?
這個問題還是比較簡單的,大部分人都可以看出來,會報錯,因為兩個模塊中都有Service1,被注冊到spring容器的時候,bean名稱會沖突,導致注冊失敗。
來個測試類,看一下效果
package com.javacode2018.lesson002.demo17;
import com.javacode2018.lesson001.demo21.Config;
import com.javacode2018.lesson002.demo17.module1.Module1Config;
import com.javacode2018.lesson002.demo17.module2.Module2Config;
import org.junit.Test;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
public class ParentFactoryTest {
@Test
public void test1() {
//定義容器
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext();
//注冊bean
context.register(Module1Config.class, Module2Config.class); //@1
//啟動容器
context.refresh();
}
}
@1:將
Module1Config、Module2Config
注冊到容器,spring內部會自動解析這兩個類上面的注解,即:@CompontentScan
注解,然后會進行包掃描,將標注了@Compontent
的類注冊到spring容器。
運行test1輸出
下面是部分輸出:
Caused by: org.springframework.context.annotation.ConflictingBeanDefinitionException: Annotation-specified bean name 'service1' for bean class [com.javacode2018.lesson002.demo17.module2.Service1] conflicts with existing, non-compatible bean definition of same name and class [com.javacode2018.lesson002.demo17.module1.Service1]
service1這個bean的名稱沖突了。
那么我們如何解決?
對module1中的Service1進行修改?這個估計是行不通的,module1是別人以jar的方式提供給我們的,源碼我們是無法修改的。
而module2是我們自己的開發的,里面的東西我們可以隨意調整,那么我們可以去修改一下module2中的Service1,可以修改一下類名,或者修改一下這個bean的名稱,此時是可以解決問題的。
不過大家有沒有想過一個問題:如果我們的模塊中有很多類都出現了這種問題,此時我們一個個去重構,還是比較痛苦的,並且代碼重構之后,還涉及到重新測試的問題,工作量也是蠻大的,這些都是風險。
而spring中的父子容器就可以很好的解決上面這種問題。
什么是父子容器
創建spring容器的時候,可以給當前容器指定一個父容器。
BeanFactory的方式
//創建父容器parentFactory
DefaultListableBeanFactory parentFactory = new DefaultListableBeanFactory();
//創建一個子容器childFactory
DefaultListableBeanFactory childFactory = new DefaultListableBeanFactory();
//調用setParentBeanFactory指定父容器
childFactory.setParentBeanFactory(parentFactory);
ApplicationContext的方式
//創建父容器
AnnotationConfigApplicationContext parentContext = new AnnotationConfigApplicationContext();
//啟動父容器
parentContext.refresh();
//創建子容器
AnnotationConfigApplicationContext childContext = new AnnotationConfigApplicationContext();
//給子容器設置父容器
childContext.setParent(parentContext);
//啟動子容器
childContext.refresh();
上面代碼還是比較簡單的,大家都可以看懂。
我們需要了解父子容器的特點,這些是比較關鍵的,如下。
父子容器特點
-
父容器和子容器是相互隔離的,他們內部可以存在名稱相同的bean
-
子容器可以訪問父容器中的bean,而父容器不能訪問子容器中的bean
-
調用子容器的getBean方法獲取bean的時候,會沿着當前容器開始向上面的容器進行查找,直到找到對應的bean為止
-
子容器中可以通過任何注入方式注入父容器中的bean,而父容器中是無法注入子容器中的bean,原因是第2點
使用父子容器解決開頭的問題
關鍵代碼
@Test
public void test2() {
//創建父容器
AnnotationConfigApplicationContext parentContext = new AnnotationConfigApplicationContext();
//向父容器中注冊Module1Config配置類
parentContext.register(Module1Config.class);
//啟動父容器
parentContext.refresh();
//創建子容器
AnnotationConfigApplicationContext childContext = new AnnotationConfigApplicationContext();
//向子容器中注冊Module2Config配置類
childContext.register(Module2Config.class);
//給子容器設置父容器
childContext.setParent(parentContext);
//啟動子容器
childContext.refresh();
//從子容器中獲取Service3
Service3 service3 = childContext.getBean(Service3.class);
System.out.println(service3.m1());
System.out.println(service3.m2());
}
運行輸出
我是module1中的Servce1中的m1方法
我是module2中的Servce1中的m2方法
這次正常了。
父子容器使用注意點
我們使用容器的過程中,經常會使用到的一些方法,這些方法通常會在下面的兩個接口中
org.springframework.beans.factory.BeanFactory
org.springframework.beans.factory.ListableBeanFactory
這兩個接口中有很多方法,這里就不列出來了,大家可以去看一下源碼,這里要說的是使用父子容器的時候,有些需要注意的地方。
BeanFactory接口,是spring容器的頂層接口,這個接口中的方法是支持容器嵌套結構查找的,比如我們常用的getBean方法,就是這個接口中定義的,調用getBean方法的時候,會從沿着當前容器向上查找,直到找到滿足條件的bean為止。
而ListableBeanFactory這個接口中的方法是不支持容器嵌套結構查找的,比如下面這個方法
String[] getBeanNamesForType(@Nullable Class<?> type)
獲取指定類型的所有bean名稱,調用這個方法的時候只會返回當前容器中符合條件的bean,而不會去遞歸查找其父容器中的bean。
來看一下案例代碼,感受一下:
@Test
public void test3() {
//創建父容器parentFactory
DefaultListableBeanFactory parentFactory = new DefaultListableBeanFactory();
//向父容器parentFactory注冊一個bean[userName->"路人甲Java"]
parentFactory.registerBeanDefinition("userName",
BeanDefinitionBuilder.
genericBeanDefinition(String.class).
addConstructorArgValue("路人甲Java").
getBeanDefinition());
//創建一個子容器childFactory
DefaultListableBeanFactory childFactory = new DefaultListableBeanFactory();
//調用setParentBeanFactory指定父容器
childFactory.setParentBeanFactory(parentFactory);
//向子容器parentFactory注冊一個bean[address->"上海"]
childFactory.registerBeanDefinition("address",
BeanDefinitionBuilder.
genericBeanDefinition(String.class).
addConstructorArgValue("上海").
getBeanDefinition());
System.out.println("獲取bean【userName】:" + childFactory.getBean("userName"));//@1
System.out.println(Arrays.asList(childFactory.getBeanNamesForType(String.class))); //@2
}
上面定義了2個容器
父容器:parentFactory,內部定義了一個String類型的bean:userName->路人甲Java
子容器:childFactory,內部也定義了一個String類型的bean:address->上海
@1:調用子容器的getBean方法,獲取名稱為userName的bean,userName這個bean是在父容器中定義的,而getBean方法是BeanFactory接口中定義的,支持容器層次查找,所以getBean是可以找到userName這個bean的
@2:調用子容器的getBeanNamesForType方法,獲取所有String類型的bean名稱,而getBeanNamesForType方法是ListableBeanFactory接口中定義的,這個接口中方法不支持層次查找,只會在當前容器中查找,所以這個方法只會返回子容器的address
我們來運行一下看看效果:
獲取bean【userName】:路人甲Java
[address]
結果和分析的一致。
那么問題來了:有沒有方式解決ListableBeanFactory接口不支持層次查找的問題?
spring中有個工具類就是解決這個問題的,如下:
org.springframework.beans.factory.BeanFactoryUtils
這個類中提供了很多靜態方法,有很多支持層次查找的方法,源碼你們可以去細看一下,名稱中包含有Ancestors
的都是支持層次查找的。
在test2方法中加入下面的代碼:
//層次查找所有符合類型的bean名稱
String[] beanNamesForTypeIncludingAncestors = BeanFactoryUtils.beanNamesForTypeIncludingAncestors(childFactory, String.class);
System.out.println(Arrays.asList(beanNamesForTypeIncludingAncestors));
Map<String, String> beansOfTypeIncludingAncestors = BeanFactoryUtils.beansOfTypeIncludingAncestors(childFactory, String.class);
System.out.println(Arrays.asList(beansOfTypeIncludingAncestors));
運行輸出
[address, userName]
[{address=上海, userName=路人甲Java}]
查找過程是按照層次查找所有滿足條件的bean。
回頭看一下springmvc父子容器的問題
問題1:springmvc中只使用一個容器是否可以?
只使用一個容器是可以正常運行的。
問題2:那么springmvc中為什么需要用到父子容器?
通常我們使用springmvc的時候,采用3層結構,controller層,service層,dao層;父容器中會包含dao層和service層,而子容器中包含的只有controller層;這2個容器組成了父子容器的關系,controller層通常會注入service層的bean。
采用父子容器可以避免有些人在service層去注入controller層的bean,導致整個依賴層次是比較混亂的。
父容器和子容器的需求也是不一樣的,比如父容器中需要有事務的支持,會注入一些支持事務的擴展組件,而子容器中controller完全用不到這些,對這些並不關心,子容器中需要注入一下springmvc相關的bean,而這些bean父容器中同樣是不會用到的,也是不關心一些東西,將這些相互不關心的東西隔開,可以有效的避免一些不必要的錯誤,而父子容器加載的速度也會快一些。
總結
-
本文需掌握父子容器的用法,了解父子容器的特點:子容器可以訪問父容器中bean,父容器無法訪問子容器中的bean
-
BeanFactory接口支持層次查找
-
ListableBeanFactory接口不支持層次查找
-
BeanFactoryUtils工具類中提供了一些非常實用的方法,比如支持bean層次查找的方法等等
案例源碼
https://gitee.com/javacode2018/spring-series
路人甲java所有案例代碼以后都會放到這個上面,大家watch一下,可以持續關注動態。
來源:https://mp.weixin.qq.com/s?__biz=MzA5MTkxMDQ4MQ==&mid=2648934382&idx=1&sn=7d37aef61cd18ec295f268c902dfb84f&chksm=88621fd0bf1596c6c9f60c966eb325c6dfe0e200666ee0bcdd1ff418597691795ad209e444f2&token=749715143&lang=zh_CN&scene=21#wechat_redirect