不詩意的女程序猿不是好廚師~
轉載請注明出處【From 李詩雨---https://blog.csdn.net/cjm2484836553/article/details/99753509】
文中源碼見github--https://github.com/junmei520/DesignPattern
0.??先看這里 --- 詩雨的學習說明
本章主要分6個部分來進行學習整理。
1,2,3比較老套主要是講解了一些基本概念。
4,5直接給出了代理模式的使用模板,并緊接著舉了一個簡單的使用實例。到這里你基本可以淺析的了解代理模式和它如何使用的。
6則是實戰了,在項目中真實的使用場景舉例并附源代碼。
關于代理模式的使用原理,我想放在下篇進行,因為這篇已經夠長的了。。。
希望你可以有耐心看完,也希望能對你有所幫助。
1.從生活中初識代理
①作為一名苦逼的程序猿今天中午又要趕進度,前端新來了一個帥氣的小哥哥,不如趁此機會讓他幫我買個飯吧;
②家里房產太多空著多浪費,還是請個中介幫我把房子租出去,掙個零花錢;
③離職的時候老板不給我發工資,那必須得請個律師幫忙打官司啊...
雖然說以上情況純屬虛構(huanxiang),但是找帥小哥哥幫忙買飯,找中介幫忙租房,找律師幫忙打官司,這些可都是代理啊。
所以說生活中代理的影子還是很多的,那代碼中呢?我們可是“農以碼為天”。所以,下面我們就開始代理模式的正式學習吧。
2.代理模式的類圖和角色
首先我們要來看下什么是代理模式呢?
其實它就是為其他對象提供一種代理以控制對這個對象的訪問。
代理模式所涉及到的四個角色:
①Subject:抽象類 --- 聲明代理對象和被代理對象的共同接口。
② RealSubject : 真實主題類(/代理類) --- 該類定義了代理所表示的真是對象。
③ ProxySubject : 代理類 --- 該類持有一個對真實主題類的引用,從而可以操作真實對象。
④ Clent :客戶類 --- 即使用代理類的客戶端。
四者的關系用類圖表示如下:
對類圖做一下簡單的說明:
為了實現客戶端可通過代理對象去間接訪問真實對象方法的目的。
①首先要保證真實類和代理類都實現相同的接口,這樣真實對象有的方法,代理對象也會有。②其次還要保證代理對象中持有真實對象的引用,這樣便可以通過代理對象來操作真實對象。
3.靜態代理和動態代理
代理模式分為: 動態代理 和 靜態代理 2種。
- 靜態代理 --- 代理類的代碼由程序員自己或通過一些自動化工具生成固定的代碼再進行百編譯,也就是說我們的代碼運行前代理類的class編譯文件就已存在了。
- 而動態代理 --- 則與靜態代理相反,通過反射機制動態地生成代理者的對象,也就是說code階段壓根就不需要知道代理誰,代理誰我們將再執行階段決定。Java給我們提供了一個便捷的動態代理接口 InvocationHandler ,實現該接口需要重寫其調用方法 invoke.
注意:動態代理的抽象角色只能是接口類,而不能是抽象類。
否則會拋異常:
Caused by: java.lang.IllegalArgumentException: com.kotlinstudy.proxypatterndemo.dynamicproxy.Subject2 is not an interface
對此的解釋是:所謂的動態代理,實際上就是在運行期間根據被代理接口動態生成一個代理類,這個代理類必須是繼承自Proxy類。同時,由于Java是單繼承的,那么我們只能通過與被代理類實現共同的接口從而實現代理,自然而然也就意味著:被代理類必須實現某個接口,所以抽象角色也必須是接口而不能是抽象類。
4.靜態代理 和 動態代理 的 使用模板
4.1 靜態代理使用模板
①聲明抽象主題角色:
public abstract class Subject {
/**
* 一個普通的業務方法
*/
public abstract void visit();
}
②定義真實角色
public class RealSubject extends Subject{
@Override
public void visit() {
//真實角色中的具體邏輯和實現
System.out.println("我是靜態代理中的 真實角色 的具體方法");
}
}
③定義代理類
public class ProxySubject extends Subject{
private RealSubject mSubject;//持有真實對象的引用
public ProxySubject(RealSubject mSubject){
this.mSubject=mSubject;
}
@Override
public void visit(){
//在真實對象方法前 添加一些操作
System.out.println("添加一些 之前操作");
//通過真實角色對象的引用 調用真實角色中的邏輯方法
mSubject.visit();
//在真實對象方法后 添加一些操作
System.out.println("添加一些 之前操作");
}
}
④MainActivity作為client類進行測試
//測試靜態代理的使用
//構造一個真實角色
RealSubject real=new RealSubject();
//通過真實對象構造一個代理對象
ProxySubject proxy=new ProxySubject(real);
//調用代理的相關方法
proxy.visit();
4.2 動態代理使用模板
動態代理的使用和靜態代理十分類似,不同的就在于動態代理的代理類,需要實現 InvocationHandler這個接口,并且需要重寫invoke()方法。我們也來走一遍。為了區分我們把相應的類后面加個2.
①創建抽象角色(只能是接口類)
public interface Subject2 {
/**
* 一個普通的業務方法
*/
void visit();
}
②定義真實角色
public class RealSubject2 implements Subject2 {
@Override
public void visit() {
//真實角色中的具體邏輯和實現
System.out.println("我是動態代理中的 真實角色中的方法");
}
}
③實現 InvocationHandler這個接口
public class MyInvocationHandler implements InvocationHandler {
private Object obj;//真實角色的引用
public MyInvocationHandler(Object obj){
this.obj=obj;
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
//調用被代理對象的方法
Object result=method.invoke(obj,args);
return result;
}
}
④MainActivity作為client類進行測試
//測試動態代理的使用
RealSubject2 real2 = new RealSubject2();
InvocationHandler invocationHandler = new MyInvocationHandler(real2);
Subject2 dynamicProxy = (Subject2) Proxy.newProxyInstance(RealSubject2.class.getClassLoader(), new Class[]{Subject2.class}, invocationHandler);
dynamicProxy.visit();
5.先從一個人盡皆知的栗子講起
雖說有些例子,在項目中根本用不到,但是很多書中卻又不得不提,我想主要還是它有助于我們去理解一個概念吧,雖然不實用,但是可以拋磚引玉,或許在以后的某個時候我們可以把它變變,它就價值翻倍了。
那下面,我就以《設計模式》中的一個人盡皆知的栗子講起吧。這里我們先結合實景理解一下代理模式,之后我們再講項目中的實際例子,這樣循序漸進或許會好點。
事情是這樣的:小民遇到了被老板拖欠工資的情況,他決定通過法律途徑來解決問題,于是他請了一個律師來作為自己的訴訟代理人。
下面我們先使用代理模式來模擬這一事件。
①首先將訴訟的流程抽象再一個接口類中,創建一個訴訟接口類:
/**
* Created by ChenJunMei on 2019/8/23.
* 訴訟接口類
*/
public interface ILawsuit {
//提交申請
void submit();
//進行舉證
void burden();
//開始辯護
void defend();
//訴訟完成
void finish();
}
②創建具體訴訟人---小民 實現ILawsuit接口,并給出具體的實現邏輯
/**
* Created by ChenJunMei on 2019/8/23.
* 具體訴訟人---小民
*/
public class XiaoMin implements ILawsuit {
@Override
public void submit() {
//老板欠小民工資,小民只好申請仲裁
System.out.println("老板拖欠工資,特此申請仲裁!");
}
@Override
public void burden() {
//小民證據充足,不怕告不贏
System.out.println("這是合同書和過去一年的銀行工資流水!");
}
@Override
public void defend() {
//鐵證如山,沒什么好說的
System.out.println("證據確鑿,我沒必要多說什么了!");
}
@Override
public void finish() {
//結果肯定是必贏
System.out.println("訴訟成功!判決老板七天內結算工資");
}
}
③創建代理律師類,該類持有一個被代理者(小民)的引用,律師所執行的方法就是簡單的調用被代理者中的方法。
/**
* Created by ChenJunMei on 2019/8/23.
* 代理律師類
*/
public class Lawyer implements ILawsuit{
//持有一個具體被代理者的引用
private ILawsuit mLawsuit;
public Lawyer(ILawsuit lawsuit){
mLawsuit=lawsuit;
}
@Override
public void submit() {
mLawsuit.submit();
}
@Override
public void burden() {
mLawsuit.burden();
}
@Override
public void defend() {
mLawsuit.defend();
}
@Override
public void finish() {
mLawsuit.finish();
}
}
④在MainActivity中進行測試
//測試小民訴訟
ILawsuit xiaomin=new XiaoMin();//構造一個小民
ILawsuit lawyer=new Lawyer(xiaomin);//構造一個代理律師并將小民作為構造參數傳遞進去
lawyer.submit();//律師提交訴訟
lawyer.burden();//律師進行舉證
lawyer.defend();//律師代替小民進行辯護
lawyer.finish();//完成訴訟
當然,一個律師可以代理多個人,所以除了小民,還可以小輝。我們只需要再定義一個XiaoHui類實現ILawsuit即可,再在客戶端類中修改高層模塊調用邏輯就OK了。
好,到這里,一個模擬訴訟的栗子就完成了。
我們再使用動態代理來實現一下:
要使用動態代理,訴訟接口類和被代理類XiaoMin都不需要變,我們只需要對代理律師類做下改動就好了。我們需要創建一個類實現InvocationHandler類并重寫invoke()方法,我們主要通過invoke()方法來調用具體的被代理方法。
/**
* Created by ChenJunMei on 2019/8/23.
* 動態代理 創建LawyerInvocationHandler 實現 InvocationHander接口
*/
public class LawyerInvocationHandler implements InvocationHandler {
private Object realObj;
public LawyerInvocationHandler(Object realObj){
this.realObj=realObj;
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
//調用被代理對象的方法
Object retObj=method.invoke(realObj,args);
return retObj;
}
}
在MainActivity中測試一下動態實現代理訴訟:
//測試動態代理方式 小民訴訟
ILawsuit xiaomin2=new XiaoMin();//構造一個小民
LawyerInvocationHandler lawyerInvocationHandler=new LawyerInvocationHandler(xiaomin2);//創建一個InvocationHandler
ClassLoader loader=xiaomin2.getClass().getClassLoader();//獲取被代理類xiaomin2的ClassLoader
//動態構造一個代理律師
ILawsuit dynamicLawyer= (ILawsuit) Proxy.newProxyInstance(loader,new Class[]{ILawsuit.class},lawyerInvocationHandler);
dynamicLawyer.submit();
dynamicLawyer.burden();
dynamicLawyer.defend();
dynamicLawyer.finish();
好了經過這個栗子是不是對代理模式有了一點感覺。下面就讓我們來看看這幾模式在真實項目中使用的栗子。
6.實際項目中代理模式的使用舉例
實例①場景描述:
購物車中的商品選中就刪除的功能。
如果只是一個干干凈凈的刪除那怎么寫都可以,但是在新版本的時候老板要求在刪除前要先做一些其他操作,刪除成功后又要做其他操作你怎么決解。
這樣的話我們就不得不為以后做考慮了,如果后期還要對刪除前后增加其他要求怎么辦。每次都直接改刪除中的代碼的話就有點low了,要是每次我們只調用xxx.delete()方法,至于刪除前和刪除后的操作都在里面封裝好就太好了。
要達到這種目的,代理模式是我們的一個很好的選擇。
下面我先用靜態代理來實現,再用動態代理。之后我再拿其他兩種不妥當的方法來做下對比,相信優劣立馬就可以體現出來。
使用靜態代理的代碼實現:
①創建商品接口 抽象類
public interface IGoods {
void delete();
}
②創建商品真實類
public class RealGoods implements IGoods{
@Override
public void delete() {
System.out.println("請求后臺刪除操作...");
}
}
③創建代理類
public class GoodsProxy implements IGoods {
private IGoods iGoods;
public GoodsProxy(IGoods iGoods) {
this.iGoods = iGoods;
}
@Override
public void delete() {
//在這增加做刪除前的一些操作
System.out.println("刪除前的一些操作...");
iGoods.delete();
//在這增加刪除后的一些操作
System.out.println("刪除后的一些操作...");
}
}
④在客戶類(MainActivity)中進行測試使用
只需要一句delete()調用, 既不影響之前的商品刪除,也增加了可擴展性。
GoodsProxy goodsProxy = new GoodsProxy(new RealGoods());
goodsProxy.delete();
靜態代理模式需要為每一個需要代理的類寫一個代理類,如果需要代理的類有N個,那就要寫N個代理類,這就有點讓人無法忍受了。所以我們考慮用動態代理來實現,這樣后期的靈活性更大些
使用動態代理的代碼實現:
①創建商品接口 抽象類
②創建商品真實類
1和2兩步都和靜態代理的一模一樣,這里主要改變的是第三步
③靜態代理實現的是我們自己定義的接口,而這里動態代理實現的是JDK提供的接口InvocationHandler,并需要重寫invoke()方法
public class GoodsInvocationHandler implements InvocationHandler {
private IGoods mIGoods;
public GoodsInvocationHandler(IGoods mIGoods) {
this.mIGoods = mIGoods;
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
//刪除前的一些操作
System.out.println("動態代理方式:刪除前的操作...");
Object invoke = method.invoke(mIGoods, args);
//刪除后的一些操作
System.out.println("動態代理方式:刪除后的操作...");
return invoke;
}
}
④使用與測試
RealGoods goods = new RealGoods();//構造一個商品真實類
GoodsInvocationHandler goodsInvocationHandler = new GoodsInvocationHandler(goods);//構造一個InvocationHandler
IGoods goodsDynacmicProxy = (IGoods) Proxy.newProxyInstance(goods.getClass().getClassLoader(), goods.getClass().getInterfaces(), goodsInvocationHandler);//動態生成一個代理類
goodsDynacmicProxy.delete();//通過動態代理對象調用刪除方法
兩種不好的方法做優劣對比(不使用代理模式的情況下):
不好的方法一:直接修改刪除邏輯代碼,在其內部直接添加刪除前和刪除后的操作
public class RealGoods implements IGoods{
@Override
public void delete() {
//直接添加刪除前的操作
System.out.println("直接添加刪除前操作");
System.out.println("請求后臺刪除操作...");
//直接添加刪除后的操作
System.out.println("直接添加刪除后操作");
}
}
違反了開閉原則,直接修改內部代碼,代碼的可擴展性也極差。
不好的方法二:
再增加一個接口,接口中添加了刪除前操作和刪除后操作。
在使用的
xxx.delete();
地方增加兩個行代碼:
xxx.beforeDelete(); //刪除前的操作
xxx.delele();
xxx.afterDelete(); //刪除后的操作
這在一定程度上增加了可擴展性,但是如果項目中使用到刪除的地方很多,那每個地方都要增加兩個方法,也實在是令人無法忍受啊。
所以,通過不好的進行對比。我們才能更加真切的體會到使用代理默模式來解決這種問題的好處。
實例場景②描述
自己的項目中嵌入了aar包,發現自己的工程和aar中都引用了系統的同一個靜態成員變量,不管任何一方從新設置這個變量,另一方的該變量都會被覆蓋 的問題。
舉個具體的栗子:
開發一個aar包嵌入到客戶的項目中。采用Thread的UncaughtExceptionHandler來記錄崩潰日志,但是客戶那邊也是用的這個方法。而Thread中的defaultUncaughtExceptionHandler是一個靜態的成員變量,如果有人已經設置了對象進去,我們再設置,就會覆蓋了人家的對象。反之,我們自己的就被覆蓋了。如果按照這種思路,那肯定就只有最后一個調用Thread.setUncaughtExceptionHandler的人才能拿到錯誤的日志信息了。
而要解決這個問題,使用代理是一個很好的解決方法。
思路是:
先取出Thread類中已有的異常處理對象defaultHandler ,拿到被代理的對象。
然后用拿到的被代理對象創建一個實現InvocationHandler 接口的實現InVocationHandler接口 ,在invoke(Object proxy, Method method, Object[] args)方法中,加上自己的操作,并調用 method.invoke(//被代理的對象, args)。
最后利用defaultHandler 和實現的InVocation 類對象,創建代理類對象并替換原有對象(被代理對象)。
具體代碼實現:
Thread.UncaughtExceptionHandler defaultHandler = Thread.getDefaultUncaughtExceptionHandler();
class InVocation implements InvocationHandler {
Thread.UncaughtExceptionHandler defaultHandler;
public InVocation(Thread.UncaughtExceptionHandler defaultHandler) {
this.defaultHandler = defaultHandler;
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
Throwable e;
Thread t;
if (args != null && args.length > 1) {
t = (Thread) args[0];
e = (Throwable) args[1];
}
/**
* 根據 t和e將錯誤信息寫到文件中
* ...
*/
//將信息傳遞給原來的
Object o = method.invoke(this.defaultHandler, args);
return o;
}
}
Thread.UncaughtExceptionHandler uncaughtExceptionHandler = (Thread.UncaughtExceptionHandler) Proxy.newProxyInstance(defaultHandler.getClass().getClassLoader(), defaultHandler.getClass().getInterfaces(), new InVocation(defaultHandler));
Thread.setDefaultUncaughtExceptionHandler(uncaughtExceptionHandler);
積累點滴,做好自己~
參考資料
《Android源碼設計模式解析與實戰》第18章
《Android插件化開發指南》第四章
知乎問答:https://zhuanlan.zhihu.com/p/41110998