注解的作用
注解(也被称为元数据)为我们在代码中添加信息提供了一种形式化的方法,使我们可以在稍后某个时刻非常方便的使用这些信息数据。
注解在一定程度上是把元数据和源代码文件结合在一起,而不是保存在外部文档中。它可以使得我们能够让编译器来测试和验证格式,存储有关程序的额外信息。注解还可以生成描述符文件,甚至或是新的类定义,并且有助于减轻编写“样板”代码的负担。
注解是在实际的源代码级别保存所有的信息,而不是某种注释性的文字,这使得代码更整洁且便于维护。
在java.lang中包含三种内置注解:
- @Override,表示当前方法定义将覆盖超类中的方法,如果签名出错会收到编译器的提示;
- @Deprecated,表示过期或不推荐的方法;
- @SuppressWarnings,关闭不当的编译器警告信息。
Java还另外提供了四种注解,专门负责新注解的创建。当创建描述符性质的类或者接口时,并且其中包含重复性工作,可以考虑使用注解来简化与自动化该过程。
定义注解
定义注解和定义接口很像,它会需要一些元注解(meta-annotation),如@Target和@Retention。使用@Target可以表明注解可应用于什么地方,@Retention表明用在哪个级别。
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface UseCase {
public int id();
public String description() default "no description";
}
元注解
Java目前只内置了三种标准注解(@Override、@Deprecated、@SuppressWarnings)和四种元注解。
@Target | 表示该注解可以用于什么地方,可能的ElementType参数包括: CONSTRUCTOR:构造器的声明 FIELD:域声明(包括enum实例) LOCAL_VARIABLE:局部变量声明 METHOD:方法声明 PACKAGE:包声明 PARAMETER:参数声明 TYPE:类、接口(包括注解类型)或enum声明 |
@Retention | 表示需要在什么级别保存该注解信息。可选的RetentionPolicy参数包括: SOURCE:注解将被编译器丢弃 CLASS:注解在class文件中可用,但会被JVM丢弃 RUNTIME:JVM将在运行也保留注解,因此可以通过反射机制读取注解的信息 |
@Document | 将此注解包含在Javadoc中 |
@Inherited | 允许子类继承父类中的注解 |
注解处理器
如果没有用来读取注解的工具,那么注解也就和注释没有任何区别,在使用注解的过程中,很重要的一部分就是创建与使用注解工具。
public class PasswordUtils {
@UseCase(id = 47, description = "Passwords must contain 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’t equal previously used ones")
public boolean checkForNewPassword(List<String> prevPasswords, String password) {
return !prevPasswords.contains(password);
}
}
public class UseClassTracker {
public 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: Missing use case-" + i);
}
}
public static void main(String[] args) {
List<Integer> useCases = new ArrayList<>();
Collections.addAll(useCases, 47, 48, 49, 50);
trackUseCases(useCases, PasswordUtils.class);
}
}
UseClassTracker类用于读取PasswordUtils类,并使用反射机制查找@UseCase标志。其中使用了两个反射方法getDeclaredMethods()和getAnnotation(),它们都属于AnnotatedElement接口(Class,Method与Field等类都实现了该接口)。getAnnotation()返回了指定类型的注解对象,这里是UseCase,如果被注解的方法上没有该类型的注解,则会返回null。然后我们通过调用id()和description()方法从返回的UseCase对象中提取元素的值。encryptPassword()方法在注解的时候没有指定description的值,因此它会使用默认值“no description”。
注解元素
注解元素的所有可用类型包括:
- 所有基本类型(int,float,boolean等)
- String
- Class
- enum
- Annotation
- 以上所有类型的数组
例如,@UseCase使用了int类型的id,String类型的description。如果你使用了其他类型,那么会编译出错。注意这里也不允许使用包装类型。
默认值限制
编译器要求,元素不能有不确定的值,也就是说元素必须具有默认值,或者在使用注解时提供元素的值。另外,对于非基本类型的元素,无论在源代码中声明时,或者在注解接口中定义默认值时,都不能以null为其值。因此对于某些表示元素存在或者缺失的状态时,我们只能定义一些特殊的值,如空字符串或负数来表示。
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface SimulatingNull {
public int id() default -1;
public String description() default "";
}
注解的应用-ORM工具
生成外部文件
在许多对象/关系映射工具(如hibernate)中,一般会需要提供除了java bean之外的XML描述文件,而这些文件脱离于源代码之外,这经常会导致同步问题。同时它也要求为项目工作的程序员,必须同时知道如何编写java程序,以及如何编辑描述文件。然而,如果使用注解,就可以将所有信息保存在java bean源文件中。
@Target(ElementType.TYPE)// Applies to classes only
@Retention(RetentionPolicy.RUNTIME)
public @interface DBTable {
public String name() default "";
}
@DBTable中有name()元素,该注解为元素处理器提供创建数据库的表名。
// Constraints
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Constraints {
boolean primaryKey() default false;
boolean allowNull() default true;
boolean unique() default false;
}
//SQLString
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface SQLString {
int value() default 0;
String name() default "";
Constraints constraints() default @Constraints;
}
//SQLInteger
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface SQLInteger {
String name() default "";
Constraints constraints() default @Constraints;
}
@Constraints表示数据库能提供的部分约束。@SQLString和@SQLInteger提供两种基本数据类型的约束。这两种基本数据类型都提供了约束默认值@Constraints,它使用了所有元素都是默认值的@Constraints注解。如果要使用unique()元素为true的@Constraints注解,则需要定义如下类型的注解:
public @interface Uniqueness {
Constraints constraints() default @Constraints(unique = true);
}
使用以上注解可以定义以下bean:
@DBTable(name = "MEMBER")
public class Member {
@SQLString(30) String firstName;
@SQLString(50) String lastName;
@SQLInteger Integer age;
@SQLString(value = 30, constraints = @Constraints(primaryKey = true))
String handle;
static int numberCount;
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。firstName和lastName被注解为@SQLString,并且限制长度分别为30和50。这个注解有个特点:1.它们都使用了嵌套的@Constraints注解的默认值;2.它们都是用了快捷方式。所谓的快捷方式是指,在注解中定义了value()的元素,并且在应用注解的时候value()是唯一需要复制的元素,则可以直接为它赋值,而不需要使用key=value的形式,这可以应用于任意的合法类型的元素上。
变通之道
以上功能只是实现方式的一种,还有其他的方式实现类似功能:
- 使用单一注解类@TableColumn,它带有一个enum元素,它可以枚举STRING、INTEGER、以及FLOAT等实例,这样就可以不用定义许多@interface来管理数据类型,不过这样会使得无法精确控制每种类型,如控制长度或者精度等。
- 使用String元素描述实际的SQL类型,如VARCHAR(30)或INTEGER,这样就可以灵活的使用SQL中的类型,但是这么做会使得Java类和数据库相互绑定,从而为数据库迁移造成麻烦。
- 使用多种注解来注释同一个域,如使用@Constants和相应的SQL类型(如@SQLInteger),这种方式代码可能会比较多,但是编译器允许这样做,需要注意的是,使用多个注解时,同一个注解不能重复使用。
最后注解是不支持继承的,所以不能使用extends来继承某个@interface。
实现数据库注解处理器
public class TableCreator {
public static void main(String[] args) throws Exception {
String names[] = { "Member" };
for (String className : names) {
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 (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 == null || anns.length < 1) {
continue;// 不是db table列
}
if (anns[0] instanceof SQLInteger) {
SQLInteger sInt = (SQLInteger) anns[0];
// 没配置,就用域名
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\t" + 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;
}
}/*output:
Table Creation SQL for Member is:
CREATE TABLE MEMBER(
FIRSTNAME VARCHAR(30));
Table Creation SQL for Member is:
CREATE TABLE MEMBER(
FIRSTNAME VARCHAR(30),
LASTNAME VARCHAR(50));
Table Creation SQL for Member is:
CREATE TABLE MEMBER(
FIRSTNAME VARCHAR(30),
LASTNAME VARCHAR(50),
AGE INT);
Table Creation SQL for Member is:
CREATE TABLE MEMBER(
FIRSTNAME VARCHAR(30),
LASTNAME VARCHAR(50),
AGE INT,
HANDLE VARCHAR(30) PRIMARY KEY );
*///:~
以上代码通过forName()方法加载每个类,然后用getAnnotation(DBTable.class)检查该类是否带有@DBTable注解,如果有,则将发现的表名保存下来,然后读取这个类的所有域,并用getDeclaredAnnotation()进行检查,最后用instanceof操作符判断这些注解的具体类型。由于没有继承机制,要获得近似多态的行为只能使用getDeclaredAnnotation()方法。
从以上可以看出,注解可以在源码中提供额外信息,针对这些信息我们可以编写注解处理器来处理它们,从而实现各种自动化功能(比如JUnit自动测试,ORM自动映射对象和实体等)。