注解(也被称为元数据)为我们在代码中添加信息提供了一种形式化的方法,使我们可以在稍后某个时刻非常方便地使用这些数据。
注解可以提供用来完整地描述程序所需的信息,而这些信息是无法用Java来表达的。因此,注解使得我们能够以将编译器来测试和验证的格式,存储有关程序的额外信息。注解可以用来生成描述符文件,甚至或是新的类定义,并且有助于减轻编写“样板”代码的负担。通过使用注解,我们可以将这些元数据保存在Java源代码中,并利用annotation API为自己的注解构造处理工具。
Java SE5内置了三种定义在java.lang中的注解:
@Override,表示当前的方法定义将覆盖超类里的方法。如果拼写错误或者方法签名对不上被覆盖的方法,编译器就会发出错误提示。
@Deprecated,如果程序员使用了注解为它的元素,那么编译器会发出警告信息。
@SuppressWarnings,关闭不当的编译器警告信息。
1 基本语法
注解的使用方法@Test:
public class Testable {
public void execute() {
System.out.println("Executing..");
}
@Test
void testExecute() {
execute();
}
} ///:~
注解的定义很像接口的定义。并且,和java借口一样,注解也会编译成class文件。
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Test {} ///:~
如上例,定义注解时,需要一些 元注释,如@Target和@Retention。
@Target 用来定义你的注解将应用于什么地方(列入一个方法,或者一个域)。
@Retention 用来定义该注解在哪一个级别可用,在源代码中(SOURCE),类文件中(CLASS)或者运行时(RUNTIME)。
在注解中,一般都会包含一些元素以表示某些值。当分析处理注解时,程序或工具可以利用这些值。注解的元素看起来就像借口方法,唯一的区别是你可以为其指定默认值。
没有元素的注解称为 标记注解 ,上例中的@Test就是标记注解。
下面是一个简单的注解,我们可以用它来跟踪一个项目中的用例。
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface UseCase {
public int id();
public String description() default "no description";
} ///:~
id和description类似方法定义。description有一个默认值。在下例,三个方法被注册为用例:
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);
}
}
注解的元素在使用时表现为名-值对应形式,并位于@UseCase声明之后的括号里。
1.2 元注解
java目前内置了三种标准注解,以及四种元注解。元注解专职负责注解其他的注解:
@Target 表示该注解用于什么地方。可能的ElementType参数包括:
- CONSTRUCTOR : 构造器的声明
- FIELD: 域声明(包括enum实例)
- LOCAL_VARIABLE:局部变量声明
- METHOD:方法声明
- PACKAGE:包声明
- PARAMETER:参数声明
- TYPE:类、接口(包括注解类型)或enum声明
@Retention 表示需要在什么级别保存该注解信息。可选的RetentionPolicy参数包括:
- SOURCE:注解将被编译器丢弃
- CLASS:注解在class文件中可用,但会被VM丢弃。
- RUNTIME:VM将在运行期也保留注解,因此可用永拓反射机制读取注解的信息
@Documented 将此注解包含在Javadoc中。
@Inherited 允许子类继承父类的注解。
2 编写注解处理器
如果没有用来读取注解的工具,那么注解也不会比注释更有用。使用注解的过程中,很重要的一个部分就是创建与使用注解处理器。
Java SE5扩展了反射机制的API,以帮助程序员构造这类工具。同时,它还提供了一个外部工具apt帮助程序员解析带有注解的Java源代码。
例子:
package annotations;//: annotations/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
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<Integer>();
Collections.addAll(useCases, 47, 48, 49, 50);
trackUseCases(useCases, PasswordUtils.class);
}
} /* Output:
Found Use Case:47 Passwords must contain at least one numeric
Found Use Case:48 no description
Found Use Case:49 New passwords can't equal previously used ones
Warning: Missing use case-50
*///:~
上述例子使用了反射方法getDeclaredMethods()和getAnnotation(),获取了注解元素。
2.1 注解元素
拿注解@UserCase距离,他包含int元素id,String元素description。注解元素可用类型如下所示:
- 所有基本类型(int,float,boolean)
- String
- Class
- enum
- Annotation
- 以上类型的数组
如果使用了其他类型,编译器会报错。注意不能使用包装类型,另注解可用嵌套。
2.2 默认值限制
元素不能有不确定的值。要么具有默认值,要么在使用注解时提供元素的值。
对于非基本类型的元素,无论是在源代码声明时,或是在注解借口中定义默认值,都不允许以null为其值。如果需要表现出一个元素的缺失状态,则需要自己定义一些默认的值,如空字符串或者负数。
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface SimulatingNull {
public int id() default -1;
public String description() default "";
} ///:~
2.3 生成外部文件
假设你希望提供一些基本的对象/关系映射功能,能够自动生成数据库表,用以存储JavaBean对象。你可以选择使用XML描述文件,指明类的名字、每个成员以及数据库映射的相关信息。
然而,如果使用注解的话,你可以将所有信息都保存在JavaBean源文件中。为此,我们需要一些新的注解,用于定义与Bean关联的数据库表的名字,以及与Bean关联的列的名字和SQL类型。
数据库表注解:
package annotations.database;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Target(ElementType.TYPE) // Applies to classes only
@Retention(RetentionPolicy.RUNTIME)
public @interface DBTable {
public String name() default "";
} ///:~
修饰JavaBean域准备的注解:
Constraints:
//: annotations/database/Constraints.java
package annotations.database;
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;
} ///:~
SQL String类型:
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface SQLString {
int value() default 0;
String name() default "";
Constraints constraints() default @Constraints;
} ///:~
SQL INTEGER类型:
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface SQLInteger {
String name() default "";
Constraints constraints() default @Constraints;
} ///:~
由于数据库类型注解的constraints()元素的默认值是 @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 memberCount;
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; }
} ///:~
以上@SQLString(30)运用了快捷方式,直接将30付给了value。
其他设计方法:
1、定义单一注解类@TableColumn,它带有一个enum元素,该枚举类定义STRING,INTEGER等枚举实例,这样就消除了每个SQL类型都需要一个@interface的负担,不过也使得以额外信息来修饰SQL类型的需求变得不可能,如长度或者精度。
2、我们可以用String元素来描述实际的SQL类型,比如VARCHAR(30)或INTEGER。这使得我们可以修饰SQL类型。同时它将JAVA类型与SQL类型绑定了在一起,如果更换数据库的话代码必须修改。
3、同时用2个注解来注解一个域,@Constraints和相应的SQL类型。这种方式可能会使代码有点乱,不过编译器允许。注意,同一个注解不能重复使用。
2.4 注解不支持继承
注解目前不支持继承。2.5 实现处理器
https://github.com/xu509/Java-practise/blob/master/src/annotations/database/TableCreator.java根据这个例子,可以根据Javabean得出建表语句3 使用apt处理注解
注解处理工具apt,是sun为了帮助注解的处理过程而提供的工具。要编译这一节出现的注解处理器,必须将tools.jar设置在你的classpath中,这个工具包含了com.sun.mirror.*接口。如果使用maven的话配置如下:<dependency> <groupId>com.sun</groupId> <artifactId>tools</artifactId> <version>1.6.0_10</version> <scope>system</scope> <!--<systemPath>C:/Program Files/Java/jdk1.6.0_10/lib/tools.jar</systemPath>--> <systemPath>${env.JAVA_HOME}/lib/tools.jar</systemPath> </dependency>
下面是一个自定义注解,使用它可以把一个类中的public方法提取出来,构造成一个新的接口。RetentionPolicy是SOURCE,因为当我们从一个使用了该注解的类抽取出接口之后,没有必要再保留这些注解信息。@Target(ElementType.TYPE) @Retention(RetentionPolicy.SOURCE) public @interface ExtractInterface { public String value(); } ///:~
下面的类有一个共有方法,我们将他抽取到一个有用的接口中去:
@ExtractInterface("IMultiplier")
public class Multiplier {
public int multiply(int x, int y) {
int total = 0;
for (int i = 0; i < x; i++)
total = add(total, y);
return total;
}
private int add(int x, int y) {
return x + y;
}
public static void main(String[] args) {
Multiplier m = new Multiplier();
System.out.println("11*16 = " + m.multiply(11, 16));
}
}
处理器:
public class InterfaceExtractorProcessor
implements AnnotationProcessor {
private final AnnotationProcessorEnvironment env;
private ArrayList<MethodDeclaration> interfaceMethods =
new ArrayList<MethodDeclaration>();
public InterfaceExtractorProcessor(
AnnotationProcessorEnvironment env) {
this.env = env;
}
public void process() {
for (TypeDeclaration typeDecl :
env.getSpecifiedTypeDeclarations()) {
ExtractInterface annot =
typeDecl.getAnnotation(ExtractInterface.class);
if (annot == null)
break;
for (MethodDeclaration m : typeDecl.getMethods())
if (m.getModifiers().contains(Modifier.PUBLIC) &&
!(m.getModifiers().contains(Modifier.STATIC)))
interfaceMethods.add(m);
if (interfaceMethods.size() > 0) {
try {
PrintWriter writer =
env.getFiler().createSourceFile(annot.value());
writer.println("package " +
typeDecl.getPackage().getQualifiedName() + ";");
writer.println("public interface " +
annot.value() + " {");
for (MethodDeclaration m : interfaceMethods) {
writer.print(" public ");
writer.print(m.getReturnType() + " ");
writer.print(m.getSimpleName() + " (");
int i = 0;
for (ParameterDeclaration parm :
m.getParameters()) {
writer.print(parm.getType() + " " +
parm.getSimpleName());
if (++i < m.getParameters().size())
writer.print(", ");
}
writer.println(");");
}
writer.println("}");
writer.close();
} catch (IOException ioe) {
throw new RuntimeException(ioe);
}
}
}
}
}
所有的工作都在process()方法中完成。
apt工具需要一个工厂类来为其指明正确的处理器,然后它才能调用处理器上的process()
public class InterfaceExtractorProcessorFactory
implements AnnotationProcessorFactory {
public AnnotationProcessor getProcessorFor(
Set<AnnotationTypeDeclaration> atds,
AnnotationProcessorEnvironment env) {
return new InterfaceExtractorProcessor(env);
}
public Collection<String> supportedAnnotationTypes() {
return
Collections.singleton("annotations.ExtractInterface");
}
public Collection<String> supportedOptions() {
return Collections.emptySet();
}
}
4.将访问者模式用于apt
import com.sun.mirror.apt.AnnotationProcessor;
import com.sun.mirror.apt.AnnotationProcessorEnvironment;
import com.sun.mirror.apt.AnnotationProcessorFactory;
import com.sun.mirror.declaration.AnnotationTypeDeclaration;
import com.sun.mirror.declaration.ClassDeclaration;
import com.sun.mirror.declaration.FieldDeclaration;
import com.sun.mirror.declaration.TypeDeclaration;
import com.sun.mirror.util.SimpleDeclarationVisitor;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.Set;
import static com.sun.mirror.util.DeclarationVisitors.NO_OP;
import static com.sun.mirror.util.DeclarationVisitors.getDeclarationScanner;
public class TableCreationProcessorFactory
implements AnnotationProcessorFactory {
public AnnotationProcessor getProcessorFor(
Set<AnnotationTypeDeclaration> atds,
AnnotationProcessorEnvironment env) {
return new TableCreationProcessor(env);
}
public Collection<String> supportedAnnotationTypes() {
return Arrays.asList(
"annotations.database.DBTable",
"annotations.database.Constraints",
"annotations.database.SQLString",
"annotations.database.SQLInteger");
}
public Collection<String> supportedOptions() {
return Collections.emptySet();
}
private static class TableCreationProcessor
implements AnnotationProcessor {
private final AnnotationProcessorEnvironment env;
private String sql = "";
public TableCreationProcessor(
AnnotationProcessorEnvironment env) {
this.env = env;
}
public void process() {
for (TypeDeclaration typeDecl :
env.getSpecifiedTypeDeclarations()) {
typeDecl.accept(getDeclarationScanner(
new TableCreationVisitor(), NO_OP));
sql = sql.substring(0, sql.length() - 1) + ");";
System.out.println("creation SQL is :\n" + sql);
sql = "";
}
}
private class TableCreationVisitor
extends SimpleDeclarationVisitor {
public void visitClassDeclaration(
ClassDeclaration d) {
DBTable dbTable = d.getAnnotation(DBTable.class);
if (dbTable != null) {
sql += "CREATE TABLE ";
sql += (dbTable.name().length() < 1)
? d.getSimpleName().toUpperCase()
: dbTable.name();
sql += " (";
}
}
public void visitFieldDeclaration(
FieldDeclaration d) {
String columnName = "";
if (d.getAnnotation(SQLInteger.class) != null) {
SQLInteger sInt = d.getAnnotation(
SQLInteger.class);
// Use field name if name not specified
if (sInt.name().length() < 1)
columnName = d.getSimpleName().toUpperCase();
else
columnName = sInt.name();
sql += "\n " + columnName + " INT" +
getConstraints(sInt.constraints()) + ",";
}
if (d.getAnnotation(SQLString.class) != null) {
SQLString sString = d.getAnnotation(
SQLString.class);
// Use field name if name not specified.
if (sString.name().length() < 1)
columnName = d.getSimpleName().toUpperCase();
else
columnName = sString.name();
sql += "\n " + columnName + " VARCHAR(" +
sString.value() + ")" +
getConstraints(sString.constraints()) + ",";
}
}
private 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;
}
}
}
} ///:~
mirror API提供了访问者设计模式的支持。
5.基于注解的单元测试
单元测试是对类中的每个方法提供一个或多个测试的一种实践,其目的是为了有规律地测试一个或多个测试的一种实践,其目的是为了有规律地测试一个类的各个部分是否具备正确的行为。
基于注解的测试框架叫做@Unit。用@Test来标记测试方法。测试方法不带参数,并返回boolean结果来说明测试成功或失败。