有两种办法:
1)在java5中,可以利用jvm加载类的一个扩展点来实现类文件的动态修改。
需要提供一个premain方法。缺点是只能在类文件加载且main方法执行之前修改,无法实现真正的运行时修改。
2)在java6中,可以使用attach API实现真正的运行时修改。需要提供一个agentmain方法。大致原理是使用agent attach api附到待更新的jvm上,然后动态加载agent,agent与premain里的几乎相同,只不过这里是在jvm已经运行起来以后加载。
二者的加载agent时机不同。premain是虚拟机启动加载类时,而agentmain是虚拟机起来以后。
本文用一个简单的例子来展示这个用法。
只是在某一个类的所有方法执行前后打印一行语句。
premain方式:
待测试类:
/**
* Hello world!
*
*/
public class App
{
public static void main( String[] args ) throws InterruptedException {
App app = new App();
app.serviceA();
app.serviceB();
}
public void serviceA(){
System.out.println("doServiceA");
}
public void serviceB(){
System.out.println("doServiceB");
}
}
将上述工程使用打一个可执行的jar包,方法就不说了。
然后写一个premain的agent:
public class MyPreAgent {
public static void premain(String agentArgs, Instrumentation inst){
inst.addTransformer(new ClassFileTransformer() {
public byte[] transform(ClassLoader loader, String className, Class> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException {
ClassPool classPool = ClassPool.getDefault();
try {
// 默认是class name: com/liyao/App
className = className.replace("/",".");
if ("com.liyao.App".equals(className)) {
CtClass clz = classPool.get("com.liyao.App");
CtMethod[] methods = clz.getDeclaredMethods();
for (CtMethod method : methods) {
method.insertBefore("System.out.println(\"before " + method.getName() + " execute!!!\");");
method.insertAfter("System.out.println(\"after " + method.getName() + " execute!!!\");");
}
return clz.toBytecode();
}
} catch (NotFoundException e) {
System.err.println(className);
e.printStackTrace();
} catch (CannotCompileException e) {
System.err.println(className);
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
return new byte[0];
}
});
}
}
这个类只会把com.liyao.App做增强处理。可以看到,整体过程是通过Instrementation类来完成的,里面需要传入一个classTransformer,具体的字节码修改就是在这个transformer里面定制的。另外可以看到premain的工作流程是在每一次的类加载时执行的,我们可以动态选择修改哪些类。
关于字节码的修改这里使用了javasist工具。所以需要引入依赖,并且指定该工具类库的classpath。这里使用的是maven,pom如下:
4.0.0
com.liyao
agentJar
1.0-SNAPSHOT
org.javassist
javassist
3.20.0-GA
org.apache.maven.plugins
maven-jar-plugin
true
lib/
com.liyao.MyPreAgent
org.apache.maven.plugins
maven-dependency-plugin
copy-dependencies
prepare-package
copy-dependencies
${project.build.directory}/lib
另外,如果一个jar是作为一个premain的agent存在的,必须在其manifest文件中指定PreMain-Class的全限定类名。这些可以在maven-jar-plugin中配置的。
然后打一个agent的jar包:
mvn clean package
然后看下jar文件里的manifest文件:
Manifest-Version: 1.0
Premain-Class: com.liyao.MyPreAgent
Archiver-Version: Plexus Archiver
Built-By: miracle
Class-Path: lib/javassist-3.20.0-GA.jar
Created-By: Apache Maven 3.6.1
Build-Jdk: 1.8.0_101
Main-Class: com.liyao.test.Main
有了classpath和premain的配置。
至此,一个premain的jar包已经搞定。下面运行之前的带增强类,也将其打一个jar包,然后在对应目录下执行命令运行:
java -javaagent:/Users/miracle/test/mvn/agentJar/target/agentJar-1.0-SNAPSHOT.jar -jar testAgent-1.0-SNAPSHOT.jar
注意替换对路径。
结果:
before main execute!!!
before serviceA execute!!!
doServiceA
after serviceA execute!!!
before serviceB execute!!!
doServiceB
after serviceB execute!!!
after main execute!!!
可以看到有增强效果。
agentmain方式:
待测试类:
/**
* Hello world!
*
*/
public class App
{
public static void main( String[] args ) throws InterruptedException {
App app = new App();
for (int i = 0; i < 10000; i++){
Thread.sleep(2000);
app.serviceA();
app.serviceB();
}
}
public void serviceA(){
System.out.println("doServiceA");
}
public void serviceB(){
System.out.println("doServiceB");
}
}
使用maven达成一个可执行的jar包,然后运行,正常来说,会有如下输出:
⇒ java -jar testAgent-1.0-SNAPSHOT.jar
doServiceA
doServiceB
doServiceA
doServiceB
下面使用agent来增强,首先下一个agentmain:
public class MyAgent {
public static void agentmain(String agentArgs, Instrumentation inst){
inst.addTransformer(new ClassFileTransformer() {
public byte[] transform(ClassLoader loader, String className, Class> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException {
ClassPool classPool = ClassPool.getDefault();
try {
className = className.replace("/",".");
CtClass clz = classPool.get(className);
CtMethod[] methods = clz.getDeclaredMethods();
for (CtMethod method : methods) {
System.out.println(method.getName());
if (method.getName().startsWith("service")) {
method.insertBefore("System.out.println(\"before " + method.getName() + " execute!!!\");");
method.insertAfter("System.out.println(\"after " + method.getName() + " execute!!!\");");
}
}
byte[] r = clz.toBytecode();
clz.detach();
return r;
} catch (NotFoundException e) {
e.printStackTrace();
} catch (CannotCompileException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
return new byte[0];
}
}, true);
for (Class clazz : inst.getAllLoadedClasses()) {
if ("com.liyao.App".equals(clazz.getName())) {
try {
System.out.println(clazz.getName());
inst.retransformClasses(clazz);
} catch (UnmodifiableClassException e) {
e.printStackTrace();
}
}
}
}
}
这里只修改了前面的带增强类的两个service打头的方法。与premain的几乎一样,只是多了一步retransformClasses的调用。
有几点需要注意的地方:
1)这里需要导入tools.jar的依赖,该jar包在jdk下是有的。如果使用maven,需要加一个scope=system的本地依赖;
2)类似premain,agentmain方式的jar包的manifest文件必须包含Agent-Class元素指明agent入口类;
3)另外还需要在manifest中指明Can-Retransform-Classes=true,才能调用retransformClasses方法,这是一个坑;
以上内容配置在pom中:
4.0.0
com.liyao
agentJar
1.0-SNAPSHOT
org.javassist
javassist
3.20.0-GA
com.sun
tools
1.5.0
/Library/Java/JavaVirtualMachines/jdk1.8.0_101.jdk/Contents/Home/lib/tools.jar
system
org.apache.maven.plugins
maven-jar-plugin
true
lib/
com.liyao.MyAgent
com.liyao.AttachMain
true
lib/tools-1.5.0.jar
org.apache.maven.plugins
maven-dependency-plugin
copy-dependencies
prepare-package
copy-dependencies
${project.build.directory}/lib
org.apache.maven.plugins
maven-compiler-plugin
6
然后打一个包。看下里面的manifest文件:
Manifest-Version: 1.0
Archiver-Version: Plexus Archiver
Built-By: miracle
Agent-Class: com.liyao.MyAgent
Can-Retransform-Classes: true
Class-Path: lib/tools-1.5.0.jar lib/javassist-3.20.0-GA.jar
Created-By: Apache Maven 3.6.1
Build-Jdk: 1.8.0_101
Main-Class: com.liyao.AttachMain
接下来需要写一个attach程序来让带增强jvm动态加载上面的agent,为了方便,这段代码写在和agent代码相同的工程里,所以上面的pom中有一些是为了这段attach代码加的,比如maven的入口类。
attach代码如下:
public class AttachMain {
public static void main(String args[]) {
try {
VirtualMachine vm = VirtualMachine.attach(args[0]);
System.out.println(args[0]);
vm.loadAgent("/Users/miracle/test/mvn/agentJar/target/agentJar-1.0-SNAPSHOT.jar");
vm.detach();
} catch (AttachNotSupportedException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
} catch (AgentLoadException e) {
e.printStackTrace();
} catch (AgentInitializationException e) {
e.printStackTrace();
}
}
}
根据java启动参数来获取java进程号,进而attach上去,loadAgent。
可以使用jps命令找到之前jvm的进程号:
miracle@localhost:~/arthas|⇒ jps
20115 RemoteMavenServer
27782 Jps
27613 jar
就是这里的27613
然后运行上述attach程序,因为代码也是在agent的工程里,所以可以直接起之前的jar包:
java -jar agentJar-1.0-SNAPSHOT.jar 27613
输出:
⇒ java -jar agentJar-1.0-SNAPSHOT.jar 27613
27613
最后看下之前的待增强的jvm的输出:
doServiceA
doServiceB
doServiceA
doServiceB
objc[27613]: Class JavaLaunchHelper is implemented in both /Library/Java/JavaVirtualMachines/jdk1.8.0_101.jdk/Contents/Home/bin/java (0x10e9da4c0) and /Library/Java/JavaVirtualMachines/jdk1.8.0_101.jdk/Contents/Home/jre/lib/libinstrument.dylib (0x11851e4e0). One of the two will be used. Which one is undefined.
com.liyao.App
main
serviceA
serviceB
before serviceA execute!!!
doServiceA
after serviceA execute!!!
before serviceB execute!!!
doServiceB
可以看到,jvm内的class已经被修改。
好了,这个例子就是这样。至于里面的原理下一次再讨论。
最后附上一个大牛的实现:https://github.com/liuzhengyang/lets-hotfix