JavaAgent学习小结

前言

最近因为公司需要,需要了解下java探针,在网上找资料,发现资料还是有很多的,但是例子太少,有的直接把公司代码粘贴出来,太复杂了,有的又特别简单不是我想要的例子, 我想要这样的一个例子:

jvm在运行,我想动态修改一个类,jvm在不用重启的情况下, 自动加载新的类定义. 动态修改类定义,听着感觉就很酷. 本文将实现一个方法监控的例子, 开始方法是没有监控的, 动态修改后, 方法执行结束会打印方法耗时.

Instrumentation介绍

使用 Instrumentation,开发者可以构建一个独立于应用程序的代理程序(Agent),用来监测和协助运行在 JVM 上的程序,甚至能够替换和修改某些类的定义。有了这样的功能,开发者就可以实现更为灵活的运行时虚拟机监控和 Java 类操作了,这样的特性实际上提供了一种虚拟机级别支持的 AOP 实现方式,使得开发者无需对 JDK 做任何升级和改动,就可以实现某些 AOP 的功能了。

在 Java SE 5 中,Instrument 要求在运行前利用命令行参数或者系统参数来设置代理类,在实际的运行之中,虚拟机在初始化之时(在绝大多数的 Java 类库被载入之前),启动instrumentation 的设置,从而可以在加载字节码之前,修改类的定义。

在 Java SE6 里面,则更进一步,可以在jvm运行时,动态修改类定义,使用就更方便了,本文也主要是讲着一种方式.

Instrumentation 类 定义如下:

 1 /*有两种获取Instrumentation接口实例的方法:
 2 1.以指示代理类的方式启动JVM时。 在这种情况下,将Instrumentation实例传递给代理类的premain方法。
 3 2. JVM提供了一种在JVM启动后的某个时间启动代理的机制。 在这种情况下,将Instrumentation实例传递给代理代码的agentmain方法。
 4 这些机制在包装规范中进行了描述。
 5 代理获取某个Instrumentation实例后,该代理可以随时在该实例上调用方法。
 6 */
 7 public interface Instrumentation {
 8     //增加一个Class 文件的转换器,转换器用于改变 Class 二进制流的数据,参数 canRetransform 设置是否允许重新转换。
 9     void addTransformer(ClassFileTransformer transformer, boolean canRetransform);
10     //注册一个转换器
11     void addTransformer(ClassFileTransformer transformer);
12
13     //删除一个类转换器
14     boolean removeTransformer(ClassFileTransformer transformer);
15
16     boolean isRetransformClassesSupported();
17
18     //在类加载之后,重新定义 Class。这个很重要,该方法是1.6 之后加入的,事实上,该方法是 update 了一个类。
19     void retransformClasses(Class<?>... classes) throws UnmodifiableClassException;
20
21     boolean isRedefineClassesSupported();
22     /*此方法用于替换类的定义,而无需引用现有的类文件字节,除了在常规JVM语义下会发生的初始化之外,此方法不会引起任何初始化。换句话说,重新定义类不会导致其初始化程序运行。静态变量的值将保持调用前的状态。
23 重新定义的类的实例不受影响。*/
24     void redefineClasses(ClassDefinition... definitions)
25         throws  ClassNotFoundException, UnmodifiableClassException;
26
27     boolean isModifiableClass(Class<?> theClass);
28     //获取所有已经加载的类
29     @SuppressWarnings("rawtypes")
30     Class[] getAllLoadedClasses();
31
32     @SuppressWarnings("rawtypes")
33     Class[] getInitiatedClasses(ClassLoader loader);
34     //获取一个对象的大小
35     long getObjectSize(Object objectToSize);
36
37     void appendToBootstrapClassLoaderSearch(JarFile jarfile);
38
39     void appendToSystemClassLoaderSearch(JarFile jarfile);
40     boolean isNativeMethodPrefixSupported();
41     void setNativeMethodPrefix(ClassFileTransformer transformer, String prefix);
42 }
  • 其中addTransformer 和 retransformClasses 是有关联的, addTransformer 注册转换器,retransformClasses 触发转换器.
  • redefineClass是除了Transformer 之外另外一中转变类定义的方式.

