Spring整理系列(16)——通過單元測試理解spring容器以及dubbo+zookeeper單元測試異常處理
一、先說一個結論:單元測試與主專案的spring容器是隔離的,也就是說,單元測試無法訪問主專案spring容器,需要自己載入spring容器。
接下來是程式碼例項,WEB主專案出於執行狀態,單元測試中可能會看到如下這樣的程式碼:
程式碼一:當前類載入式
public class TestSpring {
@Test
public void testSpring(){
LoginService loginService = this.getBean("loginService");
}
//以下為容器例項宣告及初始化、銷燬
private ClassPathXmlApplicationContext context;
@Before
public void before(){
//載入spring容器
context = new ClassPathXmlApplicationContext("spring-context.xml");
}
@After
public void after(){
context.destroy();
}
//從靜態變數applicationContext中取得Bean, 自動轉型為所賦值物件的型別.
public <T> T getBean(String name) {
return (T) context.getBean(name);
}
//從靜態變數applicationContext中取得Bean, 自動轉型為所賦值物件的型別.
public <T> T getBean(Class<T> requiredType) {
return context.getBean(requiredType);
}
}
程式碼二:繼承載入式
/**
* @Description: 登入單元測試類
*/
public class LoginTest extends SpringJunitSupport{
@Autowired
private LoginService loginService;
@Test
public void testLogin(){
loginService.login();
}
}
//讓單元測試運行於spring環境,保證擁有spring框架相關支援
@RunWith(SpringJUnit4ClassRunner.class)
//載入spring容器
@ContextConfiguration("classpath:/spring-context.xml")
public class SpringJunitSupport {
}
程式碼三:動態新增spring配置檔案式
/**
* @Description: 登入單元測試類
*/
public class LoginTest{
//使用@Before註解方式載入spring容器配置檔案,就不能通過自動裝配的方式注入bean,因為自動裝配註解執行要早於@Before
//@Autowired
private LoginService loginService;
private TestSpringContextSupport springContextSupport = null;
@Before
public void setUp() throws Exception {
springContextSupport = new TestSpringContextSupport();
//初始化spring容器時,再動態新增spring bean配置檔案
springContextSupport.init(new String[] { "classpath:/support-quartz.xml" });
loginService = springContextSupport.getBean("loginService");
}
@Test
public void testLogin(){
loginService.login();
}
}
public class TestSpringContextSupport {
//通過靜態語句塊初始化一個靜態變數,用於存放spring容器配置檔案
public static List<String> contextList = new ArrayList<String>();
static {
contextList.add("classpath:/spring-context.xml");
}
private ApplicationContext context;
//定義初始化方法,動態新增spring配置檔案到靜態配置檔案集合
public void init(String[] contextFile) {
List<String> list = new ArrayList<String>();
list.addAll(contextList);
for (int i = 0; contextFile != null && i < contextFile.length; i++) {
list.add(contextFile[i]);
}
String[] x = new String[list.size()];
list.toArray(x);
//載入spring容器
context = new ClassPathXmlApplicationContext(x);
}
//從靜態變數applicationContext中取得Bean, 自動轉型為所賦值物件的型別.
public <T> T getBean(String name) {
return (T) context.getBean(name);
}
//從靜態變數applicationContext中取得Bean, 自動轉型為所賦值物件的型別.
public <T> T getBean(Class<T> requiredType) {
return context.getBean(requiredType);
}
}
如上面三種方式,有一個共同點,就是在單元測試方法執行前,都大費周章的去載入了spring容器。
web主專案出於執行狀態,單元測試為什麼還要單獨載入spring容器,因為web主專案的spring容器對單元測試是隔離的,通過如下手段驗證:
驗證1:
把單元測試所有載入spring容器的程式碼去掉,保證主專案出於執行狀態,通過@Autowired註解(@Autowired註解可以裝配spring內部bean),獲取spring應用上下文的bean,然後再通過其獲取業務bean,程式碼如下:
/**
* @Description: 登入單元測試類
*/
public class LoginTest{
//自動裝配spring應用上下文bean
@Autowired
private ApplicationContext context;
@Test
public void testLogin(){
//通過應用上下文bean,獲取業務bean
LoginService loginService = (LoginService)context.getBean("loginService");
loginService.login();
}
}
結果一定是空指標異常,context物件為null。
驗證二,把web主專案停掉,單元測試使用上面第二種繼承的方式載入spring容器,其它同上,程式碼如下:
/**
* @Description: 登入單元測試類,繼承SpringJunitSupport載入spring容器
*/
public class LoginTest extends SpringJunitSupport{
//自動裝配spring應用上下文bean
@Autowired
private ApplicationContext context;
@Test
public void testLogin(){
//通過應用上下文bean,獲取業務bean
LoginService loginService = (LoginService)context.getBean("loginService");
loginService.login();
}
}
@RunWith(SpringJUnit4ClassRunner.class)
//載入spring容器
@ContextConfiguration("classpath:/spring-context.xml")
public class SpringJunitSupport {
}
結果一切正常,如此就驗證了單元測試與主專案的spring容器是隔離的,單元測試必須自己載入spring容器。
上面一直在說載入spring容器,其實就是載入配置檔案,把配置檔案裡面的bean載入到spring容器中,上面的驗證也一直通過在spring容器中搜索bean物件進行的,理解並應用這一點是非常重要的。
最後的彩蛋,理解是因為專案中有困惑,探究之後才能領悟透徹,比如一個例項:
1、主專案執行,提供服務介面,採用的方式為dubbo+zookeeper方式;
2、單元測試,呼叫提供者提供的服務,採用繼承式載入spring配置檔案;
3、丟擲異常:地址已經被繫結使用(Address already in use: bind)
java.lang.IllegalStateException: Failed to load ApplicationContext
......
Caused by: com.alibaba.dubbo.rpc.RpcException: Fail to start server(url: dubbo://127.0.0.1:20880/......
......
Caused by: com.alibaba.dubbo.remoting.RemotingException: Failed to bind NettyServer on /127.0.0.1:20880, cause: Failed to bind to: /0.0.0.0:20880
......
Caused by: org.jboss.netty.channel.ChannelException: Failed to bind to: /0.0.0.0:20880
......
Caused by: java.net.BindException: Address already in use: bind
......
4、異常原因:因為採用的是dubbo+zookeeper方式,主專案spring提供者註冊了127.0.0.1:20880,單元測試載入spring配置檔案想要註冊0.0.0.0:20880地址,但是20880已經被主容器佔用,所以單元測試無法正常載入。
5、解決辦法:將主容器停掉,單獨使用單元測試,即作為服務端又作為客戶端
6、再次丟擲異常:
DEBUG [2016-08-18 18:30:26,603] - ZkClient.java () - Closing ZkClient...
INFO [2016-08-18 18:30:26,603] - ZkEventThread.java () - Terminate ZkClient event thread.
DEBUG [2016-08-18 18:30:26,603] - ZkConnection.java () - Closing ZooKeeper connected to 119.254.166.167:2181
DEBUG [2016-08-18 18:30:26,603] - ZooKeeper.java () - Closing session: 0x15678a538f900ef
DEBUG [2016-08-18 18:30:26,603] - ClientCnxn.java () - Closing client for session: 0x15678a538f900ef
......
DEBUG [2016-08-18 18:30:26,808] - ZkClient.java () - Closing ZkClient...done
INFO [2016-08-18 18:30:26,810] - DubboProtocol.java () - [DUBBO] Close dubbo server: /127.0.0.1:20880, dubbo version: 2.5.4, current host: 127.0.0.1
INFO [2016-08-18 18:30:26,812] - AbstractServer.java () - [DUBBO] Close NettyServer bind /0.0.0.0:20880, export /127.0.0.1:20880, dubbo version: 2.5.4, current host: 127.0.0.1
INFO [2016-08-18 18:30:26,812] - ClientCnxn.java () - EventThread shut down for session: 0x15678a538f900ef
ERROR [2016-08-18 18:30:26,813] - FailbackRegistry.java () - [DUBBO] Failed to uregister dubbo://127.0.0.1:20880/......
com.alibaba.dubbo.rpc.RpcException: Failed to unregister dubbo://127.0.0.1:20880/業務方法......
at com.alibaba.dubbo.registry.zookeeper.ZookeeperRegistry.doUnregister(ZookeeperRegistry.java:108)
at com.alibaba.dubbo.registry.support.FailbackRegistry.unregister(FailbackRegistry.java:160)
at com.alibaba.dubbo.registry.integration.RegistryProtocol$1.unexport(RegistryProtocol.java:130)
at com.alibaba.dubbo.config.ServiceConfig.unexport(ServiceConfig.java:270)
at com.alibaba.dubbo.config.spring.ServiceBean.destroy(ServiceBean.java:255)
at org.springframework.beans.factory.support.DisposableBeanAdapter.destroy(DisposableBeanAdapter.java:258)
at org.springframework.beans.factory.support.DefaultSingletonBeanRegistry.destroyBean(DefaultSingletonBeanRegistry.java:538)
at org.springframework.beans.factory.support.DefaultSingletonBeanRegistry.destroySingleton(DefaultSingletonBeanRegistry.java:514)
at org.springframework.beans.factory.support.DefaultListableBeanFactory.destroySingleton(DefaultListableBeanFactory.java:831)
at org.springframework.beans.factory.support.DefaultSingletonBeanRegistry.destroySingletons(DefaultSingletonBeanRegistry.java:483)
at org.springframework.context.support.AbstractApplicationContext.destroyBeans(AbstractApplicationContext.java:923)
at org.springframework.context.support.AbstractApplicationContext.doClose(AbstractApplicationContext.java:897)
at org.springframework.context.support.AbstractApplicationContext$1.run(AbstractApplicationContext.java:811)
Caused by: java.lang.NullPointerException
at org.I0Itec.zkclient.ZkClient$8.call(ZkClient.java:720)
at org.I0Itec.zkclient.ZkClient.retryUntilConnected(ZkClient.java:675)
at org.I0Itec.zkclient.ZkClient.delete(ZkClient.java:716)
at com.alibaba.dubbo.remoting.zookeeper.zkclient.ZkclientZookeeperClient.delete(ZkclientZookeeperClient.java:57)
at com.alibaba.dubbo.registry.zookeeper.ZookeeperRegistry.doUnregister(ZookeeperRegistry.java:106)
... 12 more
上面需要注意的關鍵資訊可以歸結為:zookeeper客戶端關閉(Closing ZkClient…)、dubbo服務關閉(Close dubbo server….)、登出dubbo的某個方法失敗(Failed to uregister dubbo://127.0.0.1:20880/…)
雖然沒細探究到底怎麼回事,但是感覺應該是單元測試同時載入服務端和客戶端(即載入spring配置檔案),當測試方法執行完畢需要關閉服務的時候,由於先後順序問題引發的異常。
7、再次解決辦法:提供者由主容器啟動,至於單元測試,就到了上面最後的彩蛋那句話了,單元測試作為客戶端,只需要拿到服務端提供者的bean物件,就可以完成對提供者服務端的呼叫。
那麼這個物件從哪裡來,dubbo+zookeeper方式會在客戶端配置訂閱服務的配置檔案,這個裡面有提供者對應的bean,所以單元測試只需要載入客戶端訂閱配置檔案即可,程式碼如下:
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration({ "classpath:/spring/test-dubbo-consumer.xml" })
public class CodeRuleRPCImplDubboTest {
@Autowired
private UserRPC UserRPC;
@Test
public void testGetUserByCode() {
bizCode = userRPC.getUserByCode("001");
}
}
客戶端訂閱者配置檔案:
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:context="http://www.springframework.org/schema/context"
xmlns:jdbc="http://www.springframework.org/schema/jdbc" xmlns:jee="http://www.springframework.org/schema/jee"
xmlns:tx="http://www.springframework.org/schema/tx" xmlns:util="http://www.springframework.org/schema/util"
xmlns:task="http://www.springframework.org/schema/task" xmlns:dubbo="http://code.alibabatech.com/schema/dubbo"
xsi:schemaLocation="
http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-4.0.xsd
http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-4.0.xsd
http://www.springframework.org/schema/jdbc http://www.springframework.org/schema/jdbc/spring-jdbc-4.0.xsd
http://www.springframework.org/schema/jee http://www.springframework.org/schema/jee/spring-jee-4.0.xsd
http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx-4.0.xsd
http://www.springframework.org/schema/util http://www.springframework.org/schema/util/spring-util-4.0.xsd
http://www.springframework.org/schema/task http://www.springframework.org/schema/task/spring-task-4.0.xsd
http://code.alibabatech.com/schema/dubbo http://code.alibabatech.com/schema/dubbo/dubbo.xsd">
<!-- 消費方應用名稱資訊,這個相當於起一個名字,我們dubbo管理頁面比較清晰是哪個應用暴露出來的 -->
<dubbo:application name="SPRING_DUBBO_CONSUMER"></dubbo:application>
<!-- 使用zookeeper註冊中心暴露服務地址 -->
<dubbo:registry address="zookeeper://127.0.0.1:2181"
check="true"></dubbo:registry>
<!-- 要引用的使用者管理服務 -->
<dubbo:reference interface="com.test.rpc.UserRPC"
id="userRPC"></dubbo:reference>
</beans>
至此以上問題解決,同時深入理解spring容器,單元測試,spring-bean之間的關係,同時也對dubbo和zookeeper加深了了解。