項目源碼在哪里找 源代碼在哪找



文章插圖
項目源碼在哪里找 源代碼在哪找

文章插圖
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.0Can-Redefine-Classes: trueCan-Retransform-Classes: truePremain-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.0Can-Redefine-Classes: trueCan-Retransform-Classes: trueAgent-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();// 重新轉換Classvoid retransformClasses(Class<?>... classes) throws UnmodifiableClassException;boolean isRedefineClassesSupported();// 重新定義Class,熱更新void redefineClasses(ClassDefinition... definitions)throwsClassNotFoundException, 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));[email protected] 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.0Can-Redefine-Classes: trueCan-Retransform-Classes: truePremain-Class: top.javap.agent.MyAgentClass6、編寫測試類,利用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() [email protected] 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動態代理等動態生成類的場景下,而我們又想看對象的具體實現時,提供了幫助 。