前言
本文是系列文章的第二篇,Java的注解。个人建议先读完第一篇夯实基础:Java的反射,因为在本文的后半部分,将使用到一些反射的技术,学完了反射再学本文的内容更有助于你理解注解,当然,你不学或者不会反射,也不会对你学习本文的内容造成太大影响,希望大家结合自身的情况进行选择。
注解的概念
首先注解不是注释。注释大家都知道是给我们开发者看的,而注解呢是给程序看的。我们可以把注解理解为标签,这些标签可以用在Java的类、成员变量、成员方法、构造方法、形参、局部变量等等程序属性上面,并且能够在Java文件、编译期和运行时被读取,开发者可以在程序逻辑不被修改的情况下对代码嵌入补充信息。
Java内置的注解
java给我们内置提供了几个注解,下面我们分别看一下
- @Override:验证子类是否重写了父类的方法。该注解仅在Java代码时有效,编译阶段就会被丢弃
- @Deprecated:验证变量、方法等程序元素是否过时,注意这里过时不代表不可以被使用,只是有了更好的替代。该注解会一直保留到运行时
- SuppressWarnings:压制警告,里面需要接收一个value参数来表明你要压制哪种警告。该注解的有效期同@Override,仅在Java代码时有效,编译阶段就会被丢弃
- @SafeVarargs:压制堆污染警告,保留到程序运行时,仅对构造方法和成员方法有效
- @Functionallnterface:作用在接口上,保证这个接口只有一个抽象方法,一直保留到程序运行时
元注解
想了解注解之前,必须要知道什么是元注解。所谓元注解就是注解的注解,本身就是一个注解,用来修饰注解的,先来认识几个java内置的元注解
- @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();
}
里面有一个value属性,返回的是ElementType[],看一下ElementType的取值
public enum ElementType {
/** Class, interface (including annotation type), or enum declaration */
TYPE,
/** Field declaration (includes enum constants) */
FIELD,
/** Method declaration */
METHOD,
/** Formal parameter declaration */
PARAMETER,
/** Constructor declaration */
CONSTRUCTOR,
/** Local variable declaration */
LOCAL_VARIABLE,
/** Annotation type declaration */
ANNOTATION_TYPE,
/** Package declaration */
PACKAGE,
/**
* Type parameter declaration
*
* @since 1.8
*/
TYPE_PARAMETER,
/**
* Use of a type
*
* @since 1.8
*/
TYPE_USE
}
取值基本都是包、类、成员方法、成员变量、构造方法、局部变量等等的程序属性
- @Retention:中文为保留,就是说注解保留到什么阶段,从什么阶段到什么阶段有效。
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.ANNOTATION_TYPE)
public @interface Retention {
/**
* Returns the retention policy.
* @return the retention policy
*/
RetentionPolicy value();
}
里面有一个属性value,返回对象是RetentionPolicy,这是个枚举类,里面有三个枚举值,SOURCE,CLASS,RUNTIME
SOURCE:java源代码阶段
CLASS:把java文件编译成class文件阶段
RUNTIME:程序运行时,基本就等于一直存在,我们绝大多数的时候都用这个阶段
- @Documented:作用在类上,被@Documented标记的类,使用javadoc命令执行一下对应的类就会生成文档,相对来说这个注解用的情况比较少
- @Inherited:作用在子类上,被@Inherited标记的子类会继承父类的注解,一般用的也比较少
注意:Java内置的注解还有@Native、@Repeatable以及@Annotation,这些不是很常用,感兴趣可以自行google一下,上面4个注解,其中@Target和@Retention是如何注解都必须要设置的,一定要记住。
注解的本质
介绍完了元注解,我们现在来了解一下注解的本质是什么。我这里先创建了一个注解
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface TestAnno {
}
通过javac编译生成TestAnno.class,然后再用javap反编译一下TestAnno.class
直接看图,我们发现我们创建的TestAnno实际上一个继承了Annotation的接口,Annotion也是一个接口,它是所有注解的父类,到现在我们弄明白了注解的本质就是一个接口。
既然注解是一个接口,那我们就可以用对待接口的方式对待注解,接口里面有抽象方法,我们就可以在注解里面创建抽象方法
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface TestAnno {
String name();
int age();
}
使用一下这个注解
@TestAnno(name = "张三",age = 22)
public class Person extends Object {
用使用前可以看出,我们创建的是抽象方法,但是在实际用的时候好像跟属性一样,都是XX属性=xxx,其实注解里面的抽象方法就是来描述这个注解的属性的,所以我们在给方法命名的时候最好也按照属性命名。我们如果要使用的注解的话需要给里面的属性赋值,像“@TestAnno(name = "张三",age = 22)”这种,如果实在不想赋值的话,可以在创建属性的时候给默认值
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface TestAnno {
String name() default "张三";
int age() default 22;
}
所有的注解还有一个默认的属性value,当你使用了value属性,在赋值的时候可以不写前面“value=”
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface TestAnno {
String name() default "张三";
int age() default 22;
String value();
}
@TestAnno("555")//这里的555是给value赋值
public class Person extends Object {
注意:注解里面的抽象方法(属性)的返回值,只能是:基本数据类型、String类型、注解类型以及它们的数组,不能是自定义的对象类型以及void,比如
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface TestAnno {
Person person();//编译器就直接报错了
void test();//不被允许
}
自定义注解
了解了注解的本质以后,我们来自定义一个注解。我们知道修饰类的关键字是class,接口的是interface,枚举的是enum,而修饰注解的就是@interface。
注解还必须有@Target和@Retention,举个例子
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE,ElementType.FIELD,ElementType.METHOD})
public @interface TestAnno2 {
String value();
int[] ids();
TestAnno anno();
}
其实了解了注解的本质以后,自己写个注解根本不是事,然而自定义注解根本不是关键,因为现在这个注解其实没有任何意义。所以我们要解析这些注解,如果解析呢?这里就用到我们上一篇文章学到的反射了。
利用反射解析注解
首先,解释一下为什么要通过反射来解析注解。注解是作用在包、类、变量、方法等程序属性上,如果我们要想拿到注解,就必须先得拿到这些程序属性,而如何能拿到这些程序属性呢?正是通过反射!
下面我将以一个具体的例子来讲解一下
/**
* 加减乘除
* */
public class MathCalculation {
@CheckMath
public int add(int a,int b) {
return a+b;
}
@CheckMath
public int sub(int a,int b) {
return a-b;
}
@CheckMath
public int mul(int a,int b) {
return a*b;
}
@CheckMath
public int exc(int a,int b) {
return a/b;
}
}
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface CheckMath {//用来检查加减乘除四个运算
int[] aList() default {1,2,3,4,5,6,7,8,9,0};//默认值1~9
int[] bList() default {1,2,3,4,5,6,7,8,9,0};
}
上面两个代码很简单,我现在需要用CheckMath检查一下加减乘除这四个方法在a和b分别是1~9的情况下是否正确
public class Test {
public static void main(String[] args) throws Exception{
Class mathCalculationClass = MathCalculation.class;//拿到MathCalculation class类对象
MathCalculation mathCalculation = mathCalculationClass.newInstance();
Method[] methods = mathCalculationClass.getMethods();//获取MathCalculation所有的public方法
for (Method method:methods) {//遍历所有的public方法
if (method.isAnnotationPresent(CheckMath.class)) {//判断该方法是否有CheckMath.class
CheckMath checkMath = method.getAnnotation(CheckMath.class);//获取CheckMath注解对象
int[] aList = checkMath.aList();//获取a的数组
int[] bList = checkMath.bList();//获取b的数组
for (int a:aList) {
for (int b:bList) {
try {
method.invoke(mathCalculation,a,b);//调用计算方法
}catch (Exception e) {
//出错以后打印log
System.out.println("出现错误"+"a= "+a+",b="+b+" 错误原因:"+e.getCause());
}
}
}
}
}
}
}
基本上每一行都有注释了,这里就不再赘述,看一眼打印结果
当b为0报了数学异常,因此咱们的CheckMath注解还是发挥了它的作用。
注意:这里我们先用反射拿到了程序属性,再通过程序属性拿到了注解。反射拿到程序属性咱们上一节说过,那为什么程序属性就能拿到注解呢?这个其实很简单,我们打开类似Packge、Class、Constructor、Filed、Method这些程序属性类的源码会发现他们都实现了一个叫AnnotatedElement的接口,在这个AnnotatedElement接口里面定义了跟注解相关的方法,核心常用的有三个
- default boolean isAnnotationPresent(Class extends Annotation> annotationClass) {
return getAnnotation(annotationClass) != null;
} :判断程序属性是否被某个注解标记 T getAnnotation(Class annotationClass):获取指定的注解对象 - Annotation[] getAnnotations():获取所有的注解对象
总结:
注解是对程序信息的一种补充标记,本质上是一个特殊的接口,接口里面定义的方法实际上是注解的属性。注解单独使用没有任何意义,必须要结合反射来解析。解析的本质是先通过反射拿到程序信息,再通过程序信息拿到注解对象,而程序信息可以拿到注解对象是因为程序信息实现了AnnotatedElement接口。