Instrument的两种方式

第一种: JVM启动前静态Instrument

使用Javaagent命令启动代理程序。参数 javaagent 可以用于指定一个 jar 包,并且对该 java 包有2个要求:

  1. 这个 jar 包的 MANIFEST.MF 文件必须指定 Premain-Class 项。
  2. Premain-Class 指定的那个类必须实现 premain() 方法。

premain 方法,从字面上理解,就是运行在 main 函数之前的的类。当Java 虚拟机启动时,在执行 main 函数之前,JVM 会先运行-javaagent所指定 jar 包内 Premain-Class 这个类的 premain 方法 。

在命令行输入 java可以看到相应的参数,其中有 和 java agent相关的:

-agentlib:<libname>[=<选项>] 加载本机代理库 <libname>, 例如 -agentlib:hprof
    另请参阅 -agentlib:jdwp=help 和 -agentlib:hprof=help
-agentpath:<pathname>[=<选项>]
    按完整路径名加载本机代理库
-javaagent:<jarpath>[=<选项>]
    加载 Java 编程语言代理, 请参阅 java.lang.instrument

从本质上讲,Java Agent 是一个遵循一组严格约定的常规 Java 类。 上面说到 javaagent命令要求指定的类中必须要有premain()方法,并且对premain方法的签名也有要求,签名必须满足以下两种格式:

public static void premain(String agentArgs, Instrumentation inst)

public static void premain(String agentArgs)

JVM 会优先加载 带 Instrumentation 签名的方法,加载成功忽略第二种,如果第一种没有,则加载第二种方法。这个逻辑在sun.instrument.InstrumentationImpl 类中.

如何使用javaagent?

使用 javaagent 需要几个步骤:

  1. 定义一个 MANIFEST.MF 文件,必须包含 Premain-Class 选项,通常也会加入Can-Redefine-Classes 和 Can-Retransform-Classes 选项。
  2. 创建一个Premain-Class 指定的类,类中包含 premain 方法,方法逻辑由用户自己确定。
  3. 将 premain 的类和 MANIFEST.MF 文件打成 jar 包。
  4. 使用参数 -javaagent: jar包路径 启动要代理的方法。

在执行以上步骤后,JVM 会先执行 premain 方法,大部分类加载都会通过该方法,注意:是大部分,不是所有。当然,遗漏的主要是系统类,因为很多系统类先于 agent 执行,而用户类的加载肯定是会被拦截的。也就是说,这个方法是在 main 方法启动前拦截大部分类的加载活动,既然可以拦截类的加载,那么就可以去做重写类这样的操作,结合第三方的字节码编译工具,比如ASM,javassist,cglib等等来改写实现类。

MANIFREST.MF文件的常用配置:

Premain-Class :包含 premain 方法的类(类的全路径名)

Agent-Class :包含 agentmain 方法的类(类的全路径名)

Boot-Class-Path :设置引导类加载器搜索的路径列表。查找类的特定于平台的机制失败后,引导类加载器会搜索这些路径。按列出的顺序搜索路径。列表中的路径由一个或多个空格分开。路径使用分层 URI 的路径组件语法。如果该路径以斜杠字符(“/”)开头,则为绝对路径,否则为相对路径。相对路径根据代理 JAR 文件的绝对路径解析。忽略格式不正确的路径和不存在的路径。如果代理是在 VM 启动之后某一时刻启动的,则忽略不表示 JAR 文件的路径。(可选)

Can-Redefine-Classes :true表示能重定义此代理所需的类,默认值为 false(可选)

Can-Retransform-Classes :true 表示能重转换此代理所需的类,默认值为 false (可选)

Can-Set-Native-Method-Prefix: true表示能设置此代理所需的本机方法前缀,默认值为 false(可选)

列举一个premain 的例子:

 1 public class PreMainTraceAgent {
 2     public static void premain(String agentArgs, Instrumentation inst) {
 3         System.out.println("agentArgs : " + agentArgs);
 4         inst.addTransformer(new DefineTransformer(), true);
 5     }
 6
 7     static class DefineTransformer implements ClassFileTransformer{
 8         @Override
 9         public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException {
10             System.out.println("premain load Class:" + className);
11             return classfileBuffer;
12         }
13     }
14 }

