代理模式的作用是:爲其他對象提供一種代理以控制對這個對象的訪問。在某些情況下,一個客戶不想或者不能直接引用另一個對象,而代理對象可以在客戶端和目標對象之間起到中介的作用。
代理模式一般涉及到的角色有:
抽象角色:聲明真實對象和代理對象的共同接口;
代理角色:代理對象角色內部含有對真實對象的引用,從而可以操作真實對象,同時代理對象提供與真實對象相同的接口以便在任何時刻都能代替真實對象。同時,代理對象可以在執行真實對象操作時,附加其他的操作,相當於對真實對象進行封裝。
真實角色:代理角色所代表的真實對象,是我們最終要引用的對象。
1. 相關概念
1.1 代理
在某些情況下,我們不希望或是不能直接訪問對象 A,而是通過訪問一箇中介對象 B,由 B 去訪問 A 達成目的,這種方式我們就稱爲代理。
這裏對象 A 所屬類我們稱爲委託類,也稱爲被代理類,對象 B 所屬類稱爲代理類。
代理優點有:
- 隱藏委託類的實現
- 解耦,不改變委託類代碼情況下做一些額外處理,比如添加初始判斷及其他公共操作
根據程序運行前代理類是否已經存在,可以將代理分爲靜態代理和動態代理。
1.2 靜態代理
代理類在程序運行前已經存在的代理方式稱爲靜態代理。
通過上面解釋可以知道,由開發人員編寫或是編譯器生成代理類的方式都屬於靜態代理,如下是簡單的靜態代理實例:
class ClassA {
public void operateMethod1() {};
public void operateMethod2() {};
public void operateMethod3() {};
}
public class ClassB {
private ClassA a;
public ClassB(ClassA a) {
this.a = a;
}
public void operateMethod1() {
a.operateMethod1();
};
public void operateMethod2() {
a.operateMethod2();
};
// not export operateMethod3()
}
上面ClassA
是委託類,ClassB
是代理類,ClassB
中的函數都是直接調用ClassA
相應函數,並且隱藏了Class
的operateMethod3()
函數。
靜態代理中代理類和委託類也常常繼承同一父類或實現同一接口。
1.3 動態代理
代理類在程序運行前不存在、運行時由程序動態生成的代理方式稱爲動態代理。
Java 提供了動態代理的實現方式,可以在運行時刻動態生成代理類。這種代理方式的一大好處是可以方便對代理類的函數做統一或特殊處理,如記錄所有函數執行時間、所有函數執行前添加驗證判斷、對某個特殊函數進行特殊操作,而不用像靜態代理方式那樣需要修改每個函數。
2. 動態代理實例
實現動態代理包括三步:
(1). 新建委託類;
(2). 實現InvocationHandler
接口,這是負責連接代理類和委託類的中間類必須實現的接口;
(3). 通過Proxy
類新建代理類對象。
2.1 新建委託類
public interface Operate {
public void operateMethod1();
public void operateMethod2();
public void operateMethod3();
}
public class OperateImpl implements Operate {
@Override
public void operateMethod1() {
System.out.println("Invoke operateMethod1");
sleep(110);
}
@Override
public void operateMethod2() {
System.out.println("Invoke operateMethod2");
sleep(120);
}
@Override
public void operateMethod3() {
System.out.println("Invoke operateMethod3");
sleep(130);
}
private static void sleep(long millSeconds) {
try {
Thread.sleep(millSeconds);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
Operate
是一個接口,定了了一些函數,我們要統計這些函數的執行時間。OperateImpl
是委託類,實現Operate
接口。每個函數簡單輸出字符串,並等待一段時間。動態代理要求委託類必須實現了某個接口,比如這裏委託類
OperateImpl
實現了Operate
,2.2. 實現 InvocationHandler 接口
public class TimingInvocationHandler implements InvocationHandler {
private Object target;
public TimingInvocationHandler() {}
public TimingInvocationHandler(Object target) {
this.target = target;
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
long start = System.currentTimeMillis();
Object obj = method.invoke(target, args);
System.out.println(method.getName() + " cost time is:" + (System.currentTimeMillis() - start));
return obj;
}
}
函數需要去實現,參數:proxy
表示下面2.3
通過 Proxy.newProxyInstance() 生成的代理類對象
。method
表示代理對象被調用的函數。args
表示代理對象被調用的函數的參數。
調用代理對象的每個函數實際最終都是調用了InvocationHandler
的invoke
函數。這裏我們在invoke
實現中添加了開始結束計時,其中還調用了委託類對象target
的相應函數,這樣便完成了統計執行時間的需求。invoke
函數中我們也可以通過對method
做一些判斷,從而對某些函數特殊處理。
2.3. 通過 Proxy 類靜態函數生成代理對象
public class Main {
public static void main(String[] args) {
// create proxy instance
TimingInvocationHandler timingInvocationHandler = new TimingInvocationHandler(new OperateImpl());
Operate operate = (Operate)(Proxy.newProxyInstance(Operate.class.getClassLoader(), new Class[] {Operate.class},
timingInvocationHandler));
// call method of proxy instance
operate.operateMethod1();
System.out.println();
operate.operateMethod2();
System.out.println();
operate.operateMethod3();
}
}
這裏我們先將委託類對象new OperateImpl()
作爲TimingInvocationHandler
構造函數入參創建timingInvocationHandler
對象;
然後通過Proxy.newProxyInstance(…)
函數新建了一個代理對象,實際代理類就是在這時候動態生成的。我們調用該代理對象的函數就會調用到timingInvocationHandler
的invoke
函數(是不是有點類似靜態代理),而invoke
函數實現中調用委託類對象new
OperateImpl()
相應的 method(是不是有點類似靜態代理)。
public static Object newProxyInstance(ClassLoader loader, Class<?>[] interfaces, InvocationHandler h)
loader
表示類加載器interfaces
表示委託類的接口,生成代理類時需要實現這些接口h
是InvocationHandler
實現類對象,負責連接代理類和委託類的中間類
我們可以這樣理解,如上的動態代理實現實際是雙層的靜態代理,開發者提供了委託類 B,程序動態生成了代理類 A。開發者還需要提供一個實現了InvocationHandler
的子類 C,子類 C 連接代理類 A
和委託類 B,它是代理類 A 的委託類,委託類 B 的代理類。用戶直接調用代理類 A 的對象,A 將調用轉發給委託類 C,委託類 C 再將調用轉發給它的委託類 B。
3. 動態代理原理
實際上面最後一段已經說清了動態代理的真正原理。我們來仔細分析下
3.1 生成的動態代理類代碼
下面是上面示例程序運行時自動生成的動態代理類代碼,如何得到這些生成的代碼請見ProxyUtils,查看 class 文件可使用 jd-gui
2、代碼實現
- /**
- * 示例(二):代理模式 --動態代理
- *
- * 以添加用戶爲例
- */
- class User {
- private String username;
- private String password;
- public User() {
- }
- public User(String username, String password) {
- this.username = username;
- this.password = password;
- }
- public String getUsername() {
- return username;
- }
- public void setUsername(String username) {
- this.username = username;
- }
- public String getPassword() {
- return password;
- }
- public void setPassword(String password) {
- this.password = password;
- }
- @Override
- public String toString() {
- return "User [username=" + username + ", password=" + password + "]";
- }
- }
- /**
- * 目標接口
- */
- interface IUserDao {
- public void add(User user);
- }
- class UserDaoImpl implements IUserDao {
- @Override
- public void add(User user) {
- System.out.println("add a user successfully...");
- }
- }
- /**
- * 日誌類 --> 待織入的Log類
- */
- class LogEmbed implements InvocationHandler {
- private IUserDao target;
- /**
- * 對target進行封裝
- */
- public IUserDao getTarget() {
- return target;
- }
- public void setTarget(IUserDao target) {
- this.target = target;
- }
- private void beforeMethod() {
- System.out.println("add start...");
- }
- private void afterMethod() {
- System.out.println("add end...");
- }
- /**
- * 這裏用到了反射
- *
- * proxy 代理對象
- *
- * method 目標方法
- *
- * args 目標方法裏面參數列表
- */
- @Override
- public Object invoke(Object proxy, Method method, Object[] args)
- throws Throwable {
- beforeMethod();
- // 回調目標對象的方法
- method.invoke(target, args);
- System.out.println("LogEmbed --invoke-> method = " + method.getName());
- afterMethod();
- return null;
- }
- }
- /**
- * 客戶端測試類
- *
- * @author Leo
- */
- public class Test {
- public static void main(String[] args) {
- IUserDao userDao = new UserDaoImpl();
- LogEmbed log = new LogEmbed();
- log.setTarget(userDao);
- /**
- * 根據實現的接口產生代理
- */
- IUserDao userDaoProxy = (IUserDao) Proxy.newProxyInstance(userDao
- .getClass().getClassLoader(), userDao.getClass()
- .getInterfaces(), log);
- /**
- * 注意:這裏在調用IUserDao接口裏的add方法時,
- * 代理對象會幫我們調用實現了InvocationHandler接口的LogEmbed類的invoke方法。
- *
- * 這樣做,是不是有點像Spring裏面的攔截器呢?
- */
- userDaoProxy.add(new User("張三", "123"));
- }
- }