所有原创©文章禁止任何形式转载,复用,修改转发,违者追究。
场景描述
之前写过一篇通过AOP在DAO层做分表插件的文章,最近要将其实际用在生产中。因为目前项目的DAO层是从之前的项目框架中整体迁移过来的,有很大的变化,为了避免疏漏,所以要对新mapper的接口做测试验证以及问题修复。首先想到的是部署测试环境,逐个验证请求接口,再一一处理有问题的部分。目前也确实是如此做的,好在之前迁移是使用自己写的程序统一转换的,错误相对来说比较少,主要是一些遗漏的补充,如resultMap等,所以验证和修复效率比较高,没遇到特别棘手的问题。
回到正题,以前有想法是通过反射来构造默认参数和值,来统一的验证单个调用的可用性,恰好最近也有时间,所以做了个初步的实现,这里简单介绍下这种方式,懂行大佬轻拍。
思路描述
一个调用或者称为方法,都有几个要素:返回值、参数、方法名称。比如: var func(param a, param b)。我们这里只关注参数,而参数要关注的,就是参数的类型和具体值,且这里的参数类型和值是严格相关的。举个例子:对象的值,就是个新对象;基本类型如int,long 可以用Integer代替;泛型List还得关注包装的具体类型List,其他如boolean,enum,也需要不同的处理。总结来说,就是根据方法中参数类型生成对应的值,然后方法用这些参数值,来进行调用,最终可以验证方法的可用性。
那为什么要这么做呢? 在工作中,对接口,方法的测试和验证是非常常见的内容之一,除非要验证接口的逻辑功能或者关注返回值,没必要每个接口都自己手动构造有效参数来验证,非常繁琐。在项目中就遇到过对近70个接口做迁移后的使用验证,这非常考验耐性。所以,如果能自动生成默认值,至少对这个接口方法来说参数是有效的,写一个公共工具,就可以批量进行不同接口的验证。目前这个实现只是一个初步的构建,是对偏自动化测试的尝试,可以基于此做延伸,实现更复杂的功能。
代码和处理过程解析
//由于项目是springboot引入测试@RunWith(SpringRunner.class)@SpringBootTestpublic class MapperTest { //这里是对应的mapper类,因为有很多,所以从idea复制引用出来 //可以直接在包名上右键批量复制引用(References) String mappers = "com.xxx.xxx.xxx.mapper.CashXxxMapper"; List names = Lists.newArrayList(mappers.split("")); @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 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); } }}
补充内容
因为这个实现是和数据库mapper操作有关的,为了方便验证结果,在本地运行中,对执行SQL加了日志,可以很方便的追踪结果。由于项目使用的druid连接数据源,之前在jdbc连接串后面加&profileSQL=true参数失败,所以在application.yml配置文件添加了
mybatis:configuration: log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
也能起到记录执行SQL的作用。结果如下:
SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@7da40bf4] was not registered for synchronization because synchronization is not activeJDBC 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: 0Closing non transactional SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@7da40bf4]
总结
如上,通过这个实现,对Java反射有了更深的了解,功能确实非常强大,而且连private static final 修饰的字段也可以修改。总体来说,实现了最开始要求的目标,不过中间生成默认值的部分还是不够优雅,可以看到有些情况是类似递归的,而且if条件也可以优化下,提高可读性。再者后续最好能找些优秀的框架代码多看看,反射相关在通用框架里使用还是非常多的,当然也不只这一点,也可以用来学习更好的实现方式和代码规范。