代理設計模式
- 模擬計算面試時間
- 建立一個介面
//面試
public interface InterView {
void chatting();
}
複製程式碼
- 真實物件實現介面
public class Persion implements InterView{
@Override
public void chatting() {
System.out.println("is chatting ...");
try {
//模擬面試時間
Thread.sleep(new Random().nextInt(1000));
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
複製程式碼
- 用Thread.sleep()模擬面試的時間,那麼我如果想知道這個時間怎麼做
public class Persion implements InterView{
@Override
public void chatting() {
Long start = System.currentTimeMillis();
System.out.println("is chatting ...");
try {
//模擬面試時間
Thread.sleep(new Random().nextInt(1000));
} catch (InterruptedException e) {
e.printStackTrace();
}
Long end = System.currentTimeMillis();
System.out.println("chatting time: " + (end - start) );
}
}
複製程式碼
- 很簡單,在方法的前後捕捉當前時間,相減就出來了
如果這個方法來自三方庫沒有改動原始碼的許可權,怎麼辦
- 我們可以使用繼承,重寫他的方法
public class Persion2 extends Persion {
@Override
public void chatting() {
long start = System.currentTimeMillis();
super.chatting();
long end = System.currentTimeMillis();
System.out.println("chatting time : " + (end - start));
}
}
複製程式碼
- 我們也可以使用聚合(注入),同時也實現面試的介面
public class Person3 implements InterView {
private Person person;
public Person3(Person person){
this.person = person;
}
@Override
public void chatting() {
long start = System.currentTimeMillis();
person.chatting();
long end = System.currentTimeMillis();
System.out.println("chatting time : " + (end - start));
}
}
複製程式碼
- 這兩種方法都可以實現計算面試時間,哪種好點呢
我們繼續增加需求,如果我還要在面試前後做記錄(列印日誌)呢,很簡單,繼承person2,並在前後新增列印日誌即可
如果我要改變執行順序呢,先獲取時間,再列印日誌呢,可以再用一個繼承類,重寫方法,這樣會導致無限擴增
- 那前面提到的聚合(注入)不會有這樣的問題嗎,只要稍微改一下就可以,把注入的Person修改為InterView介面
public class Person3 implements InterView {
private InterView interView;
public Person3(InterView interView){
this.interView = interView;
}
@Override
public void chatting() {
long start = System.currentTimeMillis();
interView.chatting();
long end = System.currentTimeMillis();
System.out.println("chatting time : " + (end - start));
}
}
複製程式碼
- 為了看的更清楚,將P3改為PersonProxy,用於獲取方法執行時間代理的意思,同時新建PersonLogProxy代理類用於列印日誌
public class PersonLogProxy implements Interview {
private Interview interview;
public PersonLogProxy(Interview interview) {
this.interview = interview;
}
@Override
public void chatting() {
System.out.println("chatting start...");
interview.chatting();
System.out.println("chatting end...");
}
}
複製程式碼
- 如果要先記錄日誌,再獲取面試時間,可以這樣做
public class Test {
public static void main(String[] args) {
Person person = new Person();
PersonLogProxy p1 = new PersonLogProxy(person);
PersonProxy p2 = new PersonProxy(p1);
p2.chatting();
}
}
複製程式碼
- 如果需求變更,反過來,可以這樣做
public class Test {
public static void main(String[] args) {
PersonProxy p3 = new PersonProxy(person);
PersonLogProxy p4 = new PersonLogProxy(p3);
p4.chatting();
}
}
複製程式碼
這裡會出現一個問題,從表現來看,聚合可以實現靈活的執行順序,而繼承不可以,為什麼
- 繼承其實是一種包裹關係,如果要改變順序,則需要建立新的模板來實現
- 由於聚合物件和代理物件哦都是先了interView介面,類似繼承的包裹關係,可以通過傳入不同的interview來改變
- 這是利用了java多型特性,其實所有的設計模式都或多或少的用到多型
靜態代理
在上面的PersonTimeProxy的chatting方法,我們直接呼叫了interview().chatting,換而言之,PersonProxy代理傳入的interview物件,這就是典型的靜態代理實現
- 問題: 如果需要對所有方法都執行這樣的操作,同樣的程式碼至少需要重複多少次,同時也多少次建立代理類
- 如果溶蝕代理多各類,依然導致類無限擴充套件
- 如果類中有多個方法,需要反覆實現
- 是否可以用同一個代理類來代理任意物件呢,甚至代理的邏輯也可以自定義
動態代理
jdk動態代理
- 可以傳入任意物件,即動態生成代理物件
- 可以傳入任意方法
-
其實就是通過jdk自帶的Proxy.newProxyInstance()方法動態生成代理類,動態編譯,在通過反射建立物件並載入到記憶體中
-
可以檢視一下Proxy->newProxyInstance()的原始碼,代理類的動態建立通過InvocationHandler自定義
-
我們可以看一下原始碼中的InvocationHandler介面
import java.lang.reflect.Method;
/**
* proxy:指定動態生成的代理類
* method:介面中傳入的所有方法物件
* args:當前傳入方法的引數
*/
public interface InvocationHandler {
void invoke(Object proxy, Method method,Object[] args);
}
複製程式碼
使用:
- 實現InvocationHandler介面
public class MyInvocationHandler implements InvocationHandler {
//被代理物件,object型別
private Object target;
public MyInvocationHandler(Object target){
this.target = target;
}
/**
*proxy:指定動態生成的代理類,
*method:介面中傳入的物件的所有方法
*當前傳入方法的引數
*/
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
System.out.println("chatting start ...");
Object returnValue = method.invoke(target, args);
System.out.println("chatting end");
return returnValue;
}
}
複製程式碼
- 測試
public class DynamicProxyTest {
public static void main(String[] args) {
Person person = new Person();
MyInvocationHandler handler = new MyInvocationHandler(person);
/**
* 第一個引數指定代理類的類載入器,這裡傳入當前測試類載入器
* 第二個引數是代理類需要實現的介面的例項類
* 第三個引數是invocation handler,用來處理方法的呼叫,這裡傳入自己實現的handler
*/
InterView proxyObject = (InterView) Proxy.newProxyInstance(DynamicProxyTest.class.getClassLoader()
, person.getClass().getInterfaces(),
handler);
proxyObject.chatting();
}
}
複製程式碼
此時,整個方法的呼叫棧變成了這樣
-
簡而言之就是:
真實的業務類需要實現業務介面,代理類通過反射獲取真實業務物件的類載入器,從而在記憶體中建立一個真實的代理物件(靜態代理手動擴充套件那個),代理類需要實現invocationhandler介面,重寫invoke方法,invoke方法三個引數,分別是代理的介面,代理的方法,和方法引數,在該方法裡對真實的方法進行包裝,實現動態代理
-
和靜態代理的區別:靜態代理是自己去建立代理類物件,動態代理本質是在記憶體中根據傳入的引數,動態的生成一個代理類物件,通過生成的class檔案反編譯可以看到
cglib 動態代理
cglib是針對類來實現代理的,原理是針對指定的業務類生成一個子類,並覆蓋其中的業務方法,實現代理,因為用的是繼承,所以不能代理final修飾的類
- 實現MethodInterceptor介面,建立代理類
public class PersonCglib implements MethodInterceptor {
//業務物件
private Object target;
public Object getInstance(Object target){
this.target = target;
//建立增強器
Enhancer enhancer = new Enhancer();
//指定要代理的類(指定生成子類的父類)
enhancer.setSuperclass(this.target.getClass());
//設定回撥,對於代理類上所有方法的呼叫,都會呼叫callback,所以callback則需要實現intercept()方法進行攔截
enhancer.setCallback(this);
//包含三個步驟
//生成原始碼
//編譯成class檔案
//載入到jvm中,建立例項並返回
return enhancer.create();
}
//實現方法回撥
@Override
public Object intercept(Object o, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable {
System.out.println("chatting start");
//這個obj物件是cglib給我們new出來的
//cglib new出來的物件是被代理物件的子類(繼承了被代理類),繼承相當於間接持有父類的引用
methodProxy.invokeSuper(o,objects);
System.out.println("chatting end");
return null;
}
}
複製程式碼
- 使用
public static void main(String[] args) {
Person person = new Person();
PersonCglib cglib = new PersonCglib();
InterView interView = (InterView) cglib.getInstance(person);
interView.chatting();
}
複製程式碼
- 基於位元組碼,動態的建立需要代理的子類
- 建立一個原本的業務物件,建立一個代理類物件,在代理類物件中實現MethodInterceptor介面,通過傳入原本的業務物件,設定要繼承的父類物件,並設定回撥,返回一個增強物件,代理類物件實現intercept方法,傳入引數是被代理物件,攔截的方法,攔截的引數,原有業務類指向的代理物件,每次呼叫業務方法都會回撥intercept方法,實現動態代理