首页 > 编程语言 >【Java】Instrumentation热更新 premain agentmain

【Java】Instrumentation热更新 premain agentmain

时间:2022-11-11 12:34:03浏览次数:70  
标签:Instrumentation premain Java jar maven println liyao com public


有两种办法:

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如下:

<?xml version="1.0" encoding="UTF-8"?>

<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>

<groupId>com.liyao</groupId>
<artifactId>agentJar</artifactId>
<version>1.0-SNAPSHOT</version>

<dependencies>
<dependency>
<groupId>org.javassist</groupId>
<artifactId>javassist</artifactId>
<version>3.20.0-GA</version>
</dependency>
</dependencies>

<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-jar-plugin</artifactId>
<configuration>
<archive>
<manifest>
<addClasspath>true</addClasspath>
<classpathPrefix>lib/</classpathPrefix>
</manifest>
<manifestEntries>
<Premain-Class>com.liyao.MyPreAgent</Premain-Class>
</manifestEntries>
</archive>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-dependency-plugin</artifactId>
<executions>
<execution>
<id>copy-dependencies</id>
<phase>prepare-package</phase>
<goals>
<goal>copy-dependencies</goal>
</goals>
<configuration>
<outputDirectory>${project.build.directory}/lib</outputDirectory>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>
</project>

另外,如果一个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中:

<?xml version="1.0" encoding="UTF-8"?>

<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>

<groupId>com.liyao</groupId>
<artifactId>agentJar</artifactId>
<version>1.0-SNAPSHOT</version>

<dependencies>
<dependency>
<groupId>org.javassist</groupId>
<artifactId>javassist</artifactId>
<version>3.20.0-GA</version>
</dependency>
<dependency>
<groupId>com.sun</groupId>
<artifactId>tools</artifactId>
<version>1.5.0</version>
<systemPath>/Library/Java/JavaVirtualMachines/jdk1.8.0_101.jdk/Contents/Home/lib/tools.jar</systemPath>
<scope>system</scope>
</dependency>
</dependencies>

<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-jar-plugin</artifactId>
<configuration>
<archive>
<manifest>
<addClasspath>true</addClasspath>
<classpathPrefix>lib/</classpathPrefix>
</manifest>
<manifestEntries>
<Agent-Class>com.liyao.MyAgent</Agent-Class>
<Main-Class>com.liyao.AttachMain</Main-Class>
<Can-Retransform-Classes>true</Can-Retransform-Classes>
<!--system的依赖需要特殊处理-->
<Class-Path>lib/tools-1.5.0.jar</Class-Path>
</manifestEntries>
</archive>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-dependency-plugin</artifactId>
<executions>
<execution>
<id>copy-dependencies</id>
<phase>prepare-package</phase>
<goals>
<goal>copy-dependencies</goal>
</goals>
<configuration>
<outputDirectory>${project.build.directory}/lib</outputDirectory>
</configuration>
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<source>6</source>
<target>6</target>
</configuration>
</plugin>
</plugins>
</build>
</project>

然后打一个包。看下里面的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​

标签:Instrumentation,premain,Java,jar,maven,println,liyao,com,public
From: https://blog.51cto.com/u_15873544/5844594

相关文章

  • 【zookeeper】java API 例子
    之前体验了命令行客户端,这次看一下javaAPI操作zk。server还是按照之前的配置,一个server1,server2和server3的伪集群。maven:这里使用maven管理zk的jar包,大致需要zk的jar和日......
  • 狂神说Javase基础学习1
    狂神学习博客1基本的DOS命令打开CMD的方式1.开始+系统+命令提示符2.win+R,进入运行窗口,输入cmd打开控制台3.在任意的文件夹下面,Shift+鼠标右键,进入命令行窗口4.资源管......
  • 【Java】内存模型 volatile
    java堆存储对象和数组,是一块线程共享数据区,但是实际线程运行的时候,对于用到的对象都会在线程私有空间即虚拟机栈保存一个副本,为了效率。这两快内存叫主内存和工作内存。java......
  • 【Java】内存区域与对象创建
    这块内容是java很基础的部分,涉及到JVM的设计原理,很久以前就看到过,这次需要区分线程私有和共享基本java的运行时数据区可以分为五大块:程序计数器,为线程私有,每一个线程都有一......
  • 【Java】split(
    java的split函数接受一个正则表达式的分隔符为参数,将string按照分隔符划分为一个数组。我们可能会忽略这个参数的要求,这里传入的分隔符并不是一个普通的字符串,而是一个正则......
  • 【Java】多线程 数目
    今天看到一篇文章,讲多线程数目的,很棒这个问题还是很容易被忽略的,就是多线程到底是为了什么?最开始学习多线程的时候,往往将多线程和性能高划等号,只要用了多线程就能提升性能,其......
  • 【Java】NoSuchMethodError
    刚开始写代码时,特别是类似web这种需要很多第三方jar包的项目,经常会遇到这个问题。这次记录下这个报错的原因。简而言之,这个报错是肯定是因为compile时方法存在,但是runtime时......
  • 【Java】反射与单例
    双重检验与静态内部类两种方法都可以实现延迟加载的单例模式。但是无法阻止反射破坏单例,因为反射可以无视修饰权限,直接调用构造方法创建对象,下面是一个例子:packageThreadTe......
  • 【Java】序列化与单例
    之前明白了线程安全且延迟加载的单例如何写,有两种,双重检验和静态内部类。然后为了防止反射破坏单例,在私有构造方法里面加入了一个同步变量的判断,确保构造方法只调用一次。......
  • 【Java】HashMap 实现原理
    Java集合框架有两个顶级接口,一个是collection接口,另一个是map接口,hashmap便是map接口的重要实现类。首先看map接口。根据map键值对的特性,接口中必然有相关的方法,主要是:Vget......