前言:
注解(也被称为元数据)为我们在代码中添加信息提供了一种形式化的方法,是我们可以在稍后某个时刻非常方便地使用这些数据。
标准注解
注解的语法比较简单,除了@符号使用之外,它基本与Java固有的语法一致。Java SE5内置了三种,定义在java.lang中的注解。
@override:表示当前的方法定义将覆盖超类中的方法,如果你不小心拼写错误,或者方法签名对不上覆盖的方法,编译器就会发出错误的提示。
@Deprecated:(方法过时注解),如果程序员使用了注解为它的元素,那么编译器就会发出警告信息。
@SuppressWarning:关闭不当的编译器警告信息。
元注解:
java目前只内置了三种标准注解,如上所示。除此之外,还有四种元注解。元注解专职负责注解其他的注解。
@Target:表示该注解可以用于什么地方。可能的ElementType参数包括:
CONSTRUCTOR:构造器的声明
FILED:域声明(包括enum实例)
LOCAL_VARIABLE:局部变量的声明
METHOD:方法声明
PACKAGE:包声明
PARAMETER:参数声明
TYPE:类,接口(包括注解类型)或enum的声明
@Retention:表示需要在什么级别保存该注解信息。可选的RetentionPolicy包括:
SOURCE:注解将会被编译器丢弃
CLASS:注解在class文件中可用,但是会被VM丢弃
RUNTIME:VM将在运行期也保留着注解,因此可以通过反射机制读取该注解的信息。
@Documented:此注解包含在JavaDoc中。
@Inherited:允许子类继承父类的注解。
编写注解处理器
如果没有用来读取注解的工具,那么注解也不会比注释更有作用。使用注解的过程,很重要的一个部分就是创建于使用注解处理器。Java SE5扩展了反射机制的API,以帮助程序员构造这类工具。同时,它还提供了一个外部工具apt帮助程序员解析带有注解的java源代码。
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* 注解是对于方法注解,并且保存注解信息的级别是:RUNTIME,所以我们可以通过java反射来取得注解信息。
*/
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface UseCase {
public int id();
public String description() default "no description";
}
package annotation.demo1;
import java.util.List;
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 password cannot equal previosly used ones")
public boolean checkForNewPassword(List<String> prevPassword,String password){
return !prevPassword.contains(password);
}
}
package annotation.demo1;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
public class UseCaseTracker {
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> userCases = new ArrayList<Integer>();
Collections.addAll(userCases,47,48,49,50);
trackUseCases(userCases,PasswordUtils.class);
}
}
结果输出:
Found use case: 47 Passwords must contain at least one numeric
Found use case: 48 no description
Found use case: 49 new password cannot equal previosly used ones
Warning: Missing use case - 50
Process finished with exit code 0
注解元素
标签@UseCase由UseCase.java定义,其中包含int元素id,以及一个String元素description。注解元素的可用类型如下所示:
1、所有的基本类型
2、String
3、Class
4、enum
5、Annotation
6、以上类型的数组
如果你使用了其他类型,那么编译器就会报错。注意,也不允许使用任何包装类型,不过由于自动打包的存在,这个也算不上什么限制。注解也可以作为元素的类型,也就是说注解可以嵌套。
默认值限制
元素不可以有不确定的值。元素必须要么是默认值,要么是在使用注解时提供元素的值。
对于非基本类型的元素,无论是在源代码中声明时,还是在注解接口中定义默认值时都不能以null作为其值。我们只能定义一些特殊的值来表示某个元素不存在。如下:
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface UseCase {
public int id() default -1;
public String description() default "";
}
生成外部文件
有些framework需要一些额外的信息才能与你的源代码协同工作,而这种情况最适合注解表现其价值了。
假设你希望提供一些基本对象/关系映射功能,能够自动生成数据库表,用以存储JavaBean对象。你可以选择使用XML描述文件,指明类的名字,每个成员,以及数据库映射的相关信息。然而如果使用注解的话,你可以将所有的信息都保存在JavaBean源文件中。为此我们需要一些新的注解,用于定义与Bean关联的数据库表的名字,以及与Bean属性关联的列的名字和SQL类型。
framework
/**
* @DBTable 有一个name()元素,该注解通过这个元素为处理器创建数据库表提供表的名字。
*/
@Target(ElementType.TYPE) // 对于 class interface or enum
@Retention(RetentionPolicy.RUNTIME)
public @interface DBTable {
public String name() default "";
}
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Constraints {
boolean primaryKey() default false;
boolean allowNull() default true;
boolean unique() default false;
}
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface SQLInteger {
String name() default "";
Constraints constraints() default @Constraints;
}
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface SQLString {
int value() default 0;
String name() default "";
Constraints constraints() default @Constraints;
}
bean使用framework
/**
* 这个Bean类用了之前我们定义的注解
*/
@DBTable(name = "member")
public class MemberBean {
@SQLString( value = 30,constraints = @Constraints(primaryKey = true))
String id;
@SQLString(30)
String firstName;
@SQLInteger
Integer age;
}
上面的用法虽然很灵巧,但是它很快就会变得复杂起来。比如注解id为数据库表的主键。一、你不得不使用很长的名-值对形式;二、由于有特殊命名的value元素已经不再是唯一需要赋值的元素了,所以你也不能再使用快捷方式为其赋值了。
解决方法
1、我们可以使用多种不同的方式来定义自己的注解去实现上面例子中的功能。比如:我们可以使用一个单一的注解类@TableColumn,它带有一个enum元素,该元素枚举了STRING,INTEGER等。这就消除了每个SQL类型都需要一个@Interface定义的负担,不过也使得额外的信息修饰SQL类型的需求变成不可能,而这些额外信息,比如长度,精度,可能是十分有必要的。
2、 在1的解决方案中,我们没办法添加额外信息修饰。我们的另一种解决方案就是使用两个注解来注解一个解释域。
注解不支持继承
实现处理器
下面是一个注解处理器的例子,它将读取一个类文件,检查其上的数据库注解,并据此生产SQL语句。
package annotation.demo2;
import java.lang.annotation.Annotation;
import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.List;
public class TableCreator {
private static String getConstraints(Constraints constraints){
String ret = "";
if (!constraints.allowNull()){
ret += " NOT NULL ";
}
if (!constraints.primaryKey()){
ret += " PRIMARY KEY ";
}
if (!constraints.unique()){
ret += " UNIQUE ";
}
return ret;
}
public static void main(String [] args) throws Exception{
if (args.length < 1){
System.out.println("arguments: annotated Classes");
System.exit(0);
}
for (String name : args){
Class<?> cl = Class.forName(name);
DBTable dbTable = cl.getAnnotation(DBTable.class);
if (dbTable == null){
System.out.println("No DBTable annotations in class " + name);
continue;
}
String tableName = dbTable.name();
//if name is empty,use class name as table name
if (tableName.length() < 1){
tableName = cl.getName().toUpperCase();
}
List<String> columnDefs = new ArrayList<>();
for (Field field : cl.getDeclaredFields()){
String columnName = null;
Annotation[] annotations = field.getDeclaredAnnotations();
if (annotations.length < 1){
continue; // Not a db table column
}
if (annotations[0] instanceof SQLInteger){
SQLInteger sqlInteger = (SQLInteger)annotations[0];
if (sqlInteger.name().length() < 1){
columnName = field.getName().toUpperCase();
}else
columnName = sqlInteger.name();
columnDefs.add(columnName + " INT" + getConstraints(sqlInteger.constraints())); //约束条件
}
if (annotations[0] instanceof SQLString){
SQLString sqlString = (SQLString)annotations[0];
if (sqlString.name().length() < 1){
columnName = field.getName().toUpperCase();
}else{
columnName = sqlString.name();
}
columnDefs.add(columnName + " VARCHAR(" + sqlString.value() + ") " +getConstraints(sqlString.constraints()));
}
StringBuilder createCommand = new StringBuilder(
"CREATE TABLE " + tableName + "("
);
for (String columnDef : columnDefs){
createCommand.append("\n " + columnDef +",");
}
String tableCreate = createCommand.substring(0,createCommand.length() -1); //remove the last comma
System.out.println("Table SQL for " + name + " is :\n" + tableCreate);
}
}
}
}
由于Annotation里面没有继承机制,所以要获得类似多态的行为,getDeclaredAnnotation()是唯一的办法。