作者簡介:大家好,我是smart哥,前中興通訊、美團架構師,現某互聯網公司CTO
聯系qq:184480602,加我進群,大家一起學習,一起進步,一起對抗互聯網寒冬
到目前為止,在Java基礎進階這個章節,我們已經幫大家梳理了很多晦澀但極其重要的知識點,包括反射、注解和泛型。這些都是我們邁向中高級程序員的小碎步,我們已經離“成熟的碼農”越來越近了,但還不夠。今天,我們仍需一起再往前走一小步:JDK動態代理。個人認為Java基礎有“四大神獸”,除了剛才說的反射、注解和泛型,JDK動態代理就是最后一道坎。
“動態代理”四個字一出來,估計很多初學者已經開始冒冷汗。它之所以給人感覺很難,有三點原因:
- 代碼形式很詭異,讓人搞不清調用邏輯
- 用到了反射,而很多初學者不了解反射(現在你應該感覺好些了)
- 包含代理模式的思想,本身比較抽象
盡管動態代理看起來似乎有一定難度,但卻必須拿下。因為Spring的事務控制依賴于AOP,AOP底層實現便是動態代理 + 責任鏈,環環相扣。所以說,搞編程的,拼到到最后還是看基本功,要么是語言基礎、要么是計算機基礎。
一個小需求:給原有方法添加日志打印
假設你剛進入一個項目組,項目中存在一個Calculator類,代表一個計算器,它可以進行加減乘除操作:
public class Calculator {// 加public int add(int a, int b) {int result = a + b;return result;}// 減public int subtract(int a, int b) {int result = a - b;return result;}// 乘法、除法...
}
現在老大給你提了一個需求:在每個方法執行前后打印日志。
你有什么好的方案?
方案一:直接修改
很多人最直觀的想法是直接修改Calculator類:
public class Calculator {// 加public int add(int a, int b) {System.out.println("add方法開始...");int result = a + b;System.out.println("add方法結束...");return result;}// 減public int subtract(int a, int b) {System.out.println("subtract方法開始...");int result = a - b;System.out.println("subtract方法結束...");return result;}// 乘法、除法...
}
上面的方案是有問題的:
- 直接修改源程序,不符合開閉原則,即好的程序設計應該對擴展開放,對修改關閉
- 如果Calculator類內部有幾十個、上百個方法,修改量太大
- 存在重復代碼(都是在核心代碼前后打印日志)
- 日志打印硬編碼在代理類中,不利于后期維護:比如你花了一上午終于寫完了,組長告訴你這個功能不做了,于是你又要打開Calculator花十分鐘刪除日志打印的代碼(或回滾分支)!
所以,此種方案PASS!
方案二:靜態代理實現日志打印
“靜態代理”四個字包含了兩個概念:靜態、代理。我們先來了解什么叫“代理”,至于何為“靜態”,需要和“動態”對比著講。
代理是一種模式,提供了對目標對象的間接訪問方式,即通過代理訪問目標對象。如此便于在目標實現的基礎上增加額外的功能操作,前攔截,后攔截等,以滿足自身的業務需求。
常用的代理方式可以粗分為:靜態代理和動態代理。
靜態代理的實現比較簡單:編寫一個代理類,實現與目標對象相同的接口,并在內部維護一個目標對象的引用。通過構造器塞入目標對象,在代理對象中調用目標對象的同名方法,并添加前攔截,后攔截等所需的業務功能。
是不是有點暈?是的,我最討厭這種干巴巴的描述。簡而言之,就是這樣:
按上面的描述,代理類和目標類需要實現同一個接口,所以我打算這樣做:
- 將Calculator抽取為接口
- 創建目標類CalculatorImpl實現Calculator
- 創建代理類CalculatorProxy實現Calculator
抽取接口
/*** Calculator接口*/
public interface Calculator {int add(int a, int b);int subtract(int a, int b);
}
原目標類實現接口
/*** 目標類,實現Calculator接口(如果一開始就面向接口編程,其實是不存在這一步的,CalculatorImpl原本就實現Calculator接口)*/
public class CalculatorImpl implements Calculator {// 加public int add(int a, int b) {int result = a + b;return result;}// 減public int subtract(int a, int b) {int result = a - b;return result;}// 乘法、除法...
}
新增代理類并實現接口
/*** 靜態代理類,實現Calculator接口*/
public class CalculatorProxy implements Calculator {// 代理對象內部維護一個目標對象引用private Calculator target;// 通過構造方法,傳入目標對象public CalculatorProxy(Calculator target) {this.target = target;}// 調用目標對象的add,并在前后打印日志@Overridepublic int add(int a, int b) {System.out.println("add方法開始...");int result = target.add(a, b);System.out.println("add方法結束...");return result;}// 調用目標對象的subtract,并在前后打印日志@Overridepublic int subtract(int a, int b) {System.out.println("subtract方法開始...");int result = target.subtract(a, b);System.out.println("subtract方法結束...");return result;}// 乘法、除法...
}
測試案例
使用代理對象完成加減乘除,并且打印日志:
public class Test {public static void main(String[] args) {// 把目標對象通過構造器塞入代理對象Calculator calculator = new CalculatorProxy(new CalculatorImpl());// 代理對象調用目標對象方法完成計算,并在前后打印日志calculator.add(1, 2);calculator.subtract(2, 1);}
}
靜態代理的優點:可以在不修改目標對象的前提下,對目標對象進行功能的擴展和攔截。但是它也僅僅解決了上一種方案4大缺點中的第1、4兩點:
- 直接修改源程序,不符合開閉原則,即好的程序設計應該對擴展開放,對修改關閉(?,如果一開始就面向接口編程,這一步其實是不需要的)
- 如果Calculator類內部有幾十個、上百個方法,修改量太大(?,目標類有多少個方法,代理類就要重寫多少個方法)
- 存在重復代碼(都是在核心代碼前后打印日志)(?,代理類中的日志代碼是重復的)
- 日志打印硬編碼在代理類中,不利于后期維護:比如你花了一上午終于寫完了,組長告訴你這個功能不做了(?,別用代理類就好了)
靜態代理的問題
上面的代碼中,為了給目標類做日志增強,我們編寫了代理類,而且準備了一個構造器接收目標對象。代理代理對象構造器的參數類型是Calculator,這意味著它只能接受Calculator的實現類對象,亦即我們寫的代理類CalculatorProxy只能給Calculator做代理,它們綁定死了!
如果現在我們系統需要全面改造,要給其他類也添加日志打印功能,就得為其他幾百個接口都各自寫一份代理類...
自己手動寫一個類并實現接口實在太麻煩了。仔細一想,我們其實想要的并不是代理類,而是代理對象!
你細品上面加粗的這句話,是不是好像一句廢話?沒有類哪來的對象?!
其實我的意思是,能否讓JVM根據接口自動生成代理對象呢?
比如,有沒有一個方法,我傳入接口+增強的代碼(比如打印日志),它就給我自動返回代理對象呢?這樣就能省去編寫代理類這個無用的“中介”了,沒有中間商賺差價,豈不爽哉?
JDK,能做到嗎?
預知后事如何,請聽下回分解~
作者簡介:大家好,我是smart哥,前中興通訊、美團架構師,現某互聯網公司CTO
進群,大家一起學習,一起進步,一起對抗互聯網寒冬