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 

 

posted @ 2020-02-01 18:35  超超boy  阅读(1505)  评论(0编辑  收藏  举报