热门标签 | HotTags
当前位置:  开发笔记 > 编程语言 > 正文

collect的功能是什么?其底层如何实现的?_如何优雅地执行dubboquot;单测quot;

很多小伙伴所在的公司是基于Dubbo来构建技术栈的,日常开发中必不可少要写dubbo单测(单元测试),如果单测数据依赖已有的
914111675b800ff47c058caf43cb8044.png

很多小伙伴所在的公司是基于Dubbo来构建技术栈的,日常开发中必不可少要写dubbo单测(单元测试),如果单测数据依赖已有的外部dubbo服务,一般是mock数据,如果数据比较复杂,其实mock数据也是一个不小的工作量。那有没有更好的单测方式来代替我们完成”mock“数据功能呢,这时可以借助dubbo telnet功能,获取真实数据用在单测中使用。

本文会先讨论如何使用基于dubbo telnet的代理工具类(DubboTelnetProxy),然后再讨论下mockito+DubboTelnetProxy如何进行多层次的单测,最后分析下如何让单测变得更加智能(比如自动注入等)。(ps:关于dubbo和mockito这里就不展开讨论了,具体可以参考对应资料~)

1 Dubbo单测现状

dubbo单测其实和非dubbo单测的流程是一样的,初始化待测试类和单测上下文,打桩然后调用,最后检查返回结果。比如我们常用mockito来跑单测,其简单的示例如下:

public class DubboAppContextFilterTest extends BaseTest {private DubboAppContextFilter filter = new DubboAppContextFilter();@Beforepublic void setUp() {cleanUpAll();}@Afterpublic void cleanUp() {cleanUpAll();}@Testpublic void testInvokeApplicationKey() {Invoker invoker = mock(Invoker.class);Invocation invocation = mock(Invocation.class);URL url = URL.valueOf("test://test:111/test?application=serviceA");when(invoker.getUrl()).thenReturn(url);filter.invoke(invoker, invocation);verify(invoker).invoke(invocation);String application = RpcContext.getContext().getAttachment(DubboUtils.SENTINEL_DUBBO_APPLICATION_KEY);assertEquals("serviceA", application);}
}

上面代码copy于sentinel的单元测试代码。

2 DubboTelnetProxy

在dubbo服务机器上,我们可以使用telnet连接dubbo服务,然后执行invoke命令来手动调用dubbo接口并获取结果,DubboTelnetProxy就是将这一系列的手动操作按照dubbo telnet格式固化到代码中。在具体讨论DubboTelnetProxy之前,先看下其有哪些功能,DubboTelnetProxy特点:

  • 基于telnet的dubbo代理工具类,可用于本地单测中;
  • 直接使用telnet指定ip+port进行连接,无需更多的dubbo相关配置,使用便捷;
  • 可动态配置ip+port信息。

话不多说,先看下DubboTelnetProxy代码实现:

@AllArgsConstructor(access = AccessLevel.PRIVATE)
public class DubboTelnetProxy implements MethodInterceptor {private String ip;private Integer port;@Overridepublic Object intercept(Object obj, Method method, Object[] params, MethodProxy proxy) throws Throwable {if ("toString".equals(method.getName())) {return obj.getClass().getName();}TelnetClient telnetClient = new TelnetClient();telnetClient.setConnectTimeout((int) TimeUnit.SECONDS.toMillis(5));telnetClient.connect(ip, port);try {InputStream in = telnetClient.getInputStream();PrintStream out = new PrintStream(telnetClient.getOutputStream());// 1. 发送dubbo telnet请求StringBuffer request = new StringBuffer("invoke ");request.append(method.getDeclaringClass().getTypeName()).append(".");request.append(method.getName()).append("(");request.append(StringUtils.join(Arrays.stream(params).map(JSON::toJSONString).collect(Collectors.toList()), ",")).append(")");out.println(request.toString());out.flush();// 2. 结果处理int len = 0;byte[] buffer = new byte[512];String result = "";while (!result.contains(StringUtils.LF) && (len = in.read(buffer)) > 0) {result += new String(ArrayUtils.subarray(buffer, 0, len));}result = StringUtils.substringBefore(result, StringUtils.LF);if (StringUtils.isBlank(result) || !result.startsWith("{")) {throw new RuntimeException(result);}// 3. 反序列化return JSON.parseObject(result, method.getGenericReturnType());} finally {telnetClient.disconnect();}}/*** mockDubboIpPortFormat:配置格式为 -Dmock.dubbo.%s=127.0.0.1:8080,%s为当前dubbo接口的名字,class.getSimpleName()*/private final static String mockDubboIpPortPrefix = "mock.dubbo.";public final static String mockDubboIpPortFormat = mockDubboIpPortPrefix + "%s";/*** dubbo telnet建造者*/public static class Builder {final static String DEFAULT_IP = "127.0.0.1";final static Integer DEFAULT_PORT = 20880;/*** 创建dubbo telnet代理*/public static T enhance(Class clazz) {return enhance(clazz, null, null);}public static T enhance(Class clazz, String ip) {return enhance(clazz, ip, null);}public static T enhance(Class clazz, Integer port) {return enhance(clazz, null, port);}@SuppressWarnings("unchecked")public static T enhance(Class object, String ip, Integer port) {// 优先尝试从properties解析ip:port配置String ipPort = System.getProperties().getProperty(String.format(mockDubboIpPortFormat, object.getSimpleName()));if (StringUtils.isNotEmpty(ipPort)) {String[] array = StringUtils.split(ipPort, ",");ip = array[0];port = Integer.valueOf(array[1]);}Enhancer enhancer = new Enhancer();enhancer.setSuperclass(object);enhancer.setCallback(new DubboTelnetProxy(ObjectUtils.defaultIfNull(ip, DEFAULT_IP), ObjectUtils.defaultIfNull(port, DEFAULT_PORT)));return (T) enhancer.create();}}
}

DubboTelnetProxy的实现原理是使用cglib生成dubbo facade接口代理类,然后在代理类按照dubbo telnet格式拼接请求参数,最后获取返回结果并反序列化返回给应用程序。上述代码不足点是:目前每次dubbo调用都会新建telnet连接,对于单测来说是OK的,后续如果用于本地压测或者调用频繁测试场景,考虑复用连接或者使用netty client bootstrap方式避免每次都新建连接。

2.1 使用示例

手动/自动指定dubbo服务IP地址:

@Test
public void test() {// OrderQueryService为dubbo服务的一个API接口System.setProperty("mock.dubbo.OrderQueryService", "127.0.0.1:20880");OrderQueryService orderQueryService1 = DubboTelnetProxy.Builder.enhance(OrderQueryService.class);OrderQueryService orderQueryService2 = DubboTelnetProxy.Builder.enhance(OrderQueryService.class, "127.0.0.1");OrderQueryService orderQueryService3 = DubboTelnetProxy.Builder.enhance(OrderQueryService.class, "127.0.0.1", 20880);OrderDTO result = orderQueryService1.query("订单号");System.out.println(result);
}

3 DubboTelnetProxy + mockito自动注入

日常开发中,可以使用mockito进行单测,保证代码质量。在mockito中,如果想让某个DubboTelnetProxy代理类注入到待测试中,可使用FieldUtils工具类进行属性注入。

使用DubboTelnetProxy + mockito示例如下:

@RunWith(MockitoJUnitRunner.class)
public class DemoServiceClientTest {@InjectMocksDemoServiceClient demoServiceClient;@Beforepublic void before() throws IllegalAccessException {FieldUtils.writeField(demoServiceClient, "demoServiceFacade",DubboTelnetProxy.Builder.enhance(DemoServiceFacade.class), true);}@Testpublic void hello() throws IllegalAccessException {// 调用远程服务,DubboTelnetProxy方式demoServiceClient.hello("world");// 如果需要打桩,则使用Mock类DemoServiceFacade demoServiceFacade = Mockito.mock(DemoServiceFacade.class);Mockito.when(demoServiceFacade.hello("world")).thenReturn("zzz");FieldUtils.writeField(demoServiceClient, "demoServiceFacade", demoServiceFacade, true);Assert.assertEquals(demoServiceClient.hello("world"), "zzz");}
}@Component
public class DemoServiceClient {@Resourceprivate DemoServiceFacade demoServiceFacade;public String hello(String world) {return demoServiceFacade.hello(world);}
}// dubbo api
public interface DemoServiceFacade {String hello(String world);
}

3.1 如何自动注入

要实现DubboTelnetProxy的自动注入,首先判断出来待测试类中的哪些属性需要构造DubboTelnetProxy或者对应实例,一般情况下如果属性是非本工程内的接口类型,就可以认为是dubbo api接口,进行构造DubboTelnetProxy并注入;如果属性是本工程内的接口类型,则在本工程内查找对应的实现类进行反射方式的属性注入(可使用org.reflections包中的Reflections工具类来获取接口下所有实现类);如果属性是普通类,则直接反射构建对象注入即可,伪代码如下:

/*** 默认的dubbo属性构造器,如果是非本工程内属性类型并且是接口类型,直接进行DubboTelnetProxy构建*/
public static Function DEFAULT_DUBBO_FC = field -> {try {assert Objects.nonNull(targetContext.get());Class fieldClass = field.getType();if (fieldClass.isInterface()) {// 本工程内的加载其实现类,非本工程内的按照DubboTelnetProxy构建if (!isSameProjectPath(targetContext.get().getClass(), fieldClass)) {return DubboTelnetProxy.Builder.enhance(fieldClass);} else if (fieldClass.getSimpleName().endsWith("Dao")) {return Mockito.mock(fieldClass);} else {String packagePath = fieldClass.getPackage().getName() + ".impl.";return Class.forName(packagePath + fieldClass.getSimpleName() + "Impl").newInstance();}} else if (isSameProjectPath(targetContext.get().getClass(), fieldClass)) {return fieldClass.newInstance();} else {// 非工程内的类直接mock掉return Mockito.mock(fieldClass);}} catch (Exception e) {System.err.println("DEFAULT_DUBBO_FC 发生异常 field=" + field);e.printStackTrace();System.exit(-1);return null;}
};

针对待注入类有多个层次,比如测试类A中属性b类型是B,B中属性c类型是C等,那么在自动注入类A的所有属性时,需要递归进行,直至所有子类型的属性都构建完毕,示例伪代码如下:

void doWithFieldsInternal(&#64;NonNull Object target, &#64;Nullable Function fc, &#64;Nullable Boolean recursive) {assert !(target instanceof Class);// 默认fc回调直接调用默认无参构造方法fc &#61; ObjectUtils.defaultIfNull(fc, DEFAULT_FC);recursive &#61; ObjectUtils.defaultIfNull(recursive, false);List fieldList &#61; new ArrayList<>();do {Object finalTarget &#61; target;Function finalFc &#61; fc;ReflectionUtils.doWithFields(finalTarget.getClass(), field -> {Object value &#61; finalFc.apply(field));DubboReflectionUtils.setField(finalTarget, field, value);if (Objects.nonNull(value) && DEFAULT_FF.matches(field)) {fieldList.add(value);}}, filterField -> {// 默认只注入非基本类型并且为null的属性return DEFAULT_FF.matches(filterField) && DubboReflectionUtils.isNullFieldValue(finalTarget, filterField);});} while (recursive && !fieldList.isEmpty() && Objects.nonNull(target &#61; fieldList.remove(0)));
}

3.2 如何让自动注入更易用

上述示例中的自动注入是程序会递归注入待测试类中的所有属性&#xff0c;但还是需要在代码中先调用要"自动注入"的代码&#xff0c;为了更易用&#xff0c;可以使用注解方式来自动注入被注解修饰的所有类或者属性&#xff0c;类似于在Spring中对类属性配置了&#64;Resource之后&#xff0c;Spring在容器启动过程中会自动对该属性注入对应示例&#xff0c;开发者无需关注。

关于如何实现mockito&#43;DubboTelnetProxy的注解方式自动注入&#xff0c;笔者就不在赘述&#xff0c;感兴趣的小伙伴可以参考3.1中的实现思路自行实现。

说道注解&#xff0c;其实想实现针对某些注解执行一些特定逻辑&#xff08;比如执行自动注入&#xff09;&#xff0c;可以在两种阶段对其处理&#xff0c;如下所示&#xff1a;

  • 编译处理阶段&#xff1a;比如设置Java的注解处理器&#xff0c;一般是继承AbstractProcessor来实现特定业务逻辑&#xff0c;其主要的处理逻辑就是扫描、评估和处理注解的代码&#xff0c;以及生产 Java 文件。比如lombok中的&#64;Setter注解就是要产生对应属性的setter方法&#xff1b;
  • 容器启动阶段&#xff1a;这里的容器是业务程序自己定义的容器&#xff0c;比如Spring的IoC容器&#xff0c;在容器启动过程中针对注解进行处理&#xff0c;首先获取注解对应的属性&#xff0c;然后从容器中获取属性对应的实例通过反射将其注入即可。

以上两种自动注入方式在实现都是OK的&#xff0c;前者在编译阶段后者在运行时&#xff0c;不过后者由于在运行时起作用&#xff0c;因此灵活性更大。

推荐阅读

  • Java nio 空轮询bug到底是什么
  • 程序员必看| mockito原理浅析
  • Java常见几种动态代理的对比

欢迎小伙伴关注【TopCoder】精彩好文。

75dbe3e60049afff360924263dedd407.png



推荐阅读
  • 本文介绍了Java工具类库Hutool,该工具包封装了对文件、流、加密解密、转码、正则、线程、XML等JDK方法的封装,并提供了各种Util工具类。同时,还介绍了Hutool的组件,包括动态代理、布隆过滤、缓存、定时任务等功能。该工具包可以简化Java代码,提高开发效率。 ... [详细]
  • 阿,里,云,物,联网,net,core,客户端,czgl,aliiotclient, ... [详细]
  • Java学习笔记之面向对象编程(OOP)
    本文介绍了Java学习笔记中的面向对象编程(OOP)内容,包括OOP的三大特性(封装、继承、多态)和五大原则(单一职责原则、开放封闭原则、里式替换原则、依赖倒置原则)。通过学习OOP,可以提高代码复用性、拓展性和安全性。 ... [详细]
  • 第四章高阶函数(参数传递、高阶函数、lambda表达式)(python进阶)的讲解和应用
    本文主要讲解了第四章高阶函数(参数传递、高阶函数、lambda表达式)的相关知识,包括函数参数传递机制和赋值机制、引用传递的概念和应用、默认参数的定义和使用等内容。同时介绍了高阶函数和lambda表达式的概念,并给出了一些实例代码进行演示。对于想要进一步提升python编程能力的读者来说,本文将是一个不错的学习资料。 ... [详细]
  • Imtryingtofigureoutawaytogeneratetorrentfilesfromabucket,usingtheAWSSDKforGo.我正 ... [详细]
  • 预备知识可参考我整理的博客Windows编程之线程:https:www.cnblogs.comZhuSenlinp16662075.htmlWindows编程之线程同步:https ... [详细]
  • JDK源码学习之HashTable(附带面试题)的学习笔记
    本文介绍了JDK源码学习之HashTable(附带面试题)的学习笔记,包括HashTable的定义、数据类型、与HashMap的关系和区别。文章提供了干货,并附带了其他相关主题的学习笔记。 ... [详细]
  • Spring常用注解(绝对经典),全靠这份Java知识点PDF大全
    本文介绍了Spring常用注解和注入bean的注解,包括@Bean、@Autowired、@Inject等,同时提供了一个Java知识点PDF大全的资源链接。其中详细介绍了ColorFactoryBean的使用,以及@Autowired和@Inject的区别和用法。此外,还提到了@Required属性的配置和使用。 ... [详细]
  • 如何查询zone下的表的信息
    本文介绍了如何通过TcaplusDB知识库查询zone下的表的信息。包括请求地址、GET请求参数说明、返回参数说明等内容。通过curl方法发起请求,并提供了请求示例。 ... [详细]
  • 本文介绍了OpenStack的逻辑概念以及其构成简介,包括了软件开源项目、基础设施资源管理平台、三大核心组件等内容。同时还介绍了Horizon(UI模块)等相关信息。 ... [详细]
  • 本文记录了在vue cli 3.x中移除console的一些采坑经验,通过使用uglifyjs-webpack-plugin插件,在vue.config.js中进行相关配置,包括设置minimizer、UglifyJsPlugin和compress等参数,最终成功移除了console。同时,还包括了一些可能出现的报错情况和解决方法。 ... [详细]
  • 本文介绍了绕过WAF的XSS检测机制的方法,包括确定payload结构、测试和混淆。同时提出了一种构建XSS payload的方法,该payload与安全机制使用的正则表达式不匹配。通过清理用户输入、转义输出、使用文档对象模型(DOM)接收器和源、实施适当的跨域资源共享(CORS)策略和其他安全策略,可以有效阻止XSS漏洞。但是,WAF或自定义过滤器仍然被广泛使用来增加安全性。本文的方法可以绕过这种安全机制,构建与正则表达式不匹配的XSS payload。 ... [详细]
  • Explain如何助力SQL语句的优化及其分析方法
    本文介绍了Explain如何助力SQL语句的优化以及分析方法。Explain是一个数据库SQL语句的模拟器,通过对SQL语句的模拟返回一个性能分析表,从而帮助工程师了解程序运行缓慢的原因。文章还介绍了Explain运行方法以及如何分析Explain表格中各个字段的含义。MySQL 5.5开始支持Explain功能,但仅限于select语句,而MySQL 5.7逐渐支持对update、delete和insert语句的模拟和分析。 ... [详细]
  • 本文整理了315道Python基础题目及答案,帮助读者检验学习成果。文章介绍了学习Python的途径、Python与其他编程语言的对比、解释型和编译型编程语言的简述、Python解释器的种类和特点、位和字节的关系、以及至少5个PEP8规范。对于想要检验自己学习成果的读者,这些题目将是一个不错的选择。请注意,答案在视频中,本文不提供答案。 ... [详细]
  • 抽空写了一个ICON图标的转换程序
    抽空写了一个ICON图标的转换程序,支持png\jpe\bmp格式到ico的转换。具体的程序就在下面,如果看的人多,过两天再把思路写一下。 ... [详细]
author-avatar
卢健波_785
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有