由于本文不关注这种静态Instrumentation的方式,这里只是做简介,感兴趣的可以去搜索下.

第二种动态Instrumentation的方式

在 Java SE 6 的 Instrumentation 当中,有一个跟 premain“并驾齐驱”的“agentmain”方法,可以在 main 函数开始运行之后再运行。

跟 premain 函数一样, 开发者可以编写一个含有“agentmain”函数的 Java 类:

由于本文不关注这种静态Instrumentation的方式,这里只是做简介,感兴趣的可以去搜索下.
第二种动态Instrumentation的方式

在 Java SE 6 的 Instrumentation 当中,有一个跟 premain“并驾齐驱”的“agentmain”方法,可以在 main 函数开始运行之后再运行。
跟 premain 函数一样, 开发者可以编写一个含有“agentmain”函数的 Java 类:

跟 premain 函数一样,开发者可以在 agentmain 中进行对类的各种操作。其中的 agentArgs 和 Inst 的用法跟 premain 相同。

与“Premain-Class”类似,开发者必须在 manifest 文件里面设置“Agent-Class”来指定包含 agentmain 函数的类。

可是,跟 premain 不同的是,agentmain 需要在 main 函数开始运行后才启动,至于该方法如何运行,怎么跟正在运行的jvm 关联上, 就需要介绍下Attach API.

Attach API 不是 Java 的标准 API,而是 Sun 公司提供的一套扩展 API,用来向目标 JVM ”附着”(Attach)代理工具程序的。有了它,开发者可以方便的监控一个 JVM,运行一个外加的代理程序。

Attach API 很简单,只有 2 个主要的类,都在 com.sun.tools.attach 包里面: VirtualMachine 代表一个 Java 虚拟机,也就是程序需要监控的目标虚拟机,提供了 JVM 枚举,Attach 动作和 Detach 动作(Attach 动作的相反行为,从 JVM 上面解除一个代理)等等 ; VirtualMachineDescriptor 则是一个描述虚拟机的容器类,配合 VirtualMachine 类完成各种功能。

下边我们利用上边说的实现一个监控方法执行耗时的例子: 定时执行一个方法,开始方法是没有监控的, 方法重定义加上监控。

一个简单的方法监控例子

那么我们想一下需要实现这个例子,需要几个模块.

  • 一个代理模块(监控逻辑);
  • 一个main函数(运行的jvm);
  • 一个把上边两个模块关联在一起的程序.

从代理模块开始:

1. 需要监控的TimeTest类:

/**
 * @ClassName TimeTest
 * @Author jiangyuechao
 * @Date 2020/1/20-10:36
 * @Version 1.0
 */
public class TimeTest {

    public static void sayHello( ){
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("sayhHello..........");
    }

