Java javaagent 使用

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

在 Java SE 5 中,利用 java.lang.instrument 做静态 Instrumentation 是 Java SE 5 的新特性,它把 Java 的 instrument 功能从本地代码中解放出来,使之可以用 Java 代码的方式解决问题。

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

在 Java SE 5 中,Instrument 要求在运行前利用命令行参数或者系统参数来设置代理类。但在实际的运行之中,虚拟机在初始化之时(在绝大多数的 Java 类库被载入之前),instrumentation 的设置已经启动,并在虚拟机中设置了回调函数,检测特定类的加载情况,并完成实际工作。但是在实际的很多的情况下,我们没有办法在虚拟机启动之时就为其设定代理,这样实际上限制了 instrument 的应用。而 Java SE 6 改变了这种情况,通过 Java Tool API 中的 attach 方式,我们可以很方便地在运行过程中动态地设置加载代理类,以达到 instrumentation 的目的。

Instrumentation 的最大作用,就是类定义动态改变和操作。java.lang.instrument 包的实现,是基于JVMTI机制的:在 Instrumentation 的实现当中,存在一个 JVMTI 的代理程序,通过调用 JVMTI 当中 Java 类相关的函数来完成Java 类的动态操作。除开 Instrumentation 功能外,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 相关的功能。

1. 使用

Agent分为两种,一种是在主程序之前运行的Agent,一种是在主程序之后运行的Agent(前者的升级版,1.6以后提供)。

  1. 在一个普通 Java 程序(带有 main 函数的 Java 类)运行时,通过 -javaagent 参数指定一个特定的 jar 文件(包含 Instrumentation 代理)来启动 Instrumentation 的代理程序。
  2. 在一个普通 Java 程序(带有 main 函数的 Java 类)运行时,通过 Java Tool API 中的 attach 方式指定进程id和特定jar包地址,启动 Instrumentation 的代理程序。

这里测试使用 javaagent 替换类。

1.1 JVM启动前静态 Instrument

定义一个User类,getName()返回admin。

public class User {
    public String getName() {
        return "admin";
    }
}

主程序,这里的程序就是我们要代理的程序。创建User对象,调用getName()方法。

public class Main {
    public static void main(String[] args) {
        System.out.println("main start");
        System.out.println("main args :" + Arrays.toString(args));
        System.out.println("new User().getName() :" + new User().getName());
        System.out.println("main end");
    }
}

将User类的getName()方法返回值改为user,使用javac编译后,重命名编译文件User.class为User.class.2,再将User类中的getName()方法返回值改回admin。

实现 ClassFileTransformer 接口,transform 方法则完成了类定义的替换。

public class UserTransformer implements ClassFileTransformer {

	// 待替换的类文件名
    private static final String USER_CLASS_2 = "User.class.2";

    @Override
    public byte[] transform(ClassLoader loader, 
    						String className, 
    						Class<?> classBeingRedefined, 
    						ProtectionDomain protectionDomain,
    						byte[] classfileBuffer) throws IllegalClassFormatException {
        System.out.println("className :" + className);
        if (!className.contains("User")) {
            return null;
        }
        return getBytesFromFile(USER_CLASS_2);
    }

    /**
     * 根据文件名读入二进制字符流
     * @param fileName
     * @return
     */
    private byte[] getBytesFromFile(String fileName) {
        File file = new File(fileName);
        try (
                InputStream inputStream = new FileInputStream(file);
                ){
            long length = file.length();
            byte[] bytes = new byte[(int) length];

            int offset = 0;
            int numRead = 0;
            while (offset < bytes.length && 
            	(numRead = inputStream.read(bytes, offset, bytes.length - offset)) >= 0) {
                offset += numRead;
            }

            if (offset < bytes.length) {
                throw new IOException("无法完全读取文件 " + file.getName());
            }
            return bytes;
        } catch (Exception e) {
            e.printStackTrace();
            return null;
        }
    }
}

premain方法,代表着他将在主程序的main方法之前运行,agentArgs代表传递过来的参数,inst则是agent技术主要使用的API。

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

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

public class Premain {
    public static void premain(String agentArgs, Instrumentation inst) {
        System.out.println("premain start");
        System.out.println("premain args :" + agentArgs);
        // 在类加载之前,重新定义 Class 文件
        inst.addTransformer(new UserTransformer());
        System.out.println("premain end");
    }
}

META-INF/MAINIFEST.MF 文件用于描述Jar包的信息,例如指定入口函数等。
需要添加Premain-Class属性,指定带有 premain 方法类的全路径,然后将agent类打成Jar包。

