java高*之类热加载(2)

时间:2022-04-12 17:19:40

java高*之类热加载(2)

之前写过一篇文章,讨论使用自定义类加载器实现类的热加载。文章地址如下:

http://blog.csdn.net/maosijunzi/article/details/45696589

这篇文章将要讨论另一种方法实现热加载,这种方法理论上说可以对应用中所有的类

进行热加载,但是在实际应用中,我们也不可能对所有的类进行热加载,也都是指定

相关的算法类进行热加载。

本节所使用的是lang 包下的Instrumentation。下面这段话摘自百度百科。

“java.lang.instrument”包的具体实现,依赖于 JVMTI。JVMTI(Java Virtual Machine Tool Interface)是一套由 Java 虚拟机提供的,为 JVM 相关的工具提供的本地编程接口集合。

JVMTI 提供了一套”代理”程序机制,可以支持第三方工具程序以代理的方式连接和访问 JVM,并利用 JVMTI 提供的丰富的编程接口,完成很多跟 JVM 相关的功能。事实上,java.lang.instrument

包的实现,也就是基于这种机制的:在 Instrumentation 的实现当中,存在一个 JVMTI 的代理程序,通过调用 JVMTI 当中 Java 类相关的函数来完成 Java 类的动态操作。除开 Instrumentation 功能外,JVMTI

还在虚拟机内存管理,线程控制,方法和变量操作等等方面提供了大量有价值的函数。

也就是jvm实际上是提供了直接替换类的功能的。只不过需要我们告诉jvm什么时候,那些类需要重新加载。然后我们把改变后的字节码发给jvm,它就会帮我们完成替换。

下面我们直接看代码:

package com.instrument;

import java.lang.instrument.Instrumentation;
import java.lang.instrument.UnmodifiableClassException;

/** * @author chuer * @Description: 代理类:这个类需要在mainfest.mf文件中指定 * @date 2015年5月22日 下午3:30:57 * @version V1.0 */
public class AgentMain {

    /** * 这个类需要在mainfest.mf文件中指定,而且此方法名以及参数都是固定的,不能随意指定。 * 当我们告诉JVM需要热加载类的时候,JVM会自动调用此方法。 * @param agentArgs * @param inst * @throws ClassNotFoundException * @throws UnmodifiableClassException * @throws InterruptedException */
    public static void agentmain(String agentArgs, Instrumentation inst)
            throws ClassNotFoundException, UnmodifiableClassException,
            InterruptedException {
        inst.addTransformer(new Transformer(), true);
        inst.retransformClasses(TransClass.class);
        System.out.println("Agent Main Done");
    }
}
package com.instrument;

import java.io.File;
import java.io.FileInputStream;
import java.io.InputStream;
import java.lang.instrument.ClassFileTransformer;
import java.lang.instrument.IllegalClassFormatException;
import java.security.ProtectionDomain;

/** * @author chuer * @Description: 类转换 * @date 2015年5月22日 下午3:32:56 * @version V1.0 */
public class Transformer implements ClassFileTransformer {


    /** * 此方法获得新类的字节数据,发送给JVM,JVM会自动替换旧类的字节码数据 */
    public byte[] transform(ClassLoader l, String className, Class<?> c,
            ProtectionDomain pd, byte[] b) throws IllegalClassFormatException {

        className = className.replace('/', '.');
        // 重新载入类文件(完整路径)
        String classNameSimple = className.substring(className.lastIndexOf('.') + 1);
        if (!classNameSimple.equals("TransClass")) {
            return null;
        }

        String newClassFile = "D:/com/instrument/" + classNameSimple + ".class";

        System.out.println(newClassFile);
        return getBytesFromFile(newClassFile);
    }
/** * 获得class文件的字节数据 * @param fileName * @return */
    public static byte[] getBytesFromFile(String fileName) {
        File file = new File(fileName);
        long length = file.length();
        try (InputStream is = new FileInputStream(file)) {
            byte[] bytes = new byte[(int) length];

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

            if (offset < bytes.length) {
                throw new Exception("Could not completely read file "
                        + file.getName());
            }
            is.close();
            return bytes;
        } catch (Exception e) {
            System.out.println("error occurs in _ClassTransformer!"
                    + e.getClass().getName());
            return null;
        }
    }
}
package com.instrument;


/** * @author chuer * @Description: 需要进行热加载的类 * @date 2015年5月22日 下午3:32:37 * @version V1.0 */
public class TransClass {
    public int getNumber() {
        return 2222;
    }
}
package com.instrument;

public class Main {
    public static void main(String[] args) throws InterruptedException {

        while (true) {
            int number = new TransClass().getNumber();
            System.out.println(number);
            Thread.sleep(1000);
        }
    }
}

manifest.mf文件内容如下:

Manifest-Version: 1.0
Agent-Class: com.instrument.AgentMain
Can-Retransform-Classes: true

使用eclipse的export功能打包,包名为chutest.jar,使用下面命令运行包:

java -cp chutest.jar com.instrument.TestMainInJar

运行如下:

D:\>java -cp chutest.jar com.instrument.TestMainInJar
2222
2222
2222
2222
2222
2222
2222
...

另打开一个cmd命令窗口,使用jps命令查看进程号:

C:\Users\Administrator>jps
5840 TestMainInJar
2196 Jps
4252

我们看到TestMainInJar的进程号为 5840.下面的程序会用到此进程号。

我们看一下,通知JVM进行类加载的程序:

package com.instrument;
import java.util.List;

import com.sun.tools.attach.VirtualMachine;
import com.sun.tools.attach.VirtualMachineDescriptor;

public class AttachThread extends Thread {
    private final String jar;

    AttachThread(String attachJar, List<VirtualMachineDescriptor> vms) {
        jar = attachJar;
    }

    public void run() {
        VirtualMachine vm = null;
        List<VirtualMachineDescriptor> listAfter = null;
        try {
            while (true) {
                //如果被修改了则重新加载
                listAfter = VirtualMachine.list();
                for (VirtualMachineDescriptor vmd : listAfter) {
                    if ("5840".equals(vmd.id())) {//5840 是进程号
                        vm = VirtualMachine.attach(vmd);//连接JVM
                        break; 
                    } 
                }
                //通知JVM需要进行类的热加载,然后JVM会调用代理类的指定方法。
                vm.loadAgent(jar);
                vm.detach();
                break;
            }
        } catch (Exception e) {
        }
    }

    public static void main(String[] args) throws InterruptedException {
        new AttachThread("D:/chutest.jar", VirtualMachine.list()).start();
    }
}

然后修改TransClass类如下:

public class TransClass {
    public int getNumber() {
        return 3333;
    }
}

编译后把,新的class文件放到D:/com/instrument目录下:

这时候,运行AttachThread类,然后我们进入命令窗口就会看到结果,如下:

2222
2222
2222
2222
D:/com/instrument/TransClass.class
Agent Main Done
3333
3333
3333
3333
3333
3333
3333
....

说明类已经被重新加载了。