模块介绍
dmc-plugin-java
动态编译字节码
关于动态编译字节码技术参考:
https://blog.csdn.net/huxiang19851114/article/details/127881616
优化如下:
-
动态文本类改为界面配置及数据库保存
数据库表结构:
DROP TABLE IF EXISTS `compiler_info`; CREATE TABLE `compiler_info` ( `id` int(11) NOT NULL AUTO_INCREMENT, `user_id` varchar(100) NOT NULL COMMENT '最后更新用户id', `class_name` varchar(100) NOT NULL COMMENT '类全路径名,如com.paratera.console.biz.model.User(唯一)', `info` text NOT NULL COMMENT '动态类内容', `description` varchar(200) COMMENT '动态类说明', `sign` varchar(100) COMMENT '签名标记', `old_sign` varchar(100) COMMENT '上一次签名标记', `create_at` datetime NULL , `updated_at` datetime NULL , PRIMARY KEY (`id`) USING BTREE, UNIQUE INDEX `class_name`(`class_name`) USING BTREE ) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci ROW_FORMAT = Compact COMMENT = '字节码动态编译配置表';
-
类加载对象引入缓存机制
通过compiler_info.sign签名标记(其实就是一个UUID)来判断,避免频繁调用生成大量的Object执行对象。
核心代码如下:
/**
* 类加载对象缓存,key:compiler_info.sign
*/
private static Map<String,Class> classMap = new HashMap<>();
/**
* Class执行对象缓存 key:compiler_info.sign
*/
private static Map<String,Object> objMap = new HashMap<>();
------------------------------------------------------------------------------
/**
* 目标方法反射执行
*
* @param className 全路径类名,与字节码文本配置的保持一致
* @param methodName 执行方法名,与需要调度的字节码文本类方法名保持一致
* @param args 方法参数,可以多个,根据字节码文本类具体情况来传
* @return
*
* @throws Exception
*/
public Object invoke(String className, String methodName, Object... args) throws Exception {
//获取字符串代码内容
Map compilerInfo = getCompilerInfo(className);
String sign = (String) compilerInfo.get("sign");
Class<?> clazz = null;
Object obj = null;
if(classMap.containsKey(sign)){
clazz = classMap.get(sign);
obj = objMap.get(sign);
}else {
try {
//字节码编译处理,得到Class对象和执行对象
clazz = this.compileToClass(className, (String) compilerInfo.get("info"));
obj = clazz.getDeclaredConstructor().newInstance();
classMap.put(sign,clazz);
objMap.put(sign,obj);
String oldSign = (String) compilerInfo.get("oldSign");
classMap.remove(oldSign);
objMap.remove(oldSign);
} catch (Exception e) {
throw new Exception("反射获取对象实例异常:" + e.getMessage());
}
}
//反射调用目标方法
Method[] test = clazz.getDeclaredMethods();
List<Method> methods = Arrays.stream(test).filter(app ->
StringUtils.equals(app.getName(), methodName)).toList();
try {
return methods.get(0).invoke(obj, args);
} catch (Exception e) {
throw new Exception("没有该动态编译运行方法或则参数不匹配");
}
}
Mock模拟测试数据
基于使用Spring MVC拦截器的方式弊端:
1、request body只能getReader()、getInputStream()一次,不重写preHandle后执行目标方法会报错
2、拦截器只能限定在项目内部使用,做成jar包插件无法实现拦截(已实验证明)
所以改成通过Filter过滤器来处理,Filter处理当然也有他的弊端,那就是任何请求都会进入doFilter方法,如果不做好排除判断,性能是比较低的
参考之前关于Mock模拟测试数据文章:
https://blog.csdn.net/huxiang19851114/article/details/127771679
我们做了如下优化:
-
模拟数据改为界面配置及数据库保存
主要增加了服务名称,用来进行服务隔离(避免大家的测试地址相同)
数据库表结构:
DROP TABLE IF EXISTS `mock_info`;
CREATE TABLE `mock_info` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`on_off` varchar(6) NOT NULL COMMENT '开关,ON 开启,OFF 关闭',
`server` varchar(50) NOT NULL COMMENT '服务名称,与引用服务配置的dmc.space相同',
`method` varchar(20) NOT NULL COMMENT '方法类型 请使用全大写,如GET,POST,PUT等',
`uri` varchar(255) NOT NULL COMMENT '访问路径',
`profile` varchar(20) NOT NULL COMMENT 'Mock数据环境',
`user_id` varchar(100) NOT NULL COMMENT '配置用户id',
`create_at` datetime NULL ,
`updated_at` datetime NULL ,
PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci ROW_FORMAT = Compact COMMENT = 'mock模拟数据主表';
DROP TABLE IF EXISTS `mock_detail`;
CREATE TABLE `mock_detail` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`info_id` int(11) NOT NULL COMMENT 'mock_info表id',
`header` varchar(500) COMMENT 'header传参,JSON结构字符串',
`param` varchar(500) COMMENT 'param传参,JSON结构字符串',
`body` varchar(500) COMMENT 'body传参,JSON结构字符串',
`value` text COMMENT 'response返回,字符串,不限格式',
`create_at` datetime NULL ,
`updated_at` datetime NULL ,
PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci ROW_FORMAT = Compact COMMENT = 'mock模拟数据入参返回明细表';
-
拦截器实现
实现逻辑大同小异,主要增加了dmc.space来作为开关,确定是否开启Mock模拟测试
import com.fasterxml.jackson.databind.ObjectMapper; import com.paratera.dmc.plugin.facade.DmcClient; import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang3.StringUtils; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; import org.springframework.boot.autoconfigure.condition.ConditionalOnExpression; import org.springframework.context.annotation.Configuration; import org.springframework.core.Ordered; import org.springframework.util.CollectionUtils; import org.springframework.web.filter.GenericFilterBean; import javax.servlet.FilterChain; import javax.servlet.ServletException; import javax.servlet.ServletRequest; import javax.servlet.ServletResponse; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.BufferedReader; import java.io.IOException; import java.util.Enumeration; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicReference; /** * Mock模拟数据服务 过滤器 */ @ConditionalOnExpression("#{environment.getProperty('dmc.space') != null}") @Configuration @Slf4j public class MockFilter extends GenericFilterBean implements Ordered { @Autowired private DmcClient dmcClient; @Value("${spring.profiles.active}") private String profile; @Value("${dmc.space}") private String server; @Override public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException { HttpServletRequest request = (HttpServletRequest) servletRequest; HttpServletResponse response = (HttpServletResponse) servletResponse; String methodType = request.getMethod(); String uri = request.getRequestURI(); //如果发现是css、js、图片文件以及mock请求(避免dmcClient 请求进入死循环),直接放行 if (uri.contains(".css") || uri.contains(".js") || uri.contains(".png") || uri.contains(".jpg") || uri.indexOf("/s-api") != -1) { filterChain.doFilter(request, response); return; } //兼容dmc服务本身请求不带server参数 server = StringUtils.isEmpty(server) ? "dmc" : server; ObjectMapper mapper = new ObjectMapper(); //获取模拟数据配置信息 Map mockInfo = dmcClient.getMockInfo(server, methodType, uri, profile); //如果没有录入该数据,则放过 if (mockInfo == null) { filterChain.doFilter(request, response); return; } //判断开关是否开启,未开启,放过 if (!StringUtils.equals("ON", (CharSequence) mockInfo.get("onOff"))) { filterChain.doFilter(request, response); return; } List<Map> mockDetails = (List<Map>) mockInfo.get("info"); //如果没配置出入参详情,则直接返回空 if (CollectionUtils.isEmpty(mockDetails)) { response.getWriter().write(""); return; } //获取请求的header参数 Enumeration<String> headerNames = request.getHeaderNames(); Map<String, String> headers = new HashMap(); while (headerNames.hasMoreElements()) { String nextElement = headerNames.nextElement(); headers.put(nextElement, request.getHeader(nextElement)); } //获取请求的param参数 Map<String, String> params = new HashMap(); Enumeration<String> parameterNames = request.getParameterNames(); while (parameterNames.hasMoreElements()) { String nextElement = parameterNames.nextElement(); params.put(nextElement, request.getParameter(nextElement)); } //获取请求的body参数 Map<String, String> bodys = new HashMap(); BufferedReader br; try { //这个地方重写了getInputStream和getReader方法,否则会报错 br = request.getReader(); String str; String wholeParams = ""; while ((str = br.readLine()) != null) { wholeParams += str; } if (StringUtils.isNotBlank(wholeParams)) { bodys = mapper.readValue(wholeParams, Map.class); } } catch (IOException e) { log.error("IO流异常-1:" + e.getMessage()); } Map<String, String> finalBodys = new HashMap<>(); for (String s : bodys.keySet()) { finalBodys.put(s, String.valueOf(bodys.get(s))); } //开始匹配传参 AtomicReference<String> value = new AtomicReference<>(); AtomicBoolean flag = new AtomicBoolean(true); //遍历每条Mock配置信息 mockDetails.stream().forEach(app -> { //比对请求参数和配置参数,如果请求参数包含配置参数,输出value AtomicBoolean headerFlag = new AtomicBoolean(true); AtomicBoolean paramFlag = new AtomicBoolean(true); AtomicBoolean bodyFlag = new AtomicBoolean(true); Map header = (Map) app.get("header"); Map param = (Map) app.get("param"); Map body = (Map) app.get("body"); //判断header if (header != null) { header.keySet().stream().forEach(app3 -> { //传参header只要有一项不包含配置项中的header,则退出 if (!headers.containsKey(app3) || !headers.containsValue(String.valueOf(header.get(app3)))) { headerFlag.set(false); return; } }); } //判断param if (param != null) { param.keySet().stream().forEach(app3 -> { if (!params.containsKey(app3) || !params.containsValue(String.valueOf(param.get(app3)))) { paramFlag.set(false); return; } }); } //判断body if (body != null) { body.keySet().stream().forEach(app3 -> { if (!finalBodys.containsKey(app3) || !finalBodys.containsValue(String.valueOf(body.get(app3)))) { bodyFlag.set(false); return; } }); } //条件都满足,获取配置项value,设置flag为false,拦截器生效,不再访问目标接口,同时退出循环 if (headerFlag.get() && paramFlag.get() && bodyFlag.get()) { flag.set(false); value.set(StringUtils.isEmpty((String) app.get("value")) ? "" : (String) app.get("value")); return; } }); //接口输出配置项value if (!flag.get()) { try { response.getWriter().write(value.get()); return; } catch (IOException e) { log.error("IO流异常-2:" + e.getMessage()); } } filterChain.doFilter(request, response); } @Override public int getOrder() { return Ordered.LOWEST_PRECEDENCE; } }
存储数据获取
因为我们的数据中心在dmc-core,所以使用openfeign,增加一个对外访问dmc-core数据存储的接口
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import java.util.Map;
/**
* dmc远程调用
*
* @author huxiang
*/
@FeignClient(name = "dmcClient", url = "${dmc.mcs.url.dmc-core}")
public interface DmcClient {
@GetMapping("/mock/s-api/info")
public Map getMockInfo(@RequestParam("server") String server,
@RequestParam("method") String method,
@RequestParam("uri") String uri,
@RequestParam("profile") String profile);
@GetMapping("/compiler/s-api/info")
public Map getCompilerInfo(@RequestParam("className") String className);
}
dmc-core
插件数据的维护中心,主要就是围绕上面的表进行增删改查,方便用户可视化操作,没什么可说的
dmc-dict
维护数据平台一些公用并且固定的数据项,关于数据字典的实现可以参考:
https://blog.csdn.net/huxiang19851114/article/details/127556003
使用说明
插件引入配置
1、引入jar包依赖
<dependency>
<groupId>com.paratera.dmc</groupId>
<artifactId>dmc-plugin-java</artifactId>
<version>0.0.1-SNAPSHOT</version>
</dependency>
2、配置扫描路径
@SpringBootApplication(scanBasePackages = {"com.paratera.dmc"})
@EnableFeignClients(basePackages = {"com.paratera.dmc"})
3、配置服务空间
根据自己服务名称配置,注意与界面数据维护中server保持一致(取数据字典值),如:
dmc.space=Console
4、配置dmc-core服务地址
dmc.mcs.url.dmc-core=http://localhost:25000
使用范例
1、Mock模拟数据
实现无需后台提供接口,前端通过配置化实现接口模拟数据,无阻塞开发。
- 配置模拟数据主题,包括服务名,请求方式,接口路径,生效环境
- 配置request和response,可以配置0-N组,request传参为JSON格式字符串,response为任意字符串
-
测试,如果请求的主题和入参都匹配,返回结果,如:
@Test public void wheAgentByUserId() throws Exception { String url = "/sys/wheAgentByUserId"; MvcResult mvcResult = mockMvc .perform(MockMvcRequestBuilders.get(url) .contentType(MediaType.APPLICATION_JSON) .header("userid", "SELF-rQYRjc9mfQPP7F7apE8gGx6xyAAZYw6GNCTjS6wKPH8") ).andReturn(); int status = mvcResult.getResponse().getStatus(); System.out.println("请求状态码:" + status); String result = mvcResult.getResponse().getContentAsString(); System.out.println("接口返回结果:" + result); }
如果匹配不到配置项,则继续进入项目访问目标方法,目标方法不存在,则报404
2、字节码动态编译
- 配置动态类文本信息
- 调用动态编译的方法执行解析
@Autowired
private DynamicCompiler dynamicCompiler;
@Test
public void getQuota1() throws Exception {
Object obj = dynamicCompiler.invoke("com.paratera.dmc.core.service.clustercoop.CLusterService","getQuota", "sc001");
System.out.println(obj);
}