<plugin>
  <artifactId>maven-jar-plugin</artifactId>
  <version>3.0.2</version>
  <configuration>
    <archive>
      <manifest>
        <addClasspath>true</addClasspath>
      </manifest>
      <manifestEntries>
        <Main-Class>
          com.shpun.Main
        </Main-Class>
        <!--main之前-->
        <Premain-Class>
          com.shpun.Premain
        </Premain-Class>
      </manifestEntries>
    </archive>
  </configuration>
</plugin>

测试

未代理,打印admin。未代理
将 User.class.2 和 Jar 包放在同个目录下测试。

代理后,先执行 premain 方法,然后加载类,每次都经过 transform() 方法。加载完后执行 main 方法,需要加载 User 类,经过transform() 方法,替换User,打印user,代理成功。
代理1
代理2

1.2 JVM启动后动态 Instrument

User.class.2,User 类,UserTransformer 类和上面一样。

修改 Main 方法,这里判断 getName() 的值,如果是user才退出。

public class Main {
    public static void main(String[] args) throws InterruptedException {
        System.out.println("main start");
        System.out.println("main args :" + Arrays.toString(args));

        while (true) {
            Thread.sleep(1000);
            String name = new User().getName();
            System.out.println("new User().getName() :" + new User().getName());
            if ("user".equals(name)) {
                break;
            }
        }
        System.out.println("main end");
    }
}

agentmain 方法在 main 函数开始运行之后再运行。

public class AgentMain {
    public static void agentmain(String agentArgs, Instrumentation inst)  throws Exception {
        System.out.println("agentmain start");
        System.out.println("agentmain args :" + agentArgs);
        // 增加一个 Class 文件的转换器,转换器用于改变 Class 二进制流的数据,
        // 参数 canRetransform 设置是否允许重新转换,为true才能在运行时替换。
        inst.addTransformer(new UserTransformer(), true);
        // 在类加载之后,重新定义 Class。对于已经加载过的类,可以执行retransformClasses来重新触发这个Transformer的拦截。
        inst.retransformClasses(User.class);
        System.out.println("agentmain end");
    }
}

META-INF/MAINIFEST.MF 需要添加 Agent-Class 和 Can-Retransform-Classes 属性。

<plugin>
  <artifactId>maven-jar-plugin</artifactId>
  <version>3.0.2</version>
  <configuration>
    <archive>
      <manifest>
        <addClasspath>true</addClasspath>
      </manifest>
      <manifestEntries>
        <Main-Class>
          com.shpun.Main
        </Main-Class>
        <!--main之后-->
        <Agent-Class>
          com.shpun.AgentMain
        </Agent-Class>
        <Can-Retransform-Classes>
          true
        </Can-Retransform-Classes>
      </manifestEntries>
    </archive>
  </configuration>
</plugin>

测试

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

<dependency>
  <groupId>com.sun</groupId>
  <artifactId>tools</artifactId>
  <version>1.8.0</version>
  <scope>system</scope>
  <systemPath>G:\Java\jdk1.8.0_181\lib\tools.jar</systemPath>
</dependency>

VirtualMachine 代表一个 Java 虚拟机,也就是程序需要监控的目标虚拟机,提供了 JVM 枚举,Attach 动作和 Detach 动作(Attach 动作的相反行为,从 JVM 上面解除一个代理)等等 。该类允许我们通过给 attach() 方法传入一个jvm的pid(进程id),远程连接到jvm上 。然后我们可以通过 loadAgent() 方法向jvm注册一个代理程序agent,在该agent的代理程序中会得到一个Instrumentation实例,该实例可以在 Class 加载前改变 Class 的字节码,也可以在 Class 加载后重新加载。在调用Instrumentation实例的方法时,这些方法会使用ClassFileTransformer 接口中提供的方法进行处理。

public class AttachAgent {
    public static void main (String[] args) throws Exception {
        // 通过jps命令,获取进程号
        VirtualMachine virtualMachine = VirtualMachine.attach("19056");
        virtualMachine.loadAgent("E:\\IDEA_workspace\\java-agent-test\\java-agent-agentmain\\target\\java-agent-agentmain-1.0-SNAPSHOT.jar", "attach-agent");
        virtualMachine.detach();
    }
}

将 User.class.2 和 Jar 包放在同个目录下测试。

运行 Jar 包,先打印的是admin。然后执行AttachAgent,发现agentmain方法被执行了,并且在替换了类,打印user。这个表示 agentmain 已经被 Attach API 成功附着到 JVM 上,代理程序生效了。
agentmain述

参考:
Java Agent简介
javaagent使用指南
JavaAgent技术
☆基于Java Instrument的Agent实现
初探 Java agent
☆浅谈JPDA中JVMTI模块
☆JVMTI Agent 工作原理及核心源码分析
☆JVMTI Attach机制与核心源码分析
"程序包com.sun.tools.attach不存在"最简单粗暴的解决方案
agentmain 使用过程中的坑,看看你有没有遇到

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值