Javaagent学习

来源:互联网 发布:苹果远程桌面连接软件 编辑:程序博客网 时间:2024/06/07 13:20

最近团队在搭建开源的监控系统,使用到了这个工具,突然发现这个工具设计很优雅,对要监控的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.0premain-class: com.ws.demo.javaagent.agent.PremainAgentArchiver-Version: Plexus ArchiverBuilt-By: AdministratorClass-Path: lib/javassist-3.8.0.GA.jarCreated-By: Apache Maven 3.0.5Build-Jdk: 1.8.0_144Main-Class: com.ws.demo.javaagent.Main

如果你使用的是Maven,可以使用maven的插件来实现,这样MANIFEST.MF文件中自动添加了相应的配置。

<plugin>    <groupId>org.apache.maven.plugins</groupId>    <artifactId>maven-jar-plugin</artifactId>    <version>2.6</version>    <configuration>        <archive>            <manifest>                <addClasspath>true</addClasspath>                <classpathPrefix>lib/</classpathPrefix>                <mainClass>com.ws.demo.javaagent.Main</mainClass>            </manifest>            <manifestEntries>                <premain-class>com.ws.demo.javaagent.agent.PremainAgent</premain-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中配置:

 <manifestEntries>                            <premain-class>com.ws.demo.javaagent.agent.PremainAgent</premain-class>                            <Boot-Class-Path>E:/.m2/repository/jboss/javassist/3.8.0.GA/javassist-3.8.0.GA.jar</Boot-Class-Path>                        </manifestEntries>

结果:

这里写图片描述

看,所有方法的执行时间都被打印出来了,我们在没修改主程序的任何代码就实现了AOP,是不是很神奇。如果大家想更多地了解Instrumentation,可以参考:

http://blog.csdn.net/songshuaiyang/article/details/50732345

~~~好了,先写这么多,自己学习的同时,也希望可以对刚接触javaagent的同学起到一个启蒙的作用。

如有写的不妥当的地方,还望指正,谢谢!