java进阶之注解篇

注解

注解(也被称为元数据)为我们在代码中添加信息提供了一种形式化的方式,使我们可以在稍后的某个时刻更容易的使用这些数据。
注解在一定程度上是把元数据和源代码文件结合在一起的趋势所激发的,而不是保存在外部文档。这同样是对像C# 语言对于Java 语言特性压力的一种回应。
注解是Java 5 所引入的众多语言变化之一。它们提供了Java 无法表达的但是你需要完整表述程序所需的信息。因此,注解使得我们可以以编译器验证的格式存储程序的额外信息。注解可以生成描述符文件,甚至是新的类定义,并且有助于减轻编写“样板”代码的负担。通过使用注解,你可以将元数据保存在Java 源代码中。并拥有如下优势:
简单易读的代码,编译器类型检查,使用annotation API 为自己的注解构造处理工具。
即使Java 定义了一些类型的元数据,但是一般来说注解类型的添加和如何使用完全取决于你。
注解的语法十分简单,主要是在现有语法中添加@ 符号。Java 5 引入了前三种定义在java.lang 包中的注解:
• @Override:表示当前的方法定义将覆盖基类的方法。如果你不小心拼写错误,或者方法签名被错误拼写的时候,编译器就会发出错误提示。
• @Deprecated:如果使用该注解的元素被调用,编译器就会发出警告信息。
• @SuppressWarnings:关闭不当的编译器警告信息。
• @SafeVarargs:在Java 7 中加入用于禁止对具有泛型varargs 参数的方法或构造函数的调用方发出警告。
• @FunctionalInterface:Java 8 中加入用于表示类型声明为函数式接口。
还有5 种额外的注解类型用于创造新的注解。你将会在这一章学习它们。
每当创建涉及重复工作的类或接口时,你通常可以使用注解来自动化和简化流程。
例如在Enterprise JavaBean(EJB)中的许多额外工作就是通过注解来消除的。
注解的出现可以替代一些现有的系统,例如XDoclet,它是一种独立的文档化工具,专门设计用来生成注解风格的文档。与之相比,注解是真正语言层级的概念,以前构造出来就享有编译器的类型检查保护。注解在源代码级别保存所有信息而不是通过注释文字,这使得代码更加整洁和便于维护。通过使用拓展的annotation API 或稍后在本章节可以看到的外部的字节码工具类库,你会拥有对源代码及字节码强大的检查与操作能力。

基本语法

在下面的例子中,使用@Test 对testExecute() 进行注解。该注解本身不做任何事情,但是编译器要保证其类路径上有@Test 注解的定义。你将在本章看到,我们通过注解创建了一个工具用于运行这个方法:

// annotations/Testable.java
package annotations;
import onjava.atunit.*;
public class Testable {
	public void execute() {
		System.out.println("Executing..");
	}
	@Test
	void testExecute() { 
		execute(); 
	}
}

被注解标注的方法和其他方法没有任何区别。在这个例子中,注解@Test 可以和任何修饰符共同用于方法,诸如public、static 或void。从语法的角度上看,注解和修饰符的使用方式是一致的。

定义注解

如下是一个注解的定义。注解的定义看起来很像接口的定义。事实上,它们和其他Java 接口一样,也会被编译成class 文件。

// onjava/atunit/Test.java
// The @Test tag
package onjava.atunit;
import java.lang.annotation.*;
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Test {}

除了@ 符号之外,@Test 的定义看起来更像一个空接口。注解的定义也需要一些元注解(meta-annotation),比如@Target 和@Retention。@Target 定义你的注解可以应用在哪里(例如是方法还是字段)@Retention 定义了注解在哪里可用,在源代码中
(SOURCE),class 文件(CLASS)中或者是在运行时(RUNTIME)

