Java動(dòng)態(tài)代理-實(shí)戰(zhàn)

只要是寫Java的贱除,動(dòng)態(tài)代理就一個(gè)必須掌握的知識(shí)點(diǎn)生闲,當(dāng)然剛開(kāi)始接觸的時(shí)候,理解的肯定比較淺勘伺,漸漸的會(huì)深入一些,這篇文章通過(guò)實(shí)戰(zhàn)例子幫助大家深入理解動(dòng)態(tài)代理飞醉。

說(shuō)動(dòng)態(tài)代理之前冲茸,要先搞明白什么是代理缅帘,代理的字面意思已經(jīng)很容易理解了轴术,我們這里撇開(kāi)其他的解釋,我們只談設(shè)計(jì)模式中的代理模式

什么是代理模式(Proxy)

定義:給目標(biāo)對(duì)象提供一個(gè)代理對(duì)象逗栽,并由代理對(duì)象控制對(duì)目標(biāo)對(duì)象的引用

在代理模式中,是需要代理對(duì)象和目標(biāo)對(duì)象實(shí)現(xiàn)同一個(gè)接口(如果是不同的接口失暂,那就是適配器模式了)彼宠,看下面的UML圖

動(dòng)態(tài)代理 uml.png

為什么要用代理

最最最主要的原因就是弟塞,在不改變目標(biāo)對(duì)象方法的情況下對(duì)方法進(jìn)行增強(qiáng)凭峡,比如决记,我們希望對(duì)方法的調(diào)用增加日志記錄摧冀,或者對(duì)方法的調(diào)用進(jìn)行攔截,等等...

舉一個(gè)例子

現(xiàn)有一個(gè)IPerson接口索昂,只有一個(gè)方法say()

public interface IPerson {
    void say();
}

有一個(gè)Man類,實(shí)現(xiàn)了IPerson

public class Man implements IPerson{
    @Override
    public void say() {
        L.d("man say");
    }
}

現(xiàn)在需要在say方法被調(diào)用的時(shí)候扩借,記錄方法被調(diào)用的時(shí)間,最直接的就是修改Man的say方法潮罪,但是這樣做的弊端就是如果有很多實(shí)現(xiàn)了IPerson接口的類康谆,那就需要修改多處代碼凄杯,而且這樣的修改可能會(huì)導(dǎo)致其他的代碼出問(wèn)題(可能并不是所有的say都需要記錄調(diào)用時(shí)間)秉宿。怎么辦呢戒突,這時(shí)候代理就要登場(chǎng)了描睦!

靜態(tài)代理

public class ManProxy implements IPerson{

    private IPerson target;

    public IPerson getTarget() {
        return target;
    }

    public ManProxy setTarget(IPerson target) {
        this.target = target;
        return this;
    }

    @Override
    public void say() {
        if (target != null) {
            L.d("man say invoked at : " + System.currentTimeMillis());
            target.say();
        }
    }
}

這樣我們需要新建一個(gè)ManProxy類同樣實(shí)現(xiàn)IPerson接口膊存,將要代理的對(duì)象傳遞進(jìn)來(lái),這樣就可以在不修改Man的say方法的情況下實(shí)現(xiàn)了我們的需求隔崎。這其實(shí)就是靜態(tài)代理。那你可能要問(wèn)韵丑,既然有了靜態(tài)代理爵卒,為什么需要?jiǎng)討B(tài)代理呢,因?yàn)殪o態(tài)代理有一個(gè)最大的缺陷:接口與代理類是1對(duì)1的撵彻,有多個(gè)接口需要代理钓株,就需要新建多個(gè)代理類,繁瑣陌僵,類爆炸轴合。

動(dòng)態(tài)代理

我們先嘗試用動(dòng)態(tài)代理來(lái)解決上面的問(wèn)題。先新建一個(gè)類實(shí)現(xiàn)InvocationHandler碗短,

public class NormalHandler implements InvocationHandler {

    private Object target;

    public NormalHandler(Object target) {
        this.target = target;
    }

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        L.d("man say invoked at : " + System.currentTimeMillis());
        method.invoke(target, args);
        return null;
    }
}

然后可以這樣使用

Man man = new Man();
NormalHandler normalHandler = new NormalHandler(man);
AnnotationHandler annotationHandler = new AnnotationHandler();
IPerson iPerson = (IPerson) Proxy.newProxyInstance(IPerson.class.getClassLoader(),
                new Class[] {IPerson.class, IAnimal.class}, annotationHandler);
iPerson.say();

可以看到NormalHandler中代理的對(duì)象是Object類型受葛,所以它是被多個(gè)接口代理復(fù)用的,這樣就解決靜態(tài)代理類爆炸偎谁,維護(hù)困難的問(wèn)題总滩。我們重點(diǎn)看NormalHandler中的invoke方法,第二個(gè)參數(shù)method就是我們實(shí)際調(diào)用時(shí)的方法巡雨,所以動(dòng)態(tài)代理使用了反射闰渔,為了靈活稍稍犧牲一點(diǎn)性能。

