Java的代理和注解

作者: nick_young | 来源:发表于2018-02-09 19:51 被阅读58次

    都已经记不起到底何时开始使用Java语言进行编程的了,印象中起码也有三五年了吧。想象中自己应该要做后台,却阴差阳错成了Android狗,还好Java仍然陪伴着我(Java:你明明去看了Kotlin!)。



    今天打算写写关于代理模式和注解这方面的内容。这两个简直是风马牛不相及,放在一起好像有那么一丢丢别扭。不过呢,这主要是为了后面的文章做准备。作为一名伪源码爱好者,还是读过不少源码。其中Android的网络请求框架Retrofit就将动态代理和注解完美了结合在了一起,后面详细讲解吧(PS:这些内容一年前就已经看懂了,现在只为了记录)!

    1. 代理模式

    代理可以分为两类:静态代理和动态代理,说实话,Android在使用代理类这方面用的并不是很多,关于AOP(面向切面编程)这些东西使用频繁的还是在后台开发。

    代理的三个条件:

    • 共同接口:代理类和被代理类实现相同的接口
    • 真实对象:实现接口,并真正完成某些操作
    • 代理对象:实现接口,对真实对象进行代理,可以在其操作前后执行其他操作

    关于代理我的理解就是xxx(被代理类)有中间商(代理类),还可能赚差价(进行操作)。光说不练好像一点用处都没有,还是用show you my code吧!

    共同接口:

    public interface Action {
        void doSomething(float value);
    }
    

    真实对象:

    public class RealDoAction implements Action {
        @Override
        public void doSomething(float value) {
            System.out.println("我是车主我要卖车,价钱" + value + "元");
        }
    }
    

    静态代理:

    public class ActionProxy implements Action {
    
        private Action realDoAction;
    
        public ActionProxy(RealDoAction realDoAction) {
            this.realDoAction = realDoAction;
        }
    
        @Override
        public void doSomething(float value) {
            // 被代理类操作执行前执行的操作
            System.out.println("我是平台,在我这里可以进行交易。");
            realDoAction.doSomething(value);
            // 被代理类操作完成后执行的操作
            System.out.println("车卖出去了,价钱" + addValue(value) + "元,没有中间商赚差价。");
        }
    
        private float addValue(float value) {
            return value + 10;
        }
    }
    // 测试
    public static void main(String[] args) {
        Action action = new ActionProxy(new RealDoAction());
        action.doSomething(100);
    }
    

    输出:

    我是平台,在我这里可以进行交易。
    我是车主我要卖车,价钱100.0元
    车卖出去了,价钱110.0元,没有中间商赚差价。
    

    可以看到使用静态代理模式可以很方便的扩展原有的功能并且不去修改原代码,但是对于需要代理的多个类此时实现却有些麻烦。为了简单有效的解决这个问题,我们可以使用动态代理来更灵活的方式去实现代理功能。
    使用动态代理的方式也比较简单,Java提供给我们Proxy.newProxyInstance可以快速方便实现动态代理:

    // 还是原来的代码
    Action realDo = new RealDoAction();
    Action action = (Action) Proxy.newProxyInstance(Action.class.getClassLoader(), // 类加载器
            new Class[]{Action.class}, // 被代理类实现的所有接口
            new InvocationHandler() { // InvocationHandler具体逻辑写在这里,返回值是该方法的返回值。接口中有多个方法可以根据方法名称判断
                @Override
                public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
                    System.out.println("我是平台,在我这里可以进行交易。");
                    method.invoke(realDo, args);
                    for (int i = 0; i < args.length; i++) {
                        if (args[i] instanceof Float) {
                            System.out.println("车卖出去了,价钱" + ((float) args[i] + 10) + "元,没有中间商赚差价。");
                            break;
                        }
                    }
                    return null;
                }
            });
    action.doSomething(1000);
    

    看下输出,和静态代理一样:

    我是平台,在我这里可以进行交易。
    我是车主我要卖车,价钱1000.0元
    车卖出去了,价钱1010.0元,没有中间商赚差价。
    

    代理到这里基本上就结束了,东西不多。


    2. 注解

    谈起注解,可写的内容可就很多了。这里还是简单来写吧,省的又是长篇大论。
    首先说下可以用在注解上的注解——元注解

    @Retention保留期,可取值:

    • RetentionPolicy.SOURCE 注解只在源码阶段保留,编译后的class文件不存在该注解。
    • RetentionPolicy.CLASS 注解只被保留到编译进行的时候,编译后的class文件该注解仍然存在,但是不会被加载到JVM中。
    • RetentionPolicy.RUNTIME 注解可以保留到程序运行的时候,它会被加载进入到 JVM 中,所以在程序运行时可以获取到它们。

    关于RetentionPolicy.SOURCE和RetentionPolicy.CLASS这两者都可以使用AbstractProcessor来在编译时生成代码,其主要不同点在于编译后的class文件中是否存在该注解。也可以理解为是方便给IDE看还是给程序员看。

    @Target注解的作用域,可取值:

    • ElementType.ANNOTATION_TYPE 可以给一个注解进行注解
    • ElementType.CONSTRUCTOR 可以给构造方法进行注解
    • ElementType.FIELD 可以给属性进行注解
    • ElementType.LOCAL_VARIABLE 可以给局部变量进行注解
    • ElementType.METHOD 可以给方法进行注解
    • ElementType.PACKAGE 可以给一个包进行注解
    • ElementType.PARAMETER 可以给一个方法内的参数进行注解
    • ElementType.TYPE 可以给一个类型进行注解,比如类、接口、枚举
    • ElementType.TYPE_PARAMETER和ElementType.TYPE_USE(1.8新增)它们都可以限制哪个类型可以进行注解。能在局部变量、泛型类、父类和接口的实现处使用,甚至能在方法上声明异常的地方使用。(具体用途不详。。)

    @Documented有关注释的注解。
    @Inherited指可继承注解,表示某个类的父类拥有该注解,子类如果没有其他注解的话,那么该子类会继承父类的注解。
    @Repeatable(1.8新增)我的理解是为了简化一个属性拥有多个相同注解而新增的注解,这里面可以放多个注解。(解释不太清楚,用的也不多)

    上面讲了5个元注解,其实常用的也就@Retention和@Target,这里面可以根据@Retention可以分为编译时注解和运行时注解,下面分别写个例子。

    2.1 编译时注解

    关于编译时注解昨天基本上花费了一整天,原本Eclipse中的可以执行但是换成了IDEA就不能使用了。花费了一天时间,查找原因(主要还是不想用老大哥Eclipse进行展示)。下面看下例子:

    1. 创建注解
    @Target({ElementType.TYPE})
    @Retention(RetentionPolicy.CLASS) // 这里也可以改为RetentionPolicy.SOURCE,只不过在class文件中看不到这个注解
    public @interface TestAnnotation {
        String value() default "";
    }
    
    1. 继承AbstractProcessor实现编译时生成代码
    @SupportedAnnotationTypes("com.nick.demo.annotation.TestAnnotation")// 要支持的注解
    @SupportedSourceVersion(SourceVersion.RELEASE_8) // 版本
    public class Processor extends AbstractProcessor {
        // 文件创建器
        private Filer filer;
        // 消息输出者
        private Messager messager;
    
        @Override
        public synchronized void init(ProcessingEnvironment processingEnv) {
            super.init(processingEnv);
            // 初始化获取文件创建器和消息输出
            filer = processingEnv.getFiler();
            messager = processingEnv.getMessager();
        }
    
        @Override
        public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
            // 根据注解获取所有的Element
            Set<? extends Element> elements = roundEnv.getElementsAnnotatedWith(TestAnnotation.class);
            // 遍历
            for (Element element : elements) {
                // 如果类型是Class,也就是注解的作用域是ElementType.TYPE
                if (element.getKind() == ElementKind.CLASS) {
                    // 转换成TypeElement
                    TypeElement typeElement = (TypeElement) element;
                    Name simpleName = typeElement.getSimpleName();
                    // 输出名称,只为测试而用
                    messager.printMessage(Diagnostic.Kind.NOTE, simpleName);
                    // 输出全部名称
                    messager.printMessage(Diagnostic.Kind.NOTE, typeElement.getQualifiedName());
                    // 创建文件
                    createFile(typeElement.getQualifiedName().toString(), element.getAnnotation(TestAnnotation.class).value());
                }
            }
            return true;
        }
    
        private void createFile(String className, String output) {
            StringBuilder cls = new StringBuilder();
            // 获得com.xxx.A的包名,com.xxx
            String[] strings = className.split("\\.");
            StringBuilder packageName = new StringBuilder();
            for (int i = 0; i < strings.length - 1; i++) {
                packageName.append(strings[i]);
                if (i != strings.length - 2) {
                    packageName.append(".");
                }
            }
            // 代码的拼接
            cls.append("package " + packageName + ";\n\npublic class ")
                    .append(strings[strings.length - 1] + output)
                    .append(" {\n  public static void main(String[] args) {\n")
                    .append("    System.out.println(\"")
                    .append(output)
                    .append("\");\n  }\n}");
            // 输出这段代码
            messager.printMessage(Diagnostic.Kind.NOTE, cls.toString());
            try {
                // 通过文件创建器创建SourceFile
                JavaFileObject sourceFile = filer.createSourceFile(className + output);
                Writer writer = sourceFile.openWriter();
                writer.write(cls.toString());
                writer.flush();
                writer.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    
    }
    
    1. 在与java平级目录下创建resources文件夹,并在其目录下创建META-INF文件,接着在META-INF创建services,最后在services文件目录下创建文件javax.annotation.processing.Processor,内容为自定义的Processer的类全名。如下图所示:


      内容.png
    添加Processer.png
    1. 将java文件和resources文件打包成jar,idea可以通过Project Structure-> Artifacts 添加JAR-> From module,选择module并且记得将resources也添加:
    生成jar.png
    1. 这一步很关键,我们创建了编译时注解的代码的jar,我们需要告诉ide在编译的时候执行我这个jar,所以需要将其添加到Compiler的Annotation Processors中:
    添加.png
    1. 将jar导入项目中,并且使用我们刚才创建的注解,这里一定要注意把原项目定义的注解删掉,使用jar中的注解,不然不会生成代码:

      添加注解.png
    2. 编译,可以看到Messages中有我们打印的日志。编译成功可以在build/classes/main/目录下找到自定义注解生成的class文件,同时在generated目录下还有java文件的生成:

    打印了message.png 生成的文件.png

    OK,编译时注解这里基本上就讲完了。Android好多优秀的框架都使用的编译时注解框架,例如Android的Databinding框架,butterknife等等。

    2.2 运行时注解

    运行时注解就比较简单了,下面还是看下例子:

    @Target({ElementType.TYPE})
    @Retention(RetentionPolicy.RUNTIME)
    public @interface TestAnnotation {
        String value() default "";
    }
    
    @TestAnnotation("123456789")
    public class Main {
        public static void main(String[] args) {
            // 获得类的TestAnnotation注解
            TestAnnotation annotation = Main.class.getAnnotation(TestAnnotation.class);
            if (annotation != null) {
                // 将值输出
                System.out.println(annotation.value());
            }
        }
    }
    

    运行输出:

    123456789
    

    好像没什么可以说的,基于运行时注解的框架也不少,例如EventBus、Retrofit等等。

    The end

    写完收工,关于代理和注解就写这么多吧。希望能在年前将Retrofit的分析写完,加油了。


    相关文章

      网友评论

        本文标题:Java的代理和注解

        本文链接:https://www.haomeiwen.com/subject/mbkazxtx.html