注解通常会包含一些表示特定值的元素。当分析处理注解的时候,程序或工具可以利用这些值。注解的元素看起来就像接口的方法,但是可以为其指定默认值。
不包含任何元素的注解称为标记注解(marker annotation),例如上例中的@Test就是标记注解。
下面是一个简单的注解,我们可以用它来追踪项目中的用例。程序员可以使用该注解来标注满足特定用例的一个方法或者一组方法。于是,项目经理可以通过统计已经实现的用例来掌控项目的进展,而开发者在维护项目时可以轻松的找到用例用于更新,或者他们可以调试系统中业务逻辑。

// annotations/UseCase.java
import java.lang.annotation.*;
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface UseCase {
	int id();
	String description() default "no description";
}

注意id 和description 与方法定义类似。由于编译器会对id 进行类型检查,因此将跟踪数据库与用例文档和源代码相关联是可靠的方式。description 元素拥有一个default 值,如果在注解某个方法时没有给出description 的值。则该注解的处理器会使用此元素的默认值。
在下面的类中,有三个方法被注解为用例:

// annotations/PasswordUtils.java
import java.util.*;
public class PasswordUtils {
	@UseCase(id = 47, description ="Passwords must contain at least one numeric")
	public boolean validatePassword(String passwd) {
		return (passwd.matches("\\w*\\d\\w*"));
	}
	@UseCase(id = 48)
	public String encryptPassword(String passwd) {
		return new StringBuilder(passwd).reverse().toString();
	}
	@UseCase(id = 49, description ="New passwords can't equal previously used ones")
	public boolean checkForNewPassword(
	List<String> prevPasswords, String passwd) {
		return !prevPasswords.contains(passwd);
	}
}

注解的元素在使用时表现为名-值对的形式,并且需要放置在@UseCase 声明之后的括号内。在encryptPassword() 方法的注解中,并没有给出description 的值,所以在@interface UseCase 的注解处理器分析处理这个类的时候会使用该元素的默认值。你应该能够想象到如何使用这套工具来“勾勒” 出将要建造的系统,然后在建造的过程中逐渐实现系统的各项功能

元注解

Java 语言中目前有5 种标准注解(前面介绍过),以及5 种元注解。元注解用于注解其他的注解

注解解释
@Target表示注解可以用于哪些地方。可能的ElementType 参数包括:
CONSTRUCTOR:构造器的声明
FIELD:字段声明(包括enum实例)
LOCAL_VARIABLE:局部变量声明
METHOD:方法声明
PACKAGE:包声明
PARAMETER:参数声明
TYPE:类、接口(包括注解类型)或者enum 声明
@Retention表示注解信息保存的时长。可选的RetentionPolicy 参数包括:
SOURCE:注解将被编译器丢弃
CLASS:注解在class 文件中可用,但是会被VM 丢弃。
RUNTIME:VM 将在运行期也保留注解,因此可以通过反射机制读取注解的信息。
@Documented将此注解保存在Javadoc 中
@Inherited允许子类继承父类的注解
@Repeatable允许一个注解可以被使用一次或者多次(Java 8)。

大多数时候,程序员定义自己的注解,并编写自己的处理器来处理他们。

编写注解处理器

如果没有用于读取注解的工具,那么注解不会比注释更有用。使用注解中一个很重要的部分就是,创建与使用注解处理器。Java 拓展了反射机制的API 用于帮助你创造这类工具。同时他还提供了javac 编译器钩子在编译时使用注解。
下面是一个非常简单的注解处理器,我们用它来读取被注解的PasswordUtils 类,并且使用反射机制来寻找@UseCase 标记。给定一组id 值,然后列出在PasswordUtils中找到的用例,以及缺失的用例。

// annotations/UseCaseTracker.java
import java.util.*;
import java.util.stream.*;
import java.lang.reflect.*;
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() + "\n " + uc.description());
				useCases.remove(Integer.valueOf(uc.id()));
			}
		}
		useCases.forEach(i ->System.out.println("Missing use case " + i));
	}
	public static void main(String[] args) {
		List<Integer> useCases = IntStream.range(47, 51).boxed().collect(Collectors.toList());
		trackUseCases(useCases, PasswordUtils.class);
	}
}

