以前的permain
方法只能在java程序启动以前执行,并不能程序启动以后再执行,可是在实际的不少的状况下,咱们没有办法在虚拟机启动之时就为其设定代理,这样实际上限制了instrument的应用。而Java SE 6的新特性改变了这种状况,能够经过Java Tool API中的attach方式来达到这种程序启动以后设置代理的效果。html
Attach API 不是 Java 的标准 API,而是 Sun 公司提供的一套扩展 API,用来向目标 JVM “附着”(Attach)代理工具程序的。有了它,开发者能够方便的监控一个 JVM,运行一个外加的代理程序。java
Attach API 很简单,只有 2 个主要的类,都在 com.sun.tools.attach
包里面: VirtualMachine
表明一个 Java 虚拟机,也就是程序须要监控的目标虚拟机,提供了 JVM 枚举,Attach
动做和 Detach
动做(从 JVM 上面解除一个代理)等等 ; VirtualMachineDescriptor
则是一个描述虚拟机的容器类,配合VirtualMachine
类完成各类功能。编程
结合上一篇文章和Attach,看看如何使用bash
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 路径要和目标程序同样,其余无所谓jvm
package cn.jpsite.learning;
public class Dog {
}
复制代码
在以前的example01工程中新建一个带有main函数的类WhileMain.java
ide
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的嘛,稍后你就会看到神奇的一幕
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 自己产生任何影响。
文章每周持续更新,能够微信搜索「 十分钟学编程 」第一时间阅读和催更,若是这个文章写得还不错,以为有点东西的话 ~求点赞👍 求关注❤️ 求分享❤️
各位的支持和承认,就是我创做的最大动力,咱们下篇文章见!