相關背景及資源:html
曹工說Spring Boot源碼(1)-- Bean Definition究竟是什麼,附spring思惟導圖分享java
曹工說Spring Boot源碼(2)-- Bean Definition究竟是什麼,我們對着接口,逐個方法講解git
曹工說Spring Boot源碼(3)-- 手動註冊Bean Definition不比遊戲好玩嗎,咱們來試一下web
曹工說Spring Boot源碼(4)-- 我是怎麼自定義ApplicationContext,從json文件讀取bean definition的?spring
曹工說Spring Boot源碼(5)-- 怎麼從properties文件讀取beanshell
曹工說Spring Boot源碼(6)-- Spring怎麼從xml文件裏解析bean的apache
曹工說Spring Boot源碼(7)-- Spring解析xml文件,到底從中獲得了什麼(上)json
曹工說Spring Boot源碼(8)-- Spring解析xml文件,到底從中獲得了什麼(util命名空間)api
曹工說Spring Boot源碼(9)-- Spring解析xml文件,到底從中獲得了什麼(context命名空間上)數組
曹工說Spring Boot源碼(10)-- Spring解析xml文件,到底從中獲得了什麼(context:annotation-config 解析)
曹工說Spring Boot源碼(11)-- context:component-scan,你真的會用嗎(此次來講說它的奇技淫巧)
曹工說Spring Boot源碼(12)-- Spring解析xml文件,到底從中獲得了什麼(context:component-scan完整解析)
曹工說Spring Boot源碼(13)-- AspectJ的運行時織入(Load-Time-Weaving),基本內容是講清楚了(附源碼)
曹工說Spring Boot源碼(14)-- AspectJ的Load-Time-Weaving的兩種實現方式細細講解,以及怎麼和Spring Instrumentation集成
曹工說Spring Boot源碼(15)-- Spring從xml文件裏到底獲得了什麼(context:load-time-weaver 完整解析)
曹工說Spring Boot源碼(16)-- Spring從xml文件裏到底獲得了什麼(aop:config完整解析【上】)
曹工說Spring Boot源碼(17)-- Spring從xml文件裏到底獲得了什麼(aop:config完整解析【中】)
曹工說Spring Boot源碼(18)-- Spring AOP源碼分析三部曲,終於快講完了 (aop:config完整解析【下】)
曹工說Spring Boot源碼(19)-- Spring 帶給咱們的工具利器,建立代理不用愁(ProxyFactory)
曹工說Spring Boot源碼(20)-- 碼網恢恢,疏而不漏,如何記錄Spring RedisTemplate每次操做日誌
曹工說Spring Boot源碼(21)-- 爲了讓你們理解Spring Aop利器ProxyFactory,我已經拼了
曹工說Spring Boot源碼(22)-- 你說我Spring Aop依賴AspectJ,我依賴它什麼了
曹工說Spring Boot源碼(23)-- ASM又立功了,Spring原來是這麼遞歸獲取註解的元註解的
曹工說Spring Boot源碼(24)-- Spring註解掃描的瑞士軍刀,asm技術實戰(上)
工程結構圖:
上一篇,咱們講了ASM基本的使用方法,具體包括:複製一個class、修改class版本號、增長一個field、去掉一個field/method等等;同時,咱們也知道了怎麼才能生成一個全新的class。
可是,僅憑這點粗淺的知識,咱們依然不太理解能幹嗎,本篇會帶你們實現簡單的AOP功能,固然了,學完了以後,可能你像我同樣,更困惑了,那說明你變強了。
本篇的核心是,在JVM加載class的時候,去修改class,修改class的時候,加入咱們的aop邏輯。JVM加載class的時候,去修改class,這項技術就是load-time-weaver,實現load-time-weaver有兩種方式,這兩種方式,核心差異在於修改class的時機不一樣。
在直接開始前,聲明本篇文章,是基於下面這篇文章中的代碼demo,我本身稍作了修改,並附上源碼(原文是貼了代碼,可是沒有直接提供代碼地址,不貼心啊)。
目標就是給下面的測試類,加上一點點切面功能。
package org.xunche.app; public class HelloXunChe { public static void main(String[] args) throws InterruptedException { HelloXunChe helloXunChe = new HelloXunChe(); helloXunChe.sayHi(); } public void sayHi() throws InterruptedException { System.out.println("hi, xunche"); sleep(); } public void sleep() throws InterruptedException { Thread.sleep((long) (Math.random() * 200)); } }
咱們但願,class在執行的時候,可以打印方法執行的耗時,也就是,最終的class,須要是下面這樣的。
package org.xunche.app; import org.xunche.agent.TimeHolder; public class HelloXunChe { public HelloXunChe() { } public static void main(String[] args) throws InterruptedException { TimeHolder.start(args.getClass().getName() + "." + "main"); // 業務邏輯開始 HelloXunChe helloXunChe = new HelloXunChe(); helloXunChe.sayHi(); //業務邏輯結束 HelloXunChe helloXunChe = args.getClass().getName() + "." + "main"; System.out.println(helloXunChe + ": " + TimeHolder.cost(helloXunChe)); } public void sayHi() throws InterruptedException { TimeHolder.start(this.getClass().getName() + "." + "sayHi"); System.out.println("hi, xunche"); // 業務邏輯開始 this.sleep(); //業務邏輯結束 String var1 = this.getClass().getName() + "." + "sayHi"; System.out.println(var1 + ": " + TimeHolder.cost(var1)); } public void sleep() throws InterruptedException { TimeHolder.start(this.getClass().getName() + "." + "sleep"); // 業務邏輯開始 Thread.sleep((long)(Math.random() * 200.0D)); //業務邏輯結束 String var1 = this.getClass().getName() + "." + "sleep"; System.out.println(var1 + ": " + TimeHolder.cost(var1)); } }
因此,咱們大概就是,要作下面的這樣一個切面:
@Override protected void onMethodEnter() { //在方法入口處植入 String className = getClass().getName(); String s = className + "." + methodName; TimeHolder.start(s); } @Override protected void onMethodExit(int i) { //在方法出口植入 String className = getClass().getName(); String s = className + "." + methodName; long cost = TimeHolder.cost(s); System.out.println(s + ": " + cost); }
可是,習慣了動態代理的咱們,看上面的代碼可能會有點誤解。上面的代碼,不是在執行目標方法前,調用切面;而是:直接把切面代碼嵌入了目標方法。
想必你們都明確了要達成的目標了,下面說,怎麼作。
這部分,你們能夠結合開頭那個連接一塊兒學習。
首先,我請你們看看java命令行的選項。直接在cmd裏敲java,出現以下:
看了和沒看同樣,那咱們再看一張圖,在你們破解某些java編寫的軟件時,可能會涉及到jar包破解,好比:
你們可使用jad這類反編譯軟件,打開jar包看下,看看裏面是啥:
能夠發現,裏面有一個MANIFEST.MF文件,裏面指定了Premain-Class這個key-value,從這個名字,你們可能知道了,咱們平時運行java程序,都是運行main方法,這裏來個premain,那這意思,就是在main方法前面插個隊唄?
你說的沒有錯,確實是插隊了,拿上面的破解jar包舉例,裏面的Premain-Class方法,對應的Agent類,反編譯後的代碼以下:
核心代碼就是圖裏那一行:
java.lang.instrument.Instrumentation public interface Instrumentation { /** * Registers the supplied transformer. All future class definitions * will be seen by the transformer, except definitions of classes upon which any * registered transformer is dependent. * The transformer is called when classes are loaded, when they are * {@linkplain #redefineClasses redefined}. and if <code>canRetransform</code> is true, * when they are {@linkplain #retransformClasses retransformed}. * See {@link java.lang.instrument.ClassFileTransformer#transform * ClassFileTransformer.transform} for the order * of transform calls. * If a transformer throws * an exception during execution, the JVM will still call the other registered * transformers in order. The same transformer may be added more than once, * but it is strongly discouraged -- avoid this by creating a new instance of * transformer class. * <P> * This method is intended for use in instrumentation, as described in the * {@linkplain Instrumentation class specification}. * * @param transformer the transformer to register * @param canRetransform can this transformer's transformations be retransformed * @throws java.lang.NullPointerException if passed a <code>null</code> transformer * @throws java.lang.UnsupportedOperationException if <code>canRetransform</code> * is true and the current configuration of the JVM does not allow * retransformation ({@link #isRetransformClassesSupported} is false) * @since 1.6 */ void addTransformer(ClassFileTransformer transformer, boolean canRetransform); ... }
這個類,就是官方jdk提供的類,官方的本意呢,確定是讓你們,在加載class的時候,給你們提供一個機會,去修改class,好比,某個第三方jar包,咱們須要修改,可是沒有源碼,就能夠這麼幹;或者是一些要統一處理,不方便在應用中耦合的功能:好比埋點、性能監控、日誌記錄、安全監測等。
說回這個方法,參數爲ClassFileTransformer,這個接口,就一個方法,你們看看註釋:
/** * ... * * @param classfileBuffer the input byte buffer in class file format - must not be modified * * @throws IllegalClassFormatException if the input does not represent a well-formed class file * @return a well-formed class file buffer (the result of the transform), or <code>null</code> if no transform is performed. * @see Instrumentation#redefineClasses */ byte[] transform( ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException;
別的也很少說了,反正就是:jvm給你原始class,你本身修改,還jvm一個改後的class。
因此,你們估計也能猜到破解的原理了,但我仍是但願你們:有能力支持正版的話,仍是要支持。
接下來,咱們回到咱們的目標的實現上。
完整代碼:https://gitee.com/ckl111/all-simple-demo-in-work/tree/master/java-agent-premain-demo
package org.xunche.agent; import org.objectweb.asm.*; import org.objectweb.asm.commons.AdviceAdapter; import java.lang.instrument.ClassFileTransformer; import java.lang.instrument.Instrumentation; import java.security.ProtectionDomain; public class TimeAgentByJava { public static void premain(String args, Instrumentation instrumentation) { instrumentation.addTransformer(new TimeClassFileTransformer()); } }
類轉換器的詳細代碼以下:
private static class TimeClassFileTransformer implements ClassFileTransformer { @Override public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) { if (className.startsWith("java") || className.startsWith("jdk") || className.startsWith("javax") || className.startsWith("sun") || className.startsWith("com/sun")|| className.startsWith("org/xunche/agent")) { //return null或者執行異常會執行原來的字節碼 return null; } // 1 System.out.println("loaded class: " + className); ClassReader reader = new ClassReader(classfileBuffer); // 2 ClassWriter writer = new ClassWriter(reader, ClassWriter.COMPUTE_FRAMES | ClassWriter.COMPUTE_MAXS); // 3 reader.accept(new TimeClassVisitor(writer), ClassReader.EXPAND_FRAMES); // 4 return writer.toByteArray(); } }
1處,將原始的類字節碼加載到classReader中
ClassReader reader = new ClassReader(classfileBuffer);
2處,將reader傳給ClassWriter,這個咱們沒講過,大概就是使用classreader中的東西,來構造ClassWriter;能夠差很少理解爲複製classreader的東西到ClassWriter中。
你們能夠看以下代碼:
public ClassWriter(final ClassReader classReader, final int flags) { super(Opcodes.ASM6); symbolTable = new SymbolTable(this, classReader); ... }
這裏new了一個對象,SymbolTable。
SymbolTable(final ClassWriter classWriter, final ClassReader classReader) { this.classWriter = classWriter; this.sourceClassReader = classReader; // Copy the constant pool binary content. byte[] inputBytes = classReader.b; int constantPoolOffset = classReader.getItem(1) - 1; int constantPoolLength = classReader.header - constantPoolOffset; constantPoolCount = classReader.getItemCount(); constantPool = new ByteVector(constantPoolLength); constantPool.putByteArray(inputBytes, constantPoolOffset, constantPoolLength); ... }
你們直接看上面的註釋吧,Copy the constant pool binary content
。反正吧,基本能夠理解爲,classwriter拷貝了classreader中的一部分東西,應該不是所有。
爲何不是所有,由於我試了下:
public static void main(String[] args) throws IOException { ClassReader reader = new ClassReader("org.xunche.app.HelloXunChe"); ClassWriter writer = new ClassWriter(reader, ClassWriter.COMPUTE_FRAMES | ClassWriter.COMPUTE_MAXS); byte[] bytes = writer.toByteArray(); File file = new File( "F:\\gitee-ckl\\all-simple-demo-in-work\\java-agent-premain-demo\\test-agent\\src\\main\\java\\org\\xunche\\app\\HelloXunChe.class"); FileOutputStream fos = new FileOutputStream(file); fos.write(bytes); fos.close(); }
上面這樣,出來的class文件,是破損的,格式不正確的,沒法反編譯。
3處,使用TimeClassVisitor做爲writer的中間商,此時,順序變成了:
classreader --> TimeClassVisitor --> classWriter
4處,返回writer的字節碼,給jvm;jvm使用該字節碼,去redefine一個class出來
public static class TimeClassVisitor extends ClassVisitor { public TimeClassVisitor(ClassVisitor classVisitor) { super(Opcodes.ASM6, classVisitor); } // 1 @Override public MethodVisitor visitMethod(int methodAccess, String methodName, String methodDesc, String signature, String[] exceptions) { MethodVisitor methodVisitor = cv.visitMethod(methodAccess, methodName, methodDesc, signature, exceptions); // 2 return new TimeAdviceAdapter(Opcodes.ASM6, methodVisitor, methodAccess, methodName, methodDesc); } } }
咱們這裏的TimeAdviceAdapter,主要是但願在方法執行先後作點事,相似於切面,因此繼承了一個AdviceAdapter,這個AdviceAdaper,幫咱們實現了MethodVisitor的所有方法,咱們只須要覆寫咱們想要覆蓋的方法便可。
好比,AdviceAdaper,由於繼承了MethodVisitor,其visitCode方法,會在訪問方法體時被回調:
@Override public void visitCode() { super.visitCode(); // 1 onMethodEnter(); } //2 protected void onMethodEnter() {}
因此,咱們最終的TimeAdviceAdaper,代碼以下:
public static class TimeAdviceAdapter extends AdviceAdapter { private String methodName; protected TimeAdviceAdapter(int api, MethodVisitor methodVisitor, int methodAccess, String methodName, String methodDesc) { super(api, methodVisitor, methodAccess, methodName, methodDesc); this.methodName = methodName; } @Override protected void onMethodEnter() { //在方法入口處植入 if ("<init>".equals(methodName)|| "<clinit>".equals(methodName)) { return; } String className = getClass().getName(); String s = className + "." + methodName; TimeHolder.start(s); } @Override protected void onMethodExit(int i) { //在方法出口植入 if ("<init>".equals(methodName) || "<clinit>".equals(methodName)) { return; } String className = getClass().getName(); String s = className + "." + methodName; long cost = TimeHolder.cost(s); System.out.println(s + ": " + cost); } }
這份代碼看着可還行?惋惜啊,是假的,是錯誤的!寫asm這麼簡單的話,那我要從夢裏笑醒。
爲啥是假的,由於:真正的代碼,是長下面這樣的:
看到這裏,是否是想溜了,這都啥玩意,看不懂啊,不過不要着急,辦法總比困難多。
咱們先裝個idea插件,叫:asm-bytecode-outline
。這個插件的做用,簡而言之,就是幫你把java代碼翻譯成ASM的寫法。在線裝不了的,能夠離線裝:
裝好插件後,只要在咱們的TimeAdviceAdapter類,點右鍵:
就會生成咱們須要的ASM代碼,而後拷貝:
何時拷貝結束呢?
基本上,這樣就能夠了。
做爲一個常年掉坑的人,我在這個坑裏也摸爬了整整一天。
你們能夠看到,咱們的java寫的方法裏,是這樣的:
@Override protected void onMethodEnter() { //在方法入口處植入 if ("<init>".equals(methodName)|| "<clinit>".equals(methodName)) { return; } String className = getClass().getName(); // 1. String s = className + "." + methodName; TimeHolder.start(s); }
因此,asm也幫咱們貼心地生成了這樣的語句:
mv.visitFieldInsn(Opcodes.GETFIELD, "org/xunche/agent/TimeAgentByJava$TimeAdviceAdapter", "methodName", "Ljava/lang/String;");
看起來就像是說,訪問org/xunche/agent/TimeAgentByJava$TimeAdviceAdapter類的methodName字段。
可是,這是有問題的。由於,這段代碼,最終aop切面會被插入到target:
public class HelloXunChe { private String methodName = "abc"; public static void main(String[] args) throws InterruptedException { HelloXunChe helloXunChe = new HelloXunChe(); helloXunChe.sayHi(); } public void sayHi() throws InterruptedException { System.out.println("hi, xunche"); sleep(); } public void sleep() throws InterruptedException { Thread.sleep((long) (Math.random() * 200)); } }
我實話跟你說,這個target類裏,壓根訪問不到org/xunche/agent/TimeAgentByJava$TimeAdviceAdapter類的methodName字段。
我是怎麼發現這個問題的,以前一直報錯,直到我在target後來加了這麼一行:
public class HelloXunChe { private String methodName = "abc"; ... }
哎,沒個大佬帶我,真的難。
固然,我是經過這個確認了上述問題,最終解決的思路呢,就是:把你生成的class,反編譯出來看看,看看是否是你想要的。
因此,我專門寫了個main測試類,來測試改後的class是否符合預期。
public class SaveGeneratedClassWithOriginAgentTest { public static void main(String[] args) throws IOException { //1 ClassReader reader = new ClassReader("org.xunche.app.HelloXunChe"); ClassWriter writer = new ClassWriter(reader, ClassWriter.COMPUTE_FRAMES | ClassWriter.COMPUTE_MAXS); reader.accept(new TimeAgentByJava.TimeClassVisitor(writer), ClassReader.EXPAND_FRAMES); byte[] bytes = writer.toByteArray(); // 2 File file = new File( "F:\\ownprojects\\all-simple-demo-in-work\\java-agent-premain-demo\\test-agent\\src\\main\\java\\org\\xunche\\app\\HelloXunCheCopy2.class"); FileOutputStream fos = new FileOutputStream(file); fos.write(bytes); fos.close(); } }
因此,上面那段asm,你們若是看:
會發現,訪問methodname那句代碼,是這麼寫的:
mv.visitLdcInsn(methodName);
這就是,至關於直接把methodName寫死到最終的class裏去了;最終的class就會是想要的樣子:
public void sayHi() throws InterruptedException { //1 TimeHolder.start(this.getClass().getName() + "." + "sayHi"); System.out.println("hi, xunche"); this.sleep(); // 2 String var1 = this.getClass().getName() + "." + "sayHi"; System.out.println(var1 + ": " + TimeHolder.cost(var1)); }
插件中,配置Premain-Class
<plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-jar-plugin</artifactId> <version>2.3.1</version> <configuration> <archive> <manifest> <addClasspath>true</addClasspath> </manifest> <manifestEntries> <Premain-Class> org.xunche.agent.TimeAgent </Premain-Class> </manifestEntries> </archive> </configuration> </plugin>
測試模塊,沒啥開發的,就只有那個target那個類。
最終我是這麼運行的:
java -javaagent:agent.jar -classpath lib/*;java-agent-premain-demo.jar org/xunche/app/He lloXunChe
這裏指定了lib目錄,主要是agent模塊須要的jar包:
簡單的運行效果以下:
loaded class: org/xunche/app/HelloXunChe methodName = 0 <init> methodName = 0 main methodName = 0 sayHi methodName = 0 sleep hi, xunche org.xunche.app.HelloXunChe.abc: 129 org.xunche.app.HelloXunChe.abc: 129
ASM這個東西,想要不熟悉字節碼就去像我上面這樣傻瓜操做,坑仍是比較多的,比較難趟。回頭有空再介紹字節碼吧。我也是半桶水,你們一塊兒學習吧。
本節源碼:
https://gitee.com/ckl111/all-simple-demo-in-work/tree/master/java-agent-premain-demo