diff --git a/docs/book/23-Annotations.md b/docs/book/23-Annotations.md index 1c66fb0..90a5fdb 100644 --- a/docs/book/23-Annotations.md +++ b/docs/book/23-Annotations.md @@ -206,12 +206,6 @@ public @interface SimulatingNull { ### 生成外部文件 - - - - -## 使用javac处理注解 - 当有些框架需要一些额外的信息才能与你的源代码协同工作,这种情况下注解就会变得十分有用。像 Enterprise JavaBeans (EJB3 之前)这样的技术,每一个 Bean 都需要需要大量的接口和部署描述文件,而这些就是“样板”文件。Web Service,自定义标签库以及对象/关系映射工具(例如 Toplink 和 Hibernate)通常都需要 XML 描述文件,而这些文件脱离于代码之外。除了定义 Java 类,程序员还必须忍受沉闷,重复的提供某些信息,例如类名和包名等已经在原始类中已经提供的信息。每当你使用外部描述文件时,他就拥有了一个类的两个独立信息源,这经常导致代码的同步问题。同时这也要求了为项目工作的程序员在知道如何编写 Java 程序的同时,也必须知道如何编辑描述文件。 假设你想提供一些基本的对象/关系映射功能,能够自动生成数据库表。你可以使用 XML 描述文件来指明类的名字、每个成员以及数据库映射的相关信息。但是,通过使用注解,你可以把所有信息都保存在 **JavaBean** 源文件中。为此你需要一些用于定义数据库名称、数据库列以及将 SQL 类型映射到属性的注解。 @@ -277,6 +271,186 @@ public @interface SQLInteger { 另外两个 **@interface** 定义的是 SQL 类型。如果希望这个框架更有价值的话,我们应该为每个 SQL 类型都定义相应的注解。不过为为示例,两个元素足够了。 +这些 SQL 类型具有 `name()` 元素和 `constraints()` 元素。后者利用了嵌套注解的功能,将数据库列的类型约束信息嵌入其中。注意 `constraints()` 元素的默认值时 **@Constraints**。由于在 **@Constraints** 注解类型之后,没有在括号中指明 **@Constraints** 元素的值,因此,**constraints()** 的默认值为所有元素都为默认值的 **@Constraints** 注解。如果要使得嵌入的 **@Constraints** 注解中的 `unique()` 元素为 true,并作为 `constraints()` 元素的默认值,你可以像如下定义: + +```java +// annotations/database/Uniqueness.java +// Sample of nested annotations +package annotations.database; +public @interface Uniqueness { + Constraints constraints() + default @Constraints(unique = true); +} +``` + +下面是一个简单的,使用了如上注解的类: + +```java +// 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** 和 **firstName** 都被注解为 **@SQLString** 类型并且给了默认元素值分别为 30 和 50。这些注解都有两个有趣的地方:首先,他们都使用了嵌入的 **@Constraints** 注解的默认值;其次,它们都是用了快捷方式特性。如果你在注解中定义了名为 **value** 的元素,并且在使用该注解时,**value** 为唯一一个需要赋值的元素,你就不需要使用名—值对的语法,你只需要在括号中给出 **value** 元素的值即可。这可以应用于任何合法类型的元素。这也限制了你必须将元素命名为 **value**,不过在上面的例子中,这样的注解语句也更易于理解: + +```java +@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 命令: + +```java +// 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 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; + } +} +``` + +输出为: + +```sql +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); +``` + + + + + +## 使用javac处理注解 + + +