一:基本语法
1.1 声明注解与元注解
先看看@MyTest注解是如何声明的:
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface MyTest {
}
使用@interface来声明MyTest是个注解,@Target(ElementType.METHOD)来声明这个注解应用于方法上面,@Retention(RetentionPolicy.RUNTIME)声明这个注解的生存期是运行时。反编译一下MyTest:
public interface MyTest implements Annotation {
}
注解就是一个实现了Annotation接口的接口。@Target和@Retention是Java提供的元注解,元注解就是标记其他注解的注解,下面分别介绍。
- @Target用来约束注解可以应用的地方(如方法、类或字段),其中ElementType是枚举类型,代表可能的取值范围
public enum ElementType {
/** 类、接口(包括注解)、枚举声明 */
TYPE,
/** 字段声明 (包括枚举) */
FIELD,
/** 方法声明 */
METHOD,
/** 参数声明 */
PARAMETER,
/** 构造方法声明 */
CONSTRUCTOR,
/** 局部变量声明 */
LOCAL_VARIABLE,
/** 注解声明(可应用于注解之上)*/
ANNOTATION_TYPE,
/** 包声明 */
PACKAGE,
/**
* 参数类型声明
*
* jdk1.8加入的
*/
TYPE_PARAMETER,
/**
* 类型使用声明
*
* jdk1.8加入的
*/
TYPE_USE
}
当注解未指定Target值时,则此注解可以用于任何元素之上,多个值使用{}包含并用逗号隔开,如下:
@Target(value={CONSTRUCTOR, FIELD, LOCAL_VARIABLE, METHOD, PACKAGE, PARAMETER, TYPE})
- @Retention用来约束注解的生命周期,其中RetentionPolicy是枚举类型,代表可能的取值范围
public enum RetentionPolicy {
/**
* 源码级别,注解将被编译器丢弃
*/
SOURCE,
/**
* 类文件级别,注解将存在源码和class文件中,会被JVM丢弃,默认的就是CLASS级别。
*/
CLASS,
/**
* 注解信息将在运行期(JVM)也保留,因此可以通过反射机制读取注解的信息
*/
RUNTIME
}
1.2 注解元素及其数据类型
@Target(ElementType.TYPE)//应用于类上
@Retention(RetentionPolicy.RUNTIME)//运行时
public @interface MyTable {
String name() default "";//声明一个String类型的元素,default设置默认值
}
自定义MyTable注解用于数据库表与Bean类的映射(后面会有完整案例分析),@MyTable的使用方式如下:
@MyTable(name = "STUDENT")
public class Student {
...
}
注解支持的元素的数据类型如下:
- 所有的基本数据类型(byte、short、int、long、float、double、char、boolean)
- String
- Class
- Enum
- Annotation
- 上述类型的数组
下面的代码演示了上述类型的使用过程:
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface MyTable {
int age();//基本数据类型
String name() default "";//String
Class<?> testCase() default Void.class;//Class
enum sex {MALE, FEMALE};//枚举
Reference reference() default @Reference(next = true);//注解
long[] arr();//数组
}
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@interface Reference{
boolean next() default false;
}
注意,注解中所有的元素必须有值,要么是默认的值,要么是使用注解时提供的值,不能为null。注解不支持继承,一个注解不能使用关键字extends来继承其他注解。当注解中定义了名称为value的元素时,如果该元素是唯一一个需要赋值的元素,那么无需使用key=value的模式,直接在括号内给出value值即可。
1.3 Java内置注解与其他元注解
内置注解:
- @Override:源码级,用于标识此方法覆盖了父类方法。
- @Deprecated:运行时,用于标识此方法或类过时了。
- @SuppressWarnnings:源码级,用于有选择的关闭编译器对类、方法、成员变量、变量初始化的警告。
元注解:
前面分析了@Target和@Retention,还有三种元注解,@Documented和@Inherited。
- @Documented 运行时,被修饰的注解会生成到javadoc中。
- @Inherited 运行时,可以让子类Class对象使用getAnnotations()来获得父类被@Inherited修饰的注解。
- @Repeatable 运行时,它表示在同一个位置可以重复相同的注解。
二、注解与反射机制
通过反编译可以知道,Java中所有的注解都实现了Annotation接口,实现了Annotation接口就说明这个类是一个注解。注解有了,那么如何获取到注解的信息呢?Java在java.lang.reflect 反射包下新增了AnnotatedElement接口,实现了这个接口的类代表了这个类是被注解修饰的元素,可以得到相应的注解的信息,反射包中的Constructor类、Field类、Method类、Package类和Class类都实现了AnnotatedElement接口,也就是说可以通过反射技术获取到注解信息。
AnnotatedElement相关API:
返回值 | 方法名称 | 说明 |
---|---|---|
boolean | isAnnotationPresent(Class<? extends Annotation> annotationClass) | 指定类型的注解是否此元素上 |
getAnnotation(Class annotationClass) | 该元素如果存在指定类型的注解,则返回这些注解,否则返回 null。 | |
Annotation[] | getDeclaredAnnotations() | 返回直接存在于此元素上的所有注解,不包括父类的注解 |
代码说明:
@Inherited
@Documented
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@interface DocumentA {
}
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@interface DocumentB {
}
@Retention(RetentionPolicy.RUNTIME)
@Target(METHOD)
@Inherited
@interface DocumentC {
}
@DocumentA
class A{ }
//继承了A类
@DocumentB
public class DocumentDemo extends A{
@DocumentC
public static void test() {
}
public static void main(String... args) throws NoSuchMethodException {
Class<?> clazz = DocumentDemo.class;
//根据指定注解类型获取该注解
DocumentA documentA = clazz.getAnnotation(DocumentA.class);
System.out.println("A:" + documentA);
//获取该元素上的所有注解,包含从父类继承
Annotation[] an = clazz.getAnnotations();
System.out.println("an:" + Arrays.toString(an));
//获取该元素上的所有注解,但不包含继承
Annotation[] an2 = clazz.getDeclaredAnnotations();
System.out.println("an2:" + Arrays.toString(an2));
//判断注解DocumentA是否在该元素上
boolean b = clazz.isAnnotationPresent(DocumentA.class);
System.out.println("b:" + b);
//获取方法上的注解
Method method = clazz.getMethod("test");
DocumentC c = method.getAnnotation(DocumentC.class);
System.out.println("c: " + c);
}
}
输出:
A:@com.example.demo.thread.DocumentA()
an:[@com.example.demo.thread.DocumentA(), @com.example.demo.thread.DocumentB()]
an2:[@com.example.demo.thread.DocumentB()]
b:true
c: @com.example.demo.thread.DocumentC()
三、运行时注解处理器
经过上面的分析,我们已经自定义了注解,并且也获取到了注解,那么获取到注解有什么用呢?这个时候我们需要自己写一些处理逻辑来用上注解。下面通过一个实例来说明如何利用注解和反射来实现一个Java Bean和数据库表的映射功能。
/**
* 表名注解
*/
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface MyTable {
String name() default "";
}
/**
* 注解int型字段
*/
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface SQLInteger {
//该字段对应数据库表列名
String name() default "";
//嵌套注解
Constraints constraint() default @Constraints;
}
/**
* 注解String型字段
*/
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface SQLString {
//该字段对应数据库表列名
String name() default "";
//列类型分配的长度,如varchar(30)的30
int value() default 0;
//嵌套注解
Constraints constraints() default @Constraints();
}
/**
* 约束注解
*/
@Target(ElementType.FIELD)//只能应用在字段上
@Retention(RetentionPolicy.RUNTIME)
public @interface Constraints {
//判断是否作为主键约束
boolean primaryKey() default false;
//判断是否允许为null
boolean allowNull() default false;
//判断是否唯一
boolean unique() default false;
}
上面的@MyTable、@SQLInteger、@SQLString用于构建数据库表语句,@Constraints嵌套注解用于标识字段是否可以为null、是否唯一、是否是主键等信息,注解的声明周期必须是Runtime的,这样才能在运行时通过反射技术来获取注解的信息。注解定义好了,接下来使用注解:
/**
* Member Bean
*/
@MyTable(name = "MEMBER")
public class Member {
//主键
@SQLInteger(name = "ID", constraint = @Constraints(primaryKey = true, unique = true))
private Integer id;
@SQLString(name = "NAME", value = 50)
private String name;
@SQLInteger(name = "AGE")
private Integer age;
@SQLString(name = "DESCRIPTION", value = 50, constraints = @Constraints(allowNull = true))
private String description;
}
给Bean Member类使用注解。接下来获取注解并编写处理逻辑:
public class TableCreator {
public static String createTableSQL(String className) throws ClassNotFoundException {
//根据classNam通过反射获取到Class对象
Class<?> aClass = Class.forName(className);
//得到类上的注解
MyTable annotation = aClass.getAnnotation(MyTable.class);
if (annotation == null) {
return null;
}
List<String> columnDefs = new ArrayList<String>();
//得到类注解上的name元素
String tableName = annotation.name();
//获取类中所有的字段
for (Field field : aClass.getDeclaredFields()) {
Annotation[] declaredAnnotations = field.getDeclaredAnnotations();
if (declaredAnnotations.length < 1) {
continue;
}
//如果是SQLString注解
if (declaredAnnotations[0] instanceof SQLString) {
SQLString sqlString = (SQLString) declaredAnnotations[0];
String name = sqlString.name();
columnDefs.add(name + " VARCHAR(" + sqlString.value() + ")" + getConstraints(sqlString.constraints()));
}
//如果是SQLInteger注解
if (declaredAnnotations[0] instanceof SQLInteger) {
SQLInteger sqlInteger = (SQLInteger) declaredAnnotations[0];
String name = sqlInteger.name();
columnDefs.add(name + " INT" + getConstraints(sqlInteger.constraint()));
}
}
//数据库建表语句构建
StringBuilder createSQL = new StringBuilder("CREATE TABLE " + tableName + "(");
for (String columnDef : columnDefs) {
createSQL.append("\n " + columnDef + ",");
}
String tableCreate = createSQL.substring(
0, createSQL.length() - 1) + ");";
return tableCreate;
}
//获取约束条件
private static String getConstraints(Constraints con) {
String constraints = "";
if(!con.allowNull())
constraints += " NOT NULL";
if(con.primaryKey())
constraints += " PRIMARY KEY";
if(con.unique())
constraints += " UNIQUE";
return constraints;
}
public static void main(String[] args) throws ClassNotFoundException {
String[] arg={"com.example.demo.thread.Member"};
for(String className : arg) {
System.out.println("Table Creation SQL for " +
className + " is :\n" + createTableSQL(className));
}
}
}
输出:
Table Creation SQL for com.example.demo.thread.Member is :
CREATE TABLE MEMBER(
ID INT NOT NULL PRIMARY KEY UNIQUE,
NAME VARCHAR(50) NOT NULL,
AGE INT NOT NULL,
DESCRIPTION VARCHAR(50));
大概流程:先传递类名,根据Class.forName()方法得到Class对象,然后通过Class对象获得所有的成员字段Field,最后利用field.getDeclaredAnnotations()来获取字段上的注解,判断注解的类型,解析注解的元素获取到值,然后构建SQL语句。