美文网首页Java经验总结开发
Android & Java 注解和自定义注解处理器

Android & Java 注解和自定义注解处理器

作者: 费麭 | 来源:发表于2018-03-02 16:39 被阅读318次

    写在前面:本文是实际工作中学习成果,记为笔记

    目录

    1. 背景
    2. 什么是注解
    3. 注解实战:动态注解
    4. 注解实战:静态注解
    5. 注解处理器
    6. 调试注解器
    7. 注解的问题
    8. 总结
    1. 背景

    最近有些时间,突然对注解有些兴趣,很早之前也做过一些关于注解的学习,我的第一篇博客 Sqlla: 数据库操作从未如此简单里面使用到了动态注解技术,是当时学习代理和retrofit时的情况下实现的,后续又迭代了几版。最近的三方库大量使用静态注解的方式,出于学习和纳为己用的目的,打算系统的学习一下,这篇文章就是学习笔记。

    2. 什么是注解

    一切使用@interface声明的类就是注解

    所有的注解继承于Annotation类,好比所有的类继承与Object一样。

    public @interface IntentKey /*extends Annotation*/ {
    }
    

    注解用来标记类,属性,方法,参数,局部变量,包。

    @IntentKey
    private String name;
    

    注解有三大基本属性:

    1. Retention作用域
    2. Target标识目标
    3. Value常量数据值

    完整的注解:可以看出Retention和Target本身也是注解。

    @Retention(RetentionPolicy.SOURCE)
    @Target(ElementType.FIELD)
    public @interface IntentKey {
        String value() default "";
        String intentKey();
    }
    

    Retention的取值:源码级别,jar类级别,运行时级别

    public enum RetentionPolicy {
        SOURCE,
        CLASS,
        RUNTIME
    }
    

    第一个只作用于编译期,打包之后就没有了。比如@Override
    第二个作用于jar包类,打包之后还存在,如Android的@Nullable,方便编译期跨包做lint检查
    第三个作用于运行期,在运行时可通过反射取得这个注解的信息,如retrofit的@GET注解

    @IntentKey(value="abc", intentKey="name")
    private String name;
    @IntentKey(intentKey="name2")
    private String name2;
    

    技巧:当只有一个value的时候,使用时value可以省略。如果有default值,可以不填。

    坑点:

    public @interface Names {
        String[] names1();
        Object[] names2();
    }
    

    如上,names1是合法的,names2是非法的。原因是注解的值必须是常量,支持的类型如下:

    8中基本数据类型,String,Class,Annotation及子类,枚举 以及它们的数组

    3. 注解实战:动态注解

    动态注解,就是运行时注解。

    注解声明:

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

    标记类:

    public class MyActivity extends AppCompatActivity {
    
        // 标记
        @DynamicIntentKey("key_name")
        private String dynamicName;
    
        @Override
        protected void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            setContentView(R.layout.activity_my);
    
            // 注入
            DynamicUtil.inject(this);
    
            // 这里使用值
            Toast.make(this, "name = " + dynamicName, Toast.SHORT).show();
        }
    }
    

    DynamicUtil.inject(this)负责将值注入

    public class DynamicUtil {
    
        public static void inject(Activity activity) {
            Intent intent = activity.getIntent();
            // 反射
            for (Field field : activity.getClass().getDeclaredFields()) {
                if (field.isAnnotationPresent(DynamicIntentKey.class)) {
    
                    // 获取注解
                    DynamicIntentKey annotation = field.getAnnotation(DynamicIntentKey.class);
                    String intentKey = annotation.value();
    
                    // 读取实际的IntentExtra值
                    Serializable serializable = intent.getSerializableExtra(intentKey);
    
                    if (serializable == null) {
    
                        if (field.getType().isAssignableFrom(String.class)) {
                            serializable = "";
                        }
                    }
    
                    try {
                        // 插入值
                        boolean accessible = field.isAccessible();
                        field.setAccessible(true);
                        field.set(activity, serializable);
                        field.setAccessible(accessible);
                    } catch (IllegalAccessException e) {
                        e.printStackTrace();
                    }
                }
            }
        }
    }
    

    可以看到解析动态注解使用的是反射的方式,人们常说,反射会有效率问题,后面会验证是或不是。
    但是我们能看到优点:

    对于使用的地方而言,非常简单了,省去了getXXXExtra的代码,如果这个Intent很多的时候,注解的方式优势就非常明细,添加和删除intent都不需要去改动onCreate的方法,而且inject过程可以放到super类里完成。

    这个注解就特别像butterknife对view的注入,而且早期的butterknife确实是使用的同样的动态注解的方式。可是后来,静态注解出现了,如燎原之火般席卷而来。接下来是静态注解。

    4. 注解实战:静态注解

    静态注解,就非常好理解了,在编译期解释注解,并做一些生成操作。三种作用域都可以用来标示静态注解。
    注解声明:

    @Target(ElementType.FIELD)
    @Retention(RetentionPolicy.SOURCE)
    public @interface StaticIntentKey {
        String value();
    }
    

    标记类:

    public class MyActivity extends AppCompatActivity {
    
        // 标记
        @StaticIntentKey("key_name")
        private String staticName;
    
        @Override
        protected void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            setContentView(R.layout.activity_my);
    
            // 静态注入
            StaticUtil.inject(this);
    
            // 这里使用值
            Toast.make(this, "name = " + staticName, Toast.SHORT).show();
        }
    }
    

    上面为止和动态注入都是一模一样的。
    下面是静态注入的核心实现:

    public class StaticUtil {
        public static void inject(Activity activity) {
            com.alpha.staticinject.StaticMapper.bind(activity);
        }
    }
    

    ???com.alpha.staticinject.StaticMapper是个什么东西?
    这个不是东西,他就是静态注解的核心:代码生成。这个类就是静态注解处理器在编译时期生成的一个辅助类。
    下面两个类就是生成的类:

    public final class StaticMapper {
        public static final void bind(Activity activity) {
            if (activity instanceof IntentActivity) {
                IntentActivity$Binder binder = new IntentActivity$Binder();
                binder.bind((IntentActivity) activity);
            }
        }
    }
    
    public final class IntentActivity$Binder {
        public static final void bind(IntentActivity activity) {
            Intent intent = activity.getIntent();
            if (intent.hasExtra("key_name")) {
                activity.staticName = (String) intent.getSerializableExtra("key_name");
            }
        }
    }
    

    这就是注入值的过程。但是这两个类是怎么样生成的呢?

    5. 注解处理器

    Java在1.7加入了Processor类,用于处理编译时注解。通过这个类我们可以在某些目录下生成某些类,这些类可以辅助我们开发,让我们更关注业务逻辑。

    public interface Processor {
        Set<String> getSupportedOptions();
    
        Set<String> getSupportedAnnotationTypes();
    
        SourceVersion getSupportedSourceVersion();
    
        void init(ProcessingEnvironment var1);
    
        // 处理类在这里完成
        boolean process(Set<? extends TypeElement> var1, RoundEnvironment var2);
    
        Iterable<? extends Completion> getCompletions(Element var1, AnnotationMirror var2, ExecutableElement var3, String var4);
    }
    

    编写一个自定义注解的四条步骤:

    1. 创建一个java library:intent-apt (这是我项目的名字)
    2. 编写一个IntentProcessor继承与Processor的抽象类AbstractProcessor。
    3. 将IntentProcessor作为一种服务放入标记到jar包中的META_INF文件夹中
    4. 在其他项目中,如android项目中使用 annotationProcessor project(':intent-apt')引入依赖

    走一遍:

    1. 创建java library很简单,不细说
    2. 创建IntentProcessor
    public class IntentProcessor extends AbstractProcessor {
        @Override
        public boolean process(Set<? extends TypeElement> set, RoundEnvironment re) {
          // 在这里写你要生成的类
        }
    }
    

    但是这个IntentProcessor现在还没有什么核心内容,下一步我们给他添加一些东西。
    要注解器能好用,我们得先要有注解:
    1>. 提供注解的library


    注解library

    2>. 在intent-apt中引入这个library

    dependencies {
        ...
        implementation project(':intent-key')
    }
    

    3>. 上面描述的静态注解生成过程是如下这样的:
    (1) 获取需要注解的类F,这里是所有有StaticIntentKey标记的类。
    (2) 针对每一个类F,生成同包下的F$Binder类,用于实际的绑定
    (3) 将所有类合起来生成一个StaticMapper的集线器类,用于分发绑定

    完整的类如下:

    public class IntentProcessor extends AbstractProcessor {
    
        private TypeName activityClassName = ClassName.get("android.app", "Activity").withoutAnnotations();
        private TypeName intentClassName = ClassName.get("android.content", "Intent").withoutAnnotations();
    
        @Override
        public SourceVersion getSupportedSourceVersion() {
            // 支持java1.7
            return SourceVersion.RELEASE_7;
        }
    
        @Override
        public Set<String> getSupportedAnnotationTypes() {
            // 只处理 StaticIntentKey 注解
            return Collections.singleton(StaticIntentKey.class.getCanonicalName());
        }
    
        @Override
        public boolean process(Set<? extends TypeElement> set, RoundEnvironment re) {
            // StaticMapper的bind方法
            MethodSpec.Builder method = MethodSpec.methodBuilder("bind")
                    .addModifiers(Modifier.PUBLIC, Modifier.STATIC, Modifier.FINAL)
                    .addParameter(activityClassName, "activity");
    
            // 查找所有的需要注入的类描述
            List<InjectDesc> injectDescs = findInjectDesc(set, re);
    
            // log一下
            System.out.println(injectDescs);
    
            for (int i1 = 0; i1 < injectDescs.size(); i1++) {
                InjectDesc injectDesc = injectDescs.get(i1);
    
                // 创建需要注解的类的Java文件,如上面所述的 IntentActivity$Binder
                TypeName injectedType = createInjectClassFile(injectDesc);
                TypeName activityName = typeName(injectDesc.activityName);
    
                // $T导入类型
                // 生成绑定分发的代码
                method.addCode((i1 == 0 ? "" : " else ") + "if (activity instanceof $T) {\n", activityName);
                method.addCode("\t$T binder = new $T();\n", injectedType, injectedType);
                method.addCode("\tbinder.bind((IntentActivity) activity);\n", activityName, activityName);
                method.addCode("}");
            }
    
            // 创建StaticMapper类
            createJavaFile("com.alpha.staticinject", "StaticMapper", method.build());
    
            return false;
        }
    
        /**
         * 创建Java文件
         * @param pkg            包名
         * @param classShortName 类的简短名,如java.lang.String的简短名就是String
         * @param method         方法列表
         */
        private void createJavaFile(String pkg, String classShortName, MethodSpec... method) {
            TypeSpec.Builder builder = TypeSpec.classBuilder(classShortName)
                    .addModifiers(Modifier.PUBLIC, Modifier.FINAL);
            for (MethodSpec spec : method) {
                builder.addMethod(spec);
            }
            TypeSpec clazzType = builder.build();
    
            try {
                JavaFile javaFile = JavaFile.builder(pkg, clazzType)
                        .addFileComment(" This codes are generated automatically. Do not modify!")
                        .indent("    ")
                        .build();
                // write to file
                javaFile.writeTo(processingEnv.getFiler());
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    
        /**
         * 生成需要注解的类 如 IntentActivity 会生成 IntentActivity$Binder
         *
         * @param injectDesc 注解类的描述信息
         * @return 如IntentActivity$Binder
         */
        private TypeName createInjectClassFile(InjectDesc injectDesc) {
    
            ClassName activityName = className(injectDesc.activityName);
            ClassName injectedClass = ClassName.get(activityName.packageName(), activityName.simpleName() + "$Binder");
    
            MethodSpec.Builder method = MethodSpec.methodBuilder("bind")
                    .addModifiers(Modifier.PUBLIC, Modifier.STATIC, Modifier.FINAL)
                    .addParameter(activityName, "activity");
    
            // $T导入作为类,$N导入作为纯值,$S导入作为字符串
            method.addStatement("$T intent = activity.getIntent()", intentClassName);
            for (int i = 0; i < injectDesc.fieldNames.length; i++) {
                TypeName fieldTypeName = typeName(injectDesc.fieldTypeNames[i]);
                method.addCode("if (intent.hasExtra($S)) {\n", injectDesc.intentNames[i]);
                method.addCode("\tactivity.$N = ($T) intent.getSerializableExtra($S);\n", injectDesc.fieldNames[i], fieldTypeName, injectDesc.intentNames[i]);
                method.addCode("}\n");
            }
    
            // 生成最终的XXX$Binder文件
            createJavaFile(injectedClass.packageName(), injectedClass.simpleName(), method.build());
    
            return injectedClass;
        }
    
        // 查找所有的需要注入的类描述
        private List<InjectDesc> findInjectDesc(Set<? extends TypeElement> set, RoundEnvironment re) {
    
            Map<TypeElement, List<String[]>> targetClassMap = new HashMap<>();
    
            // 先获取所有被StaticIntentKey标示的元素
            Set<? extends Element> elements = re.getElementsAnnotatedWith(StaticIntentKey.class);
            for (Element element : elements) {
                // 只关心类别是属性的元素
                if (element.getKind() != ElementKind.FIELD) {
                    processingEnv.getMessager().printMessage(Diagnostic.Kind.ERROR, "only support field");
                    continue;
                }
    
                // 此处找到的是类的描述类型
                // 因为我们的StaticIntentKey的注解描述是field,所以closingElement元素是类
                TypeElement classType = (TypeElement) element.getEnclosingElement();
    
                System.out.println(classType);
    
                // 对类做缓存,避免重复
                List<String[]> nameList = targetClassMap.get(classType);
                if (nameList == null) {
                    nameList = new ArrayList<>();
                    targetClassMap.put(classType, nameList);
                }
    
                // 被注解的值,如staticName
                String fieldName = element.getSimpleName().toString();
                // 被注解的值的类型,如String,int
                String fieldTypeName = element.asType().toString();
                // 注解本身的值,如key_name
                String intentName = element.getAnnotation(StaticIntentKey.class).value();
    
                String[] names = new String[]{fieldName, fieldTypeName, intentName};
                nameList.add(names);
            }
    
            List<InjectDesc> injectDescList = new ArrayList<>(targetClassMap.size());
            for (Map.Entry<TypeElement, List<String[]>> entry : targetClassMap.entrySet()) {
                String className = entry.getKey().getQualifiedName().toString();
                System.out.println(className);
    
                // 封装成自定义的描述符
                InjectDesc injectDesc = new InjectDesc();
                injectDesc.activityName = className;
                List<String[]> value = entry.getValue();
                injectDesc.fieldNames = new String[value.size()];
                injectDesc.fieldTypeNames = new String[value.size()];
                injectDesc.intentNames = new String[value.size()];
                for (int i = 0; i < value.size(); i++) {
                    String[] names = value.get(i);
                    injectDesc.fieldNames[i] = names[0];
                    injectDesc.fieldTypeNames[i] = names[1];
                    injectDesc.intentNames[i] = names[2];
                }
                injectDescList.add(injectDesc);
            }
    
            return injectDescList;
        }
    
        /**
         * 快速获取一个类的TypeName结构
         *
         * @param className 类完整名,如java.lang.String,也可能是int, boolean
         */
        private TypeName typeName(String className) {
            return className(className).withoutAnnotations();
        }
    
        /**
         * 快速获取一个类的ClassName结构,ClassName是TypeName的子类
         *
         * @param className 类完整名,如java.lang.String,也可能是int, boolean
         */
        private ClassName className(String className) {
    
            // 基础类型描述符
            if (className.indexOf(".") <= 0) {
                switch (className) {
                    case "byte":
                        return ClassName.get("java.lang", "Byte");
                    case "short":
                        return ClassName.get("java.lang", "Short");
                    case "int":
                        return ClassName.get("java.lang", "Integer");
                    case "long":
                        return ClassName.get("java.lang", "Long");
                    case "float":
                        return ClassName.get("java.lang", "Float");
                    case "double":
                        return ClassName.get("java.lang", "Double");
                    case "boolean":
                        return ClassName.get("java.lang", "Boolean");
                    case "char":
                        return ClassName.get("java.lang", "Character");
                    default:
                }
            }
    
            // 手动解析 java.lang.String,分成java.lang的包名和String的类名
            String packageD = className.substring(0, className.lastIndexOf('.'));
            String name = className.substring(className.lastIndexOf('.') + 1);
            return ClassName.get(packageD, name);
        }
    
        /**
         * 需要注解的类的描述信息 如 IntentActivity
         */
        public static class InjectDesc {
            // IntentActivity
            public String activityName;
    
            // {staticName, staticAge}
            public String[] fieldNames;
    
            // {java.lang.String, java.lang.Integer}
            public String[] fieldTypeNames;
    
            // {key_name, key_age}
            public String[] intentNames;
    
            @Override
            public String toString() {
                return "InjectDesc{" +
                        "activityName='" + activityName + '\'' +
                        ", fieldNames=" + Arrays.toString(fieldNames) +
                        ", intentNames=" + Arrays.toString(intentNames) +
                        '}';
            }
        }
    }
    
    

    StringBuilder一行一行写生成的类的代码非常无味,还容易出错,所以上面用到了square公司出品的javapoet, 方便生成类。
    添加javapoet

    dependencies {
        ...
        implementation 'com.squareup:javapoet:1.9.0'
    }
    
    1. 想java系统注册服务,这个服务注册应该见过,在编写自定义Gradle插件的时候也需要这么做。


      注册服务

    javax.annotation.processing.Processor 文件的内容如下:
    com.icourt.intent_apt.IntentProcessor

    实际就是这个IntentProcessor的完整类名。

    当然,这么写很不优雅,不一定记得住,所以google做了一个注解,方便快速的帮我们生成这个文件,只需在IntentProcessor头部注解一下

    // AutoService本身就是一个静态注解,他在build/META-INF文件夹下生成了一个service指定文件
    @AutoService(Processor.class)
    public class IntentProcessor extends AbstractProcessor {
        ...
    }
    

    添加这个注解需要加入依赖

    dependencies {
        // google auto service注解库
        implementation 'com.google.auto.service:auto-service:1.0-rc2'
    }
    

    它生成的文件在build下面


    AutoService

    这两种方法只需要一种就可以,最终会打包到jar里面。

    1. 在需要提供编译时注解的项目(比如app)的build.gradle文件的dependences节点下添加
    dependencies {
        ...
        annotationProcessor project(':intent-apt')
    }
    

    intent-apt项目本身完整的build.gradle

    apply plugin: 'java-library'
    
    dependencies {
        implementation fileTree(dir: 'libs', include: ['*.jar'])
    
        implementation 'com.google.auto.service:auto-service:1.0-rc2'
        implementation 'com.squareup:javapoet:1.9.0'
    
        implementation project(':intent-key')
    }
    
    sourceCompatibility = "1.7"
    targetCompatibility = "1.7"
    

    好,自此,一个完整的静态注解器就完成了,可以马上用来实验。只需要build就可以使用,他会在你的目标项目(如app)的build下生成相关的类:


    生成类的路径
    6. 调试注解器

    有的时候,生成过程出错了,但我不确定是哪一步出错了,想要定位,有两种方法:

    1. 日志定位,方便快捷
    // 1. sout
    System.out.println(xxx)
    // 2. messager
    processingEnv.getMessager().printMessage(Diagnostic.Kind.ERROR, "only support field");
    
    1. 断点调试

    有三步要配置:
    第一步 配置Debug后台服务
    在gradle.properties文件中加入下面两句话,然后sync一下项目(或者在控制台执行./gradlew --daemon),会开启一个远程debug_server

    org.gradle.daemon=true
    org.gradle.jvmargs=-agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=5005
    

    第二步 配置Remote Debugger
    在AS中创建一个RemoteDebugger




    属性和参数和上一步的配置一样,(不修改就行) 开启debug

    切换到刚才创建的'运行配置',点击debug按钮。

    第三步 执行编译过程

    在需要的位置打开断点,在控制台输入

    ./gradlew assembleDebug
    

    或者,带清除功能的编译

    ./gradlew clean assembleDebug
    

    不出意外就会走到打开的断点,然后尽情的调试。

    7. 注解的问题

    (1) 性能问题:
    1000次运行的效果对比

    DynamicUtil.inject(this);
    StaticUtil.inject(this);
    

    (2) 代码量:
    注解对于业务逻辑的代码整体减少了,但是静态注解会产生 生成代码,这些代码会占用方法数,也增加编译时间。

    (3) 错误定位:
    注解的错误定位会很麻烦,如果注解处理器出了bug,较难定位。

    (4) 阅读性:
    注解会整体影响阅读性,出现逻辑断层。

    8. 总结

    本文围绕两点展开:

    1. 使用注解(注)
    2. 编写注解(解)

    使用注解非常有优势,让开发人员优化代码,关注业务。但是编写注解处理器本身是个技术活,好的处理器对使用者而言会事半功倍,但也不要滥用,做好错误处理。


    顺便给公司打个广告 (点击链接查看岗位描述,加微信细聊)

    要想走的快,一个人走;要想走的远,一群人走

    新橙科技

    相关文章

      网友评论

      本文标题:Android & Java 注解和自定义注解处理器

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