相熟Spring的小伙伴们应该都对aop比拟理解,面向切面编程容许咱们在指标办法的前后织入想要执行的逻辑,而明天要给大家介绍的Java Agent技术,在思想上与aop比拟相似,翻译过去能够被称为Java代理、Java探针技术。
Java Agent呈现在JDK1.5版本当前,它容许程序员利用agent技术构建一个独立于应用程序的代理程序,用处也十分宽泛,能够帮助监测、运行、甚至替换其余JVM上的程序,先从上面这张图直观的看一下它都被利用在哪些场景:

看到这里你是不是也很好奇,到底是什么神仙技术,可能利用在这么多场景下,那明天咱们就来开掘一下,看看神奇的Java Agent是如何工作在底层,默默撑持了这么多优良的利用。
回到文章结尾的类比,咱们还是用和aop比拟的形式,来先对Java Agent有一个大抵的理解:

作用级别:aop运行于应用程序内的办法级别,而agent可能作用于虚拟机级别
组成部分:aop的实现须要指标办法和逻辑加强局部的办法,而Java Agent要失效须要两个工程,一个是agent代理,另一个是须要被代理的主程序
执行场合:aop能够运行在切面的前后或盘绕等场合,而Java Agent的执行只有两种形式,jdk1.5提供的preMain模式在主程序运行前执行,jdk1.6提供的agentMain在主程序运行后执行

上面咱们就别离看一下在两种模式下,如何入手实现一个agent代理程序。
Premain模式
Premain模式容许在主程序执行前执行一个agent代理,实现起来非常简单,上面咱们别离实现两个组成部分。
agent
先写一个简略的性能,在主程序执行前打印一句话,并打印传递给代理的参数:
public class MyPreMainAgent {

public static void premain(String agentArgs, Instrumentation inst) {    System.out.println("premain start");    System.out.println("args:"+agentArgs);}

}
复制代码
在写完了agent的逻辑后,须要把它打包成jar文件,这里咱们间接应用maven插件打包的形式,在打包前进行一些配置。
<build>

<plugins>    <plugin>        <groupId>org.apache.maven.plugins</groupId>        <artifactId>maven-jar-plugin</artifactId>        <version>3.1.0</version>        <configuration>            <archive>                <manifest>                    <addClasspath>true</addClasspath>                </manifest>                <manifestEntries>                    <Premain-Class>com.cn.agent.MyPreMainAgent</Premain-Class>                                                <Can-Redefine-Classes>true</Can-Redefine-Classes>                    <Can-Retransform-Classes>true</Can-Retransform-Classes>                    <Can-Set-Native-Method-Prefix>true</Can-Set-Native-Method-Prefix>                </manifestEntries>            </archive>        </configuration>    </plugin></plugins>

</build>
复制代码
配置的打包参数中,通过manifestEntries的形式增加属性到MANIFEST.MF文件中,解释一下外面的几个参数:

Premain-Class:蕴含premain办法的类,须要配置为类的全门路
Can-Redefine-Classes:为true时示意可能从新定义class
Can-Retransform-Classes:为true时示意可能从新转换class,实现字节码替换
Can-Set-Native-Method-Prefix: 为true时示意可能设置native办法的前缀

其中Premain-Class为必须配置,其余几项是非必须选项,默认状况下都为false,通常也倡议退出,这几个性能咱们会在前面具体介绍。在配置实现后,应用mvn命令打包:
mvn clean package
复制代码
打包实现后生成myAgent-1.0.jar文件,咱们能够解压jar文件,看一下生成的MANIFEST.MF文件:

能够看到,增加的属性曾经被退出到了文件中。到这里,agent代理局部就实现了,因为代理不可能间接运行,须要附着于其余程序,所以上面新建一个工程来实现主程序。
主程序
在主程序的工程中,只须要一个可能执行的main办法的入口就能够了。
public class AgentTest {

public static void main(String[] args) {    System.out.println("main project start");}

}
复制代码
在主程序实现后,要思考的就是应该如何将主程序与agent工程连接起来。这里能够通过-javaagent参数来指定运行的代理,命令格局如下:
java -javaagent:myAgent.jar -jar AgentTest.jar
复制代码
并且,能够指定的代理的数量是没有限度的,会依据指定的程序先后顺次执行各个代理,如果要同时运行两个代理,就能够依照上面的命令执行:
java -javaagent:myAgent1.jar -javaagent:myAgent2.jar -jar AgentTest.jar
复制代码
以咱们在idea中执行程序为例,在VM options中退出增加启动参数:
-javaagent:F:\Workspace\MyAgent\target\myAgent-1.0.jar=Hydra
-javaagent:F:\Workspace\MyAgent\target\myAgent-1.0.jar=Trunks
复制代码
执行main办法,查看输入后果:

依据执行后果的打印语句能够看出,在执行主程序前,顺次执行了两次咱们的agent代理。能够通过上面的图来示意执行代理与主程序的执行程序。

缺点
在提供便当的同时,premain模式也有一些缺点,例如如果agent在运行过程中出现异常,那么也会导致主程序的启动失败。咱们对下面例子中agent的代码进行一下革新,手动抛出一个异样。
public static void premain(String agentArgs, Instrumentation inst) {

System.out.println("premain start");System.out.println("args:"+agentArgs);throw new RuntimeException("error");

}
复制代码
再次运行主程序:

能够看到,在agent抛出异样后主程序也没有启动。针对premain模式的一些缺点,在jdk1.6之后引入了agentmain模式。
Agentmain模式
agentmain模式能够说是premain的降级版本,它容许代理的指标主程序的jvm后行启动,再通过attach机制连贯两个jvm,上面咱们分3个局部实现。
agent
agent局部和下面一样,实现简略的打印性能:
public class MyAgentMain {

public static void agentmain(String agentArgs, Instrumentation instrumentation) {    System.out.println("agent main start");    System.out.println("args:"+agentArgs);}

}
复制代码
批改maven插件配置,指定Agent-Class:
<plugin>

<groupId>org.apache.maven.plugins</groupId><artifactId>maven-jar-plugin</artifactId><version>3.1.0</version><configuration>    <archive>        <manifest>            <addClasspath>true</addClasspath>        </manifest>        <manifestEntries>            <Agent-Class>com.cn.agent.MyAgentMain</Agent-Class>            <Can-Redefine-Classes>true</Can-Redefine-Classes>            <Can-Retransform-Classes>true</Can-Retransform-Classes>        </manifestEntries>    </archive></configuration>

</plugin>
复制代码
主程序
这里咱们间接启动主程序期待代理被载入,在主程序中应用了System.in进行阻塞,避免主过程提前结束。
public class AgentmainTest {

public static void main(String[] args) throws IOException {    System.in.read();}

}
复制代码
attach机制
和premain模式不同,咱们不能再通过增加启动参数的形式来连贯agent和主程序了,这里须要借助com.sun.tools.attach包下的VirtualMachine工具类,须要留神该类不是jvm标准规范,是由Sun公司本人实现的,应用前须要引入依赖:
<dependency>

<groupId>com.sun</groupId><artifactId>tools</artifactId><version>1.8</version><scope>system</scope><systemPath>${JAVA_HOME}\lib\tools.jar</systemPath>

</dependency>
复制代码
VirtualMachine代表了一个要被附着的java虚拟机,也就是程序中须要监控的指标虚拟机,内部过程能够应用VirtualMachine的实例将agent加载到指标虚拟机中。先看一下它的静态方法attach:
public static VirtualMachine attach(String var0);
复制代码
通过attach办法能够获取一个jvm的对象实例,这里传入的参数是指标虚拟机运行时的过程号pid。也就是说,咱们在应用attach前,须要先获取方才启动的主程序的pid,应用jps命令查看线程pid:
11140
16372 RemoteMavenServer36
16392 AgentmainTest
20204 Jps
2460 Launcher
复制代码
获取到主程序AgentmainTest运行时pid是16392,将它利用于虚拟机的连贯。
public class AttachTest {

public static void main(String[] args) {    try {        VirtualMachine  vm= VirtualMachine.attach("16392");        vm.loadAgent("F:\\Workspace\\MyAgent\\target\\myAgent-1.0.jar","param");    } catch (Exception e) {        e.printStackTrace();    }}

}
复制代码
在获取到VirtualMachine实例后,就能够通过loadAgent办法能够实现注入agent代理类的操作,办法的第一个参数是代理的本地门路,第二个参数是传给代理的参数。执行AttachTest,再回到主程序AgentmainTest的控制台,能够看到执行了了agent中的代码:

这样,一个简略的agentMain模式代理就实现实现了,能够通过上面这张图再梳理一下三个模块之间的关系。

利用
到这里,咱们就曾经简略地理解了两种模式的实现办法,然而作为高质量程序员,咱们必定不能满足于只用代理单纯地打印语句,上面咱们再来看看能怎么利用Java Agent搞点实用的货色。
在下面的两种模式中,agent局部的逻辑别离是在premain办法和agentmain办法中实现的,并且,这两个办法在签名上对参数有严格的要求,premain办法容许以上面两种形式定义:
public static void premain(String agentArgs)
public static void premain(String agentArgs, Instrumentation inst)
复制代码
agentmain办法容许以上面两种形式定义:
public static void agentmain(String agentArgs)
public static void agentmain(String agentArgs, Instrumentation inst)
复制代码
如果在agent中同时存在两种签名的办法,带有Instrumentation参数的办法优先级更高,会被jvm优先加载,它的实例inst会由jvm主动注入,上面咱们就看看能通过Instrumentation实现什么性能。
Instrumentation
先大体介绍一下Instrumentation接口,其中的办法容许在运行时操作java程序,提供了诸如扭转字节码,新增jar包,替换class等性能,而通过这些性能使Java具备了更强的动态控制和解释能力。在咱们编写agent代理的过程中,Instrumentation中上面3个办法比拟重要和罕用,咱们来着重看一下。
addTransformer
addTransformer办法容许咱们在类加载之前,从新定义Class,先看一下办法的定义:
void addTransformer(ClassFileTransformer transformer);
复制代码
ClassFileTransformer是一个接口,只有一个transform办法,它在主程序的main办法执行前,装载的每个类都要通过transform执行一次,能够将它称为转换器。咱们能够实现这个办法来从新定义Class,上面就通过一个例子看看具体如何应用。
首先,在主程序工程创立一个Fruit类:
public class Fruit {

public void getFruit(){    System.out.println("banana");}

}
复制代码
编译实现后复制一份class文件,并将其重命名为Fruit2.class,再批改Fruit中的办法为:
public void getFruit(){

System.out.println("apple");

}
复制代码
创立主程序,在主程序中创立了一个Fruit对象并调用了其getFruit办法:
public class TransformMain {

public static void main(String[] args) {    new Fruit().getFruit();}

}
复制代码
这时执行后果会打印apple,接下来开始实现premain代理局部。
在代理的premain办法中,应用Instrumentation的addTransformer办法拦挡类的加载:
public class TransformAgent {

public static void premain(String agentArgs, Instrumentation inst) {    inst.addTransformer(new FruitTransformer());}

}
复制代码
FruitTransformer类实现了ClassFileTransformer接口,转换class局部的逻辑都在transform办法中:
public class FruitTransformer implements ClassFileTransformer {

@Overridepublic byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined,                        ProtectionDomain protectionDomain, byte[] classfileBuffer){    if (!className.equals("com/cn/hydra/test/Fruit"))        return classfileBuffer;    String fileName="F:\\Workspace\\agent-test\\target\\classes\\com\\cn\\hydra\\test\\Fruit2.class";    return getClassBytes(fileName);}public static byte[] getClassBytes(String fileName){    File file = new File(fileName);    try(InputStream is = new FileInputStream(file);        ByteArrayOutputStream bs = new ByteArrayOutputStream()){        long length = file.length();        byte[] bytes = new byte[(int) length];        int n;        while ((n = is.read(bytes)) != -1) {            bs.write(bytes, 0, n);        }        return bytes;    }catch (Exception e) {        e.printStackTrace();        return null;    }}

}
复制代码
在transform办法中,次要做了两件事:

