一. 背景
今天无心写代码,整理下文章看。应用对于第三方的依赖较多,由于第三方接口测试环境可靠性不高,容易导致测试人员测试堵塞;需要特定场景的数据,但是依赖相对复杂,伪造数据的成本较高等情况,对于接口,数据库,redis等Mock的需求还是比较大的。目前公司内部不同部门有多套Mock方案,但是都没有摆脱对代码的侵入,可扩展性不高。基于目前大部分服务已经是Java技术栈的前提情况下,通过JavaAgent修改字节码的方式达到Mock的目的的条件逐渐成熟,虽然该方案开发入门较高,但从可维护,推广简易,成本效益等角度看是值得尝试的。
二. 一些提示以及注意事项
- 必须基于JVM技术栈
- 目前实现SOA接口的录制,Mock,以及方法级别的耗时分析。
- Soa 通过DUBBO,自研SOA的方式提供。
- 数据库查询,redis查询等JAVA API相对稳定(暂未实现,架构同理)
- 链路监控(架构同理)
三. 方案架构
- 架构
整体架构在目标应用层面主要划分为三部分,
- Javaagent部分,主要Transform应用代码,加载Plugin逻辑
- Plugin,负责数据收集,发送给消息队列,同步应用信息等
- 应用,通过反射的方式调用Plugin的Hook,利用Javaagent将相关逻辑编织进应用code space。
服务端,主要是用来数据管理,用户管理,ceph管理和其他操作的Portal
服务端和客户端通过long-poll http,消息队列方式进行通信。后面计划自定义Transport层,通过TCP协议传输来提高通信效率和减少资源的利用
3. classloader 方案
Classloader的实现,通过自定义Classloader来实现与应用代码空间的隔离,保证对应用不产生污染。
- 扩展类库加载
在项目进行Transform的时候初始化自定义类加载,尝试Attach路径下的PluginJar。
SPI 核心探测实现
public class SPI {
private static final Logger LOG = LoggerFactory.getLogger(SPI.class);
private static final String SPI_BASE_PATH = "META-INF/snake/services";
public static List<String> detect(JarFile jarFile, String name) {
JarEntry jarEntry = jarFile.getJarEntry(SPI_BASE_PATH + "/" + name);
if (jarEntry != null) {
try (InputStream is = jarFile.getInputStream(jarEntry)) {
List<String> entries = new LinkedList<>();
BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(is));
String readLine = null;
while ((readLine = bufferedReader.readLine()) != null) {
if (readLine.length() > 5) {
entries.add(readLine.trim());
}
}
return entries;
} catch (IOException e) {
LOG.error(e.getMessage(), e);
return Collections.emptyList();
}
}
return Collections.emptyList();
}
}
PluginAttacher核心实现
/**
* @author lonelyangel.jcw@gmail.com
* @date 2020/5/6
* MTEAT-INF/snake/service/xxxx.xxx.Pulgin
*/
public class PluginAttacher {
private static final Logger LOG = LoggerFactory.getLogger(PluginAttacher.class);
private final LinkedList<File> paths = new LinkedList<>();
private final ReentrantLock jarScanLock = new ReentrantLock();
private final ClassLoader classLoader;
private static final AtomicBoolean ATTACHED = new AtomicBoolean(false);
public PluginAttacher(final String path, ClassLoader classLoader) {
this.paths.add(new File(path));
this.classLoader = classLoader;
}
public void attemptAttach() {
if (ATTACHED.get()) {
return;
}
ATTACHED.set(true);
load().forEach(Plugin::attach);
}
private List<Plugin> load() {
List<Jar> jars = allJars();
List<String> detected = new LinkedList<>();
for (Jar jar : jars) {
detected.addAll(SPI.detect(jar.jarFile, Plugin.class.getName()));
}
return detected.stream().map(d -> {
try {
Class<?> klass = classLoader.loadClass(d);
Constructor<Plugin> constructor = (Constructor<Plugin>) klass.getConstructor();
return constructor.newInstance();
} catch (ClassNotFoundException | NoSuchMethodException
| InstantiationException | IllegalAccessException
| InvocationTargetException e) {
LOG.error(e.getMessage(), e);
return null;
}
}).filter(Objects::nonNull).collect(Collectors.toList());
}
private List<Jar> allJars() {
final LinkedList<Jar> allJars = new LinkedList<>();
jarScanLock.lock();
for (File path : paths) {
if (path.exists() && path.isDirectory()) {
String[] jarNames = path.list((dir, name) -> name.endsWith(".jar"));
if (jarNames != null) {
for (String jarName : jarNames) {
File jarFile = new File(path, jarName);
try {
Jar jar = new Jar(new JarFile(jarFile), jarFile);
allJars.add(jar);
} catch (IOException ex) {
LOG.error(ex.getMessage(), ex);
}
}
}
}
}
jarScanLock.unlock();
return allJars;
}
private class Jar {
private JarFile jarFile;
private File sourceFile;
public Jar(JarFile jarFile, File sourceFile) {
this.jarFile = jarFile;
this.sourceFile = sourceFile;
}
}
}
五. 当前功能支持
- 基于JavaAgent,对开发无感,无侵入式Mock
- 支持自研Soa服务,DUBBO服务的SOA接口Mock。
- 支持接口返回报文的录制,可支持一次访问记录的链路日志回放
- 服务方法调用链以及方法耗时记录
六. 后续
- 这篇文章主要讲下整个系统的大体架构和一些想法,具体实现方案和实现技术在后续文章详细介绍
- 暂时想到的一些点,JavaAgent技术,字节码注入技术(ASM,Javassist,Bytebuddy),Pulsar相关介绍,SOA-Dubbo简要介绍,方法耗时收集,Mock技术方案,全链路监控等。