前言介绍
JavaAgent是在JDK5之后提供的新特性,又叫叫java代理。开发人员可通过这种机制(Instrumentation)在jvm加载class文件之前修改类的字节码,动态更改类方法实现AOP,提供监控服务如:方法调用时长、jvm内存等。 修改字节码领域有三个比较常见的框架;ASM、byte-buddy、javassist,其操作方式和控制粒度不同。
ASM 更偏向于底层,直接面向字节码编程,需要了解 JVM 虚拟机中指定规范以及对局部变量以及操作数栈的知识。虽然在编写起来比较麻烦,但是它也是性能最好功能最强的字节码操作库。 CGLIB 动态代理使用的就是ASM。 Javassist与byte-buddy提供了强大的 API,操作使用上更加容易控制,可以在不了解Java字节码规范的前提下修改class文件。
案例
使用javassist统计方法执行耗时
< dependency>
< groupId> org.javassist</ groupId>
< artifactId> javassist</ artifactId>
< version> 3.25.0-GA</ version>
</ dependency>
将javassist打包到构建的javaAgent包中
< build>
< plugins>
< plugin>
< groupId> org.apache.maven.plugins</ groupId>
< artifactId> maven-compiler-plugin</ artifactId>
< version> ${maven.compiler.version}</ version>
< configuration>
< source> ${java.version}</ source>
< target> ${java.version}</ target>
< compilerArgument> -parameters</ compilerArgument>
</ configuration>
</ plugin>
< plugin>
< groupId> org.apache.maven.plugins</ groupId>
< artifactId> maven-jar-plugin</ artifactId>
< configuration>
< archive>
< manifestFile> src/main/resources/META-INF/MANIFEST.MF</ manifestFile>
</ archive>
</ configuration>
</ plugin>
< plugin>
< groupId> org.apache.maven.plugins</ groupId>
< artifactId> maven-shade-plugin</ artifactId>
< executions>
< execution>
< phase> package</ phase>
< goals>
< goal> shade</ goal>
</ goals>
</ execution>
</ executions>
< configuration>
< artifactSet>
< includes>
< include> org.javassist:javassist:jar:</ include>
</ includes>
</ artifactSet>
</ configuration>
</ plugin>
</ plugins>
</ build>
编写耗时统计,在目标方法字节码前后插入方法耗时统计逻辑
public class ClazzTransform implements ClassFileTransformer {
private final String BASE_PACKAGE ;
public ClazzTransform ( String basePackage) {
this . BASE_PACKAGE = basePackage;
}
@Override
public byte [ ] transform ( ClassLoader loader, String className, Class < ? > classBeingRedefined, ProtectionDomain protectionDomain, byte [ ] classFileBuffer) {
className = className. replace ( "/" , "." ) ;
if ( ! className. startsWith ( BASE_PACKAGE ) ) {
return null ;
}
try {
CtClass ctKlass = ClassPool . getDefault ( ) . get ( className) ;
CtBehavior [ ] behaviors = ctKlass. getDeclaredBehaviors ( ) ;
for ( CtBehavior m : behaviors) {
enhanceMethod ( m) ;
}
byte [ ] bytes = ctKlass. toBytecode ( ) ;
Files . write ( Paths . get ( "D:\\A.class" ) , bytes) ;
return bytes;
} catch ( Exception e) {
e. printStackTrace ( ) ;
}
return classFileBuffer;
}
private void enhanceMethod ( CtBehavior method) throws CannotCompileException {
if ( method. isEmpty ( ) ) {
return ;
}
String methodName = method. getName ( ) ;
method. addLocalVariable ( "start" , CtClass . longType) ;
method. insertBefore ( "start = System.currentTimeMillis();" ) ;
method. insertAfter ( String . format ( "System.out.println(\"%s cost: \" + (System.currentTimeMillis() - start) + \"ms\");" , methodName) ) ;
}
}
编写探针引导类 类似于java主类的main方法,jvm会调用java agent的premain方法
public class AgentPremain {
public static void premain ( String ages, Instrumentation instrumentation) {
instrumentation. addTransformer ( new ClazzTransform ( "com.lauor.agent" ) ) ;
}
public static void premain ( String agentArgs) {
}
}
配置jar包探针引导类
Manifest-Version: 1.0
Premain-Class: com.lauor.agent.AgentPremain
Can-Redefine-Classes: true
测试
public class AgentTest {
public static void main ( String [ ] args) throws Exception {
timeMonitor ( ) ;
}
private static void timeMonitor ( ) throws Exception {
HttpRequest request = HttpRequest . newBuilder ( URI . create ( "https://www.baidu.com/" ) ) . GET ( ) . build ( ) ;
HttpResponse < String > rs = HttpClient . newHttpClient ( ) . send ( request, HttpResponse. BodyHandlers . ofString ( ) ) ;
System . out. println ( rs) ;
}
}
-javaagent:D:\agent\target\agent-1.1.1-SNAPSHOT.jar=agentArgs
调用结果 查看修改后的class文件
使用bytebuddy统计方法调用耗时
介绍 使用bytebuddy比使用javassist更为简单,使用上类似于Java动态代理模式 引入maven依赖
< properties>
< bytebuddy.version> 1.11.22</ bytebuddy.version>
</ properties>
< dependency>
< groupId> net.bytebuddy</ groupId>
< artifactId> byte-buddy</ artifactId>
< version> ${bytebuddy.version}</ version>
</ dependency>
< dependency>
< groupId> net.bytebuddy</ groupId>
< artifactId> byte-buddy-agent</ artifactId>
< version> ${bytebuddy.version}</ version>
< scope> test</ scope>
</ dependency>
< build>
< plugins>
< plugin>
< groupId> org.apache.maven.plugins</ groupId>
< artifactId> maven-compiler-plugin</ artifactId>
< version> ${maven.compiler.version}</ version>
< configuration>
< source> ${java.version}</ source>
< target> ${java.version}</ target>
< compilerArgument> -parameters</ compilerArgument>
</ configuration>
</ plugin>
< plugin>
< groupId> org.apache.maven.plugins</ groupId>
< artifactId> maven-jar-plugin</ artifactId>
< configuration>
< archive>
< manifestFile> src/main/resources/META-INF/MANIFEST.MF</ manifestFile>
</ archive>
</ configuration>
</ plugin>
< plugin>
< groupId> org.apache.maven.plugins</ groupId>
< artifactId> maven-shade-plugin</ artifactId>
< executions>
< execution>
< phase> package</ phase>
< goals>
< goal> shade</ goal>
</ goals>
</ execution>
</ executions>
< configuration>
< artifactSet>
< includes>
< include> net.bytebuddy:byte-buddy:jar:</ include>
< include> net.bytebuddy:byte-buddy-agent:jar:</ include>
</ includes>
</ artifactSet>
</ configuration>
</ plugin>
</ plugins>
</ build>
import net. bytebuddy. implementation. bind. annotation. Origin ;
import net. bytebuddy. implementation. bind. annotation. RuntimeType ;
import net. bytebuddy. implementation. bind. annotation. SuperCall ;
import java. lang. reflect. Method ;
import java. util. concurrent. Callable ;
public class MethodMonitor {
@RuntimeType
public static Object intercept ( @Origin Method method, @SuperCall Callable < ? > callable) throws Exception {
long sTime = System . currentTimeMillis ( ) ;
try {
return callable. call ( ) ;
} finally {
System . out. println ( String . format ( "%s cost %dms" , method. getName ( ) , System . currentTimeMillis ( ) - sTime) ) ;
}
}
}
public static void premain ( String ages, Instrumentation instrumentation) {
System . out. println ( String . format ( "invoke premain args=%s" , ages) ) ;
runMethodCost ( instrumentation) ;
}
private static void runMethodCost ( Instrumentation instrumentation) {
AgentBuilder. Transformer transformer = ( builder, typeDescription, classLoader, javaModule) -> builder
. method ( ElementMatchers . any ( ) )
. intercept ( MethodDelegation . to ( MethodMonitor . class ) ) ;
new AgentBuilder
. Default ( )
. type ( ElementMatchers . nameStartsWith ( "com.lauor.agent" ) )
. transform ( transformer)
. installOn ( instrumentation) ;
}
同javassist方式一样配置探针jar包引导类,并配置带有javaagent的启动参数 运行javassist的基于jdk11 httpclient调用百度首页的例子
使用javaagent监控jvm
public class JvmMonitor {
public void gatherHeap ( ) {
MemoryMXBean memoryMXBean = ManagementFactory . getMemoryMXBean ( ) ;
MemoryUsage heapMemory = memoryMXBean. getHeapMemoryUsage ( ) ;
BigDecimal initSize = toMb ( heapMemory. getInit ( ) ) ;
BigDecimal usedSize = toMb ( heapMemory. getUsed ( ) ) ;
BigDecimal maxSize = toMb ( heapMemory. getMax ( ) ) ;
BigDecimal committedSize = toMb ( heapMemory. getCommitted ( ) ) ;
String info = String . format ( "init:%sMB,max:%sMB,committed:%sMB,used:%sMB" ,
initSize. floatValue ( ) , maxSize. floatValue ( ) , committedSize. floatValue ( ) , usedSize. floatValue ( ) ) ;
System . out. println ( info) ;
}
private BigDecimal toMb ( long bytes) {
final long mInBytes = 1024 * 1024 ;
BigDecimal value = BigDecimal . valueOf ( bytes) . divide ( BigDecimal . valueOf ( mInBytes) , 1 , RoundingMode . HALF_UP ) ;
return value;
}
public void gatherGc ( ) {
List < GarbageCollectorMXBean > gcMXBeans = ManagementFactory . getGarbageCollectorMXBeans ( ) ;
for ( GarbageCollectorMXBean gcMXBean : gcMXBeans) {
long count = gcMXBean. getCollectionCount ( ) ;
String name = gcMXBean. getName ( ) ;
long costInMill = gcMXBean. getCollectionTime ( ) ;
String memNames = Arrays . deepToString ( gcMXBean. getMemoryPoolNames ( ) ) ;
String info = String . format ( "name:%s,count:%s,cost %dms,pool name:%s" ,
name, count, costInMill, memNames) ;
System . out. println ( info) ;
}
}
}
编写定时任务,定时执行收集jvm gc与内存信息任务
public class AgentPremain {
public static void premain ( String ages, Instrumentation instrumentation) {
runJvmTask ( ) ;
}
private static void runJvmTask ( ) {
ScheduledExecutorService executorService = Executors . newScheduledThreadPool ( 1 , new ThreadFactory ( ) {
private final AtomicInteger threadNumber = new AtomicInteger ( 1 ) ;
@Override
public Thread newThread ( Runnable r) {
Thread t = new Thread ( r, "pool-jvm-" + threadNumber. getAndIncrement ( ) ) ;
if ( t. isDaemon ( ) ) {
t. setDaemon ( false ) ;
}
if ( t. getPriority ( ) != Thread . NORM_PRIORITY ) {
t. setPriority ( Thread . NORM_PRIORITY ) ;
}
return t;
}
} ) ;
JvmMonitor jvmMonitor = new JvmMonitor ( ) ;
executorService. scheduleAtFixedRate ( ( ) -> {
jvmMonitor. gatherGc ( ) ;
jvmMonitor. gatherHeap ( ) ;
} , 5 , 5 , TimeUnit . SECONDS ) ;
}
}
同javassist方式一样配置探针jar包引导类,并配置带有javaagent的启动参数 编写样例不停创建对象,模拟线上后端应用
public class AgentTest {
public static void main ( String [ ] args) throws Exception {
jvmMonitor ( ) ;
}
private static void jvmMonitor ( ) {
while ( true ) {
List < String > list = new ArrayList < > ( ) ;
list. add ( "print jvm heap" ) ;
list. add ( "print jvm gc" ) ;
list. clear ( ) ;
}
}
}
运行结果 开箱即用的基于javaagent的监控解决方案:prometheus(普罗米修斯)
介绍: prometheus是一个开源的监控系统和报警工具集合。主要有以下特点: 1:由指标名称和和键/值对标签标识的时间序列数据组成的多维数据模型。 2:强大的查询语言 PromQL。 3:不依赖分布式存储;单个服务节点具有自治能力。 4:时间序列数据是服务端通过 HTTP 协议主动拉取获得的, 也可以通过中间网关来推送时间序列数据。 5:可以通过静态配置文件或服务发现来获取监控目标。 6:支持多种类型的图表和仪表盘。 prometheus结合grafana的效果图