定义注解的说明和实现示例

1,067 阅读5分钟

前言

定义注解的目的一般有两种,本文中前言后面的部分是对第二种进行说明。

  • 第一种:安卓注解使用介绍说明的 枚举定义注解,用于数据的类型和范围检查,多工作在代码源码编辑阶段,是IDE提供的功功能。

  • 第二种:实现数据注入和代码生成,在开发过程中起到减少样板代码,优化可读性,帮助提高工作效率等作用。

注解库的结构

一个注解工具,一般分为两个模块:注解定义模块和注解处理器模块。以ButterKnife为例:

  • 添加注解定义模块
implementation 'com.jakewharton:butterknife:8.4.0'
  • 添加注解处理器模块 注解处理器一般使用 annotationProcessor 或者 kapt 方式添加。
kapt 'com.jakewharton:butterknife:8.4.0'                    // 支持kotlin源码
annotationProcessor 'com.jakewharton:butterknife:8.4.0'     // 支持java源码

自定义注解实现

注解示例的作用是实现数据注入。

注解定义

所有的注解都默认继承自java.lang.annotation.Annotation。注解成员的类型限定:8中基本数据类型,String,Class,Annotation及子类,枚举。

定义注解时可以声明0..N个成员,例如下面的定义,可以用default为成员指定默认值;成员名称可以按照程序语言的变量命名规则设定。

// Target指定 StringIntentKey 的作用范围。
@Target(ElementType.FIELD)
// 如果用动态注解处理,需要指定Retention为RUNTIME
@Retention(RetentionPolicy.RUNTIME)
public @interface StringIntentKey {
    String value() default "";
}

注解调用

引入注解定义后,就可以调用了。在使用时需要指定参数名,当成员只有一个且命名为value时,可省略。

@StringAnnotation(value = "data"),

@StringIntentKey("dynamic_data")
String dynamicData;
@StringIntentKey("static_data")
String staticData;

注解处理

注解真正起作用实现数据注入和代码生成,需要注解处理器对注解位置进行识别和处理。注解处理器分为两种:动态处理器和静态处理器。

  • 动态注解处理,使用反射技术,通过对成员变量、方法等进行扫描,识别到注解调用后进行赋值操作。所谓动态,是指操作是在运行时动态地扫描代码,所以是要有性能消耗的。
public class DynamicUtil {
    public static void inject(Activity activity) {
        Intent intent = activity.getIntent();
        // 反射
        for (Field field : activity.getClass().getDeclaredFields()) {
            if (field.isAnnotationPresent(StringIntentKey.class)) {
                // 获取注解
                StringIntentKey annotation = field.getAnnotation(StringIntentKey.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();
                }
            }
        }
    }
  • 静态注解处理,是相对于动态注解处理而言。对注解的识别和处理是在编译阶段完成,不需要在的运行时进行代码扫描,因此可以节省运行时的性能消耗,但是会增加编译时间。

静态注解处理,需要在编译阶段扫描源码文件,生成新的源代码文件,放在下面一节 代码生成 单独说明。

代码生成

静态注解出现在动态注解之后,并取代动态注解。静态注解相对于动态注解,把注解的解释过程放在编译阶段,在运行时不再需要解释,而是直接使用编译的结果。因此,编译阶段需要使用相应的工具生成所需的代码。

  1. javapoet:用来生成 Java 代码的一个 Java Library,参考文章
  2. auto-service:github.com/google/auto…

本例中为每个使用了注解的类生成一个对应的Binder类, 并提供统一的入口 StaticMapper#bind 实现数据注入,需要做的只是在初始化时调用一下这个方法。

public final class MainActivity$Binder {
    public static final void bind(MainActivity activity) {
        Intent intent = activity.getIntent();
        if (intent.hasExtra("static_data")) {
           activity.staticData = (String) intent.getSerializableExtra("static_data");
        }
    }
}
public final class StaticMapper {
    public static final void bind(Activity activity) {
        if (activity instanceof MainActivity) {
           MainActivity$Binder binder = new MainActivity$Binder();
           binder.bind((com.campusboy.annotationtest.MainActivity) activity);
        } else if (activity instanceof Main2Activity) {
           Main2Activity$Binder binder = new Main2Activity$Binder();
           binder.bind((com.campusboy.annotationtest.Main2Activity) activity);
        }}
}
  • 注解解释器需要继承自AbstractProcessor基类,并使用@AutoService(Processor.class)声明这个类是一个注解处理器。

import com.google.auto.service.AutoService;
import javax.annotation.processing.AbstractProcessor;
import javax.annotation.processing.Processor;

@AutoService(Processor.class)
public class StaticIntentProcessor extends AbstractProcessor {
}

public abstract class AbstractProcessor implements Processor {
}

