Javaagent学习

最近团队在搭建开源的监控系统,使用到了这个工具,突然发现这个工具设计很优雅,对要监控的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.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的同学起到一个启蒙的作用。

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

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值