1. JVM agent
- JVM 提供了一个类优化服务(主要通过调整修改字节码),java.lang.Instrumention
- java.lang.Instrumention 提供了一系列 为JVM添加各种各样ClassFileTrasformer(这个类的就是字节码的修改逻辑)的接口
- JVM在加载新的类文件或者重新加载类文件时,会调用所有的ClassFileTrasformer实例的transform方法(有一套调用顺序和逻辑),输入为原始类文件的字节数组,最终需要返回一个新的类文件字节数组;整个修改的流程,不允许修改原类文件内的field的和方法签名);
- JVM 会使用这个新的类文件字节数组进行类的解析,在解析生成类的时,并不会修改这个类的任何实例的状态;
2. Agent启用方式
2.1 static:在main方法执行之前执行
2.1.1 基本要求
- 要求Agent Class有一个public static void premain(String agentArgs,Intrumention instrumention)这样签名的方法
- META-INF/MANIFEST.MF 文件:要求有Premain-Class 这个key,Value就是Agent的全类名
Premain-Class: com.aruforce.myAop.jvmagent.Agent
- command-Line
java -jar app.jar -javaagent:pathto/agent.jar
2.1.2 一个Agent
pom.xml:
<?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/maven-v4_0_0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.aruforce.jvm-agent</groupId>
<artifactId>jvm-agent</artifactId>
<version>0.0.1-SNAPSHOT</version>
<packaging>jar</packaging>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<maven.compiler.encoding>UTF-8</maven.compiler.encoding>
<maven.compiler.source>1.7</maven.compiler.source>
<maven.compiler.target>1.7</maven.compiler.target>
<encoding>UTF-8</encoding>
</properties>
<build>
<finalName>${project.artifactId}-${project.version}</finalName>
<pluginManagement>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-jar-plugin</artifactId>
<configuration>
<archive>
<manifestEntries>
<Premain-Class>com.aruforce.myAop.jvmagent.Agent</Premain-Class>
</manifestEntries>
</archive>
</configuration>
</plugin>
</plugins>
</pluginManagement>
</build>
</project>
Agent:
package com.aruforce.myAop.jvmagent;
import java.lang.instrument.Instrumentation;
/**
* @Author
* JVM 提供了一种JVM启动后(在main方法之前)执行agentJar内premain方法的机制
* 启动一个pre-Main-Agent的方式是使用command-line 参数指定Jar的path:- javaagent: pathToAgentJar[agentArgs];
* 注意可以有多个-javaagent参数sample:
* java -jar HelloWorld.jar -javaagent:pathToAgentA[agentArgs] -javaagent:pathToAgentB[agentArgs]
*/
public class Agent {
public static Instrumentation instrumentation = null;
public static String agentArgs = null;
/**
* JVM初始化完成后,会按照命令行的指定Agent的顺序依次调用每个agentJar包内的Premain-class 值指定类的premain方法,全部执行完成后后再执行main方法;
* JVM会优先尝试执行{@link #premain(String agentArgs,Instrumentation instrument)},如果成功则执行下一个Agent的premain,否则尝试{@link #premain(String agentArgs)}
* @param agentArgs 命令行参数
* @param instrument JVM自动注入的一个工具类,提供了一套API 用于类文件字节码的修改buf等等,虚拟机级别的AOP 支持 ;
*/
public static void premain(String agentArgs,Instrumentation instrument){
System.out.println("now invoking method 'premain(String agentArgs,Instrumentation instrument)'");
Agent.agentArgs = agentArgs;
Agent.instrumentation = instrument;
}
/**
*
* @param agentArgs
*/
public static void premain(String agentArgs){
System.out.println("now invoking method 'premain(String agentArgs)'");
Agent.agentArgs = agentArgs;
}
}
2.1.3 使用jvm-agent
Main
package com.aruforce.myAop.app;
import com.aruforce.myAop.jvmagent.Agent;
public class Main{
public static void main(String [] args){
System.out.println("Agent.instrumention != null >>"+(Agent.instrumention!=null);
}
}
CommandLine:
java com.aruforce.myAop.app.Main -javaagent:pathto/jvm-agent-0.0.1-SNAPSHOT.jar
log:
now invoking method 'premain(String agentArgs,Instrumentation instrument)'
Agent.instrumention != null >>true
2.2. agentmain(在main方法启动后执行)
当JVM已经处于running mode时候再启用agent
2.2.1 基本要求
- 要求Agent Class有一个public static void agentmain(String agentArgs,Intrumention instrumention)这样签名的方法
- META-INF/MANIFEST.MF文件:要求有Agent-Class 这个key,Value就是Agent的全类名
Agent-Class: com.aruforce.myAop.jvmagent.Agent
2.2.0 parent-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/maven-v4_0_0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.aruforce.myAop</groupId>
<artifactId>myAop</artifactId>
<version>0.0.1-SNAPSHOT</version>
<packaging>pom</packaging>
<modules>
<module>jvm-agent</module>
<module>app</module>
</modules>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<maven.compiler.encoding>UTF-8</maven.compiler.encoding>
<maven.compiler.source>1.7</maven.compiler.source>
<maven.compiler.target>1.7</maven.compiler.target>
<encoding>UTF-8</encoding>
</properties>
<dependencyManagement>
<dependencies>
<!--about log,代码只允许使用slf4j-api-->
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
<version>1.7.25</version>
</dependency>
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-classic</artifactId>
<version>1.2.3</version>
</dependency>
</dependencies>
</dependencyManagement>
<dependencies>
<!--log start-->
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
</dependency>
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-classic</artifactId>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
</dependency>
</dependencies>
</project>
2.2.2 一个Agent
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/maven-v4_0_0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>com.aruforce.myAop</groupId>
<artifactId>myAop</artifactId>
<version>0.0.1-SNAPSHOT</version>
</parent>
<groupId>com.aruforce.myAop</groupId>
<artifactId>jvm-agent</artifactId>
<version>${parent.version}</version>
<packaging>jar</packaging>
<build>
<finalName>${project.artifactId}-${project.version}</finalName>
<pluginManagement>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-jar-plugin</artifactId>
<configuration>
<archive>
<manifestEntries>
<Agent-Class>com.aruforce.myAop.jvmagent.Agent</Agent-Class>
</manifestEntries>
</archive>
</configuration>
</plugin>
</plugins>
</pluginManagement>
</build>
</project>
Agent:
package com.aruforce.myAop.jvmagent;
import java.lang.instrument.Instrumentation;
public class Agent {
public static Instrumentation instrumentation = null;
public static String agentArgs = null;
public static void agentmain(String agentArgs,Instrumentation instrument){
System.out.println("now invoking method 'agentmain(String agentArgs,Instrumentation instrument)'");
Agent.agentArgs = agentArgs;
Agent.instrumentation = instrument;
instrumentation.addTransformer(new CustomClassTransformer());
}
public static void agentmain(String agentArgs){
System.out.println("now invoking method 'agentmain(String agentArgs)'");
Agent.agentArgs = agentArgs;
}
}
CustomClassTransformer:
package com.aruforce.myAop.jvmagent;
import java.lang.instrument.ClassFileTransformer;
import java.lang.instrument.IllegalClassFormatException;
import java.security.ProtectionDomain;
public class CustomClassTransformer implements ClassFileTransformer {
private static final String doChangeClassName = "";
@Override
public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined,ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException {
System.out.println("now ["+className+"] is loaded");
return null;//return null 相当于没有对文件进行修改,实际上可以使用AspectJ等工具在这里对类文件进行增强,classfileBuffer 就是输入的class文件字节序列(并不一定是原始的类文件,可能时上个transformer处理过后的byte[]),不允许修改,自己new一个返回
}
}
2.2.3 使用
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/maven-v4_0_0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>com.aruforce.myAop</groupId>
<artifactId>myAop</artifactId>
<version>0.0.1-SNAPSHOT</version>
</parent>
<groupId>com.aruforce.myAop</groupId>
<artifactId>app</artifactId>
<version>${parent.version}</version>
<packaging>jar</packaging>
<dependencies>
<dependency>
<groupId>com.sun</groupId>
<artifactId>tools</artifactId>
<version>1.7.0</version>
<scope>system</scope>
<systemPath>${java.home}/../lib/tools.jar</systemPath>
</dependency>
</dependencies>
<build>
<finalName>${project.artifactId}-${project.version}</finalName>
</build>
</project>
Test:
package com.aruforce.myAop.app;
import com.sun.tools.attach.AgentInitializationException;
import com.sun.tools.attach.AgentLoadException;
import com.sun.tools.attach.AttachNotSupportedException;
import java.io.IOException;
public class Test {
private static final String agentPath = "D:\\WorkSpacMvn\\myAop\\jvm-agent\\target\\jvm-agent-0.0.1-SNAPSHOT.jar";
public static void main(String[] args) throws IOException, AttachNotSupportedException {
try {
VirtualMachineAttchTools.attechAgent(agentPath); // 就是这么加载到JVM,(注意这个影响范围JVM级别的,而Spring那套是ClassLoader级别的,原理和触发机制不太一样)
} catch (AgentLoadException e) {
e.printStackTrace();
} catch (AgentInitializationException e) {
e.printStackTrace();
}
Logic.doLogic();//展示Logic.class在被类加载器加载到JVM时,会被CustomClassFileTransFormer 处理
}
}
VirtualMachineAttchTools: 一个工具类利用JVM tools attech agent到当前JVM 进程
package com.aruforce.myAop.app;
import com.sun.tools.attach.AgentInitializationException;
import com.sun.tools.attach.AgentLoadException;
import com.sun.tools.attach.AttachNotSupportedException;
import com.sun.tools.attach.VirtualMachine;
import java.io.IOException;
import java.lang.management.ManagementFactory;
public class VirtualMachineAttchTools {
public static void attechAgent(String agentPath) throws IOException, AttachNotSupportedException, AgentLoadException, AgentInitializationException {
String processDs = ManagementFactory.getRuntimeMXBean().getName();
String pid = "";
if (processDs.indexOf("@")>0){
pid = processDs.substring(0, processDs.indexOf("@"));
} else{
pid = processDs;
}
VirtualMachine currentVM = VirtualMachine.attach(pid);
currentVM.loadAgent(agentPath);
currentVM.detach();
}
}
Logic:
package com.aruforce.myAop.app;
public class Logic {
public static void doLogic(){
System.out.println("doLogic invoking");
}
}
log:
now invoking method 'agentmain(String agentArgs,Instrumentation instrument)' //这个时attachJVM时执行的
now [java/lang/IndexOutOfBoundsException] is loaded
now [com/aruforce/myAop/app/Logic] is loaded
doLogic invoking
now [java/lang/Shutdown] is loaded
now [java/lang/Shutdown$Lock] is loaded
3. Instrumention 和ClassFileTranformer
3.1 Instrumention
JVM提供的一个机制:使JVM编写的Agent能够对运行在JVM内的程序进行修改和调整,(一般是通过修改字节码的形式达成目标);
3.1.1 启动方式
上面写的command-line(permain)或者vm.loadAgent(agentmain)
3.1.2 重要的API
3.1.2.1 addTransformer(ClassFileTransformer transformer,boolean canRetransform)
- 操作:向JVM注入一个ClassFileTransformer.
- 效果:所有JVM加载或者重新定义的类文件都会被这个transformer处理,但是不包括[所有的transformer]依赖的类;除此之外,即使当前这个transformer抛出了异常,也不影响下一个transformer的调用;canRetransform表示这个transformer支不支持对一个已被自己处理过的类文件再次处理?
3.1.2.2 retransformClasses(Class<?>... classes)
功能及执行时对JVM的影响:
- 对一组已经被加载的类文件重新处理(不管是不是处理过).
- 在这个过程中如果有活动的线程在使用某些method,这些活动线程会继续使用method原来的代码;
- 这个方法不会造成类的再次重新初始化,也就是说静态代码块不会再次执行
- 这个方法要求不允许增加或者减少方法,也不允许修改方法签名,也不允许修改继承关系
tip:
无法理解如何上面的1是如何做到的.
具体的执行过程:
- 输入为原始的字节码(编译后直接生成的字节码)
- 对于不支持的重新处理的Class文件的transformer,他们之前处理的结果会被复用,而类似于直接跳过执行tansform方法;
- 对于支持的重新处理的transformer,他们的transform会被直接调用
- 处理完的结果会被JVM重新安装
注解参看下面的ClassTransformer执行顺序
- 不支持retransform的Java 编写的transformer
- 不支持retransform的Native的transformer(比如C编写的JVM扩展dll什么的)
- 支持retransform的Java 编写的transformer
- 支持retransform的Native的transformer
运行逻辑大概如下代码:
触发逻辑一般就是ClassLoader在Load或者redifineClass时间发生:
public class Instrumention{
private ArrayList<ClassFileTransformer> retransCapbleformers = new ArrayList<ClassFileTransformer>();
private ArrayList<ClassFileTransformer> retransInCapbleformers = new ArrayList<ClassFileTransformer>();
Map<ClassFileTransformer,Map<String,byte []>> tranResult = new ConcurrentHashMap<ClassFileTransformer,Map<String,byte []>>;
Map<String,byte [] > originBytes = new HashMap<String,byte []>();
public void addTransformer(ClassFileTransformer former,boolean retransCapble){
if(retransCapble){
retransCapbleformers.add(former);
}else{
retransInCapbleformers.add(former);
}
sort(transformers);//主要是排序
}
public byte [] transform(String className,byte [] classBytes){
originBytes.put(className,classBytes);
byte result = classBytes;
//先由 不能重新处理的来
for(ClassFileTransformer transformer:retransInCapbleformers){
byte [] transBytes = transformer.tranform(className,classBytes);
result = transBytes == null?result:transBytes;//是不是空?不是空就用返回的,是就用原来的
tranResult.get(transformer).put(className,transBytes);
}
//再由 能重新处理的来
for(ClassFileTransformer transformer:retransInCapbleformers){
byte [] transBytes = transformer.tranform(className,classBytes);
result = transBytes == null?result:transBytes;//是不是空?不是空就用返回的,是就用原来的
tranResult.get(transformer).put(className,transBytes);
}
return result;
}
public byte [] retransform(String className){
byte result = originBytes.get(className);
//先由 不能重新处理的来.主要是获取到原来处理结果
for(ClassFileTransformer transformer:retransInCapbleformers){
byte [] transBytes = tranResult.get(transformer).get(className);
result = transBytes == null?result:transBytes;//是不是空?不是空就用返回的,是就用原来的
}
//再由 能重新处理的来
for(ClassFileTransformer transformer:retransInCapbleformers){
byte [] transBytes = transformer.tranform(className,result);
result = transBytes == null?result:transBytes;//是不是空?不是空就用返回的,是就用原来的
tranResult.get(transformer).put(className,transBytes);
}
return result;
}
}
class ClassLoader{
Instrumention instrumention;
public Class loadClass(String className){
byte [] orignBytes = IOUTIL.loadClassFile(className);
byte [] buffedBytes = instrumention.transform(className,orignBytes);
return installClass(buffedBytes)
}
public Class reloadClass(String className){
byte [] buffedBytes = instrumention.retransform(className);
return installClass(buffedBytes)
}
public native Class installClass(byte [] classBytes);
}
native installClass
这个是我无法理解,涉及到JVM本身代码实现,到底什么情况什么时机下可以对方法栈进行替换?
3.2 ClassFileTransformer
就是一个接口,在JVM define某个类前,ClassFileTransformer可以对这个类字节码的转换;虚拟机级别的AOP支持
3.2.1 方法: transform(ClassLoader loader,String className,Class<?> classBeingRedefined,ProtectionDomain protectionDomain, byte[] classfileBuffer)
- 分为支持与不支持 retransform的两个类型
- 一旦在JVM内注册完成,任何新类被define或者任何类被重新define
- classfileBuffer 这个就是传入的类文件,read-only方法规定不允许修改, 需要返回一个new byte[] 或者 null
3.2.2 执行顺序
请参看上面的解释性代码;
4. 源代码
myAop.git 虽然其完全不是AOP,等我点了ASM的科技树,我就来还债,稍微运行一下就可以;Agent代码和文章里面稍微有点不一样,只是用来说明ClassTransformer的执行顺序;