動(dòng)態(tài)代理的成功案例

  • Square公司出品的Android Restful 網(wǎng)絡(luò)請(qǐng)求庫(kù)Retrofit
  • Spring AOP (默認(rèn)使用動(dòng)態(tài)代理鸯隅,如果沒(méi)有實(shí)現(xiàn)接口則使用CGLIB修改字節(jié)碼)

這2個(gè)庫(kù)不用多說(shuō)了向挖,Github上面Star數(shù)都是好幾萬(wàn)的網(wǎng)紅項(xiàng)目蝌以。

利用動(dòng)態(tài)代理實(shí)現(xiàn)一個(gè)低配的Retrofit

“talk is cheap, show me the code”, 所以捋起袖子干起來(lái)何之。
先新建需要用到的注解類和實(shí)體類

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface GET {
    String value();
}

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface POST {
    String value();
}

@Target(ElementType.PARAMETER)
@Retention(RetentionPolicy.RUNTIME)
public @interface Query {
    String value();
}

//更新實(shí)體類
public class CheckUpdate {

    private boolean hasUpdate;
    private String newVersion;

    public boolean isHasUpdate() {
        return hasUpdate;
    }

    public void setHasUpdate(boolean hasUpdate) {
        this.hasUpdate = hasUpdate;
    }

    public String getNewVersion() {
        return newVersion;
    }

    public void setNewVersion(String newVersion) {
        this.newVersion = newVersion;
    }

    @Override
    public String toString() {
        return "Has update : " + hasUpdate + " ; The newest version is : " + newVersion;
    }
}

接下來(lái)是接口方法類, 接口url地址這里隨便寫的跟畅,大家知道意思就OK了溶推。

public interface ApiService {

    @POST("http://www.baidu.com/login")
    Observable<User> login(@Query("username") String username, @Query("password") String password);

    @GET("http://www.baidu.com/checkupdate")
    Observable<CheckUpdate> checkUpdate(@Query("version") String version);

}

接下來(lái)就是我們的重點(diǎn)代理類RequestHandler徊件,里面的核心是解析方法注解的返回值和參數(shù)奸攻,包括返回值的泛型虱痕,在Json反序列化的時(shí)候回用到

public class RequestHandler implements InvocationHandler {

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        Annotation[] annotations = method.getAnnotations();
        if (annotations != null && annotations.length > 0) {
            Annotation annotation = annotations[0];
            if (annotation instanceof GET) {
                GET get = (GET) annotation;
                return handleGetRequest(method, get, args);
            }else if (annotation instanceof POST) {
                POST post = (POST) annotation;
                return handlePostRequest(method, post, args);
            }
        }
        return null;
    }
    
    private Observable handleGetRequest(Method method, GET get, Object[] params) {
        String url = get.value();
        Type genericType = method.getGenericReturnType();
        Parameter[] parameters = method.getParameters();

        ParameterizedType parameterizedType = (ParameterizedType) genericType;
        Class returnGenericClazz = null;
        //解析方法返回值的參數(shù)類型
        if (parameterizedType != null) {
            Type[] types = parameterizedType.getActualTypeArguments();
            for (int i = 0; i < types.length; i++) {
                Class cls = (Class) types[i];
                returnGenericClazz = cls;
                break;
            }
        }

        //解析請(qǐng)求參數(shù),然后拼接到url
        if (params != null) {
            url += "?";
            for (int i = 0; i < params.length; i++) {
                Query query = parameters[i].getAnnotation(Query.class);
                url += query.value() + "=" + params[0].toString();
                if (i < params.length - 1) {
                    url += "&";
                }
            }
        }
        final String getUrl = url;
        final Class returnClazz = returnGenericClazz;
        return Observable.create(observableEmitter -> {
            Request request = new Request.Builder().url(getUrl).build();
            Response response = new OkHttpClient()
                    .newCall(request).execute();
            if (response.isSuccessful()) {
//                    String responseStr = response.body().string();
                //這里mock返回?cái)?shù)據(jù)
                String responseStr = MockFactory.mockCheckUpdateStr();
                Object result = new Gson().fromJson(responseStr, returnClazz);
                observableEmitter.onNext(result);
            }else {
                observableEmitter.onError(new IllegalStateException("http request failed!"));
            }
            observableEmitter.onComplete();
        });
    }

    private Observable handlePostRequest(Method method, POST post, Object[] params) {
        //篇幅關(guān)系部翘,這里省略硝训,可以參考get 實(shí)現(xiàn)
        //。窖梁。。夹囚。。
    }
}

新建一個(gè)門面類Retrofit荸哟,方便調(diào)用

public class Retrofit {

    public static <T> T newProxy(Class<T> clazz) {
        return  (T) Proxy.newProxyInstance(clazz.getClassLoader(),
                new Class[] {clazz}, new RequestHandler());
    }

}

一個(gè)低配版的Retrofit就完成了假哎,趕緊去測(cè)試一下

