動態代理+註解(DynamicProxyAndAnnotations)

什麼是註解

註解是一種元數據, 能夠添加到java代碼中. 類、方法、變量、參數、包均可以被註解,註解對註解的代碼沒有直接影響.前端

定義註解用的關鍵字是 @interfacejava

爲何要引入註解

在Annotation以前,XML被普遍的應用於描述元數據。可是XML是鬆耦合的並且維護比較麻煩。 有時使用一些和代碼緊耦合的東西更加合適(好比一些服務),Annotation應運而生,並且它更加方便維護。git

目前,許多框架將XML和Annotation兩種方式結合使用,平衡二者之間的利弊。例如ButterKnife, EventBus, Retrofit, Dagger等github

註解是如何工做的

Annotations僅僅是元數據,和業務邏輯無關。也就是說Annotations只是指定了業務邏輯,它的用戶來 完成其業務邏輯,JVM即是它的用戶,它工做在字節碼層面.api

固然,前端編譯生成字節碼階段,編譯器針對註釋作了處理,若是有註解錯誤等,沒法正常編譯成字節碼.只有成功編譯生成字節碼後.在運行期JVM就能夠進行業務邏輯處理.bash

元註解

java內置的註解有Override, Deprecated, SuppressWarnings等, 做用相信你們都知道.
元註解就是用來定義註解的註解.其做用就是定義註解的做用範圍, 使用在什麼元素上等等框架

JDK5.0版本開始提供註解支持: @Documented、@Retention、@Target、@Inheriteddom

@Documented : 是否會保存到 Javadoc 文檔中。ide

@Retention : 定義該註解的生命週期。 它有三個枚舉類型: RetentionPolicy.SOURCE(只在源碼中可用)、 RetentionPolicy.CLASS(在源碼和字節碼中可用,註解默認使用這種方式)、 RetentionPolicy.RUNTIME(在源碼,字節碼,運行時都可用,咱們自定義的註解一般使用這種方式) Tips : RetentionPolicy.SOURCE – 在編譯階段丟棄。這些註解在編譯結束以後就再也不有任何意義,因此它們不會寫入字節碼。 @Override, @SuppressWarnings都屬於這類註解測試

@Target : 表示該註解用於什麼地方。若是不明確指出,該註解能夠放在任何地方。如下是一些可用的參數。 Tips : 屬性的註解是兼容的,你能夠添加多個屬性。 ElementType.TYPE:用於描述類、接口或enum聲明 ElementType.FIELD:用於描述實例變量 ElementType.METHOD:方法 ElementType.PARAMETER參數 ElementType.CONSTRUCTOR構造器 ElementType.LOCAL_VARIABLE本地變量 ElementType.ANNOTATION_TYPE 另外一個註釋 ElementType.PACKAGE 用於記錄java文件的package信息

@Inherited : 是否能夠被繼承,默認爲false

如下代碼所有經過Idea開發

一個簡單的例子

建立一個註解類

@Retention(RetentionPolicy.RUNTIME)
public @interface SingleAnno {
    String value() default "shy";
}
複製代碼

引用它

public class MyClass {

    @SingleAnno("single")
    public void run(){ }
}
複製代碼

經過反射獲取值

public class TestDemo {

    @Test
    public void test(){
        Class<MyClass> myClass = MyClass.class;

        for (Method method : myClass.getDeclaredMethods()){
            SingleAnno anno = method.getAnnotation(SingleAnno.class);
            if(anno != null){
                System.out.println(method.getName());//打印方法名
                System.out.println(anno.value());//打印註解值
            }
        }
    }
}
複製代碼

控制檯能夠看到,輸出的是single

run
single
複製代碼

註解定義規則

Annotations只支持基本類型、String及枚舉類型。註釋中全部的屬性被定義成方法,並容許提供默認值。

自定義註解以及使用

①定義註解類型(稱爲A),最好給A加上運行Retention的RUNTIME註解.默認應該是SOURCE類型. ②定義屬性,實際上是方法表示.提供默認值. ③在其餘類方法(稱爲M)等添加A註解,並給A指定屬性值. ④能夠在其餘地方獲取M方法,而後獲取M的註解,並獲取註解值等.

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface MultiAnno {
   
   enum Priority{HIGH,MID,LOW}
   enum Status {START,PAUSE,STOP}
   
   String name() default "TheShy";
   Priority priority() default Priority.HIGH;
   Status status() default Status.START;
}
複製代碼

關於代理模式:

靜態代理:

核心: 經過聚合來實現,讓代理類持有委託類的引用便可.