    public static void sayHello2(String word){
        try {
            Thread.sleep(2000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("sayhHello2.........."+word);
    }
}

2. 编写agent 代码

字节码转换类:

 1 public class MyTransformer implements ClassFileTransformer {
 2
 3     // 被处理的方法列表
 4     final static Map<String, List<String>> methodMap = new HashMap<String, List<String>>();
 5
 6     public MyTransformer() {
 7         add("com.chaochao.java.agent.TimeTest.sayHello");
 8         add("com.chaochao.java.agent.TimeTest.sayHello2");
 9     }
10
11     private void add(String methodString) {
12         String className = methodString.substring(0, methodString.lastIndexOf("."));
13         String methodName = methodString.substring(methodString.lastIndexOf(".") + 1);
14         List<String> list = methodMap.get(className);
15         if (list == null) {
16             list = new ArrayList<String>();
17             methodMap.put(className, list);
18         }
19         list.add(methodName);
20     }
21
22     @Override
23     public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined,
24                             ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException {
25         System.out.println("className:"+className);
26         if (methodMap.containsKey(className)) {// 判断加载的class的包路径是不是需要监控的类
27             try {
28                 ClassPool classPool=new ClassPool();
29                 classPool.insertClassPath(new LoaderClassPath(loader));
30                 CtClass ctClass= classPool.get(className.replace("/","."));
31 //                CtMethod ctMethod= ctClass.getDeclaredMethod("run");
32                 CtMethod[] declaredMethods = ctClass.getDeclaredMethods();
33                 for (CtMethod ctMethod : declaredMethods) {
34                        //插入本地变量
35                     ctMethod.addLocalVariable("begin",CtClass.longType);
36                     ctMethod.addLocalVariable("end",CtClass.longType);
37
38                     ctMethod.insertBefore("begin=System.currentTimeMillis();System.out.println(\"begin=\"+begin);");
39                     //前面插入:最后插入的放最上面
40                     ctMethod.insertBefore("System.out.println( \"埋点开始-1\" );");
41
42                     ctMethod.insertAfter("end=System.currentTimeMillis();System.out.println(\"end=\"+end);");
43                     ctMethod.insertAfter("System.out.println(\"性能:\"+(end-begin)+\"毫秒\");");
44
45                     //后面插入:最后插入的放最下面
46                     ctMethod.insertAfter("System.out.println( \"埋点结束-1\" );");
47                 }
48                 return ctClass.toBytecode();
49             }  catch (NotFoundException | CannotCompileException|IOException e) {
50                 e.printStackTrace();
51             }
52             return new byte[0];
53         }
54         else
55              System.out.println("没找到.");
56         return null;
57     }
58
59 }

上边的类就是在方法前后加上耗时打印.

下边是定义的AgentMainTest:

import java.lang.instrument.Instrumentation;

public class AgentMainTest {
   //关联后执行的方法
    public static void agentmain(String args, Instrumentation inst) throws Exception {
        System.out.println("Args:" + args);
        Class[] classes = inst.getAllLoadedClasses();
        for (Class clazz : classes)
        {
           System.out.println(clazz.getName());
        }
        System.out.println("开始执行自定义MyTransformer");
        // 添加Transformer
        inst.addTransformer(new MyTransformer(),true);

        inst.retransformClasses(TimeTest.class);
    }

    public static void premain(String args, Instrumentation inst) throws Exception
    {
        System.out.println("Pre Args:" + args);
        Class[] classes = inst.getAllLoadedClasses();
        for (Class clazz : classes)
        {
           System.out.println(clazz.getName());
        }
    }
}

MANIFREST.MF文件定义,注意最后一行是空格:

Manifest-Version: 1.0
Premain-Class: com.chaochao.java.agent.AgentMainTest
Agent-Class: com.chaochao.java.agent.AgentMainTest
Can-Redefine-Classes: true
Can-Retransform-Classes: true

代理模块介绍完毕, 下边是一个main函数程序.这个就很简单了.

 1 public class TestMan {
 2
 3     public static void main(String[] args) throws InterruptedException
 4     {
 5         TimeTest tt = new TimeTest();
 6         tt.sayHello();
 7         tt.sayHello2("one");
 8         while(true)
 9         {
10             Thread.sleep(60000);
11             new Thread(new WaitThread()).start();
12             tt.sayHello();
13             tt.sayHello2("two");
14         }
15     }
16
17    static class WaitThread implements Runnable
18    {
19         @Override
20         public void run()
21         {
22             System.out.println("Hello");
23         }
24    }
25 }

最后一个关联模块:

/**
 *
 * @author jiangyuechao
 *
 */
public class AttachMain {

    public static void main(String[] args) throws Exception{
        VirtualMachine vm = null;
        String pid = null;
        List<VirtualMachineDescriptor> list = VirtualMachine.list();
        for (VirtualMachineDescriptor vmd : list)
        {
            System.out.println("pid:" + vmd.id() + ":" + vmd.displayName());
            if(vmd.displayName().contains("TestMan")) {
                pid = vmd.id();
            }
        }
        //E:\eclipse-workspace\JavaStudyAll\JVMStudy\target
       // String agentjarpath = "E:/jee-workspace/javaAgent/TestAgent.jar"; //agentjar路径
        String agentjarpath = "E:/jee-workspace/javaAgent/AgentMainTest.jar"; //agentjar路径
        vm = VirtualMachine.attach(pid);//目标JVM的进程ID(PID)
        vm.loadAgent(agentjarpath, "This is Args to the Agent.");
        vm.detach();
      }

}

也很简单, 第一步获取pid ,第二步使用attach 方法关联jvm.

上便代码准备好了,那么怎么把他们运行起来呢, 需要几步:

  1. 先把agent 代码打包为jar 包
  2. 运行main 函数,执行agent

agent 打包

把agent代码打包为普通的jar 包即可, 使用eclipse或intellij 都可以. 以eclipse 为例,只需要注意一步使用你写好的MANIFREST文件

但是我推荐使用另外一种方式,命令行的方式, 使用java 命令行直接来的, 既方便又快捷.

首先把需要的类放在一个文件夹下, javac编译:

javac -encoding UTF-8 -classpath .;E:\tools\jdk1.8.0_65\lib\tools.jar;E:\eclipse-workspace\JavaStudyAll\JVMStudy\lib\javassist.jar; AgentMainTest.java MyTransformer.java

其中需要依赖tools.jar和 javassist jar包.

编译后的class文件打包为jar包:

jar cvmf MANIFEST.MF AgentMainTest.jar AgentMainTest.class MyTransformer.class

如下所示:

agent包准备好之后, 就简单了,先运行main函数,启动一个虚拟机. 运行入下:

sayhHello..........
sayhHello2..........one

运行AttachMain 类,关联agent程序,就会看到如下的输出:

可以看到 在方法执行结束后, 已经有了耗时的打印. 测试成功.

Instrumentation的局限性

大多数情况下,我们使用Instrumentation都是使用其字节码插桩的功能,或者笼统说就是类重定义(Class Redefine)的功能,但是有以下的局限性:

  1. premain和agentmain两种方式修改字节码的时机都是类文件加载之后,也就是说必须要带有Class类型的参数,不能通过字节码文件和自定义的类名重新定义一个本来不存在的类。
  2. 类的字节码修改称为类转换(Class Transform),类转换其实最终都回归到类重定义Instrumentation#redefineClasses()方法,此方法有以下限制:
    1. 新类和老类的父类必须相同;
    2. 新类和老类实现的接口数也要相同,并且是相同的接口;
    3. 新类和老类访问符必须一致。 新类和老类字段数和字段名要一致;
    4. 新类和老类新增或删除的方法必须是private static/final修饰的;
    5. 可以修改方法体。

除了上面的方式,如果想要重新定义一个类,可以考虑基于类加载器隔离的方式:创建一个新的自定义类加载器去通过新的字节码去定义一个全新的类,不过也存在只能通过反射调用该全新类的局限性。

参考:

https://www.cnblogs.com/rickiyang/p/11368932.html

https://www.ibm.com/developerworks/cn/java/j-lo-jse61/index.html

转发请注明出处: https://www.cnblogs.com/jycboy/p/12249472.html

原文地址:https://www.cnblogs.com/jycboy/p/12249472.html

时间: 2024-11-25 17:57:46

JavaAgent学习小结的相关文章

git学习小结

背景:最近因为工作原因,需要将以前的代码库由bitbucket重新布置在一台服务器上,所以就学习了下git,特此记录下 在167这台机器上搭建apache,用做git server,由于以前apache都已经搭好了,所以这里只配置git server 就可以了,此处贴出配置: 服务器搭好了,来到配置中的root目录,git clone https://[email protected]/XXXX 此时,库和服务器都搭好了,用于新库测试的机器也可以从git server上克隆库了,来,我们来试试从

网络编程学习小结

几种网络编程方式: ISAPI.CGI.WinInet.Winsock 它们之间的差别: 1)  ISAPI主要是开发基于浏览器client与server端程序.效率比CGI方式高,并且也扩展了CGI没有的一些功能.(基于TCP/IP模型中的应用层) 2)  CGI主要是开发基于浏览器client与server端程序.(基于TCP/IP模型中的应用层) 3)  WinInet主要是开发client程序.(基于TCP/IP模型中的应用层) 4)  Winsock主要是基于socket来开发clie