public static void main(String[] args) {
     ApiService apiService = ApiService apiService = Retrofit.newProxy(ApiService.class);
     Observable<CheckUpdate> checkUpdateObservable = apiService.checkUpdate("3.1.0");
     checkUpdateObservable.subscribeOn(Schedulers.io())
            .subscribe(checkUpdate -> L.d(checkUpdate.toString()),
                   throwable -> L.d(throwable.getMessage()));
                   
     //等待工作線程執(zhí)行完成
     Scanner sc = new Scanner(System.in);
     if (sc.next() != null) {}
 }

最終的執(zhí)行結(jié)果鞍历,當(dāng)然這里只是初步實(shí)現(xiàn)了Retrofit的一點(diǎn)點(diǎn)功能位谋,我們的目標(biāo)還是講解動(dòng)態(tài)代理這個(gè)技術(shù),以及它能夠干什么


執(zhí)行結(jié)果

最后一點(diǎn)小Tip

可以看到掏父,我們上面的低配的Retrofit,并沒(méi)有被代理的類秆剪,因?yàn)槲覀儍H僅通過(guò)解析ApiService接口中的注解中的信息已經(jīng)足夠我們?nèi)グl(fā)起Http請(qǐng)求,所以技術(shù)在于靈活運(yùn)用仅讽。
好了陶缺,這篇先到這里洁灵,大家開(kāi)心發(fā)財(cái)饱岸!

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市苫费,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌双抽,老刑警劉巖,帶你破解...
    沈念sama閱讀 222,104評(píng)論 6 515
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件牍汹,死亡現(xiàn)場(chǎng)離奇詭異铐维,居然都是意外死亡,警方通過(guò)查閱死者的電腦和手機(jī)嫁蛇,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,816評(píng)論 3 399
  • 文/潘曉璐 我一進(jìn)店門锨并,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)睬棚,“玉大人琳疏,你說(shuō)我怎么就攤上這事】张危” “怎么了?”我有些...
    開(kāi)封第一講書人閱讀 168,697評(píng)論 0 360
  • 文/不壞的土叔 我叫張陵新荤,是天一觀的道長(zhǎng)。 經(jīng)常有香客問(wèn)我苛骨,道長(zhǎng)篱瞎,這世上最難降的妖魔是什么痒芝? 我笑而不...
    開(kāi)封第一講書人閱讀 59,836評(píng)論 1 298
  • 正文 為了忘掉前任俐筋,我火速辦了婚禮,結(jié)果婚禮上澄者,老公的妹妹穿的比我還像新娘。我一直安慰自己请琳,他們只是感情好粱挡,可當(dāng)我...
    茶點(diǎn)故事閱讀 68,851評(píng)論 6 397
  • 文/花漫 我一把揭開(kāi)白布俄精。 她就那樣靜靜地躺著询筏,像睡著了一般竖慧。 火紅的嫁衣襯著肌膚如雪嫌套。 梳的紋絲不亂的頭發(fā)上,一...
    開(kāi)封第一講書人閱讀 52,441評(píng)論 1 310
  • 那天踱讨,我揣著相機(jī)與錄音,去河邊找鬼碳胳。 笑死勇蝙,一個(gè)胖子當(dāng)著我的面吹牛挨约,可吹牛的內(nèi)容都是我干的味混。 我是一名探鬼主播诫惭,決...
    沈念sama閱讀 40,992評(píng)論 3 421
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼翁锡,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來(lái)了夕土?” 一聲冷哼從身側(cè)響起,我...
    開(kāi)封第一講書人閱讀 39,899評(píng)論 0 276
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤怨绣,失蹤者是張志新(化名)和其女友劉穎,沒(méi)想到半個(gè)月后篮撑,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體减细,經(jīng)...
    沈念sama閱讀 46,457評(píng)論 1 318
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡赢笨,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 38,529評(píng)論 3 341
  • 正文 我和宋清朗相戀三年未蝌,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片萧吠。...
    茶點(diǎn)故事閱讀 40,664評(píng)論 1 352
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖桐筏,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情梅忌,我是刑警寧澤绊袋,帶...
    沈念sama閱讀 36,346評(píng)論 5 350
  • 正文 年R本政府宣布铸鹰,位于F島的核電站癌别,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏蹋笼。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 42,025評(píng)論 3 334
  • 文/蒙蒙 一剖毯、第九天 我趴在偏房一處隱蔽的房頂上張望圾笨。 院中可真熱鬧逊谋,春花似錦擂达、人聲如沸。這莊子的主人今日做“春日...
    開(kāi)封第一講書人閱讀 32,511評(píng)論 0 24
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至俭令,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間抄腔,已是汗流浹背瓢湃。 一陣腳步聲響...
    開(kāi)封第一講書人閱讀 33,611評(píng)論 1 272
  • 我被黑心中介騙來(lái)泰國(guó)打工赫蛇, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留绵患,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 49,081評(píng)論 3 377
  • 正文 我出身青樓悟耘,卻偏偏與公主長(zhǎng)得像藏雏,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子作煌,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,675評(píng)論 2 359

推薦閱讀更多精彩內(nèi)容