  • 注解处理器基类AbstractProcessor实现自Processor接口,其中init()和getSupportedOptions()在抽象类AbstractProcessor给出了实现,StaticIntentProcessor的主体功能是实现process()方法,完成类生成。
public interface Processor {
    Set<String> getSupportedOptions();
    // 支持的注解类的类名集合
    Set<String> getSupportedAnnotationTypes();
    // 支持的Java版本
    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);
}
  • 下面是本示例中的静态注解处理过程,识别到注解调用,并生成对应的代码文件。不再需要运行时通过反射获得。
@AutoService(Processor.class)
public class StaticIntentProcessor 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() {
        // 只处理 StringIntentKey 注解
        return Collections.singleton(StringIntentKey.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);

        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((" + activityName + ") activity);\n", activityName, activityName);
            method.addCode("}");
        }
        // 创建StaticMapper类
        createJavaFile("com.campusboy.annotationtest", "StaticMapper", method.build());

        return false;
    }

    private List<InjectDesc> findInjectDesc(Set<? extends TypeElement> set, RoundEnvironment re) {

        Map<TypeElement, List<String[]>> targetClassMap = new HashMap<>();

        // 先获取所有被StringIntentKey标示的元素
        Set<? extends Element> elements = re.getElementsAnnotatedWith(StringIntentKey.class);
        for (Element element : elements) {
            // 只关心类别是属性的元素
            if (element.getKind() != ElementKind.FIELD) {
                processingEnv.getMessager().printMessage(Diagnostic.Kind.ERROR, "only support field");
                continue;
            }

            // 此处找到的是类的描述类型
            // 因为我们的StringIntentKey的注解描述是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(StringIntentKey.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;
    }

    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();
        }
    }

    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 TypeName typeName(String className) {
        return className(className).withoutAnnotations();
    }

    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);
    }

    private static class InjectDesc {
        private String activityName;
        private String[] fieldNames;
        private String[] fieldTypeNames;
        private String[] intentNames;

        @Override
        public String toString() {
            return "InjectDesc{" +
                    "activityName='" + activityName + '\'' +
                    ", fieldNames=" + Arrays.toString(fieldNames) +
                    ", intentNames=" + Arrays.toString(intentNames) +
                    '}';
        }
    }
}

数据注入类聚合

本例中在APP模块中生成了一个StaticMapper,但是如果是多模块工程,会生成多个 StaticMapper 类文件,造成编译异常,无法完成编译过程。

本文给出的技术方案来源于# Arouter框架分析 ,对应的两个解决方案在# Arouter框架分析 也会有说明。

生成类命名规则

对于注解处理结果重名的问题比较容易解决,只需要定义一个明确的命名规则,下面给出的代码扫描方案按照既定的命名规则进行处理即可。

  • Arouter给出的方案是在注解处理器中使用ModuleName作为生成类的后缀。
public abstract class BaseProcessor extends AbstractProcessor {

    // Module name, maybe its 'app' or others
    String moduleName = null;

    @Override
    public synchronized void init(ProcessingEnvironment processingEnv) {
        super.init(processingEnv);
        // Attempt to get user configuration [moduleName]
        Map<String, String> options = processingEnv.getOptions();
        if (MapUtils.isNotEmpty(options)) {
            moduleName = options.get(KEY_MODULE_NAME);
        }

        if (StringUtils.isNotEmpty(moduleName)) {
            moduleName = moduleName.replaceAll("[^0-9a-zA-Z_]+", "");
        } else {
            throw new RuntimeException("ARouter::Compiler >>> No module name, for more information, look at gradle log.");
        }
    }
}
  • 要求在模块的gradle中配置 AROUTER_MODULE_NAME
kapt {
    arguments {
        arg("AROUTER_MODULE_NAME", project.getName())
    }
}
  • 生成类的结果
package com.alibaba.android.arouter.routes;

public class ARouter$$Group$$webview implements IRouteGroup {
  @Override
  public void loadInto(Map<String, RouteMeta> atlas) {
  }
}

生成类扫描聚合

解决了生成类重名的问题,就要对在既定规则下生成或者编辑的类进行扫描聚合操作了,目的是要为所有的类给出一个统一的初始化和调用入口。模块化架构工程内的类扫描有两种方式。

两种扫描方式都需要提供一个管理入口类 Manager ,提供一组类处理的入口 Manager.init 方法。详细参考 # [1] Arouter框架分析 中的介绍。

  • 动态扫描 动态扫描和动态注解处理一样,是在运行时利用反射技术,在 Manager.init 中对类文件进行扫描,生成一组类对象。

  • 静态扫描 静态扫描是在编译时利用 字节码插桩技术 , 向 Manager.init 中插入生成一组类对象的语句,在运行时省去扫描过程,加快初始化速度。

示例工程

示例工程:customize-annotation

代码生成库:javaPoet 使用这个库可以更方便地生成代码。