大家好,本篇分两部分,主要介绍EasyMock平台及JSF Mock实现技术,后续会继续编写一系列文章,分享更多Mock相关技术。平台21年零售内开展开源共建,并获得21年行云1024研发效能共建最佳新锐奖,目前正在集团开源共建中,欢迎联系我们一起共建!
一、EasyMock平台介绍
EasyMock平台面向集团产品、研发、测试人员,提供的一款完全模拟服务端Mock的平台,支持JSF、HTTP接口Mock服务,支持测试环境/线上环境多站点,灵活的接口出入参设置,可以方便返回想要的Mock数据。平台自21年10月上线行云,面向集团进行推广,累计接入C1部门 49个(涉及零售、科技、物流、健康等BU),涵盖用户2000+,月均Mock调用量1000万+。
平台地址: http://jagile.jd.com/mock/singleInterface
帮助文档: http://doc.jd.com/rest/mock/
咚咚支持群:1024782579
首先,我们先了解下EasyMock解决的问题:
- 解决依赖服务不可用问题,不阻碍开发/测试;
- 依赖服务复杂、异常数据无法支持,弥补场景缺失;
- 依赖服务数据经常变化,通过Mock提升自动化测试通过率;
- 项目测试时间紧张时,可不受依赖服务的排期影响;
然后,我们通过一个小的GIF,了解平台JSF Mock的使用过程
以上只是Mock平台的部分功能,平台还有更多内容等待你去发现。
接下来,我们了解EasyMock提供的平台能力:
- 支持多协议Mock:JSF、HTTP;
- 支持测试/线上环境;
- 同接口多版本、多别名支持;
- 接口与方法分开控制,支持服务透传,调用真实服务;(平台亮点)
1)方法级别透传:被测应用调用同一接口的不同方法,可实现一个方法Mock,一个方法调用真实的服务;
2)参数模版级别:被Mock的方法匹配不到参数模版时,可设置调用真实的服务(即将上线);
5. 参数数据模板管理:支持参数正则匹配、出入参自动解析、自动生成、参数化、参数传递、异常模拟等;(平台亮点)
1)支持参数正则匹配:多种参数匹配方式,优先全量匹配、部分匹配、正则匹配、默认匹配;
2)出入参自动解析、自动生成:不知道出入参格式怎么办?平台支持参数解析、出参自动生成;
3)参数传递:想返回的出参取客户端调用传进来的入参值;
4)异常模拟:支持模拟接口抛出的异常、超时(即将上线);
5)参数化:支持出参参数化、简单运算;
6. 开放API服务,方便自动化或其他平台集成;
7. 性能测试支持;
8. 更多功能持续迭代中;
二、平台实现技术解密-JSF Mock
Mock所用的技术知识点很多,比如JVM、类实例化、动态代理、反序列化、Http拦截等,本期开始,将对Mock所用技术进行一个全面的解密,本次主要分享平台的整体设计及JSF Mock的实现技术,后面也会针对某一块的技术实现或实践案例进行详细的分享。
平台整体设计
如下图所示,平台整体采用主、从服务部署,主服务面向用户,提供服务管理、模版管理、应用管理(规划中)、看板等功能,从服务提供接口Mock服务,供客户端调用,主服务通过IP分配规则控制从服务进行接口Mock开启/关闭。
JSF技术实现步骤
从技术角度来说下JSF Mock的整个流程,用户访问平台,添加要Mock的JSF接口和方法,主服务会异步下载接口所依赖的Jar包,用户开启Mock,主服务按分配规则通知从服务开启Mock,从服务将接口所依赖Java类加载到JVM,通过动态代理将接口实例化,同时将接口注册到JSF册中心,一个接口就Mock好了。这时客户端请求Mock服务,从服务接收到客户端请求,后台根据接口、方法匹配Mock的接口,同时根据客户端请求的入参进行参数匹配,匹配到设置的参数,通过反序列化将出参返回。可以将整体流程概况为7个技术知识点,然后逐一讲解:
1、Jar包下载
用户在添加JSF接口时,需要指定pom坐标,后台程序根据pom坐标去下载所需要的Jar包,并存储在NFS服务器。实现流程如下:
- 指定pom文件,未指定则去maven私服获取最新上传的jar包;这里支持排除exclusions
- 根据pom坐标,生成pom文件
- 异步下载(@EnableAsync),执行mvn命令:mvn clean dependency:copy-dependencies,这地方会将该接口所依赖的Jar包都会进行下载;
新增接口页面:
2、JVM加载
下载Jar包后,需要通过ClassLoader将Jar包加载到JVM,这里采用URLClassLoader进行加载,URLClassLoader继承于ClassLoader,支持从Jar文件和文件夹中获取Class。首先获取系统的classLoader,遍历Jar包进行动态加载,最后通过loadClass加载接口类。
示例代码:
// 拿到系统的classLoader
URLClassLoader urlClassLoaderForJvm = (URLClassLoader) ClassLoader.getSystemClassLoader();
Class<URLClassLoader> urlClass = URLClassLoader.class;
Method method = urlClass.getDeclaredMethod("addURL", new Class[]{URL.class});
method.setAccessible(true);
for (
File file : files) {
logger.info("动态加载jar包:{}", file.getAbsolutePath());
URL url = new URL("file:" + file);
method.invoke(urlClassLoaderForJvm, new Object[]{ url });
}
try {
cls = urlClassLoaderForJvm.loadClass(interfaceName);
} catch (
NoClassDefFoundError e) {
logger.error("不能正常解析类NoClassDefFoundError, name:" + interfaceName);
} catch (Exception e) {
logger.error(" 不能正常解析类Exception: " + e.toString());
}
ClassLoader结构
3、类实例化
类实例化主要通过动态代理实现,Java动态代理位于java.lang.reflect包下,一般主要涉及到以下两个类:
1. InvocationHandler:该接口中仅定义了一个方法,每一个代理都要实现接口InvocationHandler,通过invoke进行调用方法。
Object invoke(Object proxy, Method method, Object[] args)throwsThrowable
proxy:指代理类 method:被代理的方法 args:被代理的方法参数
2. Proxy:该类即为动态代理类,这个类的作用就是用来动态创建一个代理对象的类,它提供了许多的方法,但是我们用的最多的就是 newProxyInstance 这个方法:
Public static Object newProxyInstance(ClassLoader loader, Class[] interfaces, InvocationHandler handdler)throwsIllegalArgumentException
loader:一个ClassLoader对象,定义了由哪个ClassLoader对象来对生成的代理对象进行加载;
interfaces:一个Interface对象的数组,表示的是将要需要代理的对象提供一组什么接口,如果提供了一组接口给它,那么这个代理对象就宣称实现了该接口(多态),这样能调用这组接口中的方法了;
handler:一个InvocationHandler对象,表示的是当这个动态代理对象在调用方法的时候,会关联到哪一个InvocationHandler对象上返回代理类的一个实例;
动态代理实现步骤:
- 创建一个实现接口InvocationHandler的类,并实现invoke方法
- 创建被代理的类以及接口
- 调用Proxy的静态方法,创建一个代理类Proxy.newProxyInstance(classLoader, interfaces, proxy)
- 通过代理调用方法
代码示例:
/**
* JDK动态代理代理类
*
*/
@Service
public class FacadeProxy implements InvocationHandler {
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Exception {
// mock处理
// mock参数匹配
String response = matchParams(int methodId,Object[] args,Method method);
// mock出参返回
return new Gson().fromJson(response, method.getReturnType());
}
public static <T> T newMapperProxy(Class<T> mapperInterface) throws Throwable{
ClassLoader classLoader = mapperInterface.getClassLoader();
Class<?>[] interfaces = new Class[] { mapperInterface };
FacadeProxy proxy = new FacadeProxy();
T t = (T) Proxy.newProxyInstance(classLoader, interfaces, proxy);
return t;
}
}
4、JSF接口注册/注销
采用JSF API的方式进行接口注册/注销。目前的API方式和Spring方式里的属性都是一一对应的,spring的方式无非就是spring转换为api的方式进行发布。
这里参考JSF API即可: https://cf.jd.com/pages/viewpage.action?pageId=296129902
5、客户端调用
Mock接口注册到JSF注册中心,客户端调用mock别名(Alias)即可。
6、参数匹配
参数匹配这里会依顺序进行以下四种方式匹配,匹配到就直接返回。
1. 优先对象匹配:参数截取->参数转对象->对象比较
2. 字符串完成匹配、部分匹配
3. 正则匹配:Java正则匹配
3. 默认匹配: .*或*
7、参数返回
匹配到数据模版后,如何将匹配到的出参转换成客户端想要的类型呢,这里需要将出参进行反序列化,转换为mock接口对应的出参类型返回。反序列化是本文的一个难点,出参类型格式各样,我们进行了各种尝试,不敢说所有,至少当前接入的接口都已支持。参数类型主要有以下几种:基本类型、字符串、简单对象、复杂对象、泛型;对于基本类型、字符串,转换为对应类型直接返回即可;对于简单对象,通过fastjson转换即可;对于泛型、复杂对象,会尝试fastjson、gson、指定class 3种方式进行转换。在出参类型反序列化这里遇到很多坑,后面可以进行专题分享。
代码示例:
// 1.获取Mock接口出参类型:
Type genericReturnType = method.getGenericReturnType();
// 2.基本类型转换:
object = Integer.valueOf(response);
// 3.优先fastjson转换返回:
object = JSON.parseObject(resultString,genericReturnType);
// 4.fastjson转换对象失败,改为gson转换
object = gson.fromJson(resultString,genericReturnType);
// 5. 返回对象为Object,客户端解析时需要具体的类,这时需要在返回参数指定class,这个通过PojoUtils提供的realize方法转换
object=JSON.parseObject(resultString,Map.class);
object = PojoUtils.realize(object,genericReturnType.getClass());
以上为JSF Mock的实现过程,后续我们会继续分享HTTP Mock的实现过程及平台开发过程中解决的各种技术难点。目前EasyMock正在开源共建中,也欢迎更多有想法的小伙伴一起共建,进行技术交流,打造集团高质量Mock产品。
联系方式:
【平台地址】: http://jagile.jd.com/mock/singleInterface
【咚咚群】:1024782579
【联系人】:张达(bjzhangda) 郭玉锐(bjguoyurui) 陈睿(chenrui142)