自定义注解(annotation)和注解处理器(annotation processor)

1. 什么是注解

注解(annotation)以一种形式化的方式在代码中添加信息,如我们最常见的@Overide,表明当前的方法将覆盖超类中的方法。
由于注解是与源代码结合在一起,并不是像文档一样脱离代码,所以这使得编译器来测试和验证,并存储额外的信息。
另外呢,通过注解处理器我们可以生成一些描述符文件或者新的类的定义,有助于在项目中减少重复样板代码的编写。
除此之外,注解使得代码更加简洁易读以及在编译器进行类型检查等特点。

通过以上的介绍,肯定迫不及待的想进一步了解注解了吧。

2. 注解的基本知识

在我们自定义注解之前,先要了解这四种注解,这四种注解又称为元注解,主要作用就是负责创建新的注解。这四种注解分别为@Target, @Retention,@Documented,@Inherited

2. 1 @Target

@Target: 表示该注解可以用于什么地方。

可选参数 说明
ElementType.CONSTRUCTOR 构造函数的声明
ElementType.FIELD 成名变量的声明(包含enum)
LOCAL_VARIABLE 局部变量的声明
METHOD 方法的声明
PACKAGE 包声明
PARAMETER 参数声明
TYPE 类、接口(包括注解类型) 或enum声明

像我们熟知的@Override的定义:

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.SOURCE)
public @interface Override {
}

就是使用的是ElementType.METHOD, 我们看一下@Target

@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.ANNOTATION_TYPE)
public @interface Target {
    /**
     * Returns an array of the kinds of elements an annotation type
     * can be applied to.
     * @return an array of the kinds of elements an annotation type
     * can be applied to
     */
    ElementType[] value();
}

我们在定义@Target(ElementType.METHOD),其实是一种简写,真正的写法@Target(value = ElementType.METHOD. 原因是因为Target类中有一个value()元素,如果定义成别的变量,则必须要写全了。

2.2 @Retention

@Retention: 表示该注解信息保存到什么级别

可选参数 说明
RetentionPolicy.SOURCE 注解将被编译器丢弃
RetentionPolicy.CLASS 注解在class文件中可用, 但会被VM丢弃
RetentionPolicy.RUNTIME VM将运行期也保留注解信息,因此可用通过反射机制来读取注解的信息

可见这三种策略对应的生命周期文件为:

java源文件 -> .class -> 内存字节码

对于这三种保存策略,我们可以分别用在这三种场景下:

@Retention(RetentionPolicy.SOURCE) : 比如@Override只对一个方法进行检查,或者定一个枚举变量来表明这个变量的取值范围。

@Keep
@Retention(RetentionPolicy.SOURCE)
@IntDef({INIT, PLAYING, STOP, PAUSE})
public @interface PlayState {
    int INIT = 0;
    int PLAYING = 1;
    int STOP = 2;
    int PAUSE = 3;
}

比如定义一个音乐播放器的状态只能为这四种情况,我们可以定义保存级别为RetentionPolicy.SOURCE. 因为该注解只是用来在进行一个状态的检查,并不需要处理器的。

@Retention(RetentionPolicy.CLASS): 编译时注解,比如我们想在编译时期生成一些页面的路由信息,像Arouter库,注解在class文件中可用, 这也是默认的注解生命周期。这种情况最多的就是使用apt 工具实现注解处理器,在编译器期间生成相应的代码。当然如果只是仅仅本地用,直接使用RetentionPolicy.SOURCE即可,但往往有的时候需要以库的方式(.jar, .aar)的提供给其它人使用,这时候就需要在.class文件中,也需要能够访问这些注解,所以对于编译时注解我们用这种生命周期。

Arouter库中的Route注解:

@Target({ElementType.TYPE})
@Retention(RetentionPolicy.CLASS)
public @interface Route {

    /**
     * Path of route
     */
    String path();

    /**
     * Used to merger routes, the group name MUST BE USE THE COMMON WORDS !!!
     */
    String group() default "";

    /**
     * Name of route, used to generate javadoc.
     */
    String name() default "undefined";

    /**
     * Extra data, can be set by user.
     * Ps. U should use the integer num sign the switch, by bits. 10001010101010
     */
    int extras() default Integer.MIN_VALUE;

    /**
     * The priority of route.
     */
    int priority() default -1;
}

@Retention(RetentionPolicy.RUNTIME): 运行时注解,这个就不难理解了,这是为了在运行期间能够以反射的方式获取类或方法,字段,然后找到注解。如weex sdk中自定义module模块下的@JsMethod这个注解

@Retention(RetentionPolicy.RUNTIME)
@Inherited
@Target(ElementType.METHOD)
public @interface JSMethod {
  boolean uiThread() default true;

  String alias() default NOT_SET;

  String NOT_SET = "_";
}
private void generateMethodMap() {
   ...
   HashMap<String, Invoker> methodMap = new HashMap<>();
   try {
     for (Method method : mClazz.getMethods()) {
       for (Annotation anno : method.getDeclaredAnnotations()) {
         if (anno != null) {
           if(anno instanceof JSMethod) {
             JSMethod methodAnnotation = (JSMethod) anno;
             String name = JSMethod.NOT_SET.equals(methodAnnotation.alias())? method.getName():methodAnnotation.alias();
             methodMap.put(name, new MethodInvoker(method, methodAnnotation.uiThread()));
             break;
           }else if(anno instanceof WXModuleAnno) {
             WXModuleAnno methodAnnotation = (WXModuleAnno)anno;
             methodMap.put(method.getName(), new MethodInvoker(method,methodAnnotation.runOnUIThread()));
             break;
           }
         }
       }
     }
   } catch (Throwable e) {
     WXLogUtils.e("[WXModuleManager] extractMethodNames:", e);
   }
 }

2.3 @Documented @Inherited

这两个注解就比较好理解了,@Documented的注解会包含在JavaDoc中。而@Inherited表明该注解允许子类继承父类中的注解.

@Inherited
public @interface anno {
}

@anno
public class Parent {
}

public class Child extends Parent {
}

那么类child也是被一个被@anno注解的类。

好了,我们介绍完了创建新注解的四大武器之后,就摩拳擦掌创建自定义注解吧。

3. apt介绍

apt(annotation processing tool) 注解处理工具,就是操作Java源文件,当处理完源文件后编译它们,在系统创建的过程中会自动创建一些新的源文件,这些新文件会在新的一轮中的注解处理器中接受检查,直到不再有新的源文件产生为止。这个过程中是发生在编译期间(compile time),而非运行期间,所有apt产生为自定义注解性能的提高做出了大大的贡献。

3.1 AbstractProcessor

接下来,我们了解一下注解处理器的相关api. 任何一个自定义处理器都需要继承AbstractProcessor.

public class BuilderProcessor extends AbstractProcessor {
    @Override
    public synchronized void init(ProcessingEnvironment processingEvn) {
        super.init(processingEvn);
    }

    @Override
    public Set<String> getSupportedAnnotationTypes() {
    }

    @Override
    public SourceVersion getSupportedSourceVersion() {
        return SourceVersion.latestSupported();
    }

    @Override
    public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment){        return true;
    }
}

