前言
最近的项目需要大量的调试工作,公司基建不完善,一些权限还不太好申请,只好自己动手写了一系列工具,包括:
1 新增调试接口:可执行任意javaBean的任意方法
2 新增调试接口:可执行任意只读sql
3 新增调试接口:可自动采集调试日志, 并给出初步的分析建议
4 新增数据比对工具:可对任意数据源(db/http/rpc等)的数据以指定schema进行比对和结果分析。
这里和大家分享下第一个小工具:可执行任意javaBean的任意方法。
目前该工具还有些小缺陷TODO,比如不支持复杂对象的嵌套解析、不支持普通类的方法等,不过已经能满足我们项目的绝大部分需求,后面有时间在优化吧。
效果展示
先上效果图:
我们发起了一个post请求,body里面指定了一个自定义语言的脚本,response会返回服务执行这个方法的结果。
对上面的自定义脚本的解释:
remoteUserManager#listByClassCode#I|1,S|ssscode,I|1
remoteUserManager 是我的Spring项目里面的一个bean, 对应的类为RemoteUserManager;
listByClassCode 是RemoteUserManager里面的一个方法。
函数原型如下:
@Component
public class RemoteUserManager {
public List<UserDTO> listByClassCode(Integer schoolId, String classCode, Integer type) {
... }
}
I|1,S|ssscode,I|1 是我们要传给RemoteUserManager里listByClassCode()方法的参数列表,I表示Integer, S表示String, 整个参数列表对应的java值为: [Integer(1), String(“ssscode”), Integer(1)]。
整个脚本相当于执行如下调用:
remoteUserManager.listByClassCode(1, "ssscode", 1);
然后将调用的结果通过httpResponse展现出来。
工具源码
controller部分:
@RestController
public class ToolController {
@Resource
private ScriptUtil scriptUtil;
@PostMapping(value = {
"/tool/exe/script"})
public Result executeScript(@RequestBody ScriptToolParam script) {
Object r = scriptUtil.executeBeanMethodScript(script.getScript());
return Result.getSuccessResult(r);
}
}
入参:
@Data
public class ScriptToolParam {
private String script;
//private String type;
}
核心逻辑:
/**
* Created by mrpp on 2020/7/25.
* DSL执行任意bean的public方法
* todo: 支持private方法
*/
@Component
public class ScriptUtil implements ApplicationContextAware {
private static ApplicationContext context = null;
/** 配置中心指定的ip白名单,只有开发环境或白名单ip的机器才能执行 **/
@Value("#{'${whiteList.debugTool.ip}'.split(',')}")
private List<String> ipWhiteList;
/** 当前环境,只有开发环境或白名单ip的机器才能执行 **/
@Value("${spring.profiles.active}")
private String env;
@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
context = applicationContext;
}
public static Object getBean(String name) {
return context.getBean(name);
}
private static List<Class> NUMBER_CLASSES = Arrays.asList(Integer.class, Long.class, Double.class, Float.class);
private static final Map<String, Class> paramTypeMap = new HashMap<>();
static {
paramTypeMap.put("B", Boolean.class);
paramTypeMap.put("S", String.class);
paramTypeMap.put("I", Integer.class);
paramTypeMap.put("L", Long.class);
paramTypeMap.put("D", Double.class);
paramTypeMap.put("F", Float.class);
paramTypeMap.put("L", List.class);
paramTypeMap.put("M", Map.class);
paramTypeMap.put("O", Object.class);
}
/**
* @param script 自定义语法的java脚本,可执行任意bean的方法
* 分三部分: beanName#methodName#paramList
* 举例: remoteUserManager#listByClassCode#I|1,S|ssscode,I|1
* 其中paramList比较复杂, I|1, S|[ssscode], I|1 表示(Integer 1, String ssscode, Integer 1)
* String的表示方法举例: S|sss 或者 S|[sss] 或者 S|[ss,ss] 当字符串中有逗号时必须用[]包裹起来,否则可以省略包裹
* List的表示方法举例: L<S>|[[sss],fefe,[wefwer]]
* Map的表示方法举例: M<S,I>|[[sfefe,12],[[hhllg],88]]
* 对象的表示方法举例: O<cn.hpp.domain.param.SqlParam>|[[sql,[select * from ods_class limit 3]],[type, mysql]]
* 说明: cn.hpp.domain.param.SqlParam这个POJO类有两个字段: String sql 和 String type;
* list<对象>的表示方法: L<O<cn.hpp.domain.param.SqlParam>>|[[[sql,[select * from ods_class limit 1]],[type, mysql]], [[sql,[select * from ods_class limit 3]],[type, orcal]]]
* 注意:参数暂时不支持复杂对象
* todo: 简化对象表示法,尽量可以不使用全类名
* @return 返回结果 或者 错误信息
*/
public Object executeBeanMethodScript(String script) {
// 只有开发环境或白名单ip的机器才能真正执行脚本
Pair<Boolean, String> checkResult = DebugTool.debugModeAllowed(env, ipWhiteList);
if (!checkResult.getKey()) {
return "err: " + checkResult.getValue();
}
if (StringUtils.isEmpty(script)) {
return "err: blank script";
}
String[] parts = script.split("#");
if (parts.length < 2) {
return "err: invalid script";
}
String beanName = parts[0];
String methodName = parts[1];
List<Pair> paramTypeValuePairs = new ArrayList<>();
if (parts.length > 2) {
String paramString = script.substring(beanName.length(