注意:这里仅仅提供一个最简单的修改入门课程。让大家先attach 和 intrumentation 有一个比较清晰的认识。
也希望大家如果是一点经验都没有的话。最好能够根据此篇博文顺序耐心 从头到尾的 进行练习 然后再进行个人的创作和修改。这么建议是帮助大家节省学习新知识的时间。参考时间:完成练习代码并进行测试不应该超过半小时,等你的程序运行起来之后再去思考为什么是这样的。我们学习的路途中很多时候是 先有果,后有因的一个顺序。这也是为什么基本很多框架会有helloworld例子的原因。
在学习前请思考如下问题:
1、你觉得一台装有JDK并跑了很多个java 进程的计算机上,会有多少个java虚拟机呢?
从jdk 6.0 后sun jdk 中 有一个工具包叫 tool.jar 提供了 attach 功能需要的类。你能够在 %java_home%/ lib 目录下找到。
在eclipse 中如果想要使用需要你build path 加入依赖中才能够使用。(很抱歉我不会使用IDE)
一、第一部分代码 :打印出你当前机子上的所有虚拟机描述(不包含JDK1.5版本前的 虚拟机)
这里解答了第一个问题。可能比较蠢,反正在我还没有接触虚拟机的时候。我一直以为一台机子就只有一台java 虚拟机。
package com.zhu.attachagent;
import java.util.List;
import com.sun.tools.attach.VirtualMachine; import com.sun.tools.attach.VirtualMachineDescriptor;
/ 往 jvm中注入代码的注入器 @author Felix */ public class VirtualMachineInjector {
public static void main(String[] args) {
// 列表出 当前系统运行中的 jvm ,只能够列表出 jdk1.6及其以上的jvm。
// jdk1.5以前的虚拟机 你通过 jconsole 能够看到进程号,但是无法进行连接
List<VirtualMachineDescriptor> list = VirtualMachine.list();
for (VirtualMachineDescriptor virtualMachineDescriptor : list) {
System.out.println("Pid : " + virtualMachineDescriptor.id());
System.out.println("DisplayName :" + virtualMachineDescriptor.displayName());
}
}
}
当前我的运行结果:
二、接下来进行对第一部分的代码进行修改,并添加剩下其他需要使用的class的代码。
然后进行对目标 jvm中的已经加载的class的字节码内存进行替换
1.VirtualMachineInjector
package com.zhu.attachagent;
import java.util.List;
import com.sun.tools.attach.VirtualMachine; import com.sun.tools.attach.VirtualMachineDescriptor;
/ 往 jvm中注入代码的注入器 @author Felix */ public class VirtualMachineInjector {
public static void main(String[] args) {
// 列表出 当前系统运行中的 jvm ,只能够列表出 jdk1.6及其以上的jvm。
// jdk1.5以前的虚拟机 你通过 jconsole 能够看到进程号,但是无法进行连接
List<VirtualMachineDescriptor> list = VirtualMachine.list();
for (VirtualMachineDescriptor virtualMachineDescriptor : list) {
System.out.println("Pid : " + virtualMachineDescriptor.id());
System.out.println("DisplayName :" + virtualMachineDescriptor.displayName());
}
}
}
2.ToBeTransform 即将被替换的类
package com.zhu.attachagent;
/**
* 将被重新转换的一个类
* @author Felix
*
*/
public class ToBeTransform {
public void test() {
System.out.println(1);
}
}
3.MainTest 运行ToBeTransform 的 主进程类
package com.zhu.attachagent;
/**
* 测试类 测试我们的jvm中的class 是否被修改
* @author Felix
*
*/
public class MainTest {
public static void main(String[] args) throws InterruptedException {
while (true) {
ToBeTransform t = new ToBeTransform();
t.test();
Thread.sleep(3000);
}
}
}
4.修改ToBeTransform 如下后,从获取IDE或者Eclipse帮你编译好的ToBeTransform.class 放入D盘下。一般在bin或者target(maven项目)目录下。存放好D盘后,请将代码修改为System.out.println(1).
package com.zhu.attachagent;
/**
* 将被重新转换的一个类
* @author Felix
*
*/
public class ToBeTransform {
public void test() {
System.out.println(2);
}
}
5.Agent 类中 agentmain 方法不是随意写的,是按照规定来写的还有一个约定俗成的方法是premain
package com.zhu.attachagent;
import java.lang.instrument.Instrumentation;
import java.lang.instrument.UnmodifiableClassException;
public class Agent{
public static void agentmain(String args, Instrumentation inst) throws ClassNotFoundException, UnmodifiableClassException {
//打印传入的参数 就是刚才传入的一个jvm pid
System.out.println("Pid" + args);
inst.addTransformer(new MyTransformer(), true);
inst.retransformClasses(Class.forName("com.zhu.attachagent.ToBeTransform"));
}
}
6.MyTransformer 这是一个对字节码替换很重要的一个类 实现了ClassFileTransformer接口
package com.zhu.attachagent;
import java.io.File;
import java.io.FileInputStream;
import java.lang.instrument.ClassFileTransformer;
import java.lang.instrument.IllegalClassFormatException;
import java.security.ProtectionDomain;
public class MyTransformer implements ClassFileTransformer{
@Override
public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined,
ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException {
System.out.println("正在修改的类: " + className);
System.out.println("修改前的字节码大小: " + classfileBuffer.length);
// 将你修改过得ToBeTransform.class 放入下面位置等待被读取
File file = new File("D:\\ToBeTransform.class");
byte [] data = new byte[(int) file.length()];
try (FileInputStream fis = new FileInputStream(file);){
fis.read(data);
} catch (Exception e) {
e.printStackTrace();
}
System.out.println("修改后的字节码大小: " + data.length);
return data;
}
}
7.MANIFEST.MF
请注意文件的书写格式为 key: value 然后 请注意观察:1.后面一定要有一个空格 2.最后一行要空一行。这个是规范要求。具体为啥还未研究。
Manifest-Version: 1.0
Agent-Class: com.zhu.attachagent.Agent
Can-Redine-Classes: true
Can-Retransform-Classes: true
三、将 Agent 和 MyTransformer 打成agent.jar
1
2.
3.
4.
5.
四、到这里你就完成了所有的编程和环境处理。接下来就是进行测试啦
1、先运行MainTest方法 查看 console 控制台的输出情况 打印 为 1
2. 运行VirtualMachineInjector 进行 对目标类修改情况
从上面能够看出我们已经成功了修改了我们想要修改的类。而且现在console 打印编程了 2.
现在请大家思考如果 我把 MainTest 中的换成 下面 的代码。会对接下来有什么影响。
package com.zhu.attachagent;
/**
* 测试类 测试我们的jvm中的class 是否被修改
* @author Felix
*
*/
public class MainTest {
public static void main(String[] args) throws InterruptedException {
while (true) {
new ToBeTransform().test();;
Thread.sleep(3000);
}
}
}
总结:
替换完后,结果显示的是不是一样呢?为什么是这样请 深入了解 java 虚拟机中 的知识吧。
1.现在是不是想要看看tool.jar 的源代码了呢?苦于找不到代码对吗?来使用下面的功能让你能够在eclipse 中反编译代码
http://blog.youkuaiyun.com/sinat_35608637/article/details/73222583
如何以上不行请使用这个。
https://jingyan.baidu.com/article/fc07f9896da51512ffe5198a.html
2.在这里我们在MyTransformer 中的 是将一个我们修改后的class 文件读取并完全替换了原本 jvm 启动后自动加载的class 文件。
这是一个比较简单的实现。如果我们需要对一个类中某个方法进行定向的修改的话,需要你进一步的学习asm 技术 。当然 javassit 也行。不过个人觉得学习asm 技术虽然很难,
但是能够帮助你了解很多jvm 的知识。
3。注意这里这个功能 对 jdk 1.5 之前的程序是没有任何作用的。
4. 32位的 jvm 进程 与 64 的jvm 进程之间是无法进行相互 attach 的这里也需要注意。
5. 你可以对MANIFEST.ME文件进行修改,删除其中的某些配置然后再进行打包后。进行重新测试看看是否有什么问题存在。
6. 请思考一下 jdk1.6 编译的程序 是否能够注入 其他版本的 1.7 或1.8 编译的程序吗?