这是一个自定义注解处理器基本的代码框架。
init(ProcessingEnvironment processingEnv), 这个方法在整个编译期间仅仅被调用一次,作用就是初始化参数ProcessingEnvironment

public interface ProcessingEnvironment {
    Map<String, String> getOptions();
    
    Messager getMessager();

    Filer getFiler();

    Elements getElementUtils();

    Types getTypeUtils();

    SourceVersion getSourceVersion();

    Locale getLocale();
}

这个接口可以提供这些变量,getOptions 这里的map指的是在编译期间,app传给注解处理器的值,我们这里还拿Arouter库来看:

 @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);
             generateDoc = VALUE_ENABLE.equals(options.get(KEY_GENERATE_DOC_NAME));
        }
        ...
 }
 javaCompileOptions {
           annotationProcessorOptions {
               arguments = [AROUTER_MODULE_NAME: project.getName(), AROUTER_GENERATE_DOC: "enable"]
           }
       }

Messager: 在注解处理器处理注解生成新的源代码过程中,我们可用Messager来将一些错误信息打印到控制台上。

Filer: 我们可以通过这个类来创建新的文件。

Elements: 它其实是一个工具类,用来处理所有的Element 元素,而我们可以把生成代码的类中所有的元素都可以成为Element 元素,如包就是PackageElement, 类/接口为TypeElement, 变量为VariableElement, 方法为ExecutableElement

Type: 它其实也是一个工具类,只是用来处理TypeMirror. 也就是一个类的父类。TypeMirror superClassType = currentClass.getSuperclass();

process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment)这个方法是最重要的,注解处理器新生成出来的类就是在这个方法生成的。之前我们说过ProcessingEnvironment是包含了注解处理器相关的工具类和编译器配置的参数,而RoundEnvironment则是指在每一轮的扫描和处理源代码中获取被注解的Element.为了方便后面例子的叙述,我们这里再先了解一些api.

  • 获取被注解的Element
List<? extends Element>  elements = roundEnv.getElementsAnnotatedWith(Builder.class)
  • 判断某个Element的类型
element.getKind() == ElementKind.CLASS
  • 获取父类
TypeMirror superClassType = typeElement.getSuperclass();
typeElement = (TypeElement) types.asElement(superClassType);
  • 获取类中内部元素
List<? extends Element> closedElements = typeElement.getEnclosedElements()

等等。

4. 自定义build注解

我们在项目中往往喜欢写xxxBuilder.setXXX().setXXX().build()方法来构造一个对象。比如我们有一个Student类。