输出为:
Found Use Case 48
no description
Found Use Case 47
Passwords must contain at least one numeric
Found Use Case 49
New passwords can’t equal previously used ones
Missing use case 50

这个程序用了两个反射的方法:getDeclaredMethods() 和getAnnotation(),它们都属于AnnotatedElement 接口(Class,Method 与Field 类都实现了该接口)。
getAnnotation() 方法返回指定类型的注解对象,在本例中就是“UseCase”。如果被注解的方法上没有该类型的注解,返回值就为null。我们通过调用id() 和description()方法来提取元素值。注意encryptPassword() 方法在注解的时候没有指定description的值,因此处理器在处理它对应的注解时,通过description() 取得的是默认值“no description”。

注解元素

在UseCase.java 中定义的@UseCase 的标签包含int 元素id 和String 元素description。注解元素可用的类型如下所示:
• 所有基本类型(int、float、boolean 等)
• String
• Class
• enum
• Annotation
• 以上类型的数组
如果你使用了其他类型,编译器就会报错。注意,也不允许使用任何包装类型,但是由于自动装箱的存在,这不算是什么限制。注解也可以作为元素的类型。稍后你会看到,注解嵌套是一个非常有用的技巧。

默认值限制

编译器对于元素的默认值有些过于挑剔。首先,元素不能有不确定的值。也就是说,元素要么有默认值,要么就在使用注解时提供元素的值。
这里有另外一个限制:任何非基本类型的元素,无论是在源代码声明时还是在注解接口中定义默认值时,都不能使用null 作为其值。这个限制使得处理器很难表现一个元素的存在或者缺失的状态,因为在每个注解的声明中,所有的元素都存在,并且具有相应的值。为了绕开这个约束,可以自定义一些特殊的值,比如空字符串或者负数用于表达某个元素不存在

// annotations/SimulatingNull.java
import java.lang.annotation.*;
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface SimulatingNull {
	int id() default -1;
	String description() default "";
}

这是一个在定义注解的习惯用法。

生成外部文件

当有些框架需要一些额外的信息才能与你的源代码协同工作,这种情况下注解就会变得十分有用。像Enterprise JavaBeans (EJB3 之前)这样的技术,每一个Bean 都需要大量的接口和部署描述文件,而这些就是“样板” 文件。Web Service,自定义标签库以及对象/关系映射工具(例如Toplink 和Hibernate)通常都需要XML 描述文件,而这些文件脱离于代码之外。除了定义Java 类,程序员还必须忍受沉闷,重复的提供某些信息,例如类名和包名等已经在原始类中提供过的信息。每当你使用外部描述文件时,他就拥有了一个类的两个独立信息源,这经常导致代码的同步问题。同时这也要求了为项目工作的程序员在知道如何编写Java 程序的同时,也必须知道如何编辑描述文件。假设你想提供一些基本的对象/关系映射功能,能够自动生成数据库表。你可以使用XML 描述文件来指明类的名字、每个成员以及数据库映射的相关信息。但是,通过使用注解,你可以把所有信息都保存在JavaBean 源文件中。为此你需要一些用于定义数据库表名称、数据库列以及将SQL 类型映射到属性的注解。
以下是一个注解的定义,它告诉注解处理器应该创建一个数据库表:

// annotations/database/DBTable.java
package annotations.database;
import java.lang.annotation.*;
@Target(ElementType.TYPE) // Applies to classes only
@Retention(RetentionPolicy.RUNTIME)
public @interface DBTable {
	String name() default "";
}

在@Target 注解中指定的每一个ElementType 就是一个约束,它告诉编译器,这个自定义的注解只能用于指定的类型。你可以指定enum ElementType 中的一个值,或者以逗号分割的形式指定多个值。如果想要将注解应用于所有的ElementType,那么可以省去@Target 注解,但是这并不常见。
注意@DBTable 中有一个name() 元素,该注解通过这个元素为处理器创建数据库时提供表的名字。
如下是修饰字段的注解:

// annotations/database/Constraints.java
package annotations.database;
import java.lang.annotation.*;
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Constraints {
	boolean primaryKey() default false;
	boolean allowNull() default true;
	boolean unique() default false;
}
// annotations/database/SQLString.java
package annotations.database;
import java.lang.annotation.*;
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface SQLString {
	int value() default 0;
	String name() default "";
	Constraints constraints() default @Constraints;
}
// annotations/database/SQLInteger.java
package annotations.database;
import java.lang.annotation.*;
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface SQLInteger {
	String name() default "";
	Constraints constraints() default @Constraints;
}

@Constraints 注解允许处理器提供数据库表的元数据。@Constraints 代表了数据库通常提供的约束的一小部分,但是它所要表达的思想已经很清楚了。primaryKey(),allowNull() 和unique() 元素明显的提供了默认值,从而使得在大多数情况下,该注解的使用者不需要输入太多东西。
另外两个@interface 定义的是SQL 类型。如果希望这个框架更有价值的话,我们应该为每个SQL 类型都定义相应的注解。不过作为示例,两个元素足够了。
这些SQL 类型具有name() 元素和constraints() 元素。后者利用了嵌套注解
的功能,将数据库列的类型约束信息嵌入其中。注意constraints() 元素的默认值是@Constraints。由于在@Constraints 注解类型之后,没有在括号中指明@Constraints元素的值,因此,constraints() 的默认值为所有元素都为默认值的@Constraints注解。如果要使得嵌入的@Constraints 注解中的unique() 元素为true,并作为constraints() 元素的默认值,你可以像如下定义:

// annotations/database/Uniqueness.java
// Sample of nested annotations
package annotations.database;
public @interface Uniqueness {
	Constraints constraints()
	default @Constraints(unique = true);
}

下面是一个简单的,使用了如上注解的类:

// annotations/database/Member.java
package annotations.database;
@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 reference;
	static int memberCount;
	public String getReference() { return reference; }
	public String getFirstName() { return firstName; }
	public String getLastName() { return lastName; }
	@Override
	public String toString() { return reference; }
	public Integer getAge() { return age; }
}

类注解@DBTable 注解给定了元素值MEMBER,它将会作为表的名字。类的属性firstName 和lastName 都被注解为@SQLString 类型并且给了默认元素值分别为30 和50。这些注解都有两个有趣的地方:首先,他们都使用了嵌入的@Constraints注解的默认值;其次,它们都是用了快捷方式特性。如果你在注解中定义了名为value的元素,并且在使用该注解时,value 为唯一一个需要赋值的元素,你就不需要使用名—值对的语法,你只需要在括号中给出value 元素的值即可。这可以应用于任何合法类型的元素。这也限制了你必须将元素命名为value,不过在上面的例子中,这样的注解语句也更易于理解:
@SQLString(30)
处理器将在创建表的时候使用该值设置SQL 列的大小。
默认值的语法虽然很灵巧,但是它很快就变的复杂起来。以reference 字段的注解为例,上面拥有@SQLString 注解,但是这个字段也将成为表的主键,因此在嵌入的@Constraint 注解中设定primaryKey 元素的值。这时事情就变的复杂了。你不得不为这个嵌入的注解使用很长的键—值对的形式,来指定元素名称和@interface 的名称。同时,由于有特殊命名的value 也不是唯一需要赋值的元素,因此不能再使用快捷方式特性。如你所见,最终结果不算清晰易懂。

替代方案

