问题
最近工作上遇到一个需求:
- 有一个老系统,由许多模块组成,这些模块之间采用http的接口互相调用
- 现在需要了解各接口的调用情况,如调用时间、耗时、参数、返回值等
- 要求对原系统的改动越少越好
思考
面对这个需求,应该如何解决:
- 修改原系统的各模块,在调用接口的地方加代码
- 优点:简单直接,想怎么加就怎么加
- 缺点:需要对原系统的每个模块都进行改动,与需求3有很大的冲突
- 使用Spring AOP,通过配置的方式动态加代码
- 优点:实现比较简单,对原系统的改动也比较少
- 缺点:对没有采用Spring技术的模块就没办法了
有没有其他办法?
这时,想起了以前曾经了解过一些的java代理技术。这种技术能在运行时修改java代码,对原有的代码几乎不需要任何改造。
背景知识
- java代理能通过修改JVM中运行的Java应用的字节码,修改这些应用的行为
- java代理本身是一个特殊的java类
- java代理必须实现一个premain方法,作为代理的入口点
- java代理还可以实现一个agentmain方法,用于代理后启动(晚于应用启动)的场合
- java代理还必须在包里提供manifest,提供一些元数据(比如代理类的名字、是否可以修改类)
- 虚拟机通过-javaagent参数指定java代理
- 开发java代理需要对字节码进行操作,java标准库没有提供相应的API,好在社区开发了一些相关的库,如javassit和ASM
实操
了解过java代理的背景知识后,是时候开始实际操作了。这次我们将:
- 编写一个java代理
- 使用javassit库来修改HttpClient的代码,具体来说,是org.apache.http.impl.client.InternalHttpClient的doExecute方法,这可通过调查方法调用栈获得
- 实现在调用http接口时,输出参数、耗时等信息
确定依赖
- 项目需要对HttpClient进行修改,因此需依赖HttpClient,而HttpClient是目标模块本身就有的依赖,所以这里将scope设置为provided
- 项目需使用javassist进行字节码操作,因此需依赖javassist
<dependency>
<groupId>org.javassist</groupId>
<artifactId>javassist</artifactId>
<version>3.25.0-GA</version>
</dependency>
<dependency>
<groupId>org.apache.httpcomponents</groupId>
<artifactId>httpclient</artifactId>
<version>4.5.6</version>
<scope>provided</scope>
</dependency>
定义java代理类
首先需要定义java代理类。
public class Agent {
public static void premain(String args, Instrumentation instrumentation) {
modifyClass("org.apache.http.impl.client.InternalHttpClient");
}
private static void modifyClass(String className) {
try {
ClassPool classPool = ClassPool.getDefault();
CtClass ctClass = classPool.get(className);
// 找到目标方法
CtMethod ctMethod = ctClass.getDeclaredMethod("doExecute");
// 添加本地变量
ctMethod.addLocalVariable("beginTime", CtClass.longType);
ctMethod.addLocalVariable("endTime", CtClass.longType);
ctMethod.addLocalVariable("timeCost", CtClass.longType);
// 在方法最前面,添加代码(初始化beginTime)
StringBuilder stringBuilder = new StringBuilder();
stringBuilder.append("beginTime = System.currentTimeMillis();");
ctMethod.insertBefore(stringBuilder.toString());
// 在方法最后面,添加代码(计算耗时,调用记录方法)
stringBuilder = new StringBuilder();
stringBuilder.append("endTime = System.currentTimeMillis();");
stringBuilder.append("timeCost = endTime - beginTime;");
stringBuilder.append("cn.alfredzhang.instrument.agent.HttpClientRecorder.record($1, $2, $3, $_, beginTime, timeCost);");
ctMethod.insertAfter(stringBuilder.toString());
ctClass.toClass();
} catch (Exception e) {
e.printStackTrace();
}
}
}
- premain方法:这个方法在上文的背景知识中也提到过,是代理的入口
- 这个类里通过javassist的api,对类进行了修改
一些知识点:
- javassist修改代码的方式是用字符串(不用直接操作字节码,已经是比较方便了)
- 本地变量必须通过addLocalVariable声明
- insertBefore是在方法体开始的地方添加代码
- insertAfter是在方法体最后添加代码
- 可以通过一些特殊变量引用方法的参数和返回值($n代表第n个参数,$_代表返回值)
- 最后的toClass()非常重要,表示将修改后的CtClass对象编译为java的Class对象,并装载,如果不调用toClass,则前面的修改不会生效
定义辅助类HttpClientRecorder
上面的代理类里,最后我们是让InternalHttpClient的doExecute方法调用了一个名叫HttpClientRecorder的类。这个类的任务就是接收各参数,并进行处理,代码如下:
public class HttpClientRecorder {
public static void record(HttpHost target, HttpRequest request, HttpContext context, CloseableHttpResponse response, long beginTime, long timeCost) {
try {
System.out.println("agent:target=" + target + ",request=" + request + ",context=" + context + ",beginTime=" + beginTime + ",timeCost=" + timeCost);
// 对各参数进行各种解析和记录
} catch (Exception e) {
e.printStackTrace();
}
}
}
定义manifest文件
还记得背景知识里提到的,代理需要提供一个manifest文件吗?
在resources目录下创建文件META-INF/MANIFEST.MF,内容如下:
Premain-Class: cn.alfredzhang.instrument.agent.Agent
Can-Redefine-Classes: true
Can-Retransform-Classes: true
打包设置
我们使用maven-assembly-plugin,生成一个大jar(把依赖也打进去),否则运行时会找不到javassist的类。
同时别忘了在configuration下配置archive,把manifest文件也打进去。
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-assembly-plugin</artifactId>
<version>2.6</version>
<configuration>
<descriptorRefs>
<descriptorRef>jar-with-dependencies</descriptorRef>
</descriptorRefs>
<archive>
<manifestFile>src/main/resources/META-INF/MANIFEST.MF</manifestFile>
</archive>
</configuration>
<executions>
<execution>
<id>assemble-all</id>
<phase>package</phase>
<goals>
<goal>single</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
完整的pom文件:
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>cn.alfredzhang.instrument</groupId>
<artifactId>instrument-agent</artifactId>
<version>1.0-SNAPSHOT</version>
<properties>
<maven.compiler.source>1.7</maven.compiler.source>
<maven.compiler.target>1.7</maven.compiler.target>
</properties>
<dependencies>
<dependency>
<groupId>org.javassist</groupId>
<artifactId>javassist</artifactId>
<version>3.25.0-GA</version>
</dependency>
<dependency>
<groupId>org.apache.httpcomponents</groupId>
<artifactId>httpclient</artifactId>
<version>4.5.6</version>
<scope>provided</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-assembly-plugin</artifactId>
<version>2.6</version>
<configuration>
<descriptorRefs>
<descriptorRef>jar-with-dependencies</descriptorRef>
</descriptorRefs>
<archive>
<manifestFile>src/main/resources/META-INF/MANIFEST.MF</manifestFile>
</archive>
</configuration>
<executions>
<execution>
<id>assemble-all</id>
<phase>package</phase>
<goals>
<goal>single</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
</project>
编写应用类
为了检查代理是否能正常运行,编写一个简单的应用类,模拟我们系统中的某个模块。
public void test() throws IOException {
CloseableHttpClient httpClient = HttpClients.createDefault();
HttpGet httpGet = new HttpGet("http://localhost:10400/hello?x=dog");
try (CloseableHttpResponse response = httpClient.execute(httpGet)) {
System.out.println("statusLine=" + response.getStatusLine());
// 下略
}
}
测试
两个项目分别编译打包后,运行:
java -javaagent:instrument-agent-1.0-SNAPSHOT-jar-with-dependencies.jar -jar app.jar
其中,-javaagent参数指定代理的路径和jar包名。
我们得到了类似以下的输出:
agent:target=http://localhost:10400,request=GET http://localhost:10400/hello?x=dog HTTP/1.1,context=null,beginTime=1561342757914,timeCost=160
表示HttpClient的类确实已经被修改,并输出了以上信息,成功!
总结
当我们需要对已有的系统添加一些功能特性,但又不希望对原有的代码进行修改时,可以:
- 使用java代理技术,在运行时对原有的代码类进行修改
- 在此过程中,可以使用javassist提供的API,较方便地创建类、创建方法、在方法体中添加代码、替换方法体
本文介绍如何利用Java代理技术实现对已有系统的无侵入式改进,通过修改字节码实现在运行时添加功能,如接口调用监控。文章详细讲解了背景知识,包括Java代理的工作原理,以及如何使用javassist库实现对HttpClient的代码修改,记录调用参数、耗时等信息。
891

被折叠的 条评论
为什么被折叠?



