[java]注解
注解(也被称为元数据)为我们在代码中添加信息提供了一种形式化的方法,使我们可以在稍后某个时刻非常方便地使用这些数据。
注解的语法比较简单,除了 @符号的使用之外,它基本上与 Java 固有的语法一致。java SE5 内置了三种,定义在 java.lang 中的注解:
- @Override,表示当前的方法定义将覆盖父类中的方法。
- @Deprecated,如果程序员使用了注解它的元素,那么编译器将会发出警告信息。
- @Suppress Warnings,关闭不当的 编译器警告信息。在 java SE5 之前的版本中,也可以使用该注解,不过会被忽略不起作用。
一、元注解
Java 目前只内置了三种标准注解,以及四种元注解。元注解专职负责注解其他的注解:
@Target | 表示该注解可以用于什么地方。可能的 ElementType 参数包括: |
CONSTRUCTOR: 构造器的声明 | |
FIELD: 域声明(包括 enum 实例) | |
LOCAL_VARIABLE: 局部变量声明 | |
METHOD: 方法声明 | |
PACKAGE: 包声明 | |
PATAMETER: 参数声明 | |
TYPE: 类、接口(包括注解类型)或 enum 声明 |
@Retention | 表示需要在什么级别保存该注解信息。可选的 RetentionPolicy 参数包括: |
SOURCE: 注解将被编译器丢弃 | |
CLASS: 注解在 class 文件中可用,但会被 VM 丢弃 | |
RUNTIME:VM 将在运行期也保留注解,因此可以通过反射机制读取注解的信息 |
@Documented | 将此注解包含在 Javadoc 中 |
@Inherited | 允许子类继承父类中的注解 |
二、定义注解
在注解中,一般都会包含一些元素以表示某些值。当分析处理注解时,程序或工具可以利用这些值。注解的元素看起来就像接口的方法,唯一的方法是你可以为其制定默认值。
没有元素的注解称为标记注解。
@interface 用来声明一个注解,其中的每一个方法实际上是声明了一个配置参数。方法的名称就是参数的名称,返回值类型就是参数的类型(返回值类型只能是基本类型、Class、String、enum)。可以通过 default 来声明参数的默认值。
下面是一个简单的注解,我们可以用它来跟踪一个项目中的用例。
//UseCase.java
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Target(ElementType.METHOD)@Retention(RetentionPolicy.RUNTIME)public @interface UseCase {int id();
String description() default "no description";}
//PasswordUtils.java
import java.util.List;
public class PasswordUtils {@UseCase(id=47,description = "Passwords must comtain at least one numeric")
public boolean validatePassword(String password){return (password.matches("\\w*\\d\\w*"));}@UseCase(id=48)public String encryptPassword(String password){
return new StringBuilder(password).reverse().toString();}@UseCase(id = 49,description = "New passwords can not equal previously used ones")
public boolean checkForNewPassword(List<String> prevPasswords,String password){return !prevPasswords.contains(password);
}}
注解的元素在使用时表现为名 - 值对的形式,并需要置于 @UseCase 声明之后的括号内。在 encryptpassword() 方法的注解中,并没有给出 description 元素的值,因此,在 UseCase 的注解处理器分析处理这个类时会使用该元素的默认元素。
三、编写注解处理器
使用注解的过程中,很重要的一个部分就是创建与使用注解处理器。
下面是一个非常简单的注解处理器,我们将用它来读取 PasswordUtils 类,并使用反射机制查找 @UseCase 标记。我们为其提供了一组 id 值,然后他会列出在 PasswordUtils 中找到用例,以及缺失的用例。
//UseCaseTracker.java
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
public class UseCaseTracker {public static void main(String[] args) {List<Integer> useCases=new ArrayList<>();
Collections.addAll(useCases,47,48,49,50);trackUseCases(useCases,PasswordUtils.class);
}private static void trackUseCases(List<Integer> useCases, Class<?> cl) {for (Method m:cl.getDeclaredMethods()){
UseCase uc = m.getAnnotation(UseCase.class);
if (uc!=null){System.out.println("Found Use Case:"+uc.id()+" "+uc.description());useCases.remove(new Integer(uc.id()));
}}for (int i:useCases){System.out.println("Warning: Miss use case-"+i);
}}}
输出:
Found Use Case:47 Passwords must comtain at least one numericFound Use Case:48 no descriptionFound Use Case:49 New passwords can not equal previously used onesWarning: Miss use case-50
这个程序用到了两个反射的方法:getDeclaredMethod()和 getAnnotation(),它们都属于 AnnotatedElement 接口 (Class、Method 和 Field 等类都实现了该接口)。getAnnotation() 方法返回指定类型的注解对象,在这里就是 UseCase。如果被注解的方法上没有该类型的注解,则返回 null。然后我们通过调用 id()和 description()方法从返回的 UseCase 对象中提取元素的值。其中,encryptPassword()方法在注解的时候没有指定 description 的值,因此处理器在处理它对应的注解时,通过 description()方法取得的是默认值 no description。
四、注解元素
标签 @UseCase 由 UseCase.java 定义,其中包含 int 元素 id,以及一个 String 元素 description。注解元素可用的类型如下所示:
- 所有基本类型(int,float,boolean 等)
- String
- Class
- enum
- Annotation
- 以上类型的数组
如果使用其他类型,编译器会报错。
五、默认值设置
元素不能有不确定的值。也就是说,元素必须要么具有默认值,要么在使用注解时提供元素的值。
其次,对于非基本类型的元素,无论是在源代码中声明或是在注解接口中定义默认值时,都不能以 null 作为其值。这个约束使得处理器很难表现一个元素的存在或缺失的状态,因为在每个注解的声明中,所有的元素都是存在,并且都具有相应的值。。为了绕开这个约束,我们只能定义一些特殊的值,例如空字符串或负数,以表示某个元素不存在。
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Target(ElementType.METHOD)@Retention(RetentionPolicy.RUNTIME)@interface SimulatingNull {
int id() default -1;String description() default "";
}
六、生成外部文件
假设你希望提供一些基本的对象 / 关系映射功能,能够自动生成数据库表,用以存储 JavaBean 对象。你可以选择使用 XML 描述文件,指明类的名字、每个成员以及数据库映射的相关信息。然而,使用注解的话,你可以将所有信息都保存在 JavaBean 源文件中。为此,我们需要一些新的注解,用以定义与 Bean 关联的数据库表的名字,以及与 Bean 属性关联的列的名字和 SQL 类型。
以下是一个注解的定义,它告诉注解处理器,你需要生成一个数据库表:
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* Created by pc on 2016/2/18.*/@Target(ElementType.TYPE)@Retention(RetentionPolicy.RUNTIME)public @interface DBTable {public String name() default "";}
在 @Target 注解中指定的每一个 ElementType 就是一个约束,它告诉编译器,这个自定义的注解只能应用于该类型。程序员可以指定 enum ElementType 中的某一个值,或者以逗号分隔的形式指定多个值。如果想要将注解应用于所有的 ElementType 那么可以省去 @Target 元注解,不过这并不常见。
注意,@DBTable 有一个 name() 元素,该注解通过这个元素为处理器创建数据库表提供表的名字。
接下来是为修饰 JavaBean 域准备的注解:
//Constraints.java
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Target(ElementType.FIELD)@Retention(RetentionPolicy.RUNTIME)public @interface Constraints {boolean primaryKey() default false;boolean allowNull() default true;boolean unique() default false;}
//SQLString.java
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Target(ElementType.FIELD)@Retention(RetentionPolicy.RUNTIME)public @interface SQLString {int value() default 0;String name() default "";
Constraints constraints() default @Constraints;
}
//SQLInteger.java
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Target(ElementType.FIELD)@Retention(RetentionPolicy.RUNTIME)public @interface SQLInteger {String name() default "";
Constraints constraints() default @Constraints;
}
注解处理器通过 @Constraints 注解提取出数据表的元数据,另外两个 @interface 定义的是 SQL 类型。这些 SQL 类型具有 name()元素和 constraints() 元素。后者利用了注解嵌套的功能将 column 类型的数据库约束信息嵌入其中。注意 constraints()元素的默认值时 @Constraints。由于 @Constraints 注解类型之后,没有在括号中指明 @Constraints 中的元素的值。因此,constraints() 元素的默认值实际上就是一个所有元素艘为默认值的 @Constraints 注解。如果要领嵌入 @Constraints 注解中的 unique()元素为 true,并以此作为 constraints() 元素的默认值,则需要如下定义元素:
public @interface Uniqueness {Constraints constraints() default @Constraints(unique = true);}
下面是一个简单的 Bean 定义,我们在其中应用了以上的这些注解:
@DBTable(name = "MEMBER")
public class Member {static int memberCount;@SQLString(30)String firstName;@SQLString(50)String lastName;@SQLIntegerInteger age;@SQLString(value = 30, constraints = @Constraints(primaryKey = true))
String handle;public String getHandle() {
return handle;
}public String getFirstName() {
return firstName;
}public String getLastName() {
return lastName;
}public String toString() {
return handle;
}public Integer getAge() {
return age;
}}
类的注解 @DBTable 给定了值 MEMBER,它将会用来作为表的名字。Bean 的属性 firstName 和 lastName,都被注解为 @SQLString 类型,并且其元素值分别为 30 和 50,。这些注解有两个有趣的地方:第一,他们都使用了嵌入的 @Constraints 注解的默认值;第二,它们都使用了快捷方式,何为快捷方式呢?如果程序员的注解中定义了名为 value 的元素,并且在应用该注解的时候,如果该元素是唯一需要赋值的一个元素,那么此时无需使用名 - 值的这种语法,而只需要在括号内给出 value 元素所需要的值即可。这可以应用于任何合法类型的元素。当然了,这也限制了程序员必须将次元素命名为 value,不过在上面的例子中,这不但时语义更清晰,而且这样的注解语句也更容易理解:
@SQLString(30)
处理器将在创建表的时候使用该值设置 SQL 列的大小。
实现处理器:
import java.lang.annotation.Annotation;
import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.List;
/**
* Created by pc on 2016/2/18.*/public class TableCreator {public static void main(String[] args) throws Exception {if (args.length < 1) {
System.out.println("argument:annotated classes");
System.exit(0);}for (String className : args) {
Class<?> cl = Class.forName(className);DBTable dbTable = cl.getAnnotation(DBTable.class);
if (dbTable == null) {System.out.println("No DBTable annotations in class " + className);
continue;
}String tableName = dbTable.name();//if the name is empty,use the class name
if (tableName.length() < 1) {
tableName = cl.getName().toUpperCase();}List<String> columnDefs = new ArrayList<>();
for (Field field : cl.getDeclaredFields()) {
String columnName = null;
Annotation[] anns = field.getDeclaredAnnotations();if (anns.length < 1) {
continue;
}if (anns[0] instanceof SQLInteger) {SQLInteger sInt = (SQLInteger) anns[0];//Use field name if name not specified
if (sInt.name().length()< 1) {
columnName = field.getName().toUpperCase();} else {
columnName = sInt.name();}columnDefs.add(columnName + " INT " + getConstraints(sInt.constraints()));
}if (anns[0] instanceof SQLString) {SQLString sString = (SQLString) anns[0];if (sString.name().length()< 1) {
columnName = field.getName().toUpperCase();} else {
columnName = sString.name();}columnDefs.add(columnName + " VARCHAR("+ sString.value() +")" + getConstraints(sString.constraints()));}StringBuilder createCommand = new StringBuilder("CREATE TABLE"+ tableName +"(");for (String columnDef : columnDefs)
createCommand.append("\n "+ columnDef +",");String tableCreate = createCommand.substring(0, createCommand.length() - 1) + ");";
System.out.println("Table Creation SQL for "+ className +" is :\n" + 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;
}}
输入 args: Member
输出:
Table Creation SQL for Member is :
CREATE TABLEMEMBER(FIRSTNAME VARCHAR(30));Table Creation SQL for Member is :
CREATE TABLEMEMBER(FIRSTNAME VARCHAR(30),LASTNAME VARCHAR(50));Table Creation SQL for Member is :
CREATE TABLEMEMBER(FIRSTNAME VARCHAR(30),LASTNAME VARCHAR(50),AGE INT );Table Creation SQL for Member is :
CREATE TABLEMEMBER(FIRSTNAME VARCHAR(30),LASTNAME VARCHAR(50),AGE INT ,HANDLE VARCHAR(30) PRIMARY KEY);
main()方法会处理命令行传入的每一个类名。使用 forName() 方法加载每一个类,并使用 getAnnotation(DBTable.class) 检查该类是否带有 @DBTable 注解。如果有,就将发现的表名保存下来。然后读取这个类的所有域,并用 getDeclaredAnnotation()进行检查。该方法返回一个包含一个域上的所有注解的数组。最后用 instanceof 操作符来判断这些注解是否是 @SQLInteger 或 SQLString 类型,如果是的话,在对应的处理块中将构造出相应 column 名的字符串片段。注意,由于注解没有继承机制,所以要获得近似多态上网行为,使用 getDeclaredAnnotation() 是唯一的方法。
嵌套中的 @Constraint 注解被传递给 getConstraints() 方法,由它负责构造一个包含 SQL 约束的 String 对象。
需要注意的是,上面的演示的技巧对于真实的对象 / 关系映射而言,是很幼稚的。例如使用 @DBTable 类型的注解,程序员以参数的形式给出表的名字,如果程序员想要改变表的名字,这将迫使其必须重新编译 Java 代码。这不是我们希望看到的结果。现在已经有了很多可用的 framework,可以将对象映射到关系数据库,并且其中越来越多的 framework 已经开始利用注解了。