zoukankan      html  css  js  c++  java
  • Java Instrumentation


    说明:本博文是博主学习 Instrumentation 历程的总结,整合了学习过程中参考的关于Instrumentation 的教程,并加入博主自己的见解和实例。

    参考链接:

    Instrumentation 新功能

    JDK源码-java.lang.instrument-第一部分-源码学习


    Instrumentation 简介

      利用 Java 代码,即 java.lang.instrument 做动态 Instrumentation 是 Java SE 5 的新特性,它把 Java 的 instrument 功能从本地代码中解放出来,使之可以用 Java 代码的方式解决问题。使用 Instrumentation,开发者可以构建一个独立于应用程序的代理程序(Agent),用来监测和协助运行在 JVM 上的程序,甚至能够替换和修改某些类的定义。有了这样的功能,开发者就可以实现更为灵活的运行时虚拟机监控和 Java 类操作了,这样的特性实际上提供了一种虚拟机级别支持的 AOP 实现方式,使得开发者无需对 JDK 做任何升级和改动,就可以实现某些 AOP 的功能了。

      在 Java SE 6 里面,instrumentation 包被赋予了更强大的功能:启动后的 instrument、本地代码(native code)instrument,以及动态改变 classpath 等等。这些改变,意味着 Java 具有了更强的动态控制、解释能力,它使得 Java 语言变得更加灵活多变。

      “java.lang.instrument”包的具体实现,依赖于 JVMTI。JVMTI(Java Virtual Machine Tool Interface)是一套由 Java 虚拟机提供的,为 JVM 相关的工具提供的本地编程接口集合。JVMTI 是从 Java SE 5 开始引入,整合和取代了以前使用的 Java Virtual Machine Profiler Interface (JVMPI) 和 the Java Virtual Machine Debug Interface (JVMDI),而在 Java SE 6 中,JVMPI 和 JVMDI 已经消失了。JVMTI 提供了一套”代理”程序机制,可以支持第三方工具程序以代理的方式连接和访问 JVM,并利用 JVMTI 提供的丰富的编程接口,完成很多跟 JVM 相关的功能

      学习 Java Instrumentation,首先应该了解 JVMTI 的基本知识,此处推荐 IBM 的一篇文章 JVMTI 和 Agent 实现 ,文章写得相当贴切。

    Instrumentation 源码简介

    java.lang.instrument包结构



    代理监控JVM运行的JAVA程序,对字节码修改


    ClassFileTransformer(接口)

    //转换类文件的代理接口
    public interface ClassFileTransformer { 
        //protectionDomain - 要定义或重定义的类的保护域
        //classfileBuffer - 类文件格式的输入字节缓冲区(不得修改)
        byte[]
        transform(  ClassLoader         loader,
                    String              className,
                    Class<?>            classBeingRedefined,
                    ProtectionDomain    protectionDomain,
                    byte[]              classfileBuffer)
            throws IllegalClassFormatException;
    }

    ###Instrumentation(接口)

    Java SE 6新特性,
    获取 Instrumentation 接口的实例有两种方式:
    
    1. 当 JVM 以指示一个代理类的方式启动时,将传递给代理类的 premain 方法一个 Instrumentation 实例。 
    
    2. 当 JVM 提供某种机制在 JVM 启动之后某一时刻启动代理时,将传递给代理代码的 agentmain 方法一个 Instrumentation 实例

    源码解释:

    //提供检测 Java 编程语言代码所需的服务。检测是向方法中添加字节码
    //以搜集各种工具所使用的数据
    public interface Instrumentation {
        /**
         *  注册提供的转换器。
         */
        void
        addTransformer(ClassFileTransformer transformer, boolean canRetransform);
    
        /**
         * 注册提供的转换器。
         */
        void
        addTransformer(ClassFileTransformer transformer);
    
        /**
         * 注销提供的转换器
         */
        boolean
        removeTransformer(ClassFileTransformer transformer);
    
        /**
         * 返回当前 JVM 配置是否支持类的重转换。
         */
        boolean
        isRetransformClassesSupported();
    
        /**
         *  重转换提供的类集
         */
        void
        retransformClasses(Class<?>... classes) throws UnmodifiableClassException;
    
        /**
         * 返回当前 JVM 配置是否支持类的重定义
         */
        boolean
        isRedefineClassesSupported();
    
        /**
         * 使用提供的类文件重定义提供的类集
         */
        void
        redefineClasses(ClassDefinition... definitions)
            throws  ClassNotFoundException, UnmodifiableClassException;
    
    
        /**
         * 确定一个类是否可以被 retransformation 或 redefinition 修改。
         */
        boolean
        isModifiableClass(Class<?> theClass);
    
        /**
         * 返回 JVM 当前加载的所有类的数组
         */
        Class[]
        getAllLoadedClasses();
    
        /**
         * 返回所有初始化加载器是 loader 的类的数组。
         */
        Class[]
        getInitiatedClasses(ClassLoader loader);
    
        /**
         * 返回指定对象使用的特定于实现的近似存储量。
         */
        long
        getObjectSize(Object objectToSize);
    
    
        /**
         * 指定 JAR 文件,检测类由引导类加载器定义
         */
        void
        appendToBootstrapClassLoaderSearch(JarFile  jarfile);
    
        /**
         * 指定 JAR 文件,检测类由系统类加载器定义。
         */
        void
        appendToSystemClassLoaderSearch(JarFile jarfile);
    
        /**
         * 返回当前 JVM 配置是否支持设置本机方法前缀。
         */
        boolean
        isNativeMethodPrefixSupported();
    
        /**
         * 通过允许重试,将前缀应用到名称,此方法修改本机方法解析的失败处理
         */
        void setNativeMethodPrefix(ClassFileTransformer transformer, String prefix);
    }

    IllegalClassFormatException(异常)

    ClassFileTransformer.transform 的实现抛出该异常。抛出此异常的原因或者由于初始类文件字节无效,或者由于以前应用的转换损坏了字节


    UnmodifiableClassException(异常)

    在无法修改指定类之一时,由 Instrumentation.redefineClasses 的实现抛出此异常。


    ClassDefinition

    public final class ClassDefinition { 
    /** 
    * 自身class 
    */ 
    private final Class mClass;
    /**
     *  本地class文件
     */
    private final   byte[]  mClassFile;
    
    /**
     * 使用提供的类和类文件字节创建一个新的 ClassDefinition 绑定
     */
    public
    ClassDefinition(    Class<?> theClass,
                        byte[]  theClassFile) {
        if (theClass == null || theClassFile == null) {
            throw new NullPointerException();
        }
        mClass      = theClass;
        mClassFile  = theClassFile;
    }
    
    /**
     * 返回该类。
     */
    public Class<?>
    getDefinitionClass() {
        return mClass;
    }
    
    /**
     * 返回包含新的类文件的 byte 数组。
     */
    public byte[]
    getDefinitionClassFile() {
        return mClassFile;
    }
    }

    Instrumentation 基本功能和用法

      功能:通过代理,在main函数运行前或后动态的改变类的定义和其他处理操作

      具体包括:premain ,agentmain,动态改变 classpath,本地方法prefix等等,下面进行详细介绍:

    1. premain

    开发者可以让 Instrumentation 代理在 main 函数运行前执行:

    1.1 包中的类

    1.2 定义TransClass 类

    package wqz.zoom.test;
    
    public class TransClass {
        public void sayHello() {
            System.out.println("Hello!!!");
        }
    

    在此类做加载时,进行处理。

    1.3 定义 Transformer 类

    package wqz.zoom.test;
    
    import java.io.IOException;
    import java.lang.instrument.ClassFileTransformer;
    import java.lang.instrument.IllegalClassFormatException;
    import java.security.ProtectionDomain;
    import javassist.CannotCompileException;
    import javassist.ClassPool;
    import javassist.CtClass;
    import javassist.CtMethod;
    import javassist.NotFoundException;
    
    public class Transformer implements ClassFileTransformer{
    
        @Override
        public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined,
            ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException {
            // TODO Auto-generated method stub
            System.out.println("transform()");
            if(className.equals("wqz/zoom/test/TransClass")) {
                ClassPool classPool = ClassPool.getDefault();
                
                try {
                    CtClass class1 = classPool.get(className.replaceAll("/", "."));
                    CtMethod ctMethod = class1.getDeclaredMethod("sayHello");
                    if(!ctMethod.isEmpty()) {
                        ctMethod.insertBefore("System.out.println("before hello!!!");");
                    }
                    return class1.toBytecode();
                } catch (NotFoundException | CannotCompileException e) {
                    // TODO Auto-generated catch block
                    e.printStackTrace();
                } catch (IOException e) {
                    // TODO Auto-generated catch block
                    e.printStackTrace();
                }
            }
            return null;
        }
    }

      这个类实现了 ClassFileTransformer 接口。ClassFileTransformer 当中规定的 transform 方法则完成类定义的替换转换。

      此处使用 javassist 技术对字节码进行处理,对TransClass类的sayHello方法体前插入一行代码

    ctMethod.insertBefore("System.out.println("before hello!!!");");

      javassist 是功能十分强大的字节码处理工具,其使用参考 javassist使用简介

      需要将javassist的jar包导入到形目中。

    Premain 代码中:

    inst.addTransformer(new Transformer());

      addTransformer 方法并没有指明要转换哪个类。转换发生在 premain 函数执行之后,main 函数执行之前,这时每装载一个类,transform 方法就会执行一次,看看是否需要转换,所以,在 transform(Transformer 类中)方法中,程序用 ClassName.equals("wqz/zoom/test/TransClass") 来判断当前的类是否需要转换。此处需要注意className的形式。

     

    1.4 定义 Premain 类(类名称可自定义)

    编写一个 Java 类,包含如下两个方法当中的任何一个

    public static void premain(String agentArgs, Instrumentation inst);  //[1]
    public static void premain(String agentArgs); //[2]

    其中,[1] 的优先级比 [2] 高,将会被优先执行([1] 和 [2] 同时存在时,[2] 被忽略)。

    在这个 premain 函数中,开发者可以进行对类的各种操作。

    agentArgs 是 premain 函数得到的程序参数,随同 “– javaagent”一起传入。与 main 函数不同的是,这个参数是一个字符串而不是一个字符串数组,如果程序参数有多个,程序将自行解析这个字符串。

    Inst 是一个 java.lang.instrument.Instrumentation 的实例,由 JVM 自动传入。java.lang.instrument.Instrumentation 是 instrument 包中定义的一个接口,也是这个包的核心部分,集中了其中几乎所有的功能方法,例如类定义的转换和操作等等。

    package wqz.zoom.test;
    
    import java.lang.instrument.Instrumentation;
    import java.lang.instrument.UnmodifiableClassException;
    
    public class Premain {
        public static void premain(String agentArgs,Instrumentation inst) throws ClassNotFoundException,UnmodifiableClassException{
            inst.addTransformer(new Transformer());
            System.out.println("premain ok!");
        }
    }

    1.4 定义测试类 TestMainInJar

    package wqz.zoom.test;
    
    public class TestMainInJar {
        public static void main(String[] args) {
            System.out.println("TestMainInJar main()");
            new TransClass().sayHello();
        }
    }

    premain函数会在此main函数执行前执行。

    1.5 instrumentation 打包运行

    instrumentation应用需要打包执行(eclipse/cmd 两种打 jar 包的方式,自行百度)

    将wqz.zoom.test 包 打包成 test.jar, 并将javassist的jar包放于同一目录

    使用压缩工具打开test.jar,编辑 MANIFEST.MF并保存

    添加如下配置:

    Class-Path: javassist-3.15.0-GA.jar
    Premain-Class: wqz.zoom.test.Premain

    运行jar:

    java -javaagent:test.jar -cp test.jar wqz.zoom.test.TestMainInJar

    运行结果:


    重定义

    redefineClasses(ClassDefinition... definitions) 用法

    public class Premain { 
       public static void premain(String agentArgs, Instrumentation inst) 
               throws ClassNotFoundException, UnmodifiableClassException { 
           ClassDefinition def = new ClassDefinition(TransClass.class,bytes);//bytes 为TranClass的字节码数组(修改前或修改后) 
           inst.redefineClasses(new ClassDefinition[] { def }); 
           System.out.println("success"); 
       } 
    }
    ClassDefinition def = new ClassDefinition(TransClass.class,bytes);//bytes 为TranClass的字节码数组(修改前或修改后)

    此字节数组参数可与 javassist 配合使用,传入javassist修改后的数组。

    例:如果要对一个已经加载的类进行重定义,从原理上讲需要提供已经修改过的字节码,这个修改过的字节码就来源于bytes(此 bytes 可以由javassist获取原ctclass,在此基础上进行编辑与修改,获取修改后的bytes,传入ClassDefinition)。

    【注意事项】详细阅读 jdk文档!!!

     1. 仅当 Can-Retransform-Classes 清单属性在代理 JAR 文件中设置为 true(如包规范所述),且 JVM 支持此性能时,才支持重转换。

    inst.isRedefineClassesSupported();//返回当前 JVM 配置是否支持类的重定义

    2. 如果重定义的方法有活动的堆栈帧,那么这些活动的帧将继续运行原方法的字节码。将在新的调用上使用此重定义的方法。

    3. 重定义可能会更改方法体、常量池和属性。重定义不得添加、移除、重命名字段或方法


    2. agentmain 

    虚拟机启动后的动态 instrument,开发者可以在 main 函数开始执行以后,再启动自己的 Instrumentation 程序,对应premain的同样有方法:

    public static void agentmain (String agentArgs, Instrumentation inst);         // [1] 
    public static void agentmain (String agentArgs);            //[2]

    用法和premain用法基本相同,jar清单文件MANIFEST.MF 区别Premain-Class 变为  Agent-Class

    常与 retransformClasses()方法配合使用。使用前认真阅读就jdk文档

    3. 本地方法的 instrument

      在 Java SE 6 中,新的 Native Instrumentation 提出了一个新的 native code 的解析方式,作为原有的 native method 的解析方式的一个补充,来很好地解决了一些问题。这就是在新版本的 java.lang.instrument 包里,我们拥有了对 native 代码的 instrument 方式 —— 设置 prefix

      博主在应用此种方式给更改本地方法前缀时走了不少弯路,如今仍没有搞明白(T-T),但并不是无法可取,博主发现以下实现方式:

    使用重定义技术实现(使用javassist重定义本地方法,调用代理类的本地方法,在代理类的本地方法中回调需要的本地方法)!!!

  • 相关阅读:
    我们用的信息系统安全吗?
    硬件代理服务器的日志分析方法
    Solarwinds Orion NPM实战视频演示
    网络资源管理系统LANsurveyor实战体验
    2013年,我的推荐博客汇总
    如何精准高效的实现视觉稿?------前端开发辅助工具AlloyDesigner使用介绍
    SVG如何做圆形图片
    canvas绘制一定数目的圆(均分)
    如何禁止火狐onblur时alert()产生类似选中的拖蓝效果
    ABCD多选正则表达式
  • 原文地址:https://www.cnblogs.com/yelao/p/9841810.html
Copyright © 2011-2022 走看看