Java-注解

264 阅读10分钟

注解基础

注解是JDK1.5版本开始引入的一个特性,用于对代码进行说明,可以对包、类、接口、字段、方法参数、局部变量等进行注解。它主要的作用有以下四方面:

  • 生成文档,通过代码里标识的元数据生成javadoc文档。
  • 编译检查,通过代码里标识的元数据让编译器在编译期间进行检查验证。
  • 编译时动态处理,编译时通过代码里标识的元数据动态处理,例如动态生成代码。
  • 运行时动态处理,运行时通过代码里标识的元数据动态处理,例如使用反射注入实例。

1、内置注解

1.1、@Override

表示当前的方法定义将覆盖父类中的方法。

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

从它的定义我们可以看到,这个注解可以被用来修饰方法,并且它只在编译时有效,在编译后的class文件中便不再存在。这个注解的作用我们大家都不陌生,那就是告诉编译器被修饰的方法是重写的父类的中的相同签名的方法,编译器会对此做出检查,若发现父类中不存在这个方法或是存在的方法签名不同,则会报错。

1.2、@Deprecated

表示代码被弃用,如果使用了被@Deprecated注解的代码则编译器将发出警告。

@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.CONSTRUCTOR, ElementType.FIELD, ElementType.LOCAL_VARIABLE, ElementType.METHOD, ElementType.PACKAGE, ElementType.PARAMETER, ElementType.TYPE})
public @interface Deprecated {
}

从它的定义我们可以知道,它会被文档化,能够保留到运行时,能够修饰构造方法、属性、局部变量、方法、包、参数、类型。这个注解的作用是告诉编译器被修饰的程序元素已被“废弃”,不再建议用户使用。

1.3、@SuppressWarnings

表示关闭编译器警告信息。

@Target({ElementType.TYPE, ElementType.FIELD, ElementType.METHOD, ElementType.PARAMETER, ElementType.CONSTRUCTOR, ElementType.LOCAL_VARIABLE})
@Retention(RetentionPolicy.SOURCE)
public @interface SuppressWarnings {
    String[] value();
}

它能够修饰的程序元素包括类型、属性、方法、参数、构造器、局部变量,只能存活在源码时,取值为String[]。它的作用是告诉编译器忽略指定的警告信息,它可以取的值如下所示:

2、元注解

2.1、@Target

@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.ANNOTATION_TYPE})
public @interface Target {
    ElementType[] value();
}

Target注解用来说明那些被它所注解的注解类可修饰的对象范围:注解可以用于修饰 packages、types(类、接口、枚举、注解类)、类成员(方法、构造方法、成员变量、枚举值)、方法参数和本地变量(如循环变量、catch参数),在定义注解类时使用了@Target 能够更加清晰的知道它能够被用来修饰哪些对象,它的取值范围定义在ElementType 枚举中

public enum ElementType {
    TYPE,                             //类、接口、枚举类
    FIELD,                            //成员变量(包括枚举变量)
    METHOD,                           //成员方法
    PARAMETER,                        //方法参数
    CONSTRUCTOR,                      //构造方法
    LOCAL_VARIABLE,                   //局部变量
    ANNOTATION_TYPE,                  //注解类
    PACKAGE,                          //修饰包
    TYPE_PARAMETER,                   //类型参数
    TYPE_USE,                         //使用类型的任何地方
    MODULE;

    private ElementType() {
    }
}

2.2、@Retention & @RetentionTarget

@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.ANNOTATION_TYPE})
public @interface Retention {
    RetentionPolicy value();
}

Reteniton注解用来限定那些被它所注解的注解类在注解到其他类上以后,可被保留到何时,一共有三种策略,定义在RetentionPolicy枚举中。

public enum RetentionPolicy {
    SOURCE,    //注解只保留在源文件,当Java文件编译成class文件的时候,注解被遗弃
    CLASS,     //注解被保留到class文件,但jvm加载class文件时候被遗弃,这是默认的生命周期
    RUNTIME;   //注解不仅被保存到class文件中,jvm加载class文件之后,仍然存在

    private RetentionPolicy() {
    }
}

2.3、@Documented

描述在使用 javadoc 工具为类生成帮助文档时是否要保留其注解信息。

@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.ANNOTATION_TYPE})
public @interface Documented {
}

2.4、@Inherited

被它修饰的Annotation将具有继承性。如果某个类使用了被@Inherited修饰的Annotation,则其子类将自动具有该注解。

@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.ANNOTATION_TYPE})
public @interface Inherited {
}

2.5、@Repeatable

允许在同一申明类型(类,属性,或方法)的多次使用同一个注解。

@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.ANNOTATION_TYPE})
public @interface Repeatable {
    Class<? extends Annotation> value();
}

2.6、@Native (Java8)

注解修饰成员变量,则表示这个变量可以被本地代码引用,常常被代码生成工具使用。

@Documented
@Retention(RetentionPolicy.SOURCE)
@Target({ElementType.FIELD})
public @interface Native {
}

注解使用

1、运行时注解的使用

定义注解后,如何获取注解中的内容呢?反射包java.lang.reflect下的AnnotatedElement接口提供这些方法。这里注意:只有注解被定义为RUNTIME后,该注解才能是运行时可见,当class文件被装载时被保存在class文件中的Annotation才会被虚拟机读取。

AnnotatedElement 接口是所有程序元素(Class、Method和Constructor)的父接口,所以程序通过反射获取了某个类的AnnotatedElement对象之后,程序就可以调用该对象的方法来访问Annotation信息。我们看下具体的先关接口

image.png

public interface AnnotatedElement {
    //判断该程序元素上是否包含指定类型的注解,存在则返回true,否则返回false。注意:此方法会忽略注解对应的注解容器。
    default boolean isAnnotationPresent(@RecentlyNonNull Class<? extends Annotation> annotationClass) {
        throw new RuntimeException("Stub!");
    }

    //返回该程序元素上存在的、指定类型的注解,如果该类型注解不存在,则返回null。
    @RecentlyNullable
    <T extends Annotation> T getAnnotation(@RecentlyNonNull Class<T> var1);

    //返回该程序元素上存在的所有注解,若没有注解,返回长度为0的数组。
    @NonNull
    Annotation[] getAnnotations();



    //返回该程序元素上存在的、指定类型的注解数组。没有注解对应类型的注解时,返回长度为0的数组。该方法的调用者可以随意修改返回的数组,而不会对其他调用者返回的数组产生任何影响。`getAnnotationsByType`方法与 `getAnnotation`的区别在于,`getAnnotationsByType`会检测注解对应的重复注解容器。若程序元素为类,当前类上找不到注解,且该注解为可继承的,则会去父类上检测对应的注解。
    default <T extends Annotation> T[] getAnnotationsByType(@RecentlyNonNull Class<T> annotationClass) {
        throw new RuntimeException("Stub!");
    }
    
    //返回直接存在于此元素上的所有注解。与此接口中的其他方法不同,该方法将忽略继承的注释。如果没有注释直接存在于此元素上,则返回null
    @RecentlyNullable
    default <T extends Annotation> T getDeclaredAnnotation(@RecentlyNonNull Class<T> annotationClass) {
        throw new RuntimeException("Stub!");
    }
    
    //返回直接存在于此元素上的所有注解。与此接口中的其他方法不同,该方法将忽略继承的注释
    default <T extends Annotation> T[] getDeclaredAnnotationsByType(@RecentlyNonNull Class<T> annotationClass) {
        throw new RuntimeException("Stub!");
    }
    //返回直接存在于此元素上的所有注解及注解对应的重复注解容器。与此接口中的其他方法不同,该方法将忽略继承的注解。如果没有注释直接存在于此元素上,则返回长度为零的一个数组。该方法的调用者可以随意修改返回的数组,而不会对其他调用者返回的数组产生任何影响。
    @NonNull
    Annotation[] getDeclaredAnnotations();
}

2、编译时注解的使用

2.1、APT

APT即为Annotation Processing Tool,它是javac的一个工具,中文意思为编译时注解处理器。APT可以用来在编译时扫描和处理注解。通过APT可以获取到注解和被注解对象的相关信息,在拿到这些信息后我们可以根据需求来自动的生成一些代码,省去了手动编写。注意,获取注解及生成代码都是在代码编译时候完成的,相比反射在运行时处理注解大大提高了程序性能。

APT技术被广泛的运用在Java框架中,包括Android项以及Java后台项目,像ButterKnife、EventBus 、Dagger2以及阿里的ARouter路由框架等都运用到APT技术。

AbstractProcessor

public abstract class AbstractProcessor implements Processor {
    protected ProcessingEnvironment processingEnv;
    private boolean initialized = false;

    protected AbstractProcessor() {
    }

    public Set<String> getSupportedOptions() {
        SupportedOptions so = (SupportedOptions)this.getClass().getAnnotation(SupportedOptions.class);
        return so == null ? Collections.emptySet() : arrayToSet(so.value(), false);
    }

    public Set<String> getSupportedAnnotationTypes() {
        SupportedAnnotationTypes sat = (SupportedAnnotationTypes)this.getClass().getAnnotation(SupportedAnnotationTypes.class);
        boolean initialized = this.isInitialized();
        if (sat == null) {
            if (initialized) {
                this.processingEnv.getMessager().printMessage(Kind.WARNING, "No SupportedAnnotationTypes annotation found on " + this.getClass().getName() + ", returning an empty set.");
            }

            return Collections.emptySet();
        } else {
            boolean stripModulePrefixes = initialized && this.processingEnv.getSourceVersion().compareTo(SourceVersion.RELEASE_8) <= 0;
            return arrayToSet(sat.value(), stripModulePrefixes);
        }
    }

    public SourceVersion getSupportedSourceVersion() {
        SupportedSourceVersion ssv = (SupportedSourceVersion)this.getClass().getAnnotation(SupportedSourceVersion.class);
        SourceVersion sv = null;
        if (ssv == null) {
            sv = SourceVersion.RELEASE_6;
            if (this.isInitialized()) {
                Messager var10000 = this.processingEnv.getMessager();
                Kind var10001 = Kind.WARNING;
                String var10002 = this.getClass().getName();
                var10000.printMessage(var10001, "No SupportedSourceVersion annotation found on " + var10002 + ", returning " + sv + ".");
            }
        } else {
            sv = ssv.value();
        }

        return sv;
    }

    public synchronized void init(ProcessingEnvironment processingEnv) {
        if (this.initialized) {
            throw new IllegalStateException("Cannot call init more than once.");
        } else {
            Objects.requireNonNull(processingEnv, "Tool provided null ProcessingEnvironment");
            this.processingEnv = processingEnv;
            this.initialized = true;
        }
    }

    public abstract boolean process(Set<? extends TypeElement> var1, RoundEnvironment var2);

    public Iterable<? extends Completion> getCompletions(Element element, AnnotationMirror annotation, ExecutableElement member, String userText) {
        return Collections.emptyList();
    }

    protected synchronized boolean isInitialized() {
        return this.initialized;
    }

    private static Set<String> arrayToSet(String[] array, boolean stripModulePrefixes) {
        assert array != null;

        Set<String> set = new HashSet(array.length);
        String[] var3 = array;
        int var4 = array.length;

        for(int var5 = 0; var5 < var4; ++var5) {
            String s = var3[var5];
            if (stripModulePrefixes) {
                int index = s.indexOf(47);
                if (index != -1) {
                    s = s.substring(index + 1);
                }
            }

            set.add(s);
        }

        return Collections.unmodifiableSet(set);
    }
}

每一个处理器都继承自上面的AbstractProcessor

2.2、自定义注解处理器

public class MyProcessor extends AbstractProcessor {

    @Override
    public synchronized void init(ProcessingEnvironment env){ }

    @Override
    public boolean process(Set<? extends TypeElement> annoations, RoundEnvironment env) { }

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

    @Override
    public SourceVersion getSupportedSourceVersion() { }

}
  • init(ProcessingEnvironment env): 每一个注解处理器类都必须有一个空的构造函数。然而,这里有一个特殊的init()方法,它会被注解处理工具调用,并输入ProcessingEnviroment参数。ProcessingEnviroment提供很多有用的工具类ElementsTypesFiler。后面我们将看到详细的内容。

  • process(Set<? extends TypeElement> annotations, RoundEnvironment env): 这相当于每个处理器的主函数main()。你在这里写你的扫描、评估和处理注解的代码,以及生成Java文件。输入参数RoundEnviroment,可以让你查询出包含特定注解的被注解元素。后面我们将看到详细的内容。

  • getSupportedAnnotationTypes(): 这里你必须指定,这个注解处理器是注册给哪个注解的。注意,它的返回值是一个字符串的集合,包含本处理器想要处理的注解类型的合法全称。换句话说,你在这里定义你的注解处理器注册到哪些注解上。

  • getSupportedSourceVersion(): 用来指定你使用的Java版本。通常这里返回SourceVersion.latestSupported()。然而,如果你有足够的理由只支持Java 6的话,你也可以返回SourceVersion.RELEASE_6。我推荐你使用前者。

ProcessingEnvironment.java

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

    Messager getMessager();

    Filer getFiler();

    Elements getElementUtils();

    Types getTypeUtils();

    SourceVersion getSourceVersion();

    Locale getLocale();
}
RoundEnvironment.java

public interface RoundEnvironment {
    boolean processingOver();

    boolean errorRaised();

    Set<? extends Element> getRootElements();

    Set<? extends Element> getElementsAnnotatedWith(TypeElement var1);

    default Set<? extends Element> getElementsAnnotatedWithAny(TypeElement... annotations) {
        Set<Element> result = new LinkedHashSet();
        TypeElement[] var3 = annotations;
        int var4 = annotations.length;

        for(int var5 = 0; var5 < var4; ++var5) {
            TypeElement annotation = var3[var5];
            result.addAll(this.getElementsAnnotatedWith(annotation));
        }

        return Collections.unmodifiableSet(result);
    }

    Set<? extends Element> getElementsAnnotatedWith(Class<? extends Annotation> var1);

    default Set<? extends Element> getElementsAnnotatedWithAny(Set<Class<? extends Annotation>> annotations) {
        Set<Element> result = new LinkedHashSet();
        Iterator var3 = annotations.iterator();

        while(var3.hasNext()) {
            Class<? extends Annotation> annotation = (Class)var3.next();
            result.addAll(this.getElementsAnnotatedWith(annotation));
        }

        return Collections.unmodifiableSet(result);
    }
}

将注解处理器注册到javac中

  • MyProcessor.jar
    • com
      • example
        • MyProcessor.class
    • META-INF
      • services
        • javax.annotation.processing.Processor

打包进MyProcessor.jar中的javax.annotation.processing.Processor的内容是,注解处理器的合法的全名列表,每一个元素换行分割:

com.example.MyProcessor  
com.foo.OtherProcessor  
net.blabla.SpecialProcessor  

2、JavaPoet

image.png

要实现一个功能,我们通常编写一系列的java文件,如果需求发生变化,则修改这些java文件或增加一些新的java文件。为了避免为适应千变万化的需求而频繁修改项目代码,可以在运行时动态生成字节码,当然运行时生成字节码需要占用计算资源。当然,还有一种思路是根据条件动态生成java文件,而不是根据每种情况编写固定的代码,这样生成的项目与完全手工编写的代码没有任何区别。JavaPoet就是一个动态生成java文件的库,在caffine、butterknife、自动生成rpc stub文件等中间件中得到了应用。

2.1、基本功能介绍

一个java类由类声明、字段、构造方法、方法、参数、注解等元素组成,JavaPoet为这些基本组成元素分别定义了相应的类,分别用来管理、生成相应元素相关的代码。

Class功能说明
JavaFile对应编写的.java文件
TypeSpec对应编写的.java文件
MethodSpec对应一个方法或构造方法
FieldSpec对应一个字段
ParameterSpec对应方法或构造方法的一个参数
AnnotationSpec对应类型、字段、方法或构造方法上的注解
ClassName对应一个类、接口或enum的名字,由package名字和类名字两部分组成
CodeBlock代码块,一般用来生成{}包裹起来的数据块
ParameterizedTypeName泛型中的参数化类型
TypeVariableName泛型中的类型变量
WildcardTypeName泛型中的参数化类型

2.2、替换符号说明

占位符表示含义说明
$L字面量传入字符串,就跟java中的%s一样。.addStatement("result = result $L i", op),最终生成的是result = result + i;
$S字符串传入字符串,会保留双引号。.addStatement("s = $S + "_version", op),最终生成的是s= "hello" + "_version";
$T类、接口如果使用JavaFile进行构建,可以添加类的import信息,后面的代码示例中可知
$N名称自定义的MethodSpec对象或FieldSpec对象,因为二者的toString()方法的特殊性,其实就是传入就是方法名和变量名,与直接传入一个String类型的方法名或变量名等价
$1N指定顺序,实现占位符重用.addStatement("this.1N=1N = 1N", age)等于.addStatement("this.N=N = N", age, age)

2.3、个别组件使用说明

filed

// 显式创建FieldSpec


FieldSpec android = FieldSpec.builder(String.class, "description")
    .addModifiers(Modifier.PRIVATE, Modifier.FINAL)
    // 指定初始化值,可选
    .initializer("$S", "this is a example")
    .build();

ParameterizedTypeName type = ParameterizedTypeName.get(List.class, String.class);
FieldSpec android = FieldSpec.builder(type, "name").build();

//生成结果
private final String description = "this is a example";
List<String> name;

class

参考文章:

pdai.tech/md/java/bas…

juejin.cn/post/684490…

www.race604.com/annotation-…

juejin.cn/post/684490…

www.jianshu.com/p/d0686ce07…