一. 背景
今天無心寫代碼,整理下文章看。應用對於第三方的依賴較多,由於第三方接口測試環境可靠性不高,容易導致測試人員測試堵塞;需要特定場景的數據,但是依賴相對複雜,僞造數據的成本較高等情況,對於接口,數據庫,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 [email protected]
* @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技術方案,全鏈路監控等。
- 有關Java,Golang,中間件技術愛好者可以發郵件或者留言來一起探討一下(e-mail:[email protected])