Java注解理解

分类

根据注解的作用时间@Retention可将注解分为三类:

RetentionPolicy.SOURCE

此类注解只在编译过程中保留,不写入Class文件内。所以此类注解中的信息可以被编译器使用,这一过程通常需要借助注解处理器Annotation Processor
编译器触发注解处理器对注解中的信息进行读取并执行相关操作,常见的操作是自动生成代码(如:butterknife),或者时进行额外的类型检查等。

案例演示

需求:

在另一个类中,为类的域自动生成一个同名的域,并赋值为注解中的value值。

说明:

注解名: @Code
例如:

//:Tmp.java
public class Tmp {
   @Code(value= "field1Value")
   public String field1 ;
   @Code(value = "field2Value")
   public String field2;

}

经过编译器处理后,自动生成一个Tmpcode.java文件

//:Tmpcode.java
public class Tmpcode {
private final String field1 = "field1Value";
 private final String field2 = "field2Value";
}
实现:

强烈推荐《Java注解处理器》,考虑到注解处理器中可能使用第三方库并导致某些问题,所以这里将注解和注解处理器分开成两个Module。将注解置于lib_annotations将注解处理器置于lib_compiler中。

注解声明

新建java lib Module,并命名为lib_annotations,创建Code.java声明注解;

//:Code.java
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.SOURCE)
public @interface Code {
    String value() default "";
}

注解使用

public class Tmp {
    @Code(value = "field1Value")
    public String field1 ;
    @Code(value = "field2Value")
    public String field2;
}

注解处理器

在正式开始书写注解处理前,需要在lib_compiler引入两个依赖包:

	compile 'com.google.auto.service:auto-service:1.0-rc2'
    compile 'com.squareup:javapoet:1.9.0'
  • com.google.auto.service:auto-service:1.0-rc2

此依赖下含有注解@AutoService,当使用@AutoService(Processor.class)注解自定义的注解处理器时,编译时自动在META-INF/services下生成javax.annotation.processing.Processor文件,文件中含有所有自定义注解处理器的全名,供外部程序装载jar包时寻找处理器的具体实现类,完成装载,当然也可以手动建立文件结构, 可参考这里#注册你的处理器

  • com.squareup:javapoet:1.9.0

编写.java文件的工具类,如果不使用此工具类,也可手动使用Filer中提供的方法书写或者使用PrintWriter,具体可参考《JAVA编程思想》(此书中涉及的两个构建处理器的类均已淘汰,现在使用AbstractProcessor即可)。

自定义注解处理器的骨架通常如下:

//
@AutoService(Processor.class)
public class RandomProcessor extends AbstractProcessor{

    @Override
    public synchronized void init(ProcessingEnvironment processingEnvironment) {
        super.init(processingEnvironment);
    }

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

    @Override
    public Set<String> getSupportedAnnotationTypes() {
        return super.getSupportedAnnotationTypes();
    }

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

  • init(ProcessingEnvironment env): 每一个注解处理器类都必须有一个空的构造函数。然而,这里有一个特殊的init()方法,它会被注解处理工具调用,并通过传入的ProcessingEnviroment参数可获取TypesElementsFiler
    Messager
  • getSupportedSourceVersion():用来指定你使用的Java版本。通常这里返回SourceVersion.latestSupported()
  • getSupportedAnnotationTypes():返回需要此注解处理器处理的注解类全面(Class#getSimpleName())的Set集合。
  • process(Set<? extends TypeElement> annotations, RoundEnvironment env): 这相当于每个处理器的主函数main()。你在这里写你的扫描、评估和处理注解的代码,以及生成Java文件。输入参数RoundEnviroment,可以让你查询出包含你需要的注解条目。

ProcessingEnvironment中获得工具,指定java版本,指定需要处理的注解后Processor应该是这种的:

//:CodeAnnotationProcessor.java
@AutoService(Processor.class)
public class CodeAnnotationProcessor extends AbstractProcessor{
    private Types typeUtils;
    private Elements elementUtils;
    private Filer filer;
    private Messager messager;
    private  Map<String, CodeAnnotatedFieldGroup> codeGroups;

    @Override
    public synchronized void init(ProcessingEnvironment processingEnvironment) {
        super.init(processingEnvironment);
        typeUtils = processingEnvironment.getTypeUtils();
        elementUtils = processingEnvironment.getElementUtils();
        filer = processingEnvironment.getFiler();
        messager = processingEnvironment.getMessager();
    }

    @Override
    public Set<String> getSupportedAnnotationTypes() {
        Set<String> annotations = new LinkedHashSet<String>();
        annotations.add(Code.class.getCanonicalName());
        return annotations;
    }

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

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

接下来,便是在process中,从传入的参数中获取每条注解的信息,并将注解中信息写成入新的.java文件中。此时需要考虑到,注解@Code可能并不只在一个.java源文件中有使用,且一个.java文件中也会存在多条@Code注解。我们是基于面向对象编程,所以考虑将每条注解的信息(注解体中的信息和被注解Element的信息)保存在一个自定义的CodeAnnonatedField对象当中,使用另一个自定对象CodeAnnonatedField来管理注解在同一个.java源文件下的所有@Code注解。最后,使用一个Map<String, CodeAnnotatedFieldGroup>来维护所有的CodeAnnotatedFieldGroup,其中StringCodeAnnotatedFieldGroup所对应的被注解的.java源文件的全名(TypeElement.getQualifiedName().toString());

记录注解条目及被注解Element信息的CodeAnnotatedField类如下:

//:CodeAnnotatedField.java

public class CodeAnnotatedField {
    private String name;	//存储被注解域的名字
    private String value;	//存储注解条目中的value值

    public CodeAnnotatedField(Element element){
        Code annotation = element.getAnnotation(Code.class);
        name = element.getSimpleName().toString();
        value = annotation.value();
    }

    public String getName() {
        return name;
    }

    public String getValue() {
        return value;
    }
}

public class CodeAnnotatedFieldGroup {
    private static final String TAG = "CodeAnnotatedFieldGroup";
    private final String SUFFIX = "code";
    private String qualifiedClassName;
    private Map<String, CodeAnnotatedField> itemMap = new LinkedHashMap<String, CodeAnnotatedField>();

    public CodeAnnotatedFieldGroup(String qualifiedClassName) {
        this.qualifiedClassName = qualifiedClassName;
    }

    public void add(CodeAnnotatedField codeAnnotatedField){
        CodeAnnotatedField existing = itemMap.get(codeAnnotatedField.getName());
        if (existing != null){
            throw new IllegalArgumentException(String.format("Error: same id = %s", existing.getName()));
        }
        itemMap.put(codeAnnotatedField.getName(), codeAnnotatedField);
    }

    public void generateCode(Elements elementUtils, Filer filer) throws IOException {
        TypeElement superClass = elementUtils.getTypeElement(qualifiedClassName);
        String codeQualifiedClassName = superClass.getQualifiedName().toString() + SUFFIX;
        System.out.println(codeQualifiedClassName);
        String codeClassSimpleName = superClass.getSimpleName() + SUFFIX;
        PackageElement packageElement = elementUtils.getPackageOf(superClass);
        String packageName = packageElement.isUnnamed()? null: packageElement.getQualifiedName().toString();
        List<FieldSpec> fieldSpecs = new ArrayList<>();
        for (CodeAnnotatedField c : itemMap.values()){
            FieldSpec fieldSpec = FieldSpec.builder(String.class, c.getName())
                    .addModifiers(Modifier.PRIVATE, Modifier.FINAL)
                    .initializer("$S", c.getValue())
                    .build();
            System.out.println(c.getName());
            fieldSpecs.add(fieldSpec);
        }
        TypeSpec codeJava = TypeSpec.classBuilder(codeClassSimpleName)
                .addModifiers(Modifier.PUBLIC)
                .addFields(fieldSpecs)
                .build();
        System.out.println(packageName);
        JavaFile.builder(packageName, codeJava).build().writeTo(filer);
    }
}

最终的Processor如下:

@AutoService(Processor.class)
public class CodeAnnotationProcessor extends AbstractProcessor{
    private Types typeUtils;
    private Elements elementUtils;
    private Filer filer;
    private Messager messager;
    private  Map<String, CodeAnnotatedFieldGroup> codeGroups;

    @Override
    public synchronized void init(ProcessingEnvironment processingEnvironment) {
        super.init(processingEnvironment);
        typeUtils = processingEnvironment.getTypeUtils();
        elementUtils = processingEnvironment.getElementUtils();
        filer = processingEnvironment.getFiler();
        messager = processingEnvironment.getMessager();
    }

    @Override
    public Set<String> getSupportedAnnotationTypes() {
        Set<String> annotations = new LinkedHashSet<String>();
        annotations.add(Code.class.getCanonicalName());
        annotations.add(RandomString.class.getCanonicalName());
        return annotations;
    }

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

    @Override
    public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment) {
        codeGroups = new LinkedHashMap<String, CodeAnnotatedFieldGroup>();
        for (Element annotatedElement: roundEnvironment.getElementsAnnotatedWith(Code.class)){
            if (annotatedElement.getKind() != ElementKind.FIELD){
                throw new IllegalArgumentException("classes can be only annotated field");
            }
            CodeAnnotatedField code = new CodeAnnotatedField(annotatedElement);
            TypeElement typeElement = (TypeElement) annotatedElement.getEnclosingElement();//获取类Element     
            CodeAnnotatedFieldGroup codeGroup = codeGroups.get(typeElement.getQualifiedName().toString() );
            //为每个类单独建立一个CodeAnnotatedFieldGroup来维护其中的所有注解信息
            if (codeGroup == null){
                codeGroup = new CodeAnnotatedFieldGroup(typeElement.getQualifiedName().toString());
                codeGroups.put(typeElement.getQualifiedName().toString(),codeGroup);
            }
            codeGroup.add(code);
        }
        for (CodeAnnotatedFieldGroup cls : codeGroups.values()){
            try {
                cls.generateCode(elementUtils, filer);
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
        return true;
    }
}

运行与调试

运行:重新Build即可。
调试:注解处理器是运行它自己的虚拟机JVM中,javac启动一个完整Java虚拟机来运行注解处理器,所以注解处理器的debug跟普通的代码debug有点不同,需要执行compileDebugJavaWithJavac,在当前工程路径下输入命令:

gradlew --no-daemon -Dorg.gradle.debug=true :app:clean :app:compileDebugJavaWithJavac

并在Edit Configurations中新添加一个远程配置(remote),名字随意,端口为5005。
在这里插入图片描述
然后点击debug按钮,就可以连接上远程调试器进行Annotation的调试了。演示DEMO
中间的校验过程望读者自己深入学习,此文抛砖引玉。

RetentionPolicy.CLASS

此类注解在编译中会保留并会写入class文件中,但并不会被加载到JVM中。所以在运行时无法通过反射获取此类注解的信息,但在编译时,注解是可见的。
这里很重要的一点是编译多个Java文件时的情况:假如要编译A.java源码文件和B.class文件,其中A类依赖B类,并且B类上有些注解希望让A.java编译时能看到,那么B.class里就必须要持有这些注解信息才行。同时我们可能不需要让它在运行时对反射可见(例如说为了减少运行时元数据的大小之类),所以会选择CLASS而不是RUNTIME。–知乎:RednaxelaFX

此注解的使用并不常见,通常用RetentionPolicy.CLASSRentionPolicy.RUNTIME

RetentionPolicy.RUNTIME

由编译器记录在类文件中,并且会在运行时同所在的.class文件一起加载到JVM中,所以通常以反射的形式读取该类注解中的信息。演示DEMO

案例演示

反射输出读取注解信息并不算复杂。

注解声明

//:AnnotationTest.java
@Target({ElementType.METHOD, ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
public @interface AnnotationTest {
    public String name() default "null";
}

注解使用

//:DeclareSpace.java
public class DeclareSpace {
    @AnnotationTest(name = "field1")
    public String field1;
    @AnnotationTest(name = "field2")
    private int field2;

    public int method1() {
        return 0;
    }
    @AnnotationTest(name = "method2")
    private void method2() {
        return;
    }
    @AnnotationTest(name = "method3")
    private String method3(){
        return null;
    }
    @AnnotationTest
    private short method4(){
        return 0;
    }
}

反射处理注解

public class MyProcessor {
    public static void printTestMethod(Class<?> cl){
        for (Method method : cl.getDeclaredMethods()){
            AnnotationTest annotationTest = method.getAnnotation(AnnotationTest.class);
            if (annotationTest != null){
                System.out.println(annotationTest.name());
            }
        }
        for (Field field : cl.getDeclaredFields()){
            AnnotationTest annotationTest = field.getAnnotation(AnnotationTest.class);
            if (annotationTest != null){
                System.out.println(annotationTest.name());
            }
        }
    }

    public static void main(String[] args) {
        printTestMethod(DeclareSpace.class);
    }
}
//:output~
method2
null
method3
field1
field2

鸣谢

此文参考了众多网上优秀资料,但笔者水平有限,只能理解至此。
MENU
Java注解处理器

使用Android注解处理器,解放劳动生产力
java annotation 中 SOURCE 和 CLASS 的区别

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值