最近团队在搭建开源的监控系统,使用到了这个工具,突然发现这个工具设计很优雅,对要监控的JAVA项目是无侵入的,只需要在被监控的应用的启动参数中,增加一段代码即可,实现的原理就是利用javaagent特性。
以前很少接触过javaagent的知识,项目中也很少有这方面实践的机会,于是想自己亲自动手实践下,并且加深对java agent的理解。
JavaAgent 是JDK 1.5 以后引入的,也可以叫做Java代理,以往对原有的类实现代理功能,都是通过AOP实现方法的拦截,并增加一些增强的逻辑,或者通过cglib、javaassit动态生成类的字节码来实现代理,但是这种实现方式都需要对本地工程的代码作一定程度的修改,如:增加一些配置来定义要代理哪些目标类,或者直接在当前工程中直接扩展目标类的实现等等。这种做法虽然使用起来很方便,但是对已有的工程代码都是有侵入性,那么想做到无侵入,还想实现代理功能,如何实现呢?JavaAgent应运而生。
java.lang.instrument是Java SE 5 的新特性,你可以由此实现一个java agent。使用 Instrumentation,开发者可以构建一个独立于应用程序的代理程序(Agent),用来监测和协助运行在 JVM 上的程序,甚至能够替换和修改某些类的定义。有了这样的功能,开发者就可以实现更为灵活的运行时虚拟机监控和 Java 类操作了,这样的特性实际上提供了一种虚拟机级别支持的更松耦合的AOP 实现方式,使得开发者无需对 JDK 做任何升级和改动,就可以实现某些 AOP 的功能了。
想了解JavaAgent原理的同学请移步:
https://yq.aliyun.com/articles/2946?spm=5176.100239.yqblog1.45
不多废话了,进入正题吧:)
实现Java Agent大概需要如下几个步骤:
1、编写 premain 函数
编写一个 Java 类,包含如下两个方法当中的任何一个
public static void premain(String agentArgs, Instrumentation inst); [1]
public static void premain(String agentArgs); [2]
其中,[1] 的优先级比 [2] 高,不能就是说[1] 和 [2] 同时存在时,[1]将会被优先执行,而[2] 被忽略。
在这个 premain 函数中,开发者可以进行对类的各种操作。
agentArgs 是 premain 函数得到的程序参数,随同 “– javaagent”一起传入。与 main 函数不同的是,这个参数是一个字符串而不是一个字符串数组,如果程序参数有多个,程序需要自行解析这个字符串。
Inst 是一个 java.lang.instrument.Instrumentation 的实例,由 JVM 自动传入。java.lang.instrument.Instrumentation 是 instrument 包中定义的一个接口,也是这个包的核心部分,集中了其中几乎所有的功能方法,例如类定义的转换和操作等等。
代理类的代码很简单,其中的instrumentation.addTransformer(new Transformer());这行先忽略,下文继续会提到。
public class PremainAgent {
public static void premain(String args, Instrumentation instrumentation) {
System.out.println("代理类开始执行...,参数:" + args);
// instrumentation.addTransformer(new Transformer());
System.out.println("代理类执行结束...");
}
}
2、将你的应用程序打包
由于javaagent只能通过java -jar的方式运行,因此需要将你的应用程序打成jar包,且你的jar包中的manifest文件中须包含Premain-Class属性,并且值为代理类全限定名。
当在命令行启动该代理jar时,VM会根据manifest中指定的代理类,使用于main类相同的系统类加载器(即ClassLoader.getSystemClassLoader()获得的加载器)加载代理类。在执行main方法前执行premain()方法。
MANIFEST文件:
需要额外添加premain-classs: 你的代理类的全路径名
Manifest-Version: 1.0
premain-class: com.ws.demo.javaagent.agent.PremainAgent
Archiver-Version: Plexus Archiver
Built-By: Administrator
Class-Path: lib/javassist-3.8.0.GA.jar
Created-By: Apache Maven 3.0.5
Build-Jdk: 1.8.0_144
Main-Class: com.ws.demo.javaagent.Main
如果你使用的是Maven,可以使用maven的插件来实现,这样MANIFEST.MF文件中自动添加了相应的配置。
<plugin>
<groupId>org.apache.maven.pluginsgroupId>
<artifactId>maven-jar-pluginartifactId>
<version>2.6version>
<configuration>
<archive>
<manifest>
<addClasspath>trueaddClasspath>
<classpathPrefix>lib/classpathPrefix>
<mainClass>com.ws.demo.javaagent.MainmainClass>
manifest>
<manifestEntries>
<premain-class>com.ws.demo.javaagent.agent.PremainAgentpremain-class>
manifestEntries>
archive>
configuration>
plugin>
附上工程中使用到的类:
一个简单的业务接口:
package com.ws.demo.javaagent.biz;
public interface Cache {
void put(String key, String value);
}
实现类:
package com.ws.demo.javaagent.biz.impl;
import com.ws.demo.javaagent.biz.Cache;
public class RedisCache implements Cache {
@Override
public void put(String key, String value) {
System.out.println(String.format("RedisCache put [ Key:%s | Value:%s ]", key, value));
}
}
应用程序入口类:Main
package com.ws.demo.javaagent;
import com.ws.demo.javaagent.biz.Cache;
import com.ws.demo.javaagent.biz.impl.GuavaCache;
public class Main {
public static void main(String[] args) {
Cache cache = new GuavaCache();
cache.put("demo", "abc");
}
}
3、运行代理程序
**执行如下命令,会在你的应用程序(包含main方法的程序)启动前启动一个代理程序。
java -javaagent:agent_jar_path[=options] java_app_name
比如我当前的工程打包后生成了javaagent-1.0-SNAPSHOT.jar,为了方便起见,我把我的代理类和Main类在一个工程,所以执行javaagent命令时,后面跟的jarpath是一个,贴上代码:
java -javaagent:javaagent-1.0-SNAPSHOT.jar=demo -jar javaagent-1.0-SNAPSHOT.jar
执行成功, 输出:
代理类开始执行...,参数:demo
代理类执行结束...
GuavaCache put [ Key:demo | Value:abc ]
可以看到,先执行了代理,之后才执行了主应用程序,在这里注意一下,javaagent支持多个代理类,即:
java -javaagent:agent1.jar -javaagent:agent2.jar java_app_name -javaagent:agent3.jar
但是如果在你的app_name后面加的代理类是不生效的,也就是agent1、agent2可以正常被执行,但是执行不了agent3
下面我们补充之前说的关于instrumentation.addTransformer(new Transformer());
这段代码,我们把注释放开。
public class PremainAgent {
public static void premain(String args) {
}
public static void premain(String args, Instrumentation instrumentation) {
System.out.println("代理类开始执行...,参数:" + args);
instrumentation.addTransformer(new Transformer());
System.out.println("代理类执行结束...");
}
}
上文已经提到参数是JVM自动实例化并传入的,并且使用了和你的Main类同一个ClassLoader,这就使得应用中的类加载的时候, Transformer.transform方法都会被调用,并且可以利用这个特性动态的改变加载的主程序中类的逻辑,接下来我们利用这个特性来写个小DEMO。
其中Transformer是我的一个实现类,上代码:
package com.ws.demo.javaagent.transformer;
import javassist.*;
import javassist.expr.ExprEditor;
import javassist.expr.MethodCall;
import java.lang.instrument.ClassFileTransformer;
import java.lang.instrument.IllegalClassFormatException;
import java.security.ProtectionDomain;
public class Transformer implements ClassFileTransformer {
// 实现字节码转化接口,一个小技巧建议实现接口方法时写@Override,方便重构
// loader:定义要转换的类加载器,如果是引导加载器,则为 null(在这个小demo暂时还用不到)
// className:完全限定类内部形式的类名称和中定义的接口名称,例如"java.lang.instrument.ClassFileTransformer"
// classBeingRedefined:如果是被重定义或重转换触发,则为重定义或重转换的类;如果是类加载,则为 null
// protectionDomain:要定义或重定义的类的保护域
// classfileBuffer:类文件格式的输入字节缓冲区(不得修改)
// 一个格式良好的类文件缓冲区(转换的结果),如果未执行转换,则返回 null。
@Override
public byte[] transform(ClassLoader loader, String className, Class> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException {
byte[] transformed = null;
ClassPool pool = ClassPool.getDefault();
CtClass cl = null;
try {
cl = pool.makeClass(new java.io.ByteArrayInputStream(classfileBuffer));
if (cl.isInterface() == false) {
CtBehavior[] methods = cl.getDeclaredBehaviors();
for (CtBehavior ctBehavior : methods) {
if (!ctBehavior.isEmpty()) {
doMethod(ctBehavior);
}
}
transformed = cl.toBytecode();
}
} catch (Exception e) {
System.err.println("Could not instrument " + className
+ ", exception : " + e.getMessage());
} finally {
if (cl != null) {
cl.detach();
}
}
return transformed;
}
private void doMethod(CtBehavior method) throws NotFoundException,
CannotCompileException {
method.instrument(new ExprEditor() {
public void edit(MethodCall m) throws CannotCompileException {
m.replace("{ long stime = System.currentTimeMillis(); $_ = $proceed($$); System.out.println(\""
+ m.getClassName() + "." + m.getMethodName()
+ " cost:\" + (System.currentTimeMillis() - stime) + \" ms\");}");
}
});
}
}
通过阅读代码,不难发现,其主要的逻辑就是使用Javassist动态的修改加载的类的字节码,在执行完方法后,以输出了这个方法的耗时。
关于java字节码的处理,目前有很多工具,如bcel,asm(cglib只是对asm又封装了一层),javassist,这里我们使用最后一个来作演示。打包执行下:
java -javaagent:javaagent-1.0-SNAPSHOT.jar -jar javaagent-1.0-SNAPSHOT.jar
这里要特殊说明下,因为我们的程序使用到了Javassist这个包,所以需要指定依赖的类的路径,否则会因为找不到类导致你的代理程序无法正确执行,方法是在MANIFEST.MF中增加下边的代码:
Boot-Class-Path: E:/.m2/repository/jboss/javassist/3.8.0.GA/javassist-3.8.0.GA.jar
当然你可以使用MAVEN提供的插件功能来实现,方法在POM.XML中配置:
com.ws.demo.javaagent.agent.PremainAgent
E:/.m2/repository/jboss/javassist/3.8.0.GA/javassist-3.8.0.GA.jar
结果:
看,所有方法的执行时间都被打印出来了,我们在没修改主程序的任何代码就实现了AOP,是不是很神奇。如果大家想更多地了解Instrumentation,可以参考:
http://blog.csdn.net/songshuaiyang/article/details/50732345
~~~好了,先写这么多,自己学习的同时,也希望可以对刚接触javaagent的同学起到一个启蒙的作用。
如有写的不妥当的地方,还望指正,谢谢!