关于动态编译字节码技术参考:
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("没有该动态编译运行方法或则参数不匹配");
}
}
基于使用Spring MVC拦截器的方式弊端:
1、request body只能getReader()、getInputStream()一次,不重写preHandle后执行目标方法会报错
2、拦截器只能限定在项目内部使用,做成jar包插件无法实现拦截(已实验证明)
3、拦截器只能拦截项目中存在的接口,无法做到真正的前后端开发解耦
所以改成通过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);
}
插件数据的维护中心,主要就是围绕上面的表进行增删改查,方便用户可视化操作,没什么可说的
维护数据平台一些公用并且固定的数据项,关于数据字典的实现可以参考:
https://blog.csdn.net/huxiang19851114/article/details/127556003
<dependency>
<groupId>com.paratera.dmcgroupId>
<artifactId>dmc-plugin-javaartifactId>
<version>0.0.1-SNAPSHOTversion>
dependency>
@SpringBootApplication(scanBasePackages = {"com.paratera.dmc"})
@EnableFeignClients(basePackages = {"com.paratera.dmc"})
根据自己服务名称配置,注意与界面数据维护中server保持一致(取数据字典值),如:
dmc.space=Console
dmc.mcs.url.dmc-core=http://localhost:25000
实现无需后台提供接口,前端通过配置化实现接口模拟数据,无阻塞开发。
测试,如果请求的主题和入参都匹配,返回结果,如:
@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
@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);
}