動態代理+註解(DynamicProxyAndAnnotations)

TheShy_發表於2019-01-18

什麼是註解

註解是一種後設資料, 可以新增到java程式碼中. 類、方法、變數、引數、包都可以被註解,註解對註解的程式碼沒有直接影響.

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

為什麼要引入註解

在Annotation之前,XML被廣泛的應用於描述後設資料。但是XML是鬆耦合的而且維護比較麻煩。 有時使用一些和程式碼緊耦合的東西更加合適(比如一些服務),Annotation應運而生,而且它更加方便維護。

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

註解是如何工作的

Annotations僅僅是後設資料,和業務邏輯無關。也就是說Annotations只是指定了業務邏輯,它的使用者來 完成其業務邏輯,JVM便是它的使用者,它工作在位元組碼層面.

當然,前端編譯生成位元組碼階段,編譯器針對註釋做了處理,如果有註解錯誤等,無法正常編譯成位元組碼.只有成功編譯生成位元組碼後.在執行期JVM就可以進行業務邏輯處理.

元註解

java內建的註解有Override, Deprecated, SuppressWarnings等, 作用相信大家都知道.
元註解就是用來定義註解的註解.其作用就是定義註解的作用範圍, 使用在什麼元素上等等

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

@Documented : 是否會儲存到 Javadoc 文件中。

@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();
複製程式碼

繼承和聚合的區別:

動態代理+註解(DynamicProxyAndAnnotations)

接下來,觀察上面的類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…

相關文章