java agent
jdk1.5以后引入的字节码插桩技术,可以在代码中加入切点,独立于目标程序,业务侵入性相比于普通的AOP编程要低,可以用作接口的性能检测,参数可性能监控等,常见的微服务链路跟踪的实现原理之一
jdk1.5后新增了类java.lang.instrument.Instrumentation,它提供在运行时重新加载某个类的的class文件的api,部分源码如下:
public interface Instrumentation {
void
addTransformer(ClassFileTransformer transformer, boolean canRetransform);
void
addTransformer(ClassFileTransformer transformer);
boolean
removeTransformer(ClassFileTransformer transformer);
boolean
isRetransformClassesSupported();
void
retransformClasses(Class<?>... classes) throws UnmodifiableClassException;
通过addTransformer可以加入一个转换器,转换器可以实现对类加载的事件进行拦截并返回转换后新的字节码,通过redefineClasses或retransformClasses都可以触发类的重新加载事件。通过这几个方法的组合,就可以实现不修改原来代码并使用切片的目的
使用方式主要分这么几步:1.新建切片程序实现切片逻辑;2.程序入口由main函数改为premain;3.加入自定义转换器;4.修改原先程序启动方式,加入-javaagent参数(更多参数可以参考java --help)
bytebuddy
bytebuddy是一个可以在运行时动态生成java class的类库,无需编译器帮助,使用它的主要目的是它可以创建任意类,不限于实现用于创建运行时代理的接口,而且它提供了一种方便的API,可以使用java代理或在构建过程中手动更改类,算是功能强大而交互性好的类库,官网:https://bytebuddy.net,中文文档:https://notes.diguage.com/byte-buddy-tutorial
下面通过例子实现用java agent监控springboot程序的controller并打印交互数据
示例
新建目标程序
新建一个helloworld的springboot的web程序,由于程序简单,这里只贴出一些需要的代码
@RestController
public class HelloController {
@GetMapping("/hello")
public String hello() {
return "hello";
}
@PostMapping("/say")
public String say(@RequestParam("word") String word) {
return "say: " + word;
}
}
新建agent程序
新建agent程序,定义程序入口的premain方法,在MF文件中定义premain方法
public static void premain(String agentArgs, Instrumentation inst) {}
这个方法相当于普通Java程序的入口方法main,agentArgs相当于main方法里的 String[] args,但是不是数组状态,而是命令行怎么输入的,这里就怎么传递,需要手动处理,inst,就是刚刚说到的Instrumentation类,其由sun或openjdk来对其进行实现,所以具体实现可以不用管,只管应用接口即可,当然有兴趣的也可以打开源码了解一下
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-assembly-plugin</artifactId>
<version>3.3.0</version>
<configuration>
<appendAssemblyId>false</appendAssemblyId>
<descriptorRefs>
<descriptorRef>jar-with-dependencies</descriptorRef>
</descriptorRefs>
<archive>
<manifestEntries>
<Premain-Class>org.example.Main</Premain-Class>
<Can-Redefine-Classes>true</Can-Redefine-Classes>
<Can-Retransform-Classes>true</Can-Retransform-Classes>
</manifestEntries>
</archive>
</configuration>
<executions>
<execution>
<id>make-assembly</id>
<phase>package</phase>
<goals>
<goal>single</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
这个pom.xml中的plugin配置,因为要打包,这里我用assembly直接maven打包,命令:
mvn clean package
这样可以直接打包并加入引用的jar,自动生成配置好的MF文件,比较方便
public class Main {
public static void premain(String agentArgs, Instrumentation inst) {
System.out.printf("premain, agentArgs: %s%n", agentArgs);
System.out.printf("premain, inst: %s%n", inst);
try {
// 拦截spring controller
AgentBuilder.Identified.Extendable builder1 = new AgentBuilder.Default()
// 拦截@Controller 和 @RestController的类
.type(ElementMatchers.isAnnotatedWith(ElementMatchers.named("org.springframework.stereotype.Controller")
.or(ElementMatchers.named("org.springframework.web.bind.annotation.RestController"))))
.transform((builder, typeDescription, classLoader, javaModule) ->
// 拦截 @RestMapping 或者 @Get/Post/Put/DeleteMapping
builder.method(ElementMatchers.isPublic().and(ElementMatchers.isAnnotatedWith(
ElementMatchers.nameStartsWith("org.springframework.web.bind.annotation")
.and(ElementMatchers.nameEndsWith("Mapping")))))
// 拦截后交给 SpringControllerInterceptor 处理
.intercept(MethodDelegation.to(SpringControllerInterceptor.class)));
// 装载到 instrumentation 上
builder1.installOn(inst);
} catch (Exception e) {
e.printStackTrace();
}
}
}
这里用bytebuddy作了如下处理
1.监听使用@Controller或@RestController的类
2.注册自定义转换器对监听的类进行处理
3.在自定义转换器中对使用了@org.springframework.web.bind.annotation.xxxMaping注解的共有方法进行拦截,交给类SpringControllerInterceptor进行处理
4.将转换器装载到instrumentation上
@Slf4j
public class SpringControllerInterceptor {
@RuntimeType
public static Object intercept(@Origin Method method,
@AllArguments Object[] args,
@SuperCall Callable<?> callable) {
log.info("before controller: {}", method.getName());
log.info("args: {}", Arrays.toString(args));
long start = System.currentTimeMillis();
try {
Object res = callable.call();
log.info("result: {}", res);
return res;
} catch(Exception e) {
log.error("controller error: ", e);
} finally {
long end = System.currentTimeMillis();
log.info("after controller execute in {} ms", end - start);
}
return null;
}
}
在 SpringControllerInterceptor中,定义如上的intercept方法,加入@net.bytebuddy.implementation.bind.annotation.RuntimeType注解,就可以进行切片操作了。“@Origin Method method”,用于注入目标方法的反射对象,“@AllArguments Object[] args”,用于注入目标方法执行时的参数列表,“@SuperCall Callable<?> callable”,暂时不了解其含义,个人理解为目标方法执行所在线程或者其控制器,使用callable.call()方式触发目标方法执行。这里把拦截的方法的方法名、参数列表、返回值、异常信息及执行耗时打印出来
执行
先用 mvn assembly:single 将agent程序打包,然后执行目标程序,启动项如下:
nohup java -javaagent:/path/to/agent.jar spring-demo.jar > log.log &
nohup java -javaagent:./agent.jar -jar demo.jar > log.log &
[1] 69448
ll
total 56160
drwxr-xr-x 5 lixun04 wheel 160B 6 30 17:59 .
drwxrwxrwt 23 root wheel 736B 6 30 17:59 ..
-rw-r--r--@ 1 lixun04 wheel 9.3M 6 30 17:58 agent.jar
-rw-r--r-- 1 lixun04 wheel 18M 6 30 17:53 demo.jar
-rw-r--r-- 1 lixun04 wheel 84B 6 30 17:59 log.log
cat log.log
premain, agentArgs: null
premain, inst: sun.instrument.InstrumentationImpl@2626b418
. ____ _ __ _ _
/\\ / ___'_ __ _ _(_)_ __ __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
\\/ ___)| |_)| | | | | || (_| | ) ) ) )
' |____| .__|_| |_|_| |_\__, | / / / /
=========|_|==============|___/=/_/_/_/
:: Spring Boot :: (v2.5.1)
2021-06-30 17:59:30.817 [main] [INFO] com.example.demo.SpringInstrumentDemoApplication - Starting SpringInstrumentDemoApplication v0.0.1-SNAPSHOT using Java 1.8.0_282 on lixundeMacBook-Pro.local with PID 69448 (/private/tmp/instrument/demo.jar started by lixun04 in /private/tmp/instrument)
2021-06-30 17:59:30.822 [main] [INFO] com.example.demo.SpringInstrumentDemoApplication - No active profile set, falling back to default profiles: default
2021-06-30 17:59:33.629 [main] [INFO] org.springframework.boot.web.embedded.tomcat.TomcatWebServer - Tomcat initialized with port(s): 8080 (http)
2021-06-30 17:59:33.647 [main] [INFO] org.apache.coyote.http11.Http11NioProtocol - Initializing ProtocolHandler ["http-nio-8080"]
2021-06-30 17:59:33.650 [main] [INFO] org.apache.catalina.core.StandardService - Starting service [Tomcat]
2021-06-30 17:59:33.650 [main] [INFO] org.apache.catalina.core.StandardEngine - Starting Servlet engine: [Apache Tomcat/9.0.46]
2021-06-30 17:59:33.752 [main] [INFO] org.apache.catalina.core.ContainerBase.[Tomcat].[localhost].[/] - Initializing Spring embedded WebApplicationContext
2021-06-30 17:59:33.754 [main] [INFO] org.springframework.boot.web.servlet.context.ServletWebServerApplicationContext - Root WebApplicationContext: initialization completed in 2685 ms
用postman访问localhost:8080/hello接口,观察日志输出:
tail -f log.log
2021-06-30 17:59:35.093 [main] [INFO] org.apache.coyote.http11.Http11NioProtocol - Starting ProtocolHandler ["http-nio-8080"]
2021-06-30 17:59:35.131 [main] [INFO] org.springframework.boot.web.embedded.tomcat.TomcatWebServer - Tomcat started on port(s): 8080 (http) with context path ''
2021-06-30 17:59:35.157 [main] [INFO] com.example.demo.SpringInstrumentDemoApplication - Started SpringInstrumentDemoApplication in 5.386 seconds (JVM running for 6.636)
2021-06-30 18:01:49.866 [http-nio-8080-exec-2] [INFO] org.apache.catalina.core.ContainerBase.[Tomcat].[localhost].[/] - Initializing Spring DispatcherServlet 'dispatcherServlet'
2021-06-30 18:01:49.868 [http-nio-8080-exec-2] [INFO] org.springframework.web.servlet.DispatcherServlet - Initializing Servlet 'dispatcherServlet'
2021-06-30 18:01:49.876 [http-nio-8080-exec-2] [INFO] org.springframework.web.servlet.DispatcherServlet - Completed initialization in 8 ms
2021-06-30 18:01:49.991 [http-nio-8080-exec-2] [INFO] org.example.SpringControllerInterceptor - before controller: hello
2021-06-30 18:01:49.997 [http-nio-8080-exec-2] [INFO] org.example.SpringControllerInterceptor - args: []
2021-06-30 18:01:49.997 [http-nio-8080-exec-2] [INFO] org.example.SpringControllerInterceptor - result: hello
2021-06-30 18:01:49.998 [http-nio-8080-exec-2] [INFO] org.example.SpringControllerInterceptor - after controller execute in 0 ms
总结
java agent主要用于监控,所以一般来说,正常的写业务的人员很少用到,而jvm中自带的工具,如jmap,jstat,jstack等都是用javaagent方式监控,抓取jvm信息以供分析,但是我们也可以了解一下相关的内容,用于自己业务上定制的监控,如针对某些特定接口的性能监控,参数记录等,可以方便平时开发或者线上的问题排查,而使用bytebuddy则让我们更高效的处理字节码,相比于asm,javaassist,bytebuddy更强大高效
参考
https://zhuanlan.zhihu.com/p/151843984
https://www.cnblogs.com/xiaofuge/p/12868783.html