动态代理+注解(DynamicProxyAndAnnotations)

什么是注解

注解是一种元数据, 可以添加到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();

继承和聚合的区别:


接下来,观察上面的类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
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,117评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,328评论 1 293
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,839评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,007评论 0 206
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,384评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,629评论 1 219
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,880评论 2 313
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,593评论 0 198
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,313评论 1 243
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,575评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,066评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,392评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,052评论 3 236
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,082评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,844评论 0 195
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,662评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,575评论 2 270

推荐阅读更多精彩内容

  • 什么是注解(Annotation):Annotation(注解)就是Java提供了一种元程序中的元素关联任何信息和...
    九尾喵的薛定谔阅读 3,073评论 0 2
  • 以前,『XML』是各大框架的青睐者,它以松耦合的方式完成了框架中几乎所有的配置,但是随着项目越来越庞大,『XML』...
    Java大生阅读 2,364评论 3 96
  • 注:都是在百度搜索整理的答案,如有侵权和错误,希告知更改。 一、哪些情况下的对象会被垃圾回收机制处理掉  当对象对...
    Jenchar阅读 3,145评论 3 2
  • 前言   平时在开发中接触过许多的注解,如@Override,@Nullable等,但自己代码中还没怎么用过。所以...
    yizhanzjz阅读 2,702评论 0 10
  • 看見月亮在追趕太陽,懊惱為何總是追不上, 原來一早錯過了時機,只是一直誤會了距離, 雙雙經過相同的地方,卻是經歷不...
    阿帕樹阅读 74评论 0 0