MogileFS学习小结

大纲: 一.关于MogileFS 二.常见分布式文件系统 三.MogileFS基本原理 四.MogileFS的实现 一.关于MogileFS 当下我们处在一个互联网飞速发展的信息社会,在海量并发连接的驱动下每天所产生的数据量必然以几何方式增长,随着信息连接方式日益多样化,数据存储的结构也随着发生了变化.在这样的压力下使得人们不得不重新审视大量数据的存储所带来的挑战,例如:数据采集.数据存储.数据搜索.数据共享.数据传输.数据分析.数据可视化等一系列问题. 传统存储在面对海量数据存储表现出的力不从

201671010130 2016-2017-2 《Java程序设计》第四周学习小结

第四周学习小结 本次实验巩固了上次实验分隔数并求和的题,目前这个题有两种做法,一种是不断对数10求余,余数保存在sum中,然后左移一位,直到余数为零.另一种就是将数字强制转换成一个字符串数组String s=String.valueOf(num),根据方法s.toCharArray()将字符分离出来,据"x"-"0"=x,unicode码值相减即可得x的值. 父类和子类能够看两个交集,super关键字是否能够看做一个子类和超类的接口呢? 在子类中可以增加域.增加方法

初识ASP.NET---点滴的积累---ASP.NET学习小结

