注解,也叫元数据。为我们在代码中添加信息提供了一种形式化的方法,是我们可以在稍后的某个时刻非常方便的使用这些数据。
每当你创建描述符性质的类或者接口时,一旦其中包含了重复性的工作,那就可以使用注解来简化与自动化该过程。
一个简单的注解定义的实例:
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Test {
}
除了@符号以外,@Test的定义就像一个空接口。定义接口时,会需要一些元注解,所谓元注解,就是专职负责注解其他注解的注解,java目前只内置了四种元注解,他们分别是:
@Retention :表示需要在什么级别保存该注解信息。可选的RetentionPolicy包括:
@Document :此注解包含在javadoc中。
在注解中,一般都会包含一些元素以及标识某些值。当分析处理注解时,程序员或者工具可以利用这些值。注解的元素看起来就像接口的方法,唯一的区别是你可以为其指定默认值
注解在加入注解元素:
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface UseCase{
public int id();
public String description() default "no description";
}
注解使用
public class PasswordUtils {
@UseCase(id=47,description="Password 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 password can't equal previously used ones")
public boolean checkForNewPassword(List prevPasswords,String password){
return !prevPasswords.contains(password);
}
}
如果没有用来读取注解的工具,那么注解并不会比注释更有用。使用注解的过程中,很重要的部分就是注解处理器的创建和使用。java SE5扩展了反射机制的AP,以帮助程序员构造这类工具。同时还提供了一个外部的apt工具帮助程序员解析带有注解的java源代码。
public class UseCaseTracker {
public static void trackUseCases(List useCases,Class> cl){
for(Method m : cl.getDeclaredMethods()){
UseCase uc = m.getAnnotation(UseCase.class);
if(uc!=null){
System.out.println("Find 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 useCases = new ArrayList<>();
Collections.addAll(useCases, 47,48,49,50);
trackUseCases(useCases, PasswordUtils.class);
}
}
上述UseCaseTracker
类是一个简单的注解处理器。我们用它读取PasswordUtils类,并且使用反射机制查找@UseCase标记。
这个程序使用到了两个放射的方法:getDeclaredMethods()
和 getAnnotation()
他们都属于AnnotatedElement
接口 (Class、Method与Field都实现了这个接口) getAnnotation()
返回指定类型的注解对象,在这里就是UseCase。如果被注解的方法上没有该类型的注解,则会返回null。然后调用id()
和description()
方法从返回的UseCase对象中提取元素的值。
注解元素可用的类型如下:
如果使用了其他类型,编译器就会报错。注意,包装类型也是不允许使用的。注解也可以作为元素的类型,也就是说注解可以嵌套使用。
编译器对元素的默认值有很多限制
首先,元素不能有不确定的默认值。也就是说,元素必须要么有默认值,要么在注解时提供元素的值。
其次,对于非基本类型的元素,无论是源代码中声明时,或是在注解接口中定义默认值时,都不能以null作为其值。为了绕开这个约束,我们可以自定义一些特殊的值,例如空字符串或者附属,以表示这个元素不存在。
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface UseCase {
public int id() default -1;
public String description() default " ";
}
有些框架需要一些额外的信息才能和源代码协同工作。比如Hibernate,Spring等,这个时候可以使用注解替代这些配置文件。下面举一个使用注解生成数据库表的例子:
/**
* 告诉处理器生成一个数据库表
*/
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface DBTable {
public String name() default "";
}
创建修饰JavaBean域的注解:
/**
1. 修饰属性
2. 提取数据库表元数据
*/
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Constraints {
boolean primaryKey() default false;
boolean alloNull() default true;
boolean unique() default false;
}
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface SQLString {
int value() default 0;
String name() default "";
Constraints constraints() default @Constraints;
}
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface SQLInteger {
String name() default "";
Constraints constraints() default @Constraints;
}
注解处理器通过@Constraints提取数据库表中的元数据。当然,对于数据库所能提供者约束而言,这里只是他的一个子集。
另外两个注解@SQLString和@SQLInteger定义的是SQL类型。
下面是一个简单的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 memberCount;
public String getFirstName() {return firstName;}
public String getLastName() {return lastName;}
public Integer getAge() {return age;}
public String getHandle() {return handle;}
public String toString(){return handle;}
}
类的注解@DBTable给定了值MEMBER,这将成为数据库表的名字。
这里需要说明两点:
1. 他们都使用了嵌入的@Constraints注解的默认值。
2. 他们都是用了快捷方式,所谓的快捷方式就是程序员在注解中定义了名外value的元素,并且在应用该注解的时候,如果该元素是唯一需要赋值的元素,那么此时无需使用 名-值 对 这种语法。只需要在括号内给出value的值即可。
@SQLString(30) 处理器将在创建表的时候使用该值设置SQL列的大小。
这里需要注意的是注解是不支持继承的。
####实现处理器
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 annotation in class "+className);
}
String tableName = dbTable.name();
if(tableName.length() < 1){
tableName = cl.getName().toUpperCase();
}
List columnDefs = new ArrayList();
for(Field field : cl.getDeclaredFields()){
String columnName = null;
Annotation[] annos = field.getDeclaredAnnotations();
if(annos.length<1){
continue;
}
if(annos[0] instanceof SQLInteger){
SQLInteger sInt = (SQLInteger)annos[0];
if(sInt.name().length()<1)
columnName = field.getName().toUpperCase();
else
columnName = sInt.name();
columnDefs.add(columnName+" INT" + getConstraints(sInt.constraints()));
}
if(annos[0] instanceof SQLString){
SQLString sString = (SQLString)annos[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 create SQL for " + className + "is :\n"+tableCreate);
}
}
}
private static String getConstraints(Constraints con){
String constraints = "";
if(!con.alloNull())
constraints += " NOT NOLL";
if(con.primaryKey())
constraints += " PRIMARY KEY";
if(con.unique())
constraints += " UNIQUE";
return constraints;
}
}
输出:
Table create SQL for com.len.javabase.anno.base.Memberis :
CREATE TABLE MEMBER(
FIRSTNAME VARCHAR(30))
Table create SQL for com.len.javabase.anno.base.Memberis :
CREATE TABLE MEMBER(
FIRSTNAME VARCHAR(30),
LASTNAME VARCHAR(50))
Table create SQL for com.len.javabase.anno.base.Memberis :
CREATE TABLE MEMBER(
FIRSTNAME VARCHAR(30),
LASTNAME VARCHAR(50),
AGE INT)
Table create SQL for com.len.javabase.anno.base.Memberis :
CREATE TABLE MEMBER(
FIRSTNAME VARCHAR(30),
LASTNAME VARCHAR(50),
AGE INT,
HANDLE VARCHAR(30) PRIMARY KEY)
上述过程只是利用反射做了一个简单的解析。下节介绍使用apt处理注解