java初學者練手項目:java項目源碼哪裡找

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-hk/n/223302.html

(0)
打賞 微信掃一掃 微信掃一掃 支付寶掃一掃 支付寶掃一掃
投稿專員的頭像投稿專員
上一篇 2024-12-09 14:15
下一篇 2024-12-09 14:15

相關推薦

發表回復

登錄後才能評論