可以使用多种不同的方式来定义自己的注解用于上述任务。例如,你可以使用一个单一的注解类@TableColumn,它拥有一个enum 元素,元素值定义了STRING,INTEGER,FLOAT 等类型。这消除了每个SQL 类型都需要定义一个@interface的负担,不过也使得用额外信息修饰SQL 类型变的不可能,这些额外的信息例如长度或精度等,都可能是非常有用的。
你也可以使用一个String 类型的元素来描述实际的SQL 类型,比如“VARCHAR(30)” 或者“INTEGER”。这使得你可以修饰SQL 类型,但是这也将Java 类型到SQL 类型的映射绑在了一起,这不是一个好的设计。你并不想在数据库更改之后重新编译你的代码;如果我们只需要告诉注解处理器,我们正在使用的是什么“口味(favor)” 的SQL,然后注解处理器来为我们处理SQL 类型的细节,那将是一个优雅的设计。
第三种可行的方案是一起使用两个注解,@Constraints 和相应的SQL 类型(例如,@SQLInteger)去注解同一个字段。这可能会让代码有些混乱,但是编译器允许你对同一个目标使用多个注解。在Java 8,在使用多个注解的时候,你可以重复使用同一个注解。

注解不支持继承

你不能使用extends 关键字来继承@interfaces。这真是一个遗憾,如果可以定义@TableColumn 注解(参考前面的建议),同时嵌套一个@SQLType 类型的注解,将成为一个优雅的设计。按照这种方式,你可以通过继承@SQLType 来创造各种SQL类型。例如@SQLInteger 和@SQLString。如果支持继承,就会大大减少打字的工作量并且使得语法更整洁。在Java 的未来版本中,似乎没有任何关于让注解支持继承
的提案,所以在当前情况下,上例中的解决方案可能已经是最佳方案了。

实现处理器

下面是一个注解处理器的例子,他将读取一个类文件,检查上面的数据库注解,并生成用于创建数据库的SQL 命令:

// annotations/database/TableCreator.java
// Reflection-based annotation processor
// {java annotations.database.TableCreator
// annotations.database.Member}
package annotations.database;
import java.lang.annotation.Annotation;
import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.List;
public class TableCreator {
public static void
main(String[] args) throws Exception {
if (args.length < 1) {System.out.println(
"arguments: 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; // Not a db table column
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];
// Use field name if name not specified.
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 + ",");
// Remove trailing comma
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;
}
}

输出为:
Table Creation SQL for annotations.database.Member is:
CREATE TABLE MEMBER(
FIRSTNAME VARCHAR(30));
Table Creation SQL for annotations.database.Member is:
CREATE TABLE MEMBER(
FIRSTNAME VARCHAR(30),
LASTNAME VARCHAR(50));
Table Creation SQL for annotations.database.Member is:
CREATE TABLE MEMBER(
FIRSTNAME VARCHAR(30),
LASTNAME VARCHAR(50),
AGE INT);
Table Creation SQL for annotations.database.Member is:
CREATE TABLE MEMBER(
FIRSTNAME VARCHAR(30),
LASTNAME VARCHAR(50),
AGE INT,
REFERENCE VARCHAR(30) PRIMARY KEY);

主方法会循环处理命令行传入的每一个类名。每一个类都是用forName()
方法进行加载, 并使用getAnnotation(DBTable.class) 来检查该类是否带有
@DBTable 注解。如果存在,将表名存储起来。然后读取这个类的所有字段,并使用getDeclaredAnnotations() 进行检查。这个方法返回一个包含特定字段上所有注解的数组。然后使用instanceof 操作符判断这些注解是否是@SQLInteger 或者@SQLString 类型。如果是的话,在对应的处理块中将构造出相应的数据库列的字符串片段。注意,由于注解没有继承机制,如果要获取近似多态的行为,使用getDeclaredAnnotations() 似乎是唯一的方式。
嵌套的@Constraint 注解被传递给getConstraints() 方法,并用它来构造一个包含SQL 约束的String 对象。
需要提醒的是,上面演示的技巧对于真实的对象/映射关系而言,是十分幼稚的。使用@DBTable 的注解来获取表的名称,这使得如果要修改表的名字,则迫使你重新编译Java 代码。这种效果并不理想。现在已经有了很多可用的框架,用于将对象映射到数据库中,并且越来越多的框架开始使用注解了。

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值