在日常的测试过程中,测试人员(或者开发人员)总是需要构造各种各样的测试数据来满足自己的需求。数据工厂的作用就是提供统一的UI,让测试人员或者开发人员能够快速、简单地生成测试数据,提高测试效率。
所谓快速、简单,是指对于其他的(不是这条业务线的)测试、开发人员来说,都能通过简单的输入,生成自己需要的测试数据,而不用去了解接口或者数据库的设计,通过调用接口或者直接写数据库来构造数据。
数据工厂只是一个框架,其构造测试数据的业务逻辑(模块)需要各个业务线的测试人员开发,所以数据工厂的设计遵循以下原则:
类装载器是Java语言的一个创新,也是Java语言流行的重要原因之一,它使得Java类可以被动态地加载到Java虚拟机中并执行。关于类装载器,本文不打算做详细介绍,感兴趣的读者请参阅本文的参考文档:深入探讨Java类装载器。
Java的容器隔离以前有OSGI,但是OSGI显得过于沉重,已经基本被Java社区抛弃。其实通过Java的类装载器就能实现轻量级的容器隔离,因为Java中判定两个类是否相同,看的是类全名和其对应的类装载器,两者同时相同才表示相等;也就是说,通过不同的类装载器装载相同的类,在Java虚拟机中,这两个类其实是不相等的,是隔离的,是不能相互访问的。
关于Java中容器隔离的概念和实现,请参阅本文的参考文档:Java中隔离容器的实现。
系统的整体框架图如下: 系统的主要流程如下:
注册接口定义了一个注册用的方法,主要的作用是注册数据工厂服务,定义Web前端表单的结构:
public interface Register {
Module register(); // 注册数据工厂服务, 可以有多个类实现这个接口
}
register方法返回的Module类定义如下:
public class Module {
// 前端和http服务可以二选一,或者都注册
private Class<? extends Handler> handlerClass; // 处理业务请求的类,实现Handler接口,必选
/***** 注册前端需要 *****/
private String groupName; // 模块隶属的业务group,前端体现为一级菜单,注册前端必选,不能超过16个中文字符
private String moduleName; // 展示在前端的模块名称,前端体现为二级菜单,注册前端必选,不能超过16个中文字符
private List<Widget> widgets; // 前端需要展示的控件,注册前端必选
/**********************/
/***** 注册http服务需要 *****/
private String actionSpace; // 提供web service的namespace, 注册http服务必选
private String actionName; // 提供web service的actionname, 要求在同一namespace下唯一, 注册http服务必选
/**********************/
private String helpMsg; // 帮助信息(可以介绍该模块的功能,如何使用等),可选
private String author; // 模块的作者, 请使用中文名,可选
// getter和setter方法
}
这里需要说明的是,数据工厂的模块不仅可以注册前端,还可以注册Http服务。如果要注册Http服务的话,只需提供actionSpace和actionName,其他的代码不需要做任何改变。用户可以通过 http://qa.qima-inc.com/dmm/${actionSpace}/${actionName} 即可访问该Http服务(数据工厂解析用户提供的URL,通过actionSpace和actionName在数据库里找到对应的Handler类信息)。当然,这里用户提供的数据(不论是通过Http Body或者URL的参数)需要遵循约定的格式。
Module类里使用到的Widget类定义如下:
public class Widget {
private String label; // 前端控件标签, 展示给用户,必选
private String name; // 控件名称, 也就是最终传给业务处理类Handler的 json串中的key,必选
private WidgetType widgetType; // 控件类型,必选
private List<SelectOption> options; // 如果控件是select, 显示的options,可选
private String placeHolder; // input或者textarea的placeholder, 可选
private int maxLength = 0; // input或者textarea最大可输入的字符数, 0表示不限制
private boolean required = false; // input或者textarea是否是必填项
private String defaultValue; // 控件的默认值,可选
private String pattern; // 校验控件输入值的正则表达式,可选
private String title; // 与pattern结合使用,不符合格式要求时提示给用户的信息,可选
private String text; // 控件类型是PARAGRAPH时,控件显示的文本信息
// getter和setter方法
}
业务逻辑处理接口定义了一个处理业务逻辑的方法handle:
public interface Handler {
Result handle(String json); // 调用业务线的接口,或者直接操作数据库,处理传入的json字符串
}
该方法接收用户的输入(数据工厂包装好的JSON格式的字符串),通过调用业务线的接口,或者直接操作数据库,处理业务逻辑,并返回处理的结果。
返回结果Result类定义如下:
public class Result {
private boolean success = false; // 接口调用是否成功
private String result; // 如果成功,返回的结果,可以是JSON字符串;如果不成功,返回的错误信息
// getter和setter方法
}
本示例展示如何实现前端为下面图片所显示的菜单和表单的数据工厂模块: 实现注册(Register)接口的代码主体如下:
Module module = new Module().setHandlerClass(DmmDemoHandler.class);
/***** 定义前端显示的控件 *****/
module.setGroupName("Demo") // 前端一级菜单
.setModuleName("演示模块") // 前端二级菜单
.setHelpMsg("这是一个数据工厂的演示模块\n这是帮助的第二行") // 可选
.setAuthor("方金和"); // 可选
// 输入框
module.addWidget(new Widget()
.setLabel("姓名:") // 控件的label
.setName("name") // 控件的name
.setWidgetType(WidgetType.INPUT) // 控件的类型,输入框
.setPlaceHolder("请输入您的姓名") // 控件的placeholder,可选
.setMaxLength(10) // 控件接受的最大输入长度,可选
.setRequired(true) // 控件是否是必填项,可选
.setPattern("^[a-zA-Z]+$") // 校验控件的输入的正则表达式,可选
.setTitle("请使用英文名")); // 与pattern结合使用,输入校验不通过时提示给用户的信息,可选
// 选择框
Widget select = new Widget()
.setLabel("性别:")
.setName("gender")
.setWidgetType(WidgetType.SELECT);
// 选择框的option
SelectOption option1 = new SelectOption()
.setDisplayName("女") // 选择框里显示的文本
.setValue("1"); // 传递给后端的值
SelectOption option2 = new SelectOption()
.setDisplayName("男")
.setValue("2");
select.addOption(option1).addOption(option2);
module.addWidget(select);
// textarea
module.addWidget(new Widget()
.setLabel("个性说明:")
.setName("description")
.setWidgetType(WidgetType.TEXTAREA));
// 定义上述控件后,前端会显示一个输入框、一个选择框和一个textarea。
// 假设用户前端输入:姓名输入"xxx",性别选择"女",个性说明输入"这里是个性说明"
// 则用户提交表单后,传递给业务逻辑处理类DmmDemoHandler的json字符串为:
// {"name": "xxx", "gender": "1", "description": "这里是个性说明"}
如果要注册Http服务,则代码如下:
/***** 注册 Http 服务 *****/
module.setActionSpace("user").setActionName("profile");
// 注册上述Http服务后, 用户调用http的url为(支持POST或者GET方法):
// http://qa.qima-inc.com/dmm/user/profile
// 如果使用POST方法,http body请使用json格式,http body将传递给业务逻辑处理类DmmDemoHandler
// 如果使用GET方法,url后附加的http参数将被包装成json格式字符串传递给业务逻辑处理类DmmDemoHandler
/*************************/
业务逻辑处理类DmmDemoHandler的代码主体如下:
public Result handle(String json) { // 传入的用户输入为json格式的字符串
JSONObject input = JSON.parseObject(json);
String name = input.getString("name");
if (name == null) { // success设置为false,则前端会提示结果为失败
return new Result().setSuccess(false).setResult("必须输入姓名");
}
Map<String, String> result = new HashMap<>();
result.put("姓名", name);
result.put("性别", input.getString("gender").equals("1") ? "美眉" : "帅哥");
result.put("个性说明", input.getString("description"));
// result一般使用json格式的字符串
return new Result().setSuccess(true).setResult(JSON.toJSONString(result));
}
使用上述业务逻辑处理类,用户提交表单后,显示的结果为:
数据工厂的实现其实比较简单,主要集中在扫描Jar包和调用业务逻辑处理方法上(本文不展示前端的实现细节)。
扫描Jar包,调用注册类方法register的代码如下(Groovy语言):
List<Module> modules = []
def filePath = new File(jarFilePath) // jarFilePath:jar包的文件路径
URLClassLoader loader = new URLClassLoader([new URL('file:' + filePath.absolutePath)] as URL[]) // 使用不同的类装载器装载不用的jar包
Class registerClazz = loader.loadClass('com.youzan.test.dmm.register.Register') // 注册接口的class
Enumeration<JarEntry> files = new JarFile(filePath).entries()
while (files.hasMoreElements()) {
def className = files.nextElement().name
if (className.endsWith('.class')) {
className = className.replaceAll('/', '.').replaceAll('.class', '')
} else {
continue
}
Class clazz = loader.loadClass(className)
if (clazz.isInterface()) {
continue
}
if (registerClazz.isAssignableFrom(clazz)) { // 如果类实现了Register接口
def module = clazz.newInstance().register() // 调用register方法,获取module信息
if (module) {
modules.add(module)
}
}
}
// 同时保存该jar包的装载器,供后续的方法路由使用
调用业务逻辑处理方法的代码如下(Groovy语言):
def classLoader = JarParseService.getClassLoader(jarFilePath) // 使用扫描jar包时已经创建的类装载器
Thread.currentThread().setContextClassLoader(classLoader) // 将当前线程的类装载器设置为上面的类装载器
def clazz = classLoader.loadClass(handlerClassName) // 装载业务逻辑处理类
try {
def result = clazz.newInstance().handle(json) // 调用handle方法,传入用户的输入(JSON格式的字符串)
return [success: result.success, result: result.result]
} catch (Throwable e) {
// 如果有异常,返回异常堆栈信息
return [success: false, result: CommonUtils.getStackTrace(e)]
}
欢迎关注我们的公众号
Original url: Access
Created at: 2019-09-26 17:31:01
Category: default
Tags: none
未标明原创文章均为采集,版权归作者所有,转载无需和我联系,请注明原出处,南摩阿彌陀佛,知识,不只知道,要得到
最新评论