差不多十多天前学习完了北大青鸟的学习视频,没想到没几天的时间就看完了XML视频和牛腩的Javascript视频.学习完了也该总结总结,理理自己的思路,消化一下自己学习到的东西. 视频中的理论知识并不是很多,以例子驱动学起来也不会他过于乏味.全部的学习内容大概的可以用下图表示. 个人感觉这套视频的体系感不是很强,每一集之间老师的串联并不是做得很好,向我等没有教材的有些小的知识无从知晓.但是不能不说这套视频确很适合初学者学习,老师讲解的也不错,从此我也算是入门. 当然要想进一步的了解ASP.NET并

8086汇编学习小结———实时更新

初学IBM-PC 8086,对INT指令不是很理解.现从网上总计如下: 表:DOS系统功能调INT 21H AH 功能 调用参数 返回参数 00 程序终止(同INT 20H) CS=程序段前缀 01 键盘输入并回显 AL=输入字符 02 显示输出 DL=输出字符 03 异步通迅输入 AL=输入数据 04 异步通迅输出 DL=输出数据 05 打印机输出 DL=输出字符 06 直接控制台I/O DL=FF(输入)DL=字符(输出) AL=输入字符 07 键盘输入(无回显) AL=输入字符 08 键盘

《Pro AngularJS》学习小结-01

<Pro AngularJS>该书以一个SportsStore案例为主线铺开. 一.开发环境设置 该书中所用的server开发环境是Deployed,从来没听说过,而且作者也说该server没什么人用,我干脆弃用之.其他的环境包括 NodeJS--这个必须装 karma--测试环境,前期还没有用到,以后认真研究,毕竟AngularJS一大特点是Unit Test bootstrap--这个现在应该普遍使用了,O(∩_∩)O webstorm--现在唯一支持AngularJS插件的IDE 我基本

自动化测试Selenium Webdriver (JAVA)学习小结

自动化测试--Selenium学习小结 一.自动化测试的概念及意义: 1.什么是自动化测试: 一般是指软件测试的自动化,软件测试就是在预设条件下运行系统或应用程序,评估运行结果,预先条件应包括正常条件和异常条件. 2.意义: 让测试更有效率,利用更多的空余时间,减少人力资源. 二.selenium工具 我用的是java语言,所以接下来的例子和方法都是基于java的. 1.环境配置 (1)Jdk的配置: 我用的是1.7的jdk,配置方法都一样,新建一个JAVA_HOME,把你装好的jdk的路径复制

点滴的积累---J2SE学习小结

点滴的积累---J2SE学习小结 什么是J2SE J2SE就是Java2的标准版,主要用于桌面应用软件的编程:包括那些构成Java语言核心的类.比方:数据库连接.接口定义.输入/输出.网络编程. 学习感受 近半个月的坎坷,总算是将马士兵的<J2SE教程>视频看完了,期间一些其它的事一些不得不处理的事总是打断我的安排.看了视频之后认为东西确实都非常基础给我印象最深的是关于程序执行的内存分析.IO和线程,这谁在之前不管是学习VB.VB.NET还是C#中都没怎么设计到的东西. 首先,我想对于一个初学