之前的 permain
方法只能在java程序启动之前执行,并不能程序启动之后再执行,但是在实际的很多的情况下,我们没有办法在虚拟机启动之时就为其设定代理,这样实际上限制了instrument的应用。而Java SE 6的新特性改变了这种情况,可以通过Java Tool API中的attach方式来达到这种程序启动之后设置代理的效果。
Attach API 不是 Java 的标准 API,而是 Sun 公司提供的一套扩展 API,用来向目标 JVM “附着”(Attach)代理工具程序的。有了它,开发者可以方便的监控一个 JVM,运行一个外加的代理程序。
Attach API 很简单,只有 2 个主要的类,都在 com.sun.tools.attach
包里面: VirtualMachine
代表一个 Java 虚拟机,也就是程序需要监控的目标虚拟机,提供了 JVM 枚举, Attach
动作和 Detach
动作(从 JVM 上面解除一个代理)等等 ; VirtualMachineDescriptor
则是一个描述虚拟机的容器类,配合 VirtualMachine
类完成各种功能。
结合上一篇文章和Attach,看看如何使用
Agent类增加了2个agentmain()方法,它们的参数不用,2个参数的优先级大于1个参数的,所以这里只有 agentmain (String agentArgs, Instrumentation inst)
会被执行
public class JpAgent { public static void premain(String agentArgs){ System.out.println("我是一个参数的 Java Agent premain"); } public static void agentmain (String agentArgs, Instrumentation inst) throws UnmodifiableClassException { inst.addTransformer(new JpClassFileTransformerDemo(), true); // retransformClasses 是 Java SE 6 里面的新方法,它跟 redefineClasses 一样,可以批量转换类定义 inst.retransformClasses(Dog.class); System.out.println("我是两个参数的 Java Agent agentmain"); } public static void agentmain (String agentArgs){ System.out.println("我是一个参数的 Java Agent agentmain"); } } 复制代码
新增一个Dog类,内容如下,关键点在package 路径要和目标程序一样,其他无所谓
package cn.jpsite.learning; public class Dog { } 复制代码
在之前的example01工程中新建一个带有main函数的类 WhileMain.java
public class WhileMain { public static void main(String[] args) throws InterruptedException { System.out.println(new Dog().say()); int count = 0; while (true) { // 等待0.5秒 Thread.sleep(500); count++; String say = new Dog().say(); // 输出内容和次数 System.out.println(say + count); // 内容不对或者次数达到1000次以上输出 if (!"dog".equals(say) || count >= 1000) { System.out.println("有人偷了我的狗!"); //break; } } } } 复制代码
准备一个修改过的Dog.class,内容如下,单独保存到目录 D:/learning/Dog.class
// 这是一个修改后编译的.class文件,单独存放 public class Dog { public String say() { return "cat"; } } 复制代码
resource/META-INF/MANIFEST.MF新增内容
Agent-Class: cn.jpsite.learning.javaagent01.JpAgent Can-Retransform-Classes: true 复制代码
到了这里,准备工作基本已经完成,执行打包构建,此时执行以下无论哪一条,都不会有结果 agentmain
输出
java -javaagent:jpAgent.jar -cp example01-1.0-SNAPSHOT.jar cn.jpsite.learning.Main java -javaagent:jpAgent.jar -cp example01-1.0-SNAPSHOT.jar cn.jpsite.learning.WhileMain java -cp example01-1.0-SNAPSHOT.jar cn.jpsite.learning.WhileMain 复制代码
结果如下,并没用调用 agentmain
方法,这该怎么办呢?
这时候就要用到com.sun.tools.attach来帮助我们达到虚拟机启动之后的代理设置,代码如下:
import com.sun.tools.attach.AttachNotSupportedException; import com.sun.tools.attach.VirtualMachine; import com.sun.tools.attach.VirtualMachineDescriptor; import java.io.IOException; import java.util.List; import java.util.Objects; /** * * @author jiangpeng * @date 2019/12/1 0001 */ public class AttachThread extends Thread { /** * 记录程序启动时的 VM 集合 */ private final List<VirtualMachineDescriptor> listBefore; /** 要加载的agent.jar */ private final String jar; private AttachThread(String attachJar, List<VirtualMachineDescriptor> vms) { listBefore = vms; jar = attachJar; } @Override public void run() { VirtualMachine vm; List<VirtualMachineDescriptor> listAfter; int count = 0; try { while (true) { listAfter = VirtualMachine.list(); vm = hasNewVm(listAfter); if(vm == null){ System.out.println("没有新jvm程序,请手动指定java pid"); try{ vm = VirtualMachine.attach("7716"); }catch (AttachNotSupportedException e){ //System.out.println("拒绝访问 Disconnected from the target VM"); } } Thread.sleep(1000); System.out.println(count++); if (null != vm || count >= 100) { break; } } Objects.requireNonNull(vm).loadAgent(jar); vm.detach(); } catch (Exception e) { System.out.println("异常:" + e); } } /** * 判断是否有新的 JVM 程序运行 */ private VirtualMachine hasNewVm(List<VirtualMachineDescriptor> listAfter) throws IOException, AttachNotSupportedException { for (VirtualMachineDescriptor vmd : listAfter) { if (!listBefore.contains(vmd)) { // 如果 VM 有增加,,我们开始监控这个 VM System.out.println("有新的 vm 程序:"+ vmd.displayName()); return VirtualMachine.attach(vmd); } } return null; } public static void main(String[] args) { new AttachThread("D:/learning/jpAgent.jar", VirtualMachine.list()).start(); } } 复制代码
其中 while
循环部分每隔1秒获取一次java进程集合,如果没有的话就会提示手动指定一个java程序进行 attach
, 当循环了100次或者 获取到了 VirtualMachine
,则退出 while(true)
去加载指定的 agent.jar
。
先执行 java -cp example01-1.0-SNAPSHOT.jar cn.jpsite.learning.WhileMain
启动example01程序,看到如下结果,还记得我们之前准备了一份Dog.class,里面的say()内容是cat的嘛,稍后你就会看到神奇的一幕
接下运行刚刚的AttachThread.java,看到如下内容
使用jps -l
命令查看所有有运行中的java程序端口号
修改AttachThread.java中的 VirtualMachine.attach("7716");
代码为 VirtualMachine.attach("16304");
16304为上图中WhileMain java 进程id,重新启动AttachThread.java,结果如下
loader className: cn/jpsite/learning/Dog
我是两个参数的 Java Agent agentmain
可以看到agentmain已经执行了,而且原Dog.say()方法里的内容dog也被改变了,成了cat
首先启动AttachThread.java,然后执行 java -cp example01-1.0-SNAPSHOT.jar cn.jpsite.learning.WhileMain
,可以很快就看到结果被改变了
Can-Set-Native-Method-Prefix System-Class-Path Boot-Class-Path 复制代码
注意几点。首先,我们加入到 classpath 的 jar 文件中不应当带有任何和系统的 instrumentation 有关的系统同名类,不然,一切都陷入不可预料之中
我们要注意到虚拟机的 ClassLoader 的工作方式,它会记载解析结果。比如,我们曾经要求读入某个类 someclass,但是失败了,ClassLoader 会记得这一点。即使我们在后面动态地加入了某一个 jar,含有这个类,ClassLoader 依然会认为我们无法解析这个类,与上次出错的相同的错误会被报告。
再次我们知道在 Java 语言中有一个系统参数“java.class.path”,这个 property 里面记录了我们当前的 classpath,但是,我们使用这两个函数,虽然真正地改变了实际的 classpath,却不会对这个 property 本身产生任何影响。