一個小例子: 咱們用一個隨機睡眠時間模擬火車運行的時間。若是我要計算運行時間,而且這個類沒法改動.

public interface Runnable {
    void running();
}

public class Train implements Runnable {

    public void running() {
        System.out.println("Train is running......");
        int ranTime = new Random().nextInt(1000);
        try {
            Thread.sleep(ranTime);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}
複製代碼

這裏有不少解決方案: 例如在調用方法地方的先後記錄,繼承(繼承Train調用父類方法),聚合(新建Train2,構造方法傳入Train對象,而後調用running).

可是若是再增長需求:在running方法先後打印日誌,並控制執行順序,固然是用繼承仍是能夠實現,可是要繼續建立新的子類,致使無限擴展......

這時候修改聚合,使其成爲靜態代理就能夠完美解決這個問題: 將構造方法改傳入Runnable接口:

//代理-在方法先後打印日誌
public class TrainLogProxy implements Runnable {

    private Runnable runnable;

    public TrainLogProxy(Runnable runnable) {
        this.runnable = runnable;
    }

    public void running() {
        System.out.println("Train running start...");
        runnable.running();
        System.out.println("Train running end...");
    }
}
複製代碼
//代理-計算執行時間
public class TrainTimeProxy implements Runnable {

    private Runnable runnable;

    public TrainTimeProxy(Runnable runnable) {
        this.runnable = runnable;
    }

    public void running() {

        long start = System.currentTimeMillis();

        runnable.running();

        long end = System.currentTimeMillis();

        System.out.println("run time = " + (end - start));
    }
}
複製代碼

接下來:

Train train = new Train();
        //想先計算執行時間,後打印log
//        TrainTimeProxy trainTimeProxy = new TrainTimeProxy(train);
//        TrainLogProxy trainLogProxy = new TrainLogProxy(trainTimeProxy);
//        trainLogProxy.running();

        //想先打印log,後計算執行時間
        TrainLogProxy trainLogProxy = new TrainLogProxy(train);
        TrainTimeProxy trainTimeProxy = new TrainTimeProxy(trainLogProxy);
        trainTimeProxy.running();
複製代碼

繼承和聚合的區別:

接下來,觀察上面的類TimeProxy,在它的fly方法中咱們直接調用了Runable->run()方法。換而言之,TrainTimeProxy其實代理了傳入的Runnable對象,這就是典型的靜態代理實現。 從表面上看,靜態代理已經完美解決了咱們的問題。但是,試想一下,若是咱們須要計算SDK中100個方法的運行時間,一樣的代碼至少須要重複100次,而且建立至少100個代理類。往小了說,若是Train類有多個方法,咱們須要知道其餘方法的運行時間,一樣的代碼也至少須要重複屢次。所以,靜態代理至少有如下兩個侷限性問題:

  • 若是同時代理多個類,依然會致使類無限制擴展
  • 若是類中有多個方法,一樣的邏輯須要反覆實現

那麼,咱們是否可使用同一個代理類來代理任意對象呢?咱們以獲取方法運行時間爲例,是否可使用同一個類(例如:TrainProxy)來計算任意對象的任一方法的執行時間呢?甚至再大膽一點,代理的邏輯也能夠本身指定。好比,獲取方法的執行時間,打印日誌,這類邏輯均可以本身指定。

動態代理

核心原理 :

首先經過Proxy.newProxyInstance方法獲取代理類實例,然後能夠經過這個代理類實例調用代理類的方法,對代理類的方法的調用實際上都會調用中介類(調用處理器)的invoke方法,在invoke方法中咱們調用委託類的相應方法,而且能夠添加本身的處理邏輯。

  • 委託類:委託類必須實現某個接口,這裏咱們實現的是Runnable接口.
  • 代理類:動態生成,調用Proxy類的newProxyInstance方法來獲取一個代理類實例.
  • 中介類:中介類必須實現InvocationHandler接口,做爲調用處理器」攔截「對代理類方法的調用 步驟 :
  • Proxy->newProxyInstance(infs, handler) 用於生成代理對象
  • InvocationHandler:這個接口主要用於自定義代理邏輯處理
  • 爲了完成對被代理對象的方法攔截,咱們須要在InvocationHandler對象中傳入被代理對象實例。
Runnable runnable = (Runnable) Proxy.newProxyInstance(Runnable.class.getClassLoader(), new Class[]{Runnable.class}, new InvocationHandler() {
            public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
                System.out.println("before");
                Object invoke =  method.invoke(new Train(), args);
                System.out.println("after");
                return invoke;
            }
        });
        runnable.running();
複製代碼

以上咱們就成功的經過不修改Train類在執行running()先後打印了日誌.

代理模式

代理模式最大的特色就是代理類和實際業務類實現同一個接口(或繼承同一父類),代理對象持有一個實際對象的引用,外部調用時操做的是代理對象,而在代理對象的內部實現中又會去調用實際對象的操做

Java動態代理其實內部也是經過Java反射機制來實現的,即已知的一個對象,而後在運行時動態調用其方法,這樣在調用先後做一些相應的處理

agent

仿寫Retrofit

經過動態代理+註解,完成相似retrofit效果,在InvocationHandler的Invoke方法處獲取方法、方法註解、方法參數註解、方法參數等信息,根據狀況設置adapter,完成業務邏輯. (固然,這裏省略了adapter的動做)

