Java 注解
Java注解(Annotation),是在Java 5.0之后引入的一种机制。它和注释的区别是什么呢?注释是为了给程序员留下信息,告知其代码块的功能和参数意义等等。而注解则可以为JVM留下重要的配置信息,以便JVM在运行时(RUNTIME)通过反射机制获取它。
注解的作用
1、 在编译时进行格式检查,比如使用@Override
检测该方法是否属于重写方法,使用@FunctionalInterface
来检测该接口是否满足函数式接口的规范……等等。
2、 在运行时为JVM提供参数信息,替代原本配置文件的功能。
> Spring 3.0+ 开始已经逐渐放弃了`XML`来配置`bean`。尤其是后续的SpringBoot微服务框架则完全采用了**约定大于配置**的思想,通过大量的注解来替代了原本在`XML`中需要配置的大量标签内容。
>
> 总体来说,通过注解的方式,牺牲了代码耦合性,但是却大大提高了开发效率。
定义一个Java 注解
首先来定义一个最简单(且不那么完整)的注解:
public @interface PlainAnnotation{}
从形式上来看,有点像是定义了一个接口,只不过在关键字上有些区别:是@interface
而非interface
。它表示实现了java.lang.annotation.Annotation
接口。
此注解的类型即它的名字:PlainAnnotation
。IntelliJ IDEA会对注解性质的类型进行黄色高亮字标注。
我们不需要显示声明extends关系,因为Annotation接口的实现细节在底层由编译器完成。
目前的注解没有任何内容,它目前只属于标记注解。而我们希望在注解内留下一些内容。
在注解中留下信息
注解内可以声明变量以保存一些信息,形式如下:
public @interface PlainAnotation {
int value() default 2;
String name() default "annoy";
}
我们在这个注解中留下了一个value
值和一个name
值,并默认初始值为2。注意:从形式上来看,这些属性值是通过方法来获取的,因此一定要带上()
;另外,这些值一定要有默认值,通过default
关键字来实现。
注解内部元素的数据类型是受限的
注解内部的元素类型支持以下类型:
byte
,short
,Int
等八种基本数据类型。String
Class
enum
Annotation
- 以上类型的数组类型:
xxx[]
。
这份白名单里没有包装类:也就是无法使用Integer
,Double
等类型。下面用代码来表示:
//基本数据类型
int INT() default 0;
//String类型
String STRING() default "";
//定义一个内部枚举类
enum Status {OK,ERROR}
//Status枚举类型
Status STATUS() default Status.OK;
//嵌套注解类型。
//注:该注解是笔者自定义的注解,主要是为了展示写法格式。
IntegerValue INTEGER_VALUE() default @IntegerValue(100);
//Class类型
Class<?> CLASS() default Void.class;
//数组类型,不需要额外的default关键字。
int[] INTS();
注解的简单使用
我们用上一节中声明的PlainAnnotation
,试着用在我们的程式中的任意一处使用此注解(现在没有对这个注解做出任何限制,在下一节的元注解中就会详细介绍),并为value
和name
元素赋值:
@PlainAnotation(value = 2,name = "author")
注意,当注解只存在一个名字为value
的元素时,注解赋值可简化如下:
@PlainAnotation(100)
再或者,这个注解内没有任何元素(即只作为一个单纯的标记注解),则可以省去参数。
@PlainAnotation
元注解
声明一个自定义注解时,我们通常还要定义这个注解修饰的类型,以及这个注解的生命周期(或者说策略属性),以此来声明一个规范,功能明确的注解。而定义这些规范我们同样使用注解去完成。这些用于定义注解的注解,我们又称为元注解。
@Target(ElementType._)
此元注解决定了自定义注解能够修饰哪些内容,通过ElementType
枚举类来表示。这个枚举类的值包括了以下内容:
值 | 含义 |
---|---|
ElementType.CONSTRUCTOR |
这个注解用于修饰类构造器 |
ElementType.FIELD |
这个注解用于修饰类的成员变量(域) |
ElementType.LOCAL_VARIABLE |
这个注解用于修饰某个方法的局部变量 |
ElementType.METHOD |
这个注解用于修饰某个类的方法 |
ElementType.PACKAGE |
这个注解用于修饰包声明 |
ElementType.PARAMETER |
这个注解用于修饰参数声明 |
ElementType.TYPE |
这个注解用于修饰类,接口(包含注解类),枚举类声明 |
如果没有显示声明此元注解,则表示注解可以存在于任何地方。注解可以作用在多处,写法如下:
@Target(value = {ElementType.TYPE,ElementType.FIELD})
@Retention(RetentionPolicy._)
这个元注解决定了注解的生存周期,通过RententionPolicy
枚举类表示,这个枚举类的值包含了以下内容:
值 | 含义 |
---|---|
RetentionPolicy.SOURCE |
注解只在.java 源代码中保留 |
RetentionPolicy.CLASS |
注解在.class 文件中保留 |
RetentionPolicy.RUNTIME |
注解在运行期间保留 |
其中,只有生存周期为RUNTIME
的注解所保存的信息在运行时可以被VM通过反射机制捕获。如果没有显示声明此注解,则默认此注解的生存周期为CLASS
。
@Inherited
此元注解表示:当这个注解所修饰某个类时,它的子类能否获得此注解。注意,注解本身是不可以继承的。使用@interface
修饰的接口在编译时继承唯一的java.lang.annotation.Annotation
接口,这个工作由编译器执行。
用文字叙述比较费解,因此用代码举一个实例:
@Inherited
public @interface PlainAnnotation {}
@PlainAnnotation
class Parent {}
class Son extends Parent {
//由于@PlainAnnotation是可继承的,因此子类也获得了来自父类的@PlainAnnotation接口。
}
稍后介绍如何通过反射的方式来获取一个类内的注解。
@Repeatable
在Java 1.8版本之前,是不允许在同一处地方出现相同的注解的。而@Repeatable
注解的加入解决了这个问题。下面用一个实例来说明。作为一个Person
,我希望TA同时具备两个(或以上)的身份,并且每一个身份通过一个@Identify
注解来标注出来。
public @interface Identify {
enum CAREER{TEACHER,STUDENT,FATHER,PERSON}
CAREER value() default CAREER.STUDENT;
}
而为了能让一个Person
同时具备两个相同注解,我们还需要另外声明一个**“容器注解”**@Identifies
:
public @interface Identifies {
Identify[] value();
}
它内部的元素只有一个@Identify
注解数组,并声明为value()
。同时,我们还需要在原先的@Identify
注解中添加@Repetable
元注解,表示使用@Identifies
作为装入重复@Identify
的容器。
//补充后的Identify注解
@Repeatable(Identifies.class)
public @interface Identify {
enum CAREER{TEACHER,STUDENT,FATHER,SPIDER_MAN}
CAREER value() default CAREER.STUDENT;
}
随后我们就可以在一个Person
类中做如下标注:
//这个Person的身份即是父亲,又是老师。
@Identify(Identify.CAREER.TEACHER)
@Identify(Identify.CAREER.FATHER)
public class Person { }
两点约束
注意,@Identify
和其容器注解@Identifies
相比:
1、 前者的元注解@Target
所表示的范围要比后者大(后者适用更多的ElementType
值,且包含后者所有的ElementType
值)或者相同。
2、 后者(容器注解)的元注解@Rentention
所标记的生命周期要比前者长或者相同。
> 声明周期:SOURCE < CLASS < RUNTIME
如果不满足以上任何一个条件,编译器则会提示两种编译错误。详情可以参考下方链接:
注意
实际上,后续通过反射来验证,编译器认为该Person类仍然只持有一个注解。因为所有重复的@Identify
都装入到了一个@Identify[]
类型的value数组中。(实际是在给@Identifies
赋值)
@Documented
若一个类使用了被元注解@Documented
标记的注解,在生成javadoc文档时会保留该注解。比如我们将上文的@Identifies
注解添加该元注解:
@Documented
public @interface Identifies {
Identify[] value();
}
在命令行中使用javadoc
命令为Person.java
生成注释:
javadoc -d <docPath> <javaPath> -version -author
在文档中,Person
类的注释将被标记出来。
Java内部提供的实用注解
除了元注解以外,Java还提供了许多其它的实用注解:
注解 | 功能 |
---|---|
@Deprecated |
该注解标记的内容表示“过时”,编译器会发出警告,并标记为删除线。 |
@Override |
只能标注方法,编译器检测该方法是否是覆盖了父类中的方法。 |
@SuppressWarnings |
编译器忽略指定类型的警告,不再弹出warnings. |
这些注解的生存周期为CLASS
,即只用于编译器对代码的检查,而不会影响运行期间程序的执行。
通过反射获取注解
java.lang.Reflect
反射包下新增了AnnotatedElement
接口,它表示JVM在运行期间已载入的注解元素。反射包的Construtor
类,Field
类,Method
类,Package
类和Class
类都实现了AnnotationElement
接口。
AnnotationElement
接口在1.5版本提供了前4个实用方法。在1.8版本后又额外提供了后2个方法,用于获取被@Repetable
标注的可重复注解(下文会介绍两个方法的细节)。
返回值 | 方法名称 | 作用 |
---|---|---|
<A extends Annotation> |
getAnnotation(Class<A> AnoClass) |
如果该元素上存在指定的注解,则返回,反之为null。 |
Annotation[] |
getAnnotations() |
获取此元素上的所有注解,如果该元素属于Class,则还包括其父类中标记为@Inherited 的注解。 |
boolean |
isAnnotationPresent(Class<? extends Annotation> AnoClass) |
如果该元素上存在指定的注解,则返回true 。 |
Annotation[] |
getDaclaredAnnotations() |
返回直接存在于此元素上的注解。若没有注解可返回,则返回长度为0的数组。 |
<A extends Annotation> A[] |
getAnnotationsByType(Class<A> annotationClass) |
[Java 8] 返回此元素上的注解,对@Repetable 注解同样生效。可能会返回父类的注解。 |
<A extends Annotation> A[] |
getDeclaredAnnotationsByType(Class<A> annotationClass) |
[Java 8] 返回此元素本身的注解,对@Repetable 注解同样生效。 |
通过一个完整实例尝试捕获注解
假设Student
目前有以下注解(这些注解的生存周期都是RUNTIME
):
/**
* I'm spider man!
*/
@DBTable(TableName = "students")
@Identify(Identify.CAREER.SPIDER_MAN)
public class Student extends Person {
@StringValue("Peter")
private String name;
@IntegerValue(22)
private int age;
}
为了获取所有的注解,我们需要从Student
类的Class
以及Fields
中通过上述方法反射注解。完整逻辑代码如下:
Class<?> clazz = Student.class;
//获取Student类中的注解
for (Annotation annotation : clazz.getAnnotations()) {
System.out.println(annotation);
}
//获取Student每个私有域的所有注解
for (Field field : clazz.getDeclaredFields()) {
field.setAccessible(true);
for (Annotation annotation : field.getAnnotations()) {
System.out.println(annotation);
}
}
//需要 throws NoSuchFieldException
StringValue stringValue = clazz.getDeclaredField("name").getAnnotation(StringValue.class);
if (stringValue != null) System.out.println("通过getAnnotation获取了name域的注解:" + stringValue);
if (clazz.getDeclaredField("age").isAnnotationPresent(IntegerValue.class))
{
System.out.println("通过isAnnotationPresent方法检测age域是否存在IntegerValue注解:true");
}
最后屏幕的打印结果:
@ano.DBTable(TableName=students)
@ano.Identify(value=SPIDER_MAN)
@ano.StringValue(value=Peter)
@ano.IntegerValue(value=22)
通过getAnnotation获取了name域的注解:@ano.StringValue(value=Peter)
通过isAnnotationPresent方法检测age域是否存在IntegerValue注解:true
Process finished with exit code 0
获取@Repetable标记的重复注解
在Java 1.8版本之后,主要是通过getAnnotationsByType
和getDeclaredAnnotationsByType
来获取重复的标签。注意,之
假设现在有如下的继承关系:
/**
* He is a Teacher and father.
*/
@Identify(Identify.CAREER.TEACHER)
@Identify(Identify.CAREER.FATHER)
public class Person { }
/**
* I'm spider man!
*/
@DBTable(TableName = "students")
@Identify(Identify.CAREER.SPIDER_MAN)
public class Student extends Person {}
补充@Identify
和@Identifies
的相关细节。注意,一定要显式将@Retention
补充为运行时,否则程序无法通过反射捕获;另外,这两个注解都是可以在子类中被捕获的:
@Documented
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Inherited
public @interface Identifies {
Identify[] value();
}
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Repeatable(Identifies.class)
@Inherited
public @interface Identify {
enum CAREER{TEACHER,STUDENT,FATHER,SPIDER_MAN}
CAREER value() default CAREER.STUDENT;
}
通过getAnnotationsByType
来获取Student
上所有保留的可重复@Identify
标签:
Class<?> clazz = Student.class;
//只获取Student上的注解。
Identify[] identifies = clazz.getDeclaredAnnotationsByType(Identify.class);
for (Identify identify : identifies) {
System.out.println(identify.value());
}
System.out.println("length:" + identifies.length);
打印结果是:SPIDER_MAN,length:1
。那我们使用了getDeclaredAnnotationsByType
方法,是不是就能获取Student
和Person
类中所有的Identify
注解了呢?
我们修改注解的获取方法,重新执行一次这个方法。
//试图获取Student和Person所有的Identify注解。可行吗?
Identify[] identifies = clazz.getDeclaredAnnotationsByType(Identify.class);
但是结果并没有变化。我们进入到AnnotatedElement
中查看getAnnotationsByType
相关的源码:
default <T extends Annotation> T[] getAnnotationsByType(Class<T> annotationClass) {
/*
* Definition of associated: directly or indirectly present OR
* neither directly nor indirectly present AND the element is
* a Class, the annotation type is inheritable, and the
* annotation type is associated with the superclass of the
* element.
*/
T[] result = getDeclaredAnnotationsByType(annotationClass);
if (result.length == 0 && // Neither directly nor indirectly present
this instanceof Class && // the element is a class
AnnotationType.getInstance(annotationClass).isInherited()) { // Inheritable
Class<?> superClass = ((Class<?>) this).getSuperclass();
if (superClass != null) {
// Determine if the annotation is associated with the
// superclass
result = superClass.getAnnotationsByType(annotationClass);
}
}
return result;
}
也就是说,getAnnotationsByType
在执行之前,首先会尝试getDeclaredAnnotationsByType
方法获取类自身的注解,只有在当前类没有找到任何有关的注解时,才会尝试去父类当中寻找注解,并且前提是这个注解是允许子类继承的。
一言以蔽之,我们要么只能获取Student
类的@Identify
注解,要么只能获取Person
类的@Idenfity
注解,且需要保证:@Identify
和@Identifies
注解是可继承的;Student
类中没有Identify
注解。
为了证明这个结论,我们使用代码来验证。我们将Student
类中的@Identify
注解抹掉,然后重新运行主程序:
/**
* I'm spider man!
*/
@DBTable(TableName = "students")
public class Student extends Person {}
这次,主程序中打印的是:TEACHER,FATHER;length:2
。此刻我们再将@Identify
和@Identifies
的@Inherited
元注解也取消掉,再次运行主程序:
@Documented
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface Identifies {
Identify[] value();
}
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Repeatable(Identifies.class)
public @interface Identify {
enum CAREER{TEACHER,STUDENT,FATHER,SPIDER_MAN}
CAREER value() default CAREER.STUDENT;
}
主程序显示:length:0
。
参数行注解
参数行注解的形式如下。Spring为我们提供的@PathVariable
,@RequestBody
等都属于参数行注解。
//@NotNull就是Java为我们提供的参数行注解。
public static void PrintString(@NotNull String s)
{
System.out.println(s);
}
使用参数行注解来实现参数为空的默认赋值操作
我们完全可以使用Optional[T]来快速实现功能,在这里只是为了对参数行注解的定义和使用进行介绍。
我们定义一个新的注解@DefaultInt
,并为它的value
赋值为1。我们希望它提供一个保障机制:当外部传入的参数为null时,则使用@DefaultInt
提供的value做为默认值。
另外,将@Target
元注解的值声明为ElementType.PARAMETER
,来表示它是一个参数行注解。
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Target(ElementType.PARAMETER)
@Retention(RetentionPolicy.RUNTIME)
public @interface DefaultInt {
int value() default 1;
}
随后我们定义一个安全的add方法,并通过注解为参数a
和b
赋值。这里由我们自己手写检查机制:
public static int safeAdd(@DefaultInt(1) Integer a,@DefaultInt(2) Integer b) throws Exception {
//--首先通过反射获取方法--------//
Method safeAdd = Main.class.getDeclaredMethod("safeAdd", Integer.class, Integer.class);
//每个参数可能又包含多个注解,因此返回值是一个二维数组。
Annotation[][] parameterAnnotations = safeAdd.getParameterAnnotations();
List<Integer> DEFAULTINTS = new LinkedList<>();
//遍历每个参数的(多个)注解。
for (Annotation[] p: parameterAnnotations) {
DefaultInt defaultInt = null;
//检查是否存在DefaultInt注解
for (Annotation e : p){
if (e instanceof DefaultInt) {defaultInt = (DefaultInt) e;break;}
}
if (defaultInt!=null) {
DEFAULTINTS.add(defaultInt.value());
}
}
//-----在这里其实忽略了很多对DEFAULTINTS的检查细节---------------//
a = a==null ? DEFAULTINTS.get(0) : a;
b = b==null ? DEFAULTINTS.get(1) : b;
return a + b;
}
随后,我们在主程序中故意声明两个为空的Integer
,并将它们传入到safeAdd
方法中:
Integer A = null;
Integer B = null;
try {
System.out.println(safeAdd(A,B));
} catch (Exception e) {
e.printStackTrace();
}
由于safeAdd
方法在真正进行计算之前,会首先通过注解获取默认值,因此即便A
和B
为null
,程序也不会报错。
简单的示例:根据注解拼装SQL语句
假设我们已经拥有了数据库连接,并且希望能够利用注解配置的方式,自动组合SQL,从而来实现一个最简易的ORM框架:
/**
* I'm spider man!
*/
@DBTable(TableName = "students")
public class Student extends Person {
@StringValue("Peter")
private String name;
@IntegerValue(22)
private int age;
}
设@DBTable
为配置的数据库表名,而@StringValue
和@IntValue
为默认的配置值。
希望程序能够根据这些注解信息就可以组装成SQL语句:
insert into students(name,age) values('Peter',22)
接下来使用注解配合反射的形式来实现这个功能:
public static String SQL(Class<?> c, db.Method method) {
StringBuilder SQLBuilder = new StringBuilder();
if (method == Method.INSERT) {
SQLBuilder.append("insert into");
}
DBTable dbTable = c.getAnnotation(DBTable.class);
//insert into student() values()
if (dbTable != null) {
SQLBuilder.append(" ");
SQLBuilder.append(dbTable.TableName());
}
Field[] declaredFields = c.getDeclaredFields();
StringBuilder columnBuilder = new StringBuilder("(");
StringBuilder valueBuilder =new StringBuilder(" values(");
for (Field f:
declaredFields) {
columnBuilder.append(f.getName());
columnBuilder.append(",");
Annotation[] annotations = f.getAnnotations();
for (Annotation a:
annotations) {
if(a instanceof IntegerValue){
valueBuilder.append(((IntegerValue) a).value());
valueBuilder.append(",");
}
if(a instanceof StringValue){
valueBuilder.append("'");
valueBuilder.append(((StringValue) a).value());
valueBuilder.append("',");
}
}
}
columnBuilder.deleteCharAt(columnBuilder.lastIndexOf(","));
columnBuilder.append(")");
valueBuilder.deleteCharAt(valueBuilder.lastIndexOf(","));
valueBuilder.append(")");
return SQLBuilder.append(columnBuilder.toString()).append(valueBuilder.toString()).toString();
}
//----通过枚举类表示数据库操作----------//
public enum Method {
//笔者只定义了一种。
INSERT
}
我们在主函数中只需要传入”POJO”对象,并表示要执行Insert
方法,程序就会自动生成SQL语句了。
public static void main(String[] args) throws IllegalAccessException {
System.out.println(SQL(Student.class, Method.INSERT));
}
这种操作方式,是否想起了Hibernate框架?
参考文献
1、 深入理解Java注解类型
2、 菜鸟教程: Java 注解
3、 java注解-最通俗易懂的讲解