Java自学者论坛

 找回密码
 立即注册

手机号码,快捷登录

恭喜Java自学者论坛(https://www.javazxz.com)已经为数万Java学习者服务超过8年了!积累会员资料超过10000G+
成为本站VIP会员,下载本站10000G+会员资源,会员资料板块,购买链接:点击进入购买VIP会员

JAVA高级面试进阶训练营视频教程

Java架构师系统进阶VIP课程

分布式高可用全栈开发微服务教程Go语言视频零基础入门到精通Java架构师3期(课件+源码)
Java开发全终端实战租房项目视频教程SpringBoot2.X入门到高级使用教程大数据培训第六期全套视频教程深度学习(CNN RNN GAN)算法原理Java亿级流量电商系统视频教程
互联网架构师视频教程年薪50万Spark2.0从入门到精通年薪50万!人工智能学习路线教程年薪50万大数据入门到精通学习路线年薪50万机器学习入门到精通教程
仿小米商城类app和小程序视频教程深度学习数据分析基础到实战最新黑马javaEE2.1就业课程从 0到JVM实战高手教程MySQL入门到精通教程
查看: 545|回复: 0

通过单元测试理解spring容器以及dubbo+zookeeper单元测试异常处理

[复制链接]
  • TA的每日心情
    奋斗
    2024-11-24 15:47
  • 签到天数: 804 天

    [LV.10]以坛为家III

    2053

    主题

    2111

    帖子

    72万

    积分

    管理员

    Rank: 9Rank: 9Rank: 9

    积分
    726782
    发表于 2021-9-3 09:12:51 | 显示全部楼层 |阅读模式

    一、先说一个结论:单元测试与主项目的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); } 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加深了了解。

    哎...今天够累的,签到来了1...
    回复

    使用道具 举报

    您需要登录后才可以回帖 登录 | 立即注册

    本版积分规则

    QQ|手机版|小黑屋|Java自学者论坛 ( 声明:本站文章及资料整理自互联网,用于Java自学者交流学习使用,对资料版权不负任何法律责任,若有侵权请及时联系客服屏蔽删除 )

    GMT+8, 2024-12-22 19:09 , Processed in 0.060994 second(s), 30 queries .

    Powered by Discuz! X3.4

    Copyright © 2001-2021, Tencent Cloud.

    快速回复 返回顶部 返回列表