因为addTransformer办法不能指明须要转换的类,所以须要通过className判断以后加载的class是否咱们要拦挡的指标class,对于非指标class间接返回原字节数组,留神className的格局,须要将类全限定名中的.替换为/
读取咱们之前复制进去的class文件,读入二进制字符流,替换原有classfileBuffer字节数组并返回,实现class定义的替换

将agent局部打包实现后,在主程序增加启动参数:
-javaagent:F:\Workspace\MyAgent\target\transformAgent-1.0.jar
复制代码
再次执行主程序,后果打印:
banana
复制代码
这样,就实现了在main办法执行前class的替换。
redefineClasses
咱们能够直观地从办法的名字上来了解它的作用,重定义class,艰深点来讲的话就是实现指定类的替换。办法定义如下:
void redefineClasses(ClassDefinition... definitions) throws ClassNotFoundException, UnmodifiableClassException;
复制代码
它的参数是可变长的ClassDefinition数组,再看一下ClassDefinition的构造方法:
public ClassDefinition(Class<?> theClass,byte[] theClassFile) {...}
复制代码
ClassDefinition中指定了的Class对象和批改后的字节码数组,简略来说,就是应用提供的类文件字节,替换了原有的类。并且,在redefineClasses办法重定义的过程中,传入的是ClassDefinition的数组,它会依照这个数组程序进行加载,以便满足在类之间相互依赖的状况下进行更改。
上面通过一个例子来看一下它的失效过程,premain代理局部:
public class RedefineAgent {

public static void premain(String agentArgs, Instrumentation inst)         throws UnmodifiableClassException, ClassNotFoundException {    String fileName="F:\\Workspace\\agent-test\\target\\classes\\com\\cn\\hydra\\test\\Fruit2.class";    ClassDefinition def=new ClassDefinition(Fruit.class,            FruitTransformer.getClassBytes(fileName));    inst.redefineClasses(new ClassDefinition[]{def});}

}
复制代码
主程序能够间接复用下面的,执行后打印:
banana
复制代码
能够看到,用咱们指定的class文件的字节替换了原有类,即实现了指定类的替换。
retransformClasses
retransformClasses利用于agentmain模式,能够在类加载之后从新定义Class,即触发类的从新加载。首先看一下该办法的定义:
void retransformClasses(Class<?>... classes) throws UnmodifiableClassException;
复制代码
它的参数classes是须要转换的类数组,可变长参数也阐明了它和redefineClasses办法一样,也能够批量转换类的定义。
上面,咱们通过例子来看看如何应用retransformClasses办法,agent代理局部代码如下:
public class RetransformAgent {

public static void agentmain(String agentArgs, Instrumentation inst)        throws UnmodifiableClassException {    inst.addTransformer(new FruitTransformer(),true);    inst.retransformClasses(Fruit.class);    System.out.println("retransform success");}

}
复制代码
看一下这里调用的addTransformer办法的定义,与下面略有不同:
void addTransformer(ClassFileTransformer transformer, boolean canRetransform);
复制代码
ClassFileTransformer转换器仍旧复用了下面的FruitTransformer,重点看一下新加的第二个参数,当canRetransform为true时,示意容许从新定义class。这时,相当于调用了转换器ClassFileTransformer中的transform办法,会将转换后class的字节作为新类定义进行加载。
主程序局部代码,咱们在死循环中一直的执行打印语句,来监控类是否产生了扭转:
public class RetransformMain {

public static void main(String[] args) throws InterruptedException {    while(true){        new Fruit().getFruit();        TimeUnit.SECONDS.sleep(5);    }}

}
复制代码
最初,应用attach api注入agent代理到主程序中:
public class AttachRetransform {

public static void main(String[] args) throws Exception {    VirtualMachine vm = VirtualMachine.attach("6380");    vm.loadAgent("F:\\Workspace\\MyAgent\\target\\retransformAgent-1.0.jar");}

}
复制代码
回到主程序控制台,查看运行后果:

