反射实现批量验证DAO层接口

场景描述

之前写过一篇通过AOP在DAO层做分表插件的文章,最近要将其实际用在生产中。因为目前项目的DAO层是从之前的项目框架中整体迁移过来的,有很大的变化,为了避免疏漏,所以要对新mapper的接口做测试验证以及问题修复。首先想到的是部署测试环境,逐个验证请求接口,再一一处理有问题的部分。目前也确实是如此做的,好在之前迁移是使用自己写的程序统一转换的,错误相对来说比较少,主要是一些遗漏的补充,如resultMap等,所以验证和修复效率比较高,没遇到特别棘手的问题。

回到正题,以前有想法是通过反射来构造默认参数和值,来统一的验证单个调用的可用性,恰好最近也有时间,所以做了个初步的实现,这里简单介绍下这种方式,懂行大佬轻拍。

思路描述

一个调用或者称为方法,都有几个要素:返回值、参数、方法名称。比如: var func(param a, param b)。我们这里只关注参数,而参数要关注的,就是参数的类型和具体值,且这里的参数类型和值是严格相关的。举个例子:对象的值,就是个新对象;基本类型如int,long 可以用Integer代替;泛型List还得关注包装的具体类型List<string>,其他如boolean,enum,也需要不同的处理。总结来说,就是根据方法中参数类型生成对应的值,然后方法用这些参数值,来进行调用,最终可以验证方法的可用性。/<string>

那为什么要这么做呢? 在工作中,对接口,方法的测试和验证是非常常见的内容之一,除非要验证接口的逻辑功能或者关注返回值,没必要每个接口都自己手动构造有效参数来验证,非常繁琐。在项目中就遇到过对近70个接口做迁移后的使用验证,这非常考验耐性。所以,如果能自动生成默认值,至少对这个接口方法来说参数是有效的,写一个公共工具,就可以批量进行不同接口的验证。目前这个实现只是一个初步的构建,是对偏自动化测试的尝试,可以基于此做延伸,实现更复杂的功能。


反射实现批量验证DAO层接口


代码和处理过程解析

<code>
//由于项目是springboot引入测试
@RunWith(SpringRunner.class)
@SpringBootTest
public class MapperTest {

//这里是对应的mapper类,因为有很多,所以从idea复制引用出来
//可以直接在包名上右键批量复制引用(References)
String mappers = "com.xxx.xxx.xxx.mapper.CashXxxMapper\\n";
List<string> names = Lists.newArrayList(mappers.split("\\n"));

@Resource
private ApplicationContext context;//用来获取bean,即DAO层的各种mapper

@Test
public void mapperTest() {
for (String s : names) {
try {
Class> clazz = Class.forName(s);
Method[] methods = clazz.getMethods();
Object obj = context.getBean(clazz);
for (Method method : methods) {
//用来存储参数的值
List<object> values = Lists.newArrayList();
//拿到方法
for (Parameter parameter : method.getParameters()) {
String type = parameter.getType().getName();
Object o;
//基本类型和数字
if (type.equals("long") || type.equals("int") || type.contains("lang.Long") || type.contains("lang.Integer")) {
o = new Integer(2);
//列表泛型的处理
} else if (type.contains("util.List")) {
String rawType = parameter.getParameterizedType().getTypeName().replace("java.util.List", "");

Object raw;
//封装类型处理
if (rawType.contains("lang.Integer") || rawType.contains("lang.Long") || rawType.equals("long") || rawType.equals("int")) {
raw = new Integer(3);
} else {
raw = Class.forName(rawType).getDeclaredConstructor().newInstance();
}
ArrayList list = new ArrayList();
list.add(raw);
o = list;
} else if (type.contains("java.util.Date")) {
//项目里边用到
o = new Date();
} else {
o = Class.forName(type).getDeclaredConstructor().newInstance();
//过滤其他元类型
if (!type.contains("java.lang")) {
try {
//对象参数
fieldsFill(o);
} catch (Exception e) {
System.err.println(type);
}
}
}
values.add(o);
}
try {
method.invoke(obj, values.toArray());
} catch (Exception e) {
System.out.println(e.getMessage());
}
}
} catch (Exception e) {
e.printStackTrace();
}
}
// context.getBean()
}

//填充对象参数,可以看到和上面比较重复
private void fieldsFill(Object object) throws ClassNotFoundException, NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {
Class clazz = object.getClass();
Field[] fields = clazz.getDeclaredFields();
for (Field f : fields) {
int modifier = f.getModifiers();

//exclude static final
//前缀修饰符,很好理解是一个数字
if (Modifier.isFinal(modifier) && Modifier.isStatic(modifier)) {
continue;
}
String type = f.getType().getName();
//业务字段特殊需要,基于用thrift生成的对象
if (type.contains("_") || type.contains("metaDataMap") || type.contains("Enum")) {
continue;
}
f.setAccessible(true);
Object fo;
if (type.equals("long") || type.equals("int") || type.contains("lang.Long") || type.contains("lang.Integer")) {
fo = new Integer(2);
} else if (type.contains("List")) {
fo = new ArrayList<>();
} else if (type.equals("boolean")) {
continue;
} else {
fo = Class.forName(type).getDeclaredConstructor().newInstance();
}
f.set(object, fo);
}
}
}

/<object>/<string>/<code>

补充内容

因为这个实现是和数据库mapper操作有关的,为了方便验证结果,在本地运行中,对执行SQL加了日志,可以很方便的追踪结果。由于项目使用的druid连接数据源,之前在jdbc连接串后面加&profileSQL=true参数失败,所以在application.yml配置文件添加了

<code>mybatis:
configuration:
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
/<code>

也能起到记录执行SQL的作用。结果如下:

<code>SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@7da40bf4] was not registered for synchronization because synchronization is not active
JDBC Connection [com.mysql.cj.jdbc.ConnectionImpl@3f36c191] will not be managed by Spring
==> Preparing: select `cash`, `event`, `create_time` from `xxx_record_2` WHERE `biz_type` = ? AND `user_id` = ? ORDER BY `create_time` DESC LIMIT ?, ?
==> Parameters: 2(Integer), 2(Long), 2(Integer), 2(Integer)
<== Total: 0
Closing non transactional SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@7da40bf4]
/<code>

总结

如上,通过这个实现,对Java反射有了更深的了解,功能确实非常强大,而且连private static final 修饰的字段也可以修改。总体来说,实现了最开始要求的目标,不过中间生成默认值的部分还是不够优雅,可以看到有些情况是类似递归的,而且if条件也可以优化下,提高可读性。再者后续最好能找些优秀的框架代码多看看,反射相关在通用框架里使用还是非常多的,当然也不只这一点,也可以用来学习更好的实现方式和代码规范。


分享到:


相關文章: