1. 前言
為什麼會接觸JavaAgent呢?
這起源於筆者最近在讀Dubbo的源碼,Dubbo有一個很有意思的功能——SPI,它可以根據運行時的URI參數,自適應的調用特定的實現類。大致的原理其實也能猜到,無非就是生成一個代理類,反射解析URI參數里的值,然後再調用對應的實現類。雖然大概可以猜到實現原理,但畢竟只是猜想,抱著科學嚴謹的精神,還是想看看Dubbo的實現源碼,此時就有了一個想法,能不能把Dubbo生成的代理對象的Class類Dump下來,然後反編譯看看它的源碼呢?
理論上是完全可行的,阿里有一個很好用的開源工具Arthas,它的jad命令就支持對JVM已經載入的類進行反編譯查看源碼,筆者把Arthas項目源碼down下來了,查看以後發現,需要用到JavaAgent技術。
2. JavaAgent規範
在JDK1.5以後,我們可以使用JavaAgent技術,以「零侵入」的方式對Java程序做增強。例如阿里雲的Arms應用監控服務,就可以通過JavaAgent的方式接入一個探針,它會把應用的運行數據上報到阿里雲,開發者可以在後台查看到應用的運行數據。這種方式,不需要我們對應用做任何改動,就可以輕鬆實現應用監控。
JavaAgent是一種規範,它分為兩類:主程序運行前Agent、主程序運行後Agent。它可以在JVM載入Class文件前,對位元組碼做修改,甚至允許修改已經載入過的Class,這樣我們就可以對應用做增強、以及實現代碼熱部署。
主程序運行前Agent的步驟:
1、編寫Agent類,該類必須有靜態方法premain()。
public class MyAgentClass {
// JVM優先執行該方法
public static void premain(String agentArgs, Instrumentation inst) {
System.err.println("main before...");
}
public static void premain(String agentArgs) {
System.err.println("main before...");
}
}
2、在resources/META-INF目錄下編寫MANIFEST.MF文件,指定Premain-Class,然後將程序打成Jar包。
Manifest-Version: 1.0
Can-Redefine-Classes: true
Can-Retransform-Classes: true
Premain-Class: top.javap.agent.MyAgentClass
// 注意,這裡必須空一行
使用Maven構建程序時,也可使用如下配置。
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-jar-plugin</artifactId>
<configuration>
<archive>
<manifest>
<addClasspath>true</addClasspath>
</manifest>
<manifestEntries>
<Premain-Class>top.javap.agent.MyAgentClass</Premain-Class>
<Can-Retransform-Classes>true</Can-Retransform-Classes>
<Can-Redefine-Classes>true</Can-Redefine-Classes>
</manifestEntries>
</archive>
</configuration>
</plugin>
3、啟動目標程序時,指定JVM參數,如下:
java -javaagent:agent-1.0-SNAPSHOT.jar JavaApp
主程序運行後Agent的步驟:
這種是針對已經運行的JVM進程,我們可以通過attach機制,啟動一個新的JVM進程發送指令給它執行。
1、編寫Agent類,該類必須有靜態方法agentmain()。
public class MyAgentClass {
public static void agentmain(String agentArgs, Instrumentation inst) {
System.err.println("main after...");
}
}
2、在resources/META-INF目錄下編寫MANIFEST.MF文件,指定Premain-Class,然後將程序打成Jar包。
Manifest-Version: 1.0
Can-Redefine-Classes: true
Can-Retransform-Classes: true
Agent-Class: top.javap.agent.MyAgentClass
// 注意,這裡必須空一行
3、編寫attach程序,啟動並attach到目標JVM進程。
public static void main(String[] args) throws Exception {
VirtualMachine vm = VirtualMachine.attach("8080");
vm.loadAgent("/dev/agent.jar");
}
3. 相關組件
3.1 Instrumentation
編寫的AgentClass類必須有premain()方法,其中一個比較重要的參數就是Instrumentation。它是JavaAgent技術用到的主要API,介面定義如下:
public interface Instrumentation {
/**
* 添加Class文件轉換器,底層採用數組存儲
* JVM載入Class文件前,需要依次經過轉換
* @param transformer
* @param canRetransform 是否允許轉換
*/
void addTransformer(ClassFileTransformer transformer, boolean canRetransform);
void addTransformer(ClassFileTransformer transformer);
// 刪除Class文件轉換器
boolean removeTransformer(ClassFileTransformer transformer);
boolean isRetransformClassesSupported();
// 重新轉換Class
void retransformClasses(Class<?>... classes) throws UnmodifiableClassException;
boolean isRedefineClassesSupported();
// 重新定義Class,熱更新
void redefineClasses(ClassDefinition... definitions)
throws ClassNotFoundException, UnmodifiableClassException;
boolean isModifiableClass(Class<?> theClass);
@SuppressWarnings("rawtypes")
Class[] getAllLoadedClasses();
@SuppressWarnings("rawtypes")
Class[] getInitiatedClasses(ClassLoader loader);
// 獲取對象大小
long getObjectSize(Object objectToSize);
void appendToBootstrapClassLoaderSearch(JarFile jarfile);
void appendToSystemClassLoaderSearch(JarFile jarfile);
boolean isNativeMethodPrefixSupported();
void setNativeMethodPrefix(ClassFileTransformer transformer, String prefix);
}
重要的方法筆者已經寫上注釋了,本文會用到的方法主要是addTransformer()。它可以用來添加Class轉換器,JVM在載入Class前,會先經過這些轉換器進行加工。
3.2 ClassFileTransformer
Class文件轉換器,JVM載入某個Class前,會先經過它轉換,我們可以在這裡去修改位元組碼以達到功能增強的目的。它只有一個方法transform():
public interface ClassFileTransformer{
/**
* 轉換Class
* @param loader 類載入器
* @param className 類名
* @param classBeingRedefined 原始Class
* @param ProtectionDomain
* @param classfileBuffer Class文件位元組數組
*/
byte[] transform( ClassLoader loader,
String className,
Class<?> classBeingRedefined,
ProtectionDomain protectionDomain,
byte[] classfileBuffer)
throws IllegalClassFormatException;
}
本文主要用到的就是classfileBuffer,有了Class的位元組數組,只要把它導出到磁碟,通過IDEA反編譯就能看到源碼了。
4. 實戰
【需求】
支持將任意Java對象的Class文件導出到磁碟,通過反編譯查看源碼,包括動態生成的類。
【實現】
1、編寫InstrumentationHolder,持有Instrumentation實例,後續操作全靠它。
public class InstrumentationHolder {
private static Instrumentation INSTANCE;
public static void init(Instrumentation ins) {
INSTANCE = ins;
}
public static Instrumentation get() {
if (INSTANCE == null) {
throw new RuntimeException("檢查 -javaagent 配置");
}
return INSTANCE;
}
}
2、編寫MyAgentClass,保存Instrumentation實例。
public class MyAgentClass {
public static void premain(String agentArgs, Instrumentation inst) {
System.err.println("main before...");
InstrumentationHolder.init(inst);
}
}
3、編寫ClassDumpTransformer,獲取Class文件位元組數組,導出到磁碟。
public class ClassDumpTransformer implements ClassFileTransformer {
private final File file;
private final Set<Class<?>> classes = new HashSet<>();
public ClassDumpTransformer(String path, Class<?>... classes) {
this.file = new File(path);
this.classes.addAll(Arrays.asList(classes));
}
@Override
public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException {
if (classes.contains(classBeingRedefined)) {
FileUtil.writeBytes(classfileBuffer, file);
}
return null;
}
}
4、編寫ClassUtil工具類,支持導出Class文件。
public class ClassUtil {
public static void classDump(Class<?> c, String path) {
ClassDumpTransformer transformer = new ClassDumpTransformer(path, c);
Instrumentation inst = InstrumentationHolder.get();
inst.addTransformer(transformer, true);
try {
inst.retransformClasses(c);
} catch (UnmodifiableClassException e) {
e.printStackTrace();
} finally {
inst.removeTransformer(transformer);
}
}
}
5、編寫MANIFEST.MF文件,構建Jar包。
Manifest-Version: 1.0
Can-Redefine-Classes: true
Can-Retransform-Classes: true
Premain-Class: top.javap.agent.MyAgentClass
6、編寫測試類,利用JDK動態代理生成代理類,然後將代理類的Class文件導出。
public class AgentDemo {
public static void main(String[] args) throws Exception {
Object instance = Proxy.newProxyInstance(A.class.getClassLoader(), new Class[]{A.class}, new InvocationHandler() {
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
return null;
}
});
ClassUtil.classDump(instance.getClass(),
"/target/X.class");
}
public static interface A {
void a();
}
}
7、設置-javaagent參數並啟動程序。
java -javaagent:agent.jar AgentDemo
此時,target目錄下就會生成X.class文件,通過IDEA打開即可看到JDK生成的代理類源碼。
5. 總結
JavaAgent十分強大,通過它可以在JVM載入Class文件前修改位元組碼,甚至修改JVM已經載入的Class。基於此,我們可以「零侵入」的對應用程序做增強,服務實現熱部署等等。
本文通過一個小示例,編寫ClassFileTransformer實現類導出對象的Class文件,反編譯查看其源碼。這對於ASM操作位元組碼、JDK動態代理等動態生成類的場景下,而我們又想看對象的具體實現時,提供了幫助。
原創文章,作者:投稿專員,如若轉載,請註明出處:https://www.506064.com/zh-tw/n/223302.html