概述:
1. 什麼是代理
2. 代理的分類
3. Spring AOP對動態代理的應用
一、什麼是代理
你需要乘飛機,但是去不了機場,機票代理點就能讓你實現買機票的需求。
你需要辦理車過戶,但是你不知道流程,在門口找一個專門代你辦理的人,他都給你辦了,這就是代理。
可見代理是個中間商,他代替原來的事務部門,滿足你的需求,這就是代理模式的意義。
想象一下,你想修改某個類以實現特殊的功能,但是這個類在SDK包裡,或者在遠端機器上,怎麼辦?
這時候你可以找個代理,不就是想實現自定義功能嗎?不用去改原始類了,你在我這隨便改,我把原始類整合進來,這樣我既有原始類的功能,又有你自定義的功能,不就完美了。
這就是代理模式。
二、代理的分類
1. 靜態代理
這個 不好類比說明,因為java程式中有執行中的概念,靜態代理就相當於執行前,你就已經寫好了代理類,然後編譯直接呼叫。
比如有如下場景,目前有個生產玩具的類,在不改變這個類的前提下,增加統計這個類生產玩具方法用時的功能,這個怎麼實現?
1 /** 2 * 委託者,原始類,一個生產玩偶的工廠 3 */ 4 public class ToyFactory implements Produce { 5 @Override 6 public void produce_cat() { 7 System.out.println("生產了一隻小貓"); 8 try { 9 Thread.sleep(new Random().nextInt(1000)); 10 } catch (InterruptedException e) { 11 e.printStackTrace(); 12 } 13 } 14 15 @Override 16 public void produce_deer() { 17 System.out.println("生產了一隻小鹿"); 18 try { 19 Thread.sleep(new Random().nextInt(1000)); 20 } catch (InterruptedException e) { 21 e.printStackTrace(); 22 } 23 } 24 }
1 /** 2 * 生產方法統計時間的代理類 3 */ 4 public class ToyFactoryTimeProxy implements Produce{ 5 private ToyFactory toyFactory; 6 7 8 public ToyFactoryTimeProxy(ToyFactory toyFactory) { 9 this.toyFactory = toyFactory; 10 } 11 12 @Override 13 public void produce_cat() { 14 long startTime = System.currentTimeMillis(); 15 this.toyFactory.produce_cat(); 16 long endTime = System.currentTimeMillis(); 17 long takeTime = endTime - startTime; 18 System.out.println("log-----cat take time="+takeTime); 19 } 20 21 @Override 22 public void produce_deer() { 23 long startTime = System.currentTimeMillis(); 24 this.toyFactory.produce_deer(); 25 long endTime = System.currentTimeMillis(); 26 long takeTime = endTime - startTime; 27 System.out.println("log-----deer take time="+takeTime); 28 29 } 30 }
1 public static void main(String[] args) { 2 ToyFactory toyFactory = new ToyFactory(); 3 ToyFactoryTimeProxy toyFactoryTimeProxy = new ToyFactoryTimeProxy(toyFactory); 4 toyFactoryTimeProxy.produce_cat(); 5 }
執行結果:
生產了一隻小貓
log-----cat take time=226
這就是靜態代理的實現,這種方式屬於聚合的方式,其實還有一種方式能實現類似的效果,就是繼承。
我們可以繼承工廠類,然後重寫造小貓的方法,在這方法中寫統計時間的邏輯,但是繼承方式有弊端,如果我們再要一個功能,就是在統計完時間後,還列印日誌,這無非就是再寫一個子類,繼承時間代理類,但是如果新的需求是先列印日誌,再統計時間,對於繼承來說,之前寫的就要不了了,得再寫一個工廠類的子類,作為日誌代理類,再寫一個日誌代理類的子類,作為時間代理類。
然而通過聚合的方式,可以利用java多型的特性,既然所有的代理類和委託類都需要實現同一個介面,那麼我們就直接都聚合介面,而不是具體的委託類,這樣就可以實現代理類之間也可以互相代理了。
首先把時間代理類中的ToyFactory改成Produce。
1 /** 2 * 生產方法統計時間的代理類 3 */ 4 public class ToyFactoryTimeProxy implements Produce{ 5 private Produce produce; 6 7 8 public ToyFactoryTimeProxy(Produce produce) { 9 this.produce = produce; 10 } 11 12 @Override 13 public void produce_cat() { 14 long startTime = System.currentTimeMillis(); 15 this.produce.produce_cat(); 16 long endTime = System.currentTimeMillis(); 17 long takeTime = endTime - startTime; 18 System.out.println("log-----cat take time="+takeTime); 19 } 20 21 @Override 22 public void produce_deer() { 23 long startTime = System.currentTimeMillis(); 24 this.produce.produce_deer(); 25 long endTime = System.currentTimeMillis(); 26 long takeTime = endTime - startTime; 27 System.out.println("log-----deer take time="+takeTime); 28 29 } 30 }
1 /** 2 * 這是個生產方法打日誌的代理類 3 */ 4 public class ToyFactoryLogProxy implements Produce{ 5 private Produce Produce; 6 7 8 public ToyFactoryLogProxy(Produce Produce) { 9 this.Produce = Produce; 10 } 11 12 @Override 13 public void produce_cat() { 14 this.Produce.produce_cat(); 15 System.out.println("log-----cat is produced"); 16 } 17 18 @Override 19 public void produce_deer() { 20 this.Produce.produce_deer(); 21 System.out.println("log-----deer is produced"); 22 } 23 }
1 public static void main(String[] args) { 2 ToyFactory toyFactory = new ToyFactory(); 3 ToyFactoryTimeProxy toyFactoryTimeProxy = new ToyFactoryTimeProxy(toyFactory); 4 ToyFactoryLogProxy toyFactoryLogProxy = new ToyFactoryLogProxy(toyFactoryTimeProxy); 5 toyFactoryLogProxy.produce_cat(); 6 }
執行結果:
生產了一隻小貓 log-----cat take time=914 log-----cat is produced
如果想反過來,只需要把main方法中的聚合順序調整一下就可以了。
這裡跑題一下,積累一下多型的知識:
面向介面程式設計的概念,用電腦主機板和顯示卡來舉例。 如果你主機板上鍊接的是具體的某個記憶體條,那麼會造成一種什麼情況: 在組裝電腦之處,你對記憶體的要求就是2G就能滿足,你new了一個2G的記憶體條,隨著使用2G不夠了,這個時候你已經沒法切換了。 《物件導向軟體構造(Object Oriented Software Construction)》中提出了開閉原則,它的原文是這樣:“Software entities should be open for extension,but closed for modification”。
翻譯過來就是:“軟體實體應當對擴充套件開放,對修改關閉”。這句話說得略微有點專業,我們把它講得更通俗一點,也就是:軟體系統中包含的各種元件,例如模組(Modules)、類(Classes)以及功能(Functions)等等,應該在不修改現有程式碼的基礎上,引入新功能。
開閉原則中“開”,是指對於元件功能的擴充套件是開放的,是允許對其進行功能擴充套件的;開閉原則中“閉”,是指對於原有程式碼的修改是封閉的,即修改原有的程式碼對外部的使用是透明的。 然而要解決這個問題,答案其實就在介面上了,你約定好了一個規範介面,所有顯示卡物件要想接入主機板,必須實現這個介面,那麼你在設計功能的時候,壓根不用考慮具體實現,
2G不夠直接拔了換4G,ArrayList不行就直接換LinkList,具體實現與主體類就實現瞭解耦,而面向介面程式設計,本質上就是運用了java多型的特性。
靜態代理雖然也實現了功能,但是存在兩個問題:
1. 如果SDK包裡有100個委託類需要代理,那麼就得寫100個代理類,這個在現實工作中並不稀奇,最常用到的就是AOP,你需要攔截符合條件的所有類的方法,給他們附加上功能,這個要用靜態代理實現就是把每個類都加上程式碼。
2. 就算委託類很少,但是裡面的方法很多,也會造成很大的工作量,而且同樣的程式碼會重複寫很多次,100個方法就得寫一百次統計時間的那段程式碼,極其繁瑣。
如果我們自己去解決這兩個問題,會怎麼寫,首先,需要根據委託類靈活的去生成對應的代理類,這個必須是一個自動的過程,如問題1,可能巨量的類需要代理,必須全自動才能解決量的問題。
再有就是對於委託類中方法的解決方案,如果你動態生成的代理類裡,還是一個一個的去實現方法,問題2就解決不掉,最好是有一個通用的方法,這個方法能代表委託類中的所有方法(或者符合條件的方法),然後在這個類中加上你想加的程式碼,就等於所有方法都有了,實現了這兩種解決方案的,就是動態代理。
2. 動態代理
繼續上面的思路,我們的問題轉移到怎麼生成一個動態代理上面來了。
繼續思考,你要生成這個樣的一個代理,首先你要獲取到委託類實現了哪些介面,因為我們將要生成的代理類也要實現介面,其次是我們們要這個代理類幹啥活,打日誌也好,篩選返回值也好,你得告訴它。
我們看看JDK的是否跟我們說的一樣:
public class Proxy implements java.io.Serializable {
public static Object newProxyInstance(ClassLoader loader,Class<?>[] interfaces,InvocationHandler h) throws IllegalArgumentException 6 { 9 final Class<?>[] intfs = interfaces.clone(); 15 /* 16 * Look up or generate the designated proxy class. 17 */ 18 Class<?> cl = getProxyClass0(loader, intfs); 20 /* 21 * Invoke its constructor with the designated invocation handler. 22 */ 23 try { 28 final Constructor<?> cons = cl.getConstructor(constructorParams); 29 final InvocationHandler ih = h; 30 if (!Modifier.isPublic(cl.getModifiers())) { 31 AccessController.doPrivileged(new PrivilegedAction<Void>() { 32 public Void run() { 33 cons.setAccessible(true); 34 return null; 35 } 36 }); 37 } 38 return cons.newInstance(new Object[]{h});
48 } catch (NoSuchMethodException e) { 49 throw new InternalError(e.toString(), e); 50 } 51 }
}
可以看到,在生成動態代理類的方法中,跟我們預想的只多了一個ClassLoader,委託類實現的一些介面(Class<?>[] interfaces),和我們需要的委託類做的事(InvocationHandler h),這裡都有。
我們需要重點關注Class<?> cl = getProxyClass0(loader, intfs)這句程式碼,這裡產生了代理類,這個類就是動態代理的關鍵。
可以通過java自帶的類方法ProxyGenerator.generateProxyClass,看看jdk給我生成的代理檔案是什麼樣子的:
1 byte[] Proxy0s = ProxyGenerator.generateProxyClass("12345", ToyFactory.class.getInterfaces()); 2 String path = "C:\\Users\\panda_zhu\\Desktop\\12345.class"; 3 try{ 4 FileOutputStream fos = new FileOutputStream(path); 5 fos.write(Proxy0s); 6 fos.flush(); 7 System.out.println("編譯檔案生成完畢!"); 8 } catch (Exception e) { 9 e.printStackTrace(); 10 }
委託類:
/** * 委託者,原始類,一個生產玩偶的工廠 */ public class ToyFactory implements Produce { @Override public void produce_cat() { System.out.println("生產了一隻小貓"); try { Thread.sleep(new Random().nextInt(1000)); } catch (InterruptedException e) { e.printStackTrace(); } } @Override public void produce_deer() { System.out.println("生產了一隻小鹿"); try { Thread.sleep(new Random().nextInt(1000)); } catch (InterruptedException e) { e.printStackTrace(); } } }
生成的class檔案反編譯後如下(部分):
public final class 12345 extends Proxy implements Produce { private static Method m1; private static Method m4; private static Method m2; private static Method m3; private static Method m0; public 12345(InvocationHandler paramInvocationHandler)throws { super(paramInvocationHandler); }
//從這裡可以很清晰的看到,利用反射,把委託類中的方法取出,聚合到代理類中,然後通過父類的屬性InvocationHandler中的invoke方法執行,這個方法後續說。從而實現了代理委託類的功能。 static { try { m1 = Class.forName("java.lang.Object").getMethod("equals", new Class[] { Class.forName("java.lang.Object") }); m4 = Class.forName("com.example.design.proxy.Produce").getMethod("produce_deer", new Class[0]); m2 = Class.forName("java.lang.Object").getMethod("toString", new Class[0]); m3 = Class.forName("com.example.design.proxy.Produce").getMethod("produce_cat", new Class[0]); m0 = Class.forName("java.lang.Object").getMethod("hashCode", new Class[0]); return; } } public final boolean equals(Object paramObject) throws { try { return ((Boolean)this.h.invoke(this, m1, new Object[] { paramObject })).booleanValue(); } } public final void produce_deer() throws { try { this.h.invoke(this, m4, null); return; } } } public final void produce_cat() throws { try { this.h.invoke(this, m3, null); return; } } }
到此為止,我們至少解決了一個問題,那就是動態生成一個代理檔案。
但是,這裡其實有一個疑問點,就是這個生成的代理類,是怎麼知道我的委託類是誰的,這裡也就依據和委託類實現同一個介面而寫了方法的空殼子而已,真正實現都是人家InvocationHandler的invoke方法去實現的,不論是生產鹿也好生產貓也好,就這一個方法,當然這也是我們們最初的設想,即上一節通用方法的解決方案,但是是怎麼實現的呢?又是怎麼精準定位委託類的呢?
我們看看這個類的原始碼:
public interface InvocationHandler { public Object invoke(Object proxy, Method method, Object[] args) throws Throwable; }
就一個方法,第一個引數傳入代理類,我們們自動生成的代理類,傳入的是他自己。
第二個是需要執行的方法,這個代理類傳的是他反射出來介面的方法。
第三個是這些方法的引數,這裡為了方便看我們們沒有引數。
通過這個方法也得不出這個答案,因為代理類本來也沒法說清楚委託類是誰,第二個頂多告訴這個通用的方法,我要執行的是哪個方法,所以可以推斷,這些都應該落在自己定義的Invocation上。
/** * 自定義的invoaction類, */ public class MyInvocationHandler implements InvocationHandler { private ToyFactory toyFactory; public MyInvocationHandler(ToyFactory toyFactory){ this.toyFactory = toyFactory; } @Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { System.out.println("開始執行自定義方法。。"); long startTime = System.currentTimeMillis(); method.invoke(toyFactory,new Object[]{}); long endTime = System.currentTimeMillis(); System.out.println("執行"+method.getName()+"方法共耗時:"+(endTime-startTime)); return null; } }
答案在這裡,跟預想的一樣,是在自定義invacation類裡聚合了委託類,並且通過method.invoke()方法,實現傳入哪個方法,呼叫託管類哪個方法這種靈活性的。
這裡有一個疑問,就是傳進來的proxy物件好像沒有用上,這個是幹啥的,其實這個引數是為了返回值,jdk文件中表示,這個invoke方法的返回值必須跟傳入的proxy返回值對應。
1 ToyFactory toyFactory = new ToyFactory(); 2 //使用動態代理 3 Produce o = (Produce)Proxy.newProxyInstance(toyFactory.getClass().getClassLoader(), toyFactory.getClass().getInterfaces(), new MyInvocationHandler(toyFactory)); 4 o.produce_deer();
開始執行自定義方法。。
生產了一隻小鹿
執行produce_deer方法共耗時:974
至此,我們們靜態變數遇到的問題就算是徹底解決了。
動態生成代理類解決了需要一直自己寫代理類的事,method.invoke方法解決了每個方法都需要寫重複程式碼的問題。
3. Spring AOP 對動態代理的應用
Spring AOP是基於動態代理的,如果要代理的物件,實現了某個介面,那麼Spring AOP會使用JDK Proxy,去建立代理物件,而對於沒有實現介面的物件,就無法使用 JDK Proxy 去進行代理了,這時候Spring AOP會使用Cglib ,這時候Spring AOP會使用 Cglib 生成一個被代理物件的子類來作為代理。
在web開發中,我們通常將專案分為controller、service、dao層,這種分層是一種縱向的,我們為了好理解,可以把它想象層一個豎狀的圓柱形,資料從中川流不息。
而AOP則是從這個圓柱截面中插入一個濾網,也就是我們說的面向切面。
在日常開發中,日誌攔截、許可權處理、異常攔截、事務等,都是基於這種切面完成的。
那spring在哪呼叫了動態代理呢?
final class JdkDynamicAopProxy implements AopProxy, InvocationHandler, Serializable { public Object getProxy(@Nullable ClassLoader classLoader) { if (logger.isTraceEnabled()) { logger.trace("Creating JDK dynamic proxy: " + this.advised.getTargetSource()); } return Proxy.newProxyInstance(classLoader, this.proxiedInterfaces, this); } }
最後那個返回值是不是很眼熟了。
題外知識點練習:
如何使用spring aop。
POM中引入依賴:
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-aop</artifactId> <version>2.2.6.RELEASE</version> </dependency>
由於springboot預設配置啟動aop,所以不用另外配置了。
1 @Aspect 2 @Component 3 public class WebAspect { 4 /* 5 * 切點,用來匹配需要切入的並增強的目標方法 6 * 下面的表示com.example.design.proxy.aop包下所有類的所有方法 7 * 匹配規則很靈活可以自行百度 8 */ 9 @Pointcut("execution(* com.example.design.proxy.aop.*.*(..))") 10 public void pointCut(){ 11 12 } 13 14 /* 15 * 在方法執行前開始執行 16 * */ 17 @Before("pointCut()") 18 public void beforeAdvice(JoinPoint joinPoint){ 19 System.out.println("前置通知開始。。"); 20 Signature signature = joinPoint.getSignature(); 21 System.out.println("目前代理的是哪一個方法:"+ signature.getName()); 22 } 23 24 /* 25 * 在方法執行後開始執行 26 * */ 27 @After("pointCut()") 28 public void afterAdvice(){ 29 System.out.println("後置通知開始。。"); 30 } 31 32 33 @AfterReturning(value = "execution(* com.example.design.proxy.aop.*.*(..))",returning = "args") 34 public void afterReturningAdvice(JoinPoint joinPoint,String args){ 35 System.out.println("後置返回通知開始。。"); 36 System.out.println("返回值是:"+args); 37 } 38 39 40 41 }
寫一個切面類,確定攔截那些目標類,如果是使用動態代理的話,這一步就是挑選委託類和組裝自定義invocation的地方,這裡只是挑選了幾個基本的通知方式,其實還有環繞通知,異常通知等等,對應的是我們們在自定義invocation中方法執行不同位置寫入的增強程式碼。
@RestController @RequestMapping("/aop") public class AopController { @RequestMapping("before") public String testBeforeAdvice(){ return "testBeforeAdvice方法開始執行!"; } }
http://localhost:8080/aop/before
前置通知開始。。
目前代理的是哪一個方法:testBeforeAdvice
後置返回通知開始。。
返回值是:testBeforeAdvice方法開始執行!
後置通知開始。。
全文涉及知識點:
1. 代理模式,包括動態代理,靜態代理。
2. java多型。
3. spring aop對於動態代理的使用。
4. aop在springboot中使用示例。
練習原始碼:https://github.com/panda-zhu/design
全文借鑑:
Spring AOP實現原理: https://blog.csdn.net/moreevan/article/details/11977115/
10分鐘看懂動態代理模式: https://www.cnblogs.com/faster/p/10874371.html