  • 整體思路 :經過註解中使用的參數,動態的生成Request而後由OKHttp去調用
@Documented
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Get {
    public String value();
}
複製代碼
public interface ServerAPI {

    @Get("https://www.baidu.com/")
    public String getBaiduHome(@Query("type") String type);

    @Post("https://www.baidu.com/update")
    public String getBaiduUser(@Field("name") String name, @Field("age") String age);

}
複製代碼
public class APICreater {

    public static ServerAPI create(Class<ServerAPI> api){

        ServerAPI serverAPI = (ServerAPI) Proxy.newProxyInstance(api.getClassLoader(), new Class[]{api}, new InvocationHandler() {
            public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
                getMethodMsg(method, args);

                if ("getBaiduHome".equals(method.getName())) {
                    return "I am getBaiduHome return by proxy";
                }
                if ("getBaiduUser".equals(method.getName())) {
                    return "I am getBaiduUser return by proxy";
                }
                ServerAPI obj = getAPI();
                return method.invoke(obj, args);
            }
        });
        return serverAPI;
    }

    private static ServerAPI getAPI() {
        return new ServerAPI() {
            @Override
            public String getBaiduHome(String type) {
                return null;
            }

            @Override
            public String getBaiduUser(String name, String age) {
                return null;
            }
        };
    }


    // 獲取了註解信息和參數信息,結合起來就能夠實現本身的自定義方法.
    private static void getMethodMsg(Method method, Object[] args) {
        AnnoBean bean = new AnnoBean();
        bean.setMethodName(method.getName());

        Annotation[] annotations = method.getAnnotations();
        for (Annotation annotation : annotations) {
            if (annotation instanceof Get) {
                Get getAnni = (Get) annotation;
                String value = getAnni.value();
                bean.setMethodAnniType("Get");
                bean.setMethodAnniValue(value);
            }
            if (annotation instanceof Post) {
                Post getAnni = (Post) annotation;
                String value = getAnni.value();
                bean.setMethodAnniType("Post");
                bean.setMethodAnniValue(value);
            }
        }

        bean.setMethodArgs(Arrays.asList(args));

        Annotation[][] parameterAnnotations = method.getParameterAnnotations();
        for (Annotation[] annotation : parameterAnnotations) {
            for (Annotation annotation1 : annotation) {
                if (annotation1 instanceof Field) {
                    List<String> list = bean.getParamAnniList();
                    if (list == null) {
                        list = new ArrayList<String>();
                    }
                    list.add("paramAnniType: field " + " value: " + ((Field) annotation1).value());
                    bean.setParamAnniList(list);
                }
                if (annotation1 instanceof Query) {
                    List<String> list = bean.getParamAnniList();
                    if (list == null) {
                        list = new ArrayList<String>();
                    }
                    list.add("paramAnniType: query " + " value: " + ((Query) annotation1).value());
                    bean.setParamAnniList(list);
                }
            }
        }
        System.out.println(bean.toString());
    }
}
複製代碼
public class TestRetrofitDemo {

    @Test
    public void testRetrofit(){

        ServerAPI serverAPI = APICreater.create(ServerAPI.class);
        String homeeeeee = serverAPI.getBaiduHome("Homeeeeee");
        System.out.println("-----" + homeeeeee);

    }
}
複製代碼

最後測試一下輸出結果:

AnniBean{methodName='getBaiduHome', methodArgs=[Homeeeeee], methodAnniType='Get', methodAnniValue='https://www.baidu.com/', paramAnniList=[paramAnniType: query    value: type]}
-----I am getBaiduHome return by proxy
複製代碼

github地址 : github.com/saurylip/An…

相關文章
相關標籤/搜索