JavaAgent学习小结

前言

最近由于公司须要,须要了解下java探针,在网上找资料,发现资料仍是有不少的,可是例子太少,有的直接把公司代码粘贴出来,太复杂了,有的又特别简单不是我想要的例子, 我想要这样的一个例子:html

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

Instrumentation介绍

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

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

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

Instrumentation 类 定义以下:jvm

 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 方法 。ide

在命令行输入 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. 能够修改方法体。

除了上面的方式,若是想要从新定义一个类,能够考虑基于类加载器隔离的方式:建立一个新的自定义类加载器去经过新的字节码去定义一个全新的类,不过也存在只能经过反射调用该全新类的局限性。

参考:

http://www.javashuo.com/article/p-dzoqxtxo-gu.html

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

 

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

相关文章
相关标签/搜索