Java语言高级特性 -注解

168 阅读7分钟

什么是注解

注解,可以看作是对 一个 类/方法 的一个扩展的模版,每个 类/方法 按照注解类中的规则,来为 类/方法 注解不同的参数,在用到的地方可以得到不同的 类/方法 中注解的各种参数与值。

从JDK5开始,java增加了对元数据(描述数据属性的信息)的支持。其实说白就是代码里的特殊标志,这些标志可以在编译,类加载,运行时被读取,并执行相应的处理,以便于其他工具补充信息或者进行部署。

注解声明

声明一个注解类型

java中所有的注解,默认实现Annotation接口

Annotation源码

package java.lang.annotation;


public interface Annotation {
   
    boolean equals(Object obj);
   
    int hashCode();

    String toString();

    Class<? extends Annotation> annotationType();
}

注解的声明使用@interface关键字

/**
 * @ProjectName:jetpackDemo
 * @Description:
 *      声明一个注解
 * @Author: arrom
 * @createTime:2022/1/18
 */
public @interface Arrom {
}

元注解

所谓元注解就是标记其他注解的注解

/**
 * @ProjectName:jetpackDemo
 * @Description:
 *      声明一个注解
 * @Author: arrom
 * @createTime:2022/1/18
 */
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Arrom {
}

@Target 用来约束注解可以应用的地方,ElementType是枚举类型

package java.lang.annotation;

public enum ElementType {
    TYPE,//标明该注解可以用于类、接口(包括注解类型)或enum声明
    FIELD,//标明该注解可以用于字段(域)声明,包括enum实例
    METHOD,//标明该注解可以用于方法声明
    PARAMETER,//标明该注解可以用于参数声明
    CONSTRUCTOR,//标明注解可以用于构造函数声明
    LOCAL_VARIABLE,//标明注解可以用于局部变量声明
    ANNOTATION_TYPE,//标明注解可以用于注解声明(应用于另一个注解上)
    PACKAGE,//标明注解可以用于包声明
    TYPE_PARAMETER,//标明注解可以用于类型参数声明
    TYPE_USE,//类型使用声明
    MODULE; //标明注解可以用于模块

    private ElementType() {
    }
}

@Retention用来约束注解的生命周期,分别有三个值,

  1. RetentionPolicy.SOURCE - 标记的注解仅保留在源级别中,并被编译器忽略。
  2. RetentionPolicy.CLASS - 标记的注解在编译时由编译器保留,但 Java 虚拟机(JVM)会忽略。
  3. RetentionPolicy.RUNTIME - 标记的注解由 JVM 保留,因此运行时环境可以使用它。

注解元素和数据类型

通过上述对@Arrom注解的定义,我们了解了注解定义的过程,由于@Test内部没有定义其他元素,所以@Arrom也称为标记注解(marker annotation),但在自定义注解中,一般都会包含一些元素以表示某些值,方便处理器使用。

/**
 * 对应数据表注解
*/
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface DBTable {
    String name() default "";
}

上述定义一个名为DBTable的注解,该用于主要用于数据库表与Bean类的映射(稍后会有完整案例分析),与前面Arrom注解不同的是,我们声明一个String类型的name元素,其默认值为空字符,但是必须注意到对应任何元素的声明应采用方法的声明方式,同时可选择使用default提供默认值,@DBTable使用方式

@DBTable(name = "USER")
public class User {
   //
}

注解支持的元素数据类型

  • 所有基本类型(int,float,boolean,byte,double,char,long,short)
  • String
  • Class
  • enum
  • Annotation
  • 上述类型的数组

在使用注解时,如果定义的注解中的类型元素无默认值,则必须进行传值

注解应用场景

RetentionPolicy.SOURCE

作用于源码级别的注解,可提供给IDE语法检查,APT等场景使用。在类中使用source级别的注解,编译之后class中会被丢弃。

在Android开发中,support-annotations与androidx.annotation中均有提供@IntDef注解

@Retention(SOURCE) //源码级别注解
@Target({ANNOTATION_TYPE})
public @interface IntDef {
 /** Defines the allowed constants for this element */
 int[] value() default {};

 /** Defines whether the constants can be used as a flag, or just as an enum (the default) */
 boolean flag() default false;

 /**
  * Whether any other values are allowed. Normally this is
  * not the case, but this allows you to specify a set of
  * expected constants, which helps code completion in the IDE
  * and documentation generation and so on, but without
  * flagging compilation warnings if other values are specified.
  */
 boolean open() default false;
}

此注解的意义在于能够取代枚举,实现方法入参显示。

使用枚举
public class Main {
 
    public enum WeekDays {
        SUNDAY, MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY, SATURDAY
    }
 
    private WeekDays currentDay = WeekDays.SUNDAY;
 
    public static void main(String[] args) {
        // TODO Auto-generated method stub
        Main obj = new Main();
        obj.setCurrentDay(WeekDays.WEDNESDAY);
 
        WeekDays today = obj.getCurrentDay();
 
        switch (today) {
        case SUNDAY:
            System.out.println("Today is SUNDAY");
            break;
        case MONDAY:
            System.out.println("Today is MONDAY");
            break;
        case TUESDAY:
            System.out.println("Today is TUESDAY");
            break;
        case WEDNESDAY:
            System.out.println("Today is WEDNESDAY");
            break;
        case THURSDAY:
            System.out.println("Today is THURSDAY");
            break;
        case FRIDAY:
            System.out.println("Today is FRIDAY");
            break;
        case SATURDAY:
            System.out.println("Today is SATURDAY");
            break;
 
        default:
            break;
        }
    }
 
    public void setCurrentDay(WeekDays currentDay) {
        this.currentDay = currentDay;
    }
 
    public WeekDays getCurrentDay() {
        return currentDay;
    }
 
}
使用@IntDef
public class MainActivity extends Activity {
 
    public static final int SUNDAY = 0;
    public static final int MONDAY = 1;
    public static final int TUESDAY = 2;
    public static final int WEDNESDAY = 3;
    public static final int THURSDAY = 4;
    public static final int FRIDAY = 5;
    public static final int SATURDAY = 6;
 
    @IntDef({SUNDAY, MONDAY,TUESDAY,WEDNESDAY,THURSDAY,FRIDAY,SATURDAY})
    @Retention(RetentionPolicy.SOURCE)
    public @interface WeekDays {}
 
    @WeekDays int currentDay = SUNDAY;
 
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        setCurrentDay(WEDNESDAY);
 
        @WeekDays int today = getCurrentDay();
 
        switch (today){
            case SUNDAY:
                break;
            case MONDAY:
                break;
            case TUESDAY:
                break;
            case WEDNESDAY:
                break;
            case THURSDAY:
                break;
            case FRIDAY:
                break;
            case SATURDAY:
                break;
            default:
                break;
        }
 
    }
 
    public void setCurrentDay(@WeekDays int currentDay) {
        this.currentDay = currentDay;
    }
 
    @WeekDays
    public int getCurrentDay() {
        return currentDay;
    }
}

在android系统中使用枚举会创建多个对象,消耗内存会变大。

APT注解处理器的原理

APT 的原理就是在需要使用的元素上(类、变量、方法、参数等)加上我们的注解,然后在编译时把使用了这个注解的元素都收集起来,做统一的处理。

创建一个注解处理器分为如下几步:

  • 创建注解类
  • 创建一个继承自 AbstractProcessor 的类,这就是 APT 的核心类
  • 创建公开 API 及辅助工具
  • 创建配置文件
  • 使用
AbstractProcessor类中主要方法

getSupportedAnnotationTypes 返回一个注解处理器支持的注解类型。 process 处理解析注解元素并生成java代码

public abstract boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv);

第一个参数 annotations 就是当前注解处理器支持的注解集合,第二个 roundEnv 表示当前的 APT 环境,其中提供了很多API,可以通过它获取到注解元素的一些信息。 其中最重要的就是 getElementsAnnotatedWith 方法,通过它可以获取到所有使用了该注解的元素。

//获取所有使用了 BindView 注解的元素
val bindViewElementSet = roundEnv.getElementsAnnotatedWith(xxx::class.java)

RetentionPolicy.CLASS

编译时注解(RetentionPolicy.CLASS),指@Retention(RetentionPolicy.CLASS)作用域class字节码上,生命周期只有在编译器间有效。编译时注解注解处理器的实现主要依赖于AbstractProcessor来实现,这个类是在javax.annotation.processing包中,同时为了我们自己生成java源文件方便。

定义为 CLASS 的注解,会保留在class文件中,但是会被虚拟机忽略(即无法在运行期反射获取注解)。此时完全符合此种注解的应用场景为字节码操作。如:AspectJ、热修复Roubust中应用此场景。

编译时注解的核心就是实现AbstractProcessor的process() 方法,一般来说主要有以下两个步骤
1 搜集信息,包括被注解的类的类信息,方法,字段等信息,还有注解的值
2 生成对应的java源代码,主要根据上一步的信息,生成响应的代码

编译时注解的Demo

注解

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

    int value();

}

新建BindProcessor继承与AbstractProcessor并实现其process方法

@AutoService(Processor.class)
public class BindProcessor extends AbstractProcessor{

    /**
     * java源文件操作相关类,主要用于生成java源文件
     */
    private Filer mFiler;
    /**
     * 注解类型工具类,主要用于后续生成java源文件使用
     * 类为TypeElement,变量为VariableElement,方法为ExecuteableElement
     */
    private Elements mElementsUtils;
    /**
     * 日志打印,类似于log,可用于输出错误信息
     */
    private Messager mMessager;

    private static final ClassName sClassName = ClassName.get("com.qiyei.ioc.api", "Test");

    /**
     * 初始化,主要用于初始化各个变量
     * @param processingEnv
     */
    @Override
    public synchronized void init(ProcessingEnvironment processingEnv) {
        super.init(processingEnv);

        mFiler = processingEnv.getFiler();
        mElementsUtils = processingEnv.getElementUtils();
        mMessager = processingEnv.getMessager();
    }

    /**
     * 支持的注解类型
     * @return
     */
    @Override
    public Set<String> getSupportedAnnotationTypes() {

        Set<String> typeSet = new LinkedHashSet<>();

        typeSet.add(Bind.class.getCanonicalName());

        return typeSet;
    }

    /**
     * 支持的版本
     * @return
     */
    @Override
    public SourceVersion getSupportedSourceVersion() {
        return SourceVersion.latestSupported();
    }

    /**
     *
     * 1.搜集信息
     * 2.生成java源文件
     * @param annotations
     * @param roundEnv
     * @return
     */
    @Override
    public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {

        if (!annotations.isEmpty()){
            //获取Bind注解类型的元素,这里是类类型TypeElement
            Set<? extends Element> bindElement = roundEnv.getElementsAnnotatedWith(Bind.class);

            try {
                generateCode(bindElement);
            } catch (IOException e) {
                e.printStackTrace();
            }
            return true;
        }

        return false;
    }

    /**
     *
     * @param elements
     */
    private void generateCode(Set<? extends Element> elements) throws IOException{

        for (Element element : elements){

            //由于是在类上注解,那么获取TypeElement
            TypeElement typeElement = (TypeElement) element;

            //获取全限定类名
            String className = typeElement.getQualifiedName().toString();

            mMessager.printMessage(Diagnostic.Kind.WARNING,"className:" + className);

            //获取包路径
            PackageElement packageElement = mElementsUtils.getPackageOf(typeElement);
            String packageName = packageElement.getQualifiedName().toString();

            mMessager.printMessage(Diagnostic.Kind.WARNING,"packageName:" + packageName);

            //获取用于生成的类名
            className = getClassName(typeElement,packageName);

            //获取注解值
            Bind bindAnnotation = typeElement.getAnnotation(Bind.class);
            int value = bindAnnotation.value();
            System.out.println("value:" + value);

            //生成方法
            MethodSpec.Builder methodBuilder = MethodSpec
                    .methodBuilder("sayHello")
                    .addModifiers(Modifier.PUBLIC)
                    .addParameter(TypeName.INT,"n")
                    .returns(TypeName.INT);

            //$L表示字面量 $T表示类型
            methodBuilder.addStatement("return $L",value);

            //生成的类
            TypeSpec type = TypeSpec
                    .classBuilder(className + "$$" + value)
                    .addModifiers(Modifier.PUBLIC)
                    .addMethod(methodBuilder.build())
                    .build();

            //创建javaFile文件对象
            JavaFile javaFile = JavaFile.builder(packageName,type).build();
            //写入源文件
            javaFile.writeTo(mFiler);

        }
    }

    /**
     * 根据type和package获取类名
     * @param type
     * @param packageName
     * @return
     */
    private static String getClassName(TypeElement type, String packageName) {
        int packageLen = packageName.length() + 1;
        return type.getQualifiedName().toString().substring(packageLen)
                .replace('.', '$');
    }
}

在Activity上使用Bind

@Bind(10)
public class MainActivity extends BaseSkinActivity {

    private RecyclerView mRecyclerView;

    private static final int MY_PERMISSIONS_REQUEST_WRITE_STORE = 1;

    /**
     * ViewModel
     */
    private MainMenuViewModel mMenuViewModel;

    private MainMenuAdapter mMenuAdapter;

    /**
     * 标题栏
     */
    private CommonTitleBar mTitleBar = null;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
//        PermissionManager.requestAllDangerousPermission(this);
        initData();
        initView();
        LogManager.i(TAG,"onCreate");
    }
    .......

}

RetentionPolicy.RUNTIME

注解保留至运行期,意味着我们能够在运行期间结合反射技术获取注解中的所有信息。

总结

注解本质就是继承了Annotation的特殊接口,其具体实现类是Java运行时生成的动态代理类。通过动态代理对象调用自定义注解(接口)的方法,会最终调用AnnotationInvocationHandler的invoke方法