通過單元測試理解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加深了了解。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM