什么是元数据(metadata)
元数据由metadata译来,所谓的元数据就是“关于数据的数据”,更通俗的说就是描述数据的数据,对数据及信息资源的描述性信息.比如说一个文本文件,有创建时间,创建人,文件大小等数据,这都可以理解为是元数据.
在java中,元数据以标签的形式存在java代码中,它的存在并不影响程序代码的编译和执行,通常它被用来生成其它的文件或运行时知道被运行代码的描述信息。java当中的javadoc和注解都属于元数据.
什么是注解(Annotation)?
注解是从java 5.0开始加入,可以用于标注包,类,方法,变量等.比如我们常见的@Override,再或者Android源码中的@hide,@systemApi,@privateApi等
对于@Override,多数人往往都是知其然而不知其所以然,今天我就来聊聊Annotation背后的秘密,开始正文.
元注解
元注解就是定义注解的注解,是java提供给我们用于定义注解的基本注解.在java.lang.annotation包中我们可以看到目前元注解共有以下几个:
- @Retention
- @Target
- @Inherited
- @Documented
- @interface
下面我们将集合@Override注解来解释着5个基本注解的用法.
@interface
@interface是java中用于声明注解类的关键字.使用该注解表示将自动继承java.lang.annotation.Annotation类,该过程交给编译器完成.
因此我们想要定义一个注解只需要如下做即可,以@Override注解为例
public @interface Override {
}
需要注意:在定义注解时,不能继承其他注解或接口.
@Retention
@Retention:该注解用于定义注解保留策略,即定义的注解类在什么时候存在(源码阶段 or 编译后 or 运行阶段).该注解接受以下几个参数: RetentionPolicy.SOURCE,RetentionPolicy.CLASS,RetentionPolicy.RUNTIME
,其具体使用及含义如下:
注解保留策略 | 含义 |
---|---|
@Retention(RetentionPolicy.SOURCE) | 注解仅在源码中保留,class文件中不存在 |
@Retention(RetentionPolicy.CLASS) | 注解在源码和class文件中都存在,但运行时不存在,即运行时无法获得,该策略也是默认的保留策略 |
@Retention(RetentionPolicy.RUNTIME) | 注解在源码,class文件中存在且运行时可以通过反射机制获取到 |
来看一下@Override注解的保留策略:
@Retention(RetentionPolicy.SOURCE)
public @interface Override {
}
这表明@Override注解只在源码阶段存在,javac在编译过程中去去掉该注解.
@Target
该注解用于定义注解的作用目标,即注解可以用在什么地方,比如是用于方法上还是用于字段上,该注解接受以下参数:
作用目标 | 含义 |
---|---|
@Target(ElementType.TYPE) | 用于接口(注解本质上也是接口),类,枚举 |
@Target(ElementType.FIELD) | 用于字段,枚举常量 |
@Target(ElementType.METHOD) | 用于方法 |
@Target(ElementType.PARAMETER) | 用于方法参数 |
@Target(ElementType.CONSTRUCTOR) | 用于构造参数 |
@Target(ElementType.LOCAL_VARIABLE) | 用于局部变量 |
@Target(ElementType.ANNOTATION_TYPE) | 用于注解 |
@Target(ElementType.PACKAGE) | 用于包 |
以@Override为例,不难看出其作用目标为方法:
@Target(ElementType.METHOD)
public @interface Override {
}
到现在,通过@interface,@Retention,@Target已经可以完整的定义一个注解,来看@Override完整定义:
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.SOURCE)
public @interface Override {
}
@Inherited
默认情况下,我们自定义的注解用在父类上不会被子类所继承.如果想让子类也继承父类的注解,即注解在子类也生效,需要在自定义注解时设置@Inherited.一般情况下该注解用的比较少.
@Documented
该注解用于描述其它类型的annotation应该被javadoc文档化,出现在api doc中.
比如使用该注解的@Target会出出现在api说明中.
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.ANNOTATION_TYPE)
public @interface Target {
ElementType[] value();
}
借助@Interface,@Target,@Retention,@Inherited,@Documented这五个元注解,我们就可以自定义注解了,其中前三个注解是任何一个注解都必备具备的.
你以为下面会直接来将如何自定义注解嘛?不,你错了,我们还是来聊聊java自带的几个注解.
系统注解
java设计者已经为我们自定义了几个常用的注解,我们称之为系统注解,主要是这三个:
系统注解 | 含义 |
---|---|
@Override | 用于修饰方法,表示此方法重写了父类方法 |
@Deprecated | 用于修饰方法,表示此方法已经过时 |
@SuppressWarnnings | 该注解用于告诉编译器忽视某类编译警告 |
如果你已经完全知道这三者的用途,跳过这一小节,直接往下看.
@Override
它用作标注方法,说明被标注的方法重写了父类的方法,其功能类似断言.如果在一个没有重写父类方法的方法上使用该注解,java编译器将会以一个编译错误提示:

@Deprecated
当某个类型或者成员使用该注解时意味着
编译器不推荐开发者使用被标记的元素.另外,该注解具有”传递性”,子类中重写该注解标记的方法,尽管子类中的该方法未使用该注解,但编译器仍然报警.
public class SimpleCalculator {
@Deprecated
public int add(int x, int y) {
return x+y;
}
}
public class MultiplCalculator extends SimpleCalculator {
// 重写SimpleCalculator中方法,但不使用@Deprecated
public int add(int x, int y) {
return Math.abs(x)+Math.abs(y);
}
}
//test code
public class Main {
public static void main(String[] args) {
new SimpleCalculator().add(3, 4);
new MultiplCalculator().add(3,5);
}
}
对于像new SimpleCalculator().add(3,4)这种直接调用的,Idea会直接提示,而像第二种则不是直接提示:

但是在编译过程中,编译器都会警告:
需要注意@Deprecated和@deprecated这两者的区别,前者被javac识别和处理,而后者则是被javadoc工具识别和处理.因此当我们需要在源码标记某个方法已经过时应该使用@Deprecated,如果需要在文档中说明则使用@deprecated,因此可以这么:
public class SimpleCalculator {
/**
* @param x
* @param y
* @return
*
* @deprecated deprecated As of version 1.1,
* replace by <code>SimpleCalculator.add(double x,double y)</code>
*/
@Deprecated
public int add(int x, int y) {
return x+y;
}
public double add(double x,double y) {
return x+y;
}
}
@SuppressWarnning
该注解被用于有选择的关闭编译器对类,方法,成员变量即变量初始化的警告.该注解可接受以下参数:
参数 | 含义 |
---|---|
deprecated | 使用已过时类,方法,变量 |
unchecked | 执行了未检查的转告时的警告,如使用集合是为使用泛型来制定集合保存时的类型 |
fallthrough | 使用switch,但是没有break时 |
path | 类路径,源文件路径等有不存在的路径 |
serial | 可序列化的类上缺少serialVersionUID定义时的警告 |
finally | 任何finally字句不能正常完成时的警告 |
all | 以上所有情况的警告 |
滋溜一下,我们飞过了2016年,不,是看完了上一节.继续往下飞.
自定义注解
了解完系统注解之后,现在我们就可以自己来定义注解了,通过上面@Override的实例,不难看出定义注解的格式如下:
public @interface 注解名 {定义体}
定义体就是方法的集合,每个方法实则是声明了一个配置参数.方法的名称作为配置参数的名称,方法的返回值类型就是配置参数的类型.和普通的方法不一样,可以通过default关键字来声明配置参数的默认值.
需要注意:
- 此处只能使用public或者默认的defalt两个权限修饰符
- 配置参数的类型只能使用基本类型(byte,boolean,char,short,int,long,float,double)和String,Enum,Class,annotation
- 对于只含有一个配置参数的注解,参数名建议设置中value,即方法名为value
- 配置参数一旦设置,其参数值必须有确定的值,要不在使用注解的时候指定,要不在定义注解的时候使用default为其设置默认值,对于非基本类型的参数值来说,其不能为null.
像@Override这样,没有成员定义的注解称之为标记注解.
现在我们来自定义个注解@UserMeta,这个注解目前并没啥用,就是为了演示一番:
@Documented
@Target(ElementType.CONSTRUCTOR)
@Retention(RetentionPolicy.RUNTIME)
public @interface UserMeta {
public int id() default 0;
public String name() default "";
public int age() default ;
}
有了米饭,没有筷子没法吃啊(手抓饭的走开),下面来看看如何处理注解.
注解处理器
上面我们已经学会了如何定义注解,要想注解发挥实际作用,需要我们为注解编写相应的注解处理器.根据注解的特性,注解处理器可以分为运行时注解处理和编译时注解处理器.运行时处理器需要借助反射机制实现,而编译时处理器则需要借助APT来实现.
无论是运行时注解处理器还是编译时注解处理器,主要工作都是读取注解及处理特定注解,从这个角度来看注解处理器还是非常容易理解的.
先来看看如何编写运行时注解处理器.
运行时注解处理器
熟悉java反射机制的同学一定对java.lang.reflect包非常熟悉,该包中的所有api都支持读取运行时Annotation的能力,即属性为@Retention(RetentionPolicy.RUNTIME)的注解.
在java.lang.reflect中的AnnotatedElement接口是所有程序元素的(Class,Method)父接口,我们可以通过反射获取到某个类的AnnotatedElement对象,进而可以通过该对象提供的方法访问Annotation信息,常用的方法如下:
方法 | 含义 |
---|---|
<T extends Annotation> T getAnnotation(Class<T> annotationClass) | 返回该元素上存在的制定类型的注解 |
Annotation[] getAnnotations() | 返回该元素上存在的所有注解 |
default <T extends Annotation> T[] getAnnotationsByType(Class<T> annotationClass) | 返回该元素指定类型的注解 |
default <T extends Annotation> T getDeclaredAnnotation(Class<T> annotationClass) | 返回直接存在与该元素上的所有注释 |
default <T extends Annotation> T[] getDeclaredAnnotationsByType(Class<T> annotationClass) | 返回直接存在该元素岸上某类型的注释 |
Annotation[] getDeclaredAnnotations() | 返回直接存在与该元素上的所有注释 |
编写运行时注解大体就需要了解以上知识点,下面来做个小实验.
简单示例
首先我们用一个简单的实例来介绍如何编写运行时注解处理器:我们的系统中存在一个User实体类:
public class User {
private int id;
private int age;
private String name;
@UserMeta(id=1,name="dong",age = 10)
public User() {
}
public User(int id, int age, String name) {
this.id = id;
this.age = age;
this.name = name;
}
//...省略setter和getter方法
@Override
public String toString() {
return "User{" +
"id=" + id +
", age=" + age +
", name='" + name + '\'' +
'}';
}
}
我们希望可以通过 @UserMeta(id=1,name="dong",age = 10)
(这个注解我们在上面提到了)来为设置User实例的默认值。
自定义注解类如下:
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.CONSTRUCTOR)
public @interface UserMeta {
public int id() default 0;
public String name() default "";
public int age() default 0;
}
该注解类作用于构造方法,并在运行时存在,这样我们就可以在运行时通过反射获取注解进而为User实例设值,看看如何处理该注解吧.
运行时注解处理器:
public class AnnotationProcessor {
public static void init(Object object) {
if (!(object instanceof User)) {
throw new IllegalArgumentException("[" + object.getClass().getSimpleName() + "] isn't type of User");
}
Constructor[] constructors = object.getClass().getDeclaredConstructors();
for (Constructor constructor : constructors) {
if (constructor.isAnnotationPresent(UserMeta.class)) {
UserMeta userFill = (UserMeta) constructor.getAnnotation(UserMeta.class);
int age = userFill.age();
int id = userFill.id();
String name = userFill.name();
((User) object).setAge(age);
((User) object).setId(id);
((User) object).setName(name);
}
}
}
}
测试代码:
public class Main {
public static void main(String[] args) {
User user = new User();
AnnotationProcessor.init(user);
System.out.println(user.toString());
}
}
运行测试代码,便得到我们想要的结果:
User{id=1, age=10, name=’dong’}
这里通过反射获取User类声明的构造方法,并检测是否使用了@UserMeta注解。然后从注解中获取参数值并将其赋值给User对象。
正如上面提到,运行时注解处理器的编写本质上就是通过反射获取注解信息,随后进行其他操作。编译一个运行时注解处理器就是这么简单。运行时注解通常多用于参数配置类模块。