public class Student {
   public String mName;
   public int mAge;
   public int mGender;
   public int mEnglishScore;
   public int mMathScore;
}

我们要为它写一个Builder类,类似于这样:

public class StudentBuilder {
   private String mName;
   private int mAge;
   private int mGender;
   private int mEnglishScore;
   private int mMathScore;

   public StudentBuilder setName(String name) {
       mName = name;
       return this;
   }

   public StudentBuilder setAge(int age) {
       mAge = age;
       return this;
   }

   public StudentBuilder setGender(int gender) {
       mGender = gender;
       return this;
   }

   public StudentBuilder setEnglishScore(int englishScore) {
       mEnglishScore = englishScore;
       return this;
   }

   public StudentBuilder setMathScore(int mathScore) {
       mMathScore = mathScore;
       return this;
   }

   public Student build() {
       Student student = new Student();
       student.mName = mName;
       student.mAge = mAge;
       student.mGender = mGender;
       student.mEnglishScore = mEnglishScore;
       student.mMathScore = mMathScore;
       return student;
   }

这样的代码经常格式是一样的,我们是否可以用自定义注解来自动帮我们生成这些样板代码呢,答案是可以的,那我们开始吧。
首先定义两个注解,一个注解表示哪个类需要构建,一个注解表示这个类哪些字段可以通过setXXX方法来设置。

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.SOURCE)

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

public @interface BuilderField {
    String value();
}

接下来我们定一个被注解的类:

@Builder("AptStudent")
public class AptStudent {

    @BuilderField("name")
    public String name;

    @BuilderField("age")
    public int age;

    @BuilderField("gender")
    public int gender;

    @BuilderField("englishScore")
    public int englishScore;

    @BuilderField("mathScore")
    public int mathScore;
}

接下来我们定义一个注解处理器:

 @Override
    public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment) {
        try {
            Set<? extends Element> elements = roundEnvironment.getElementsAnnotatedWith(Builder .class);
            for (Element element : elements) {
                if (!ValidCheckUtil.isValidClass(element)) {
                    continue;
                }

                List<? extends Element> memberFields = mElementUtils.getAllMembers((TypeElement)
                        element);
                List<VariableElement> annoFields = new ArrayList<>();

                if (memberFields == null) {
                    return false;
                }

                for (Element member : memberFields) {
                    if (ValidCheckUtil.isValidField(member)) {
                        annoFields.add((VariableElement) member);
                    }
                }

                mCodeGenerator.generatorCode((TypeElement) element, annoFields);
            }

            return true;
        } catch (Exception e) {
            e.printStackTrace();
        }

        return true;

    }

这里,我们主要看一下process方法,其它的代码具体见文末的链接地址。这里我们是遍历出用BuilderBuilderField注解的类和变量,然后用工具类BuilderCodeGenerator来生成代码,接下来我们看一下BuilderCodeGenerator这个类。

private MethodSpec generateBuildMethodCode(TypeElement typeElement, List<VariableElement> fields) {
        String methodName = "build";
        TypeName typeName = TypeName.get(typeElement.asType());

        MethodSpec.Builder methodBuilder = MethodSpec.methodBuilder(methodName)
                .addModifiers(Modifier.PUBLIC)
                .returns(typeName);

        String code = "{0} obj = new {1}();\n";
        methodBuilder.addCode(MessageFormat.format(code, typeName, typeName));

        for (VariableElement field : fields) {
            BuilderField fieldAnno = field.getAnnotation(BuilderField.class);
            String codeTxt = "obj.{0} = {1};\n";
            methodBuilder.addCode(MessageFormat.format(codeTxt, fieldAnno.value(), fieldAnno.value()));
        }

        methodBuilder.addStatement("return obj");
        mBuildMethodSpec = methodBuilder.build();
        return mBuildMethodSpec;
    }

这里,我们就看build()这个方法是如何生成的。生成代码我们用的是JavaPoet这个库。可以很方便的生成代码。生成一个类就是构建一个TypeSpec,生成一个方法就是构建一个MethodSpec,生成一个变量就是用FieldSpec. 我们可以直接使用addStatement()来生成一段代码,也可以使用`addCode`来生成,addCode里面我们要注意里面的几个通配符。

 * $T: 类型替换 addStatement("$T student", Student.class)  => Student student
 * $L: 字面量替换 addStatement("$L = null", "student") => student = null
 * $S: 字符串 addStatement("student = new Student($S)", "amy") => student = new Student("amy")
 * $N: 名称替换 MethodSpec methodSpec = MethodSpec.methodBuilder("Student").build();
 * ("$N", methodSpec) => Student.

好了,这样我们的自定义注解就讲完了,具体的代码细节还请看地址:
https://github.com/thh0613/BuildPatternDemo.git

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

推荐阅读更多精彩内容