- 靜態(tài)代理: 由程序員創(chuàng)建或工具生成代理類的源碼榨汤,再編譯代理類。所謂靜態(tài)也就是在程序運行前就已經存在代理類的字節(jié)碼文件馏艾,代理類和委托類的關系在運行前就確定了举瑰。
- 動態(tài)代理: 在實現階段不用關心代理類捣辆,而在運行階段才指定哪一個對象。
情境
假設此迅,有個汽車類具有移動和停止兩個方法汽畴,我們要怎么在不改動源碼的情況下:
1.添加日志
2.添加事務
IMovable.java
public interface IMovable {
void move();
void stop();
}
Car.java
public class Car implements IMovable {
@Override
public void move() {
System.out.println("汽車移動");
}
@Override
public void stop() {
System.out.println("汽車停止");
}
}
靜態(tài)代理
繼承
1.添加日志
CarLog.java
public class CarLog extends Car {
@Override
public void move() {
System.out.println("開始執(zhí)行move");
super.move();
System.out.println("執(zhí)行move完成");
}
@Override
public void stop() {
System.out.println("開始執(zhí)行stop");
super.stop();
System.out.println("執(zhí)行stop完成");
}
}
Client.java
public class Client {
public static void main(String[] args) {
IMovable log = new CarLog();
log.move();
log.stop();
}
}
- 從上面的代碼可以看出旧巾,我們定義了一個類并且繼承于Car
- 重寫父類中的方法,在super(調用父類中的方法)前后加入打印日志的代碼
運行截圖:
2.添加事務
CarTransaction.java
public class CarTransaction extends Car {
@Override
public void move() {
System.out.println("move事務開始");
super.move();
System.out.println("move事務提交");
}
@Override
public void stop() {
System.out.println("stop事務開始");
super.stop();
System.out.println("stop事務提交");
}
}
運行結果:
- 很明顯忍些,對于事務的做法與日志的做法一致
3.先添加日志再開啟事務
CarLog2Trans.java
public class CarLog2Trans extends CarTransaction{
@Override
public void move() {
System.out.println("開始執(zhí)行move");
super.move();
System.out.println("執(zhí)行move完成");
}
@Override
public void stop() {
System.out.println("開始執(zhí)行stop");
super.stop();
System.out.println("執(zhí)行stop完成");
}
}
運行結果:
4.先開啟事務再添加日志
CarTrans2Log.java
public class CarTrans2Log extends CarLog {
@Override
public void move() {
System.out.println("move事務開始");
super.move();
System.out.println("move事務提交");
}
@Override
public void stop() {
System.out.println("stop事務開始");
super.stop();
System.out.println("stop事務提交");
}
}
運行結果:
- 從上面代碼可以看出如果我們添加功能的話鲁猩,就要創(chuàng)建新的類
情境: 有四輛汽車A,B,C,D,A汽車要做到先添加日志再開啟事務罢坝,B汽車要做到先開啟事務再添加日志,C汽車只需要添加日志廓握,D汽車只需要開啟事務
顯然為了完成這樣的功能使用繼承的方式,我們必須要有四個類才能完成嘁酿,哪有沒有更好的方式呢隙券?
接口(聚合)
1.添加日志
CarLogProxy.java
public class CarLogProxy implements IMovable {
private IMovable movable;
public CarLogProxy(IMovable movable) {
this.movable = movable;
}
@Override
public void move() {
System.out.println("開始執(zhí)行move");
movable.move();
System.out.println("執(zhí)行move完成");
}
@Override
public void stop() {
System.out.println("開始執(zhí)行stop");
movable.stop();
System.out.println("執(zhí)行stop完成");
}
}
Client.java
public class Client {
public static void main(String[] args) {
IMovable movable = new Car();
IMovable log = new CarLogProxy(movable);
log.move();
log.stop();
}
}
- 從上面的代碼可以看出,我們實現了IMovable接口(目標接口)闹司,并傳入了需要被代理的對象
2.添加事務
CarTransactionProxy.java
public class CarTransactionProxy implements IMovable {
private IMovable movable;
public CarTransactionProxy(IMovable movable) {
this.movable = movable;
}
@Override
public void move() {
System.out.println("move事務開始");
movable.move();
System.out.println("move事務提交");
}
@Override
public void stop() {
System.out.println("stop事務開始");
movable.stop();
System.out.println("stop事務提交");
}
}
Client.java
public class Client {
public static void main(String[] args) {
IMovable movable = new Car();
IMovable transaction = new CarTransactionProxy(movable);
transaction.move();
transaction.stop();
}
}
3.先添加日志再開啟事務
Client.java
public class Client {
public static void main(String[] args) {
IMovable movable = new Car();
IMovable transaction = new CarTransactionProxy(movable);
IMovable log = new CarLogProxy(transaction);
log.move();
log.stop();
}
}
4.先開啟事務再添加日志
Client.java
public class Client {
public static void main(String[] args) {
IMovable movable = new Car();
IMovable log = new CarLogProxy(movable);
IMovable transaction = new CarTransactionProxy(log);
transaction.move();
transaction.stop();
}
}
- 從3與4的Client可以看出娱仔,使用聚合的辦法就只要用兩個類就能實現需求
- 顯然,使用實現目標接口的方式進行代理游桩,讓代理和被代理對象之間都可以相互靈活轉換
- 所以一般靜態(tài)代理使用聚合的方式進行實現牲迫,使用繼承的方式多多少少有些過于笨重
想必認真的人都看的出來,靜態(tài)代理的方式隨著功能的增多借卧,必然要生成更多的代理對象盹憎,這樣不利于維護。而且谓娃,就目前的要求來看脚乡,對 move() 和 stop() 兩個方法添加日志,其中代碼出現了冗余的情況滨达,無法復用。那么有什么方式可以解決呢俯艰?
動態(tài)代理
Client.java
public class Client {
public static void main(String[] args) {
IMovable movable = new Car();
IMovable logProxy = (IMovable) Proxy.newProxyInstance(Car.class.getClassLoader(), Car.class.getInterfaces(),
new InvocationHandler() {
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
System.out.println("開始執(zhí)行" + method.getName());
Object invoke =method.invoke(movable, args);
System.out.println("執(zhí)行" + method.getName() + "完成");
return invoke;
}
});
logProxy.move();
logProxy.stop();
System.out.println();
IMovable transProxy = (IMovable) Proxy.newProxyInstance(Car.class.getClassLoader(), Car.class.getInterfaces(),
new InvocationHandler() {
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
System.out.println(method.getName() + "開啟事務");
Object invoke = method.invoke(logProxy, args);
System.out.println(method.getName() + "事務提交");
return invoke;
}
});
transProxy.move();
transProxy.stop();
}
}
運行結果:
- 從運行結果來看捡遍,我們使用動態(tài)代理實現了上面靜態(tài)代理的例子,且沒有編寫多余的類
- 從上面的代碼可以看出竹握,要使用動態(tài)代理就必須要有目標接口
- 在 InvocationHandler 的方法中可以獲取要執(zhí)行的 Method 實例
- 通過 Method 的實例可以通過反射來執(zhí)行画株,不過要傳入被代理對象
- 在反射前后可以進行添加日志和事務的操作
- 而且也可以靈活的讓進行代理對象與被代理對象之間的轉換
- 由于使用了反射,對性能有一定的損耗
動態(tài)代理源碼解析
對于動態(tài)代理的源碼其實最重要的就是下面兩個方法啦辐,我們下面開始對他們進行深入分析谓传,做到知其然知其所以然。
Proxy.newProxyInstance 部分代碼
public static Object newProxyInstance(ClassLoader loader,
Class<?>[] interfaces,
InvocationHandler h)
throws IllegalArgumentException
{
...
Class<?> cl = getProxyClass0(loader, intfs);
try {
...
final Constructor<?> cons = cl.getConstructor(constructorParams);
final InvocationHandler ih = h;
...
return cons.newInstance(new Object[]{h});
}catch (IllegalAccessException|InstantiationException e) {
throw new InternalError(e.toString(), e);
}
...
}
- loader 定義代理類的類加載器
- interfaces 代理類要實現的接口列表
- h 指派方法調用的調用處理程序(注:動態(tài)代理的關鍵)
- 將其他多余的部分代碼忽略芹关,找核心的代碼(因為有些偏底層我也看不懂 -.- )
- getProxyClass0(loader, intfs); 獲得代理類
- cl.getConstructor(constructorParams); 獲得代理類的構造方法
- cons.newInstance(new Object[]{h}); 反射生成代理對象续挟,并傳入 InvocationHandler
所以我們往下看看它是如何得到代理對象的
Proxy.getProxyClass0 代碼
private static final WeakCache<ClassLoader, Class<?>[], Class<?>>
proxyClassCache = new WeakCache<>(new KeyFactory(), new ProxyClassFactory());
private static Class<?> getProxyClass0(ClassLoader loader,
Class<?>... interfaces) {
if (interfaces.length > 65535) {
throw new IllegalArgumentException("interface limit exceeded");
}
// If the proxy class defined by the given loader implementing
// the given interfaces exists, this will simply return the cached copy;
// otherwise, it will create the proxy class via the ProxyClassFactory
return proxyClassCache.get(loader, interfaces);
- 注釋翻譯: 如果存在給定接口的給定裝入器定義的代理類存在,則只返回緩存的副本侥衬;否則诗祸,它將通過proxyclassfactory創(chuàng)建代理類
所以我們就要進一步分析 (proxyClassCache)WeakCache 類是怎么進行緩存的跑芳。(個人能力有限對于WeakCache還有較多疑惑,之后會進行總結更新)
學習資料:
知道了是得到創(chuàng)建代理類直颅,我們繼續(xù)往下分析
InvocationHandler.java
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable;
- proxy 在其上調用方法的代理實例
- method 對應于在代理實例上調用的接口方法的 Method 實例博个,目標對象被調用的方法
- args 包含傳入代理實例上方法調用的參數值的對象數組
InvocationHandler用來連接代理對象與目標對象
分析代理類對象
我們可以使用如下代碼獲取代理類$Proxy0.class文件
public static void main(String[] args) {
System.getProperties().put("sun.misc.ProxyGenerator.saveGeneratedFiles", "true");
System.out.println("$Proxy0.class: "+Proxy.getProxyClass(Inter.class.getClassLoader(), Inter.class));
//
IMovable movable = new Car();
IMovable logProxy = (IMovable) Proxy.newProxyInstance(Car.class.getClassLoader(), Car.class.getInterfaces(),
new InvocationHandler() {
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
System.out.println("開始執(zhí)行" + method.getName());
Object invoke =method.invoke(movable, args);
System.out.println("執(zhí)行" + method.getName() + "完成");
return invoke;
}
});
logProxy.move();
logProxy.stop();
}
運行結果:
- System.getProperties().put("sun.misc.ProxyGenerator.saveGeneratedFiles", "true"); 打開保存開關
- System.out.println("$Proxy0.class全名: "+Proxy.getProxyClass(IMovable.class.getClassLoader(), IMovable.class)); 可以通過打印信息獲取class在項目中路徑
- 關于這個類是如何生成的,需要往下跟蹤 Proxy.getProxyClass0方法中的proxyClassCache.get(loader, interfaces),這與緩存相掛鉤未進行詳細分析(之后還會更新文章)功偿。
在得到 $Proxy0.class 之后我們可以使用一些工具將class進行反編譯,這里我使用了JD_GUI
$Proxy0.java部分代碼
public final class $Proxy0
extends Proxy
implements IMovable
{
private static Method m1;
private static Method m4;
private static Method m2;
private static Method m3;
private static Method m0;
public $Proxy0(InvocationHandler paramInvocationHandler)
throws
{
super(paramInvocationHandler);
}
...
public final void move()
throws
{
try
{
this.h.invoke(this, m4, null);
return;
}
catch ...
}
public final void stop()
throws
{
try
{
this.h.invoke(this, m3, null);
return;
}
catch ...
}
...
static
{
try
{
m1 = Class.forName("java.lang.Object").getMethod("equals", new Class[] { Class.forName("java.lang.Object") });
m4 = Class.forName("com.zzz.proxy.IMovable").getMethod("move", new Class[0]);
m2 = Class.forName("java.lang.Object").getMethod("toString", new Class[0]);
m3 = Class.forName("com.zzz.proxy.IMovable").getMethod("stop", new Class[0]);
m0 = Class.forName("java.lang.Object").getMethod("hashCode", new Class[0]);
return;
}
catch ...
}
}
- 構造方法傳入InvocationHandler
- 在靜態(tài)代碼塊中盆佣,可以看到我們接口定義的 move 和 stop 分別為 m4 和 m3
- 所以在我們調用代理對象時,就使用 InvocationHandler 回調出去械荷,而 invoke 方法正是由我們實現的罪塔。