能够看到在注入代理后,打印语句发生变化,阐明类的定义曾经被扭转并进行了从新加载。
其余
除了这几个次要的办法外,Instrumentation中还有一些其余办法,这里仅简略列举一下罕用办法的性能:

removeTransformer:删除一个ClassFileTransformer类转换器
getAllLoadedClasses:获取以后曾经被加载的Class
getInitiatedClasses:获取由指定的ClassLoader加载的Class
getObjectSize:获取一个对象占用空间的大小
appendToBootstrapClassLoaderSearch:增加jar包到启动类加载器
appendToSystemClassLoaderSearch:增加jar包到零碎类加载器
isNativeMethodPrefixSupported:判断是否能给native办法增加前缀,即是否可能拦挡native办法
setNativeMethodPrefix:设置native办法的前缀

Javassist
在下面的几个例子中,咱们都是间接读取的class文件中的字节来进行class的重定义或转换,然而在理论的工作环境中,可能更多的是去动静的批改class文件的字节码,这时候就能够借助javassist来更简略的批改字节码文件。
简略来说,javassist是一个剖析、编辑和创立java字节码的类库,在应用时咱们能够间接调用它提供的api,以编码的模式动静扭转或生成class的构造。绝对于ASM等其余要求理解底层虚拟机指令的字节码框架,javassist真的是非常简单和快捷。
上面,咱们就通过一个简略的例子,看看如何将Java agent和Javassist联合在一起应用。首前先引入javassist的依赖:
<dependency>

<groupId>org.javassist</groupId><artifactId>javassist</artifactId><version>3.20.0-GA</version>

</dependency>
复制代码
咱们要实现的性能是通过代理,来计算方法执行的工夫。premain代理局部和之前基本一致,先增加一个转换器:
public class Agent {

public static void premain(String agentArgs, Instrumentation inst) {    inst.addTransformer(new LogTransformer());}static class LogTransformer implements ClassFileTransformer {    @Override    public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined,                             ProtectionDomain protectionDomain, byte[] classfileBuffer)         throws IllegalClassFormatException {        if (!className.equals("com/cn/hydra/test/Fruit"))            return null;        try {            return calculate();        } catch (Exception e) {            e.printStackTrace();            return null;        }    }}

}
复制代码
在calculate办法中,应用javassist动静的扭转了办法的定义:
static byte[] calculate() throws Exception {

ClassPool pool = ClassPool.getDefault();CtClass ctClass = pool.get("com.cn.hydra.test.Fruit");CtMethod ctMethod = ctClass.getDeclaredMethod("getFruit");CtMethod copyMethod = CtNewMethod.copy(ctMethod, ctClass, new ClassMap());ctMethod.setName("getFruit$agent");StringBuffer body = new StringBuffer("{\n")        .append("long begin = System.nanoTime();\n")        .append("getFruit$agent($$);\n")        .append("System.out.println(\"use \"+(System.nanoTime() - begin) +\" ns\");\n")        .append("}");copyMethod.setBody(body.toString());ctClass.addMethod(copyMethod);return ctClass.toBytecode();

}
复制代码
在下面的代码中,次要实现了这些性能:

利用全限定名获取类CtClass
依据办法名获取办法CtMethod,并通过CtNewMethod.copy办法复制一个新的办法
批改旧办法的办法名为getFruit$agent
通过setBody办法批改复制进去办法的内容,在新办法中进行了逻辑加强并调用了旧办法,最初将新办法增加到类中

https://zhuanlan.zhihu.com/p/...
https://zhuanlan.zhihu.com/p/...

https://zhuanlan.zhihu.com/p/...

https://zhuanlan.zhihu.com/p/...

https://zhuanlan.zhihu.com/p/...

https://zhuanlan.zhihu.com/p/...

https://zhuanlan.zhihu.com/p/...

https://zhuanlan.zhihu.com/p/...