`

充分利用 Java 的元数据,第 3 部分:高级处理

 
阅读更多
作者:Jason Hunter

了解一些用于在运行时(甚至是编译时)处理批注以及更改程序行为的技术和机制。

在本系列文章(共四篇)的第一篇文章中,我介绍了 Java 的新元数据工具以及内置的批注类型 @Override@Deprecated@SuppressWarning。在第二篇文章中,我介绍了如何编写自定义批注类型并使用 java.lang.annotation 中的元批注控制批注行为。在这第三篇文章中,我将演示用于在运行时(甚至是编译时)处理批注并更改程序行为的技术和机制。

运行时批注处理的价值

能够在运行时与批注交互可以提供重要的价值。设想一个为利用批注而构建的下一代测试工具。此类工具可以运行标记为 @Test 的方法 — 没有用于区分测试方法与支持方法的方法名掩饰。通过对 @Test 批注使用参数,每个测试都可以按逻辑分组,可以控制它所依赖的测试并可以接受各种测试用例参数。(这样的测试工具并非仅是一种假想,实际上您在 beust.com/testng 中就可以找到这样一个工具。)

在 J2EE 5.0 环境(其中批注驱动的“资源注入”似乎成为了标准操作过程)中,这些可能性继续存在。使用资源注入,容器可以将值“注入”到其受管理对象特殊批注的变量 中。例如,如果 servlet 需要一个数据源,则 J2SE 1.4 中的模型将从 JNDI 中提取资源:

public javax.sql.DataSource getCatalogDS() {
try {
javax.naming.IntialContext initCtx = new InitialContext();
catalogDS = (javax.sql.DataSource)
initCtx.lookup("java:comp/env/jdbc/catalogDS");
}
catch (javax.naming.NamingException ex) {
// Handle failure
}
}

public Products[] getProducts() {
javax.sql.DataSource catalogDS = getCatalogDS();
Connection con = catalogDS.getConnection();
// ...
}
以上代码不但非常复杂,而且为了使资源可用于 JNDI 查找,该 servlet 必须在其单独的 web.xml 部署描述符中声明一个 <resource-ref> 条目:
<resource-ref>
<description>Catalog DataSource</description>
<res-ref-name>jdbc/catalogDS</res-ref-name>
<res-type>javax.sql.DataSource</res-type>
<res-auth>Container</res-auth>
<res-sharing-scope>Shareable</res-sharing-scope>
</resource-ref>
而在提供了资源注入功能的 J2SE 5.0 环境中,该 servlet 只需在代码中为它的要求设置批注,并在执行代码前使数据源能够引用“注入的资源”:
@Resource javax.sql.DataSource catalogDS;

public Products[] getProducts() {
Connection con = catalogDS.getConnection();
// ...
}
尤其值得称道的是,批注本身用作部署描述符,从而不必在 web.xml 部署描述符中声明一个单独的条目。@Resource 批注可以接受指示资源名、类型、验证以及作用域等值的参数。即使没有参数,也可以方便的推断出这些项。 JSR-181(用于 Java 的 Web 服务元数据)现已完成,我们将看到为指导容器部署 Web 服务而广泛使用的批注。批注正逐渐成为定义容器与受管理的对象之间协议的原始材料。我将在本系列的第四篇文章即最后一篇文章中介绍 JSR-181 Web 服务。

运行时反射

Java 使用反射公开批注并使程序能够在运行时更改它们的行为。Java 在 J2SE 1.2 中引入了基本反射,并且在 J2SE 5.0 中添加了 AnnotatedElementAnnotation 接口来支持批注。这两个接口使您可以定位批注,并在您获得批注的句柄后进行调用以检索它的参数。

新的 AnnotatedElement 接口位于 java.lang.reflect 程序包中并由 ClassConstructorFieldMethodPackage 等类实现:
public interface AnnotatedElement {
Annotation[] getAnnotations();
Annotation[] getDeclaredAnnotations();

<T extends Annotation> T getAnnotation(Class<T>);

boolean isAnnotationPresent(Class<? extends Annotation>);
}
getAnnotations() 方法返回附加到给定元素的所有批注。getDeclaredAnnotations() 方法与该方法相似,但只返回在该位置经过特殊声明的批注,而非 @Inherited 批注。 getAnnotation() 方法接受一个类类型,返回该类型的批注。该方法使用范型,因此将把返回的值相应地进行隐式转换。无论将何种类型的类传递给该方法,它均返回此类型。isAnnotationPresent() 方法使您可以查看批注是否存在而不必检索批注。它也使用范型强制它所接受的 Class 类型必须是实现 Annotation 的类。 每个批注类型自动实现 java.lang.annotation.Annotation 接口。这在您使用 @interface 关键字声明一个新批注类型时将在后台实施此接口。使用 AnnotatedElement 上的方法,您可以获取任何 Annotation 类型。例如,以下代码提取前一篇文章中的 UnfinishedDemo@Unfinished 批注并请求它的优先级:
Unfinished u =
UnfinishedDemo.class.getAnnotation(Unfinished.class);
u.priority();
注意,范型使接口声明显得有点凌乱,但使功能代码却非常雅致!此外,还有一点比较有意义的是,用于声明批注参数的已被删除的方法最终成为被调用来检索其值的方法。 以下示例演示了如何对 UnfinishedDemo 类所有未完成的部分执行完整的“转储”。
import com.servlets.*;
import java.lang.reflect.*;
import java.util.*;

public class UnfinishedDump {
public static void main(String[] args) {
Class c = UnfinishedDemo.class;
System.out.println("Package:");
dump(c.getPackage());
System.out.println("Class:");
dump(c);
System.out.println("Constructor:");
dump(c.getConstructors());
System.out.println("Methods:");
dump(c.getMethods());
}

public static void dump(AnnotatedElement[] elts) {
for(AnnotatedElement e :elts) { dump(e); }
}

// Written specifically for Unfinished annotation type
public static void dump(AnnotatedElement e) {
if (e == null ||
!e.isAnnotationPresent(Unfinished.class)) {
return;
}
Unfinished u = e.getAnnotation(Unfinished.class);
String desc = u.value();
Unfinished.Priority prio = u.priority();
String[] owner = u.owner();
System.out.println(" " + desc + "; prio:" + prio +
"; owner:" + Arrays.asList(owner));
}
}
下面我们将逐步介绍此代码。main() 方法请求有关 UnfinishedDemo 类、它的程序包、它的构造函数及其每个方法的信息转储。dump() 方法有两个重载的变体形式。第一个变体使用 Java 的新 foreach 循环接受一个数组,并对数组中的每一项调用 dump()。第二个变体接受一项并执行批量操作。 起主要作用 dump() 方法先检查 Unfinished 批注是否存在。如果不存在则不显示任何内容,因此它将返回。如果存在,它将使用 getAnnotation() 获取批注,并获取它的值、优先级以及所有者列表,然后将这些值打印到控制台。它使用 List 包装该数组,作为一种打印数组的简单方法。 输出如下所示:
Package:
Package scope; prio:MEDIUM; owner: []
Class:
Class scope; prio:LOW; owner: []
Constructor:
Constructor; prio:MEDIUM; owner: []
Methods:
Method; prio:MEDIUM; owner:[Jason]
一个基本的 @Test 工具 我在前面指出了 @Test 批注驱动的测试工具的可能性。以下是 java.sun.com/j2se/1.5.0/docs/guide/language/annotations.html 中的基本示例。 我们将保持批注本身的简单性。它不接受参数,一直持续存在到运行时,并只能应用于方法:
import java.lang.annotation.*;

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface Test { }
我们将指定这样一个约定:如果在执行时未抛出异常,则任何标记为 @Test 的方法将通过,而如果异常传播到调用者,则该方法将失败。因此,以下简单示例包含一个成功的方法、一个失败的方法以及一个未测试的方法:
public class Foo {
@Test public static void m1() { }
public static void m2() { }
@Test public static void m3() {
throw new RuntimeException("boom");
}
}
可以将测试工具实现为如下所示的简单代码:
import java.lang.reflect.*;
import static java.lang.System.out;

public class RunTests {
public static void main(String[] args) throws Exception {
int passed = 0, failed = 0;
for (Method m :Class.forName(args[0]).getMethods()) {
if (m.isAnnotationPresent(Test.class)) {
try {
m.invoke(null);
passed++;
} catch (Throwable ex) {
out.printf(
"Test %s failed:%s %n", m, ex.getCause());
failed++;
}
}
}
out.printf("Passed:%d, Failed:%d%n", passed, failed);
}
}
main() 方法加载指定为 args[0] 的类并迭代它的每个方法。对于每个方法,它将检查 @Test 批注是否存在,如果存在则调用该方法。成功的返回将使传递的计数递增。任何异常将触发捕获、错误报告以及失败递增。当可测试的方法全部完成运行后,该方法将打印一个最终的摘要。一个更高级的版本将检查 Annotation 以确定是否以及如何调用该方法。 细心的用户将注意到 out.printf() 调用。printf() 方法是 J2SE 5.0 引入的一个新方法,其作用与 C 语言中我们最常使用的旧 printf() 方法非常相似。格式字符串中的 %d 替换为一个十进制值,而 %n 解析为特定于平台的换行符。由于在代码开头使用了 static import 语句,因此我们能够用“out”代替“System.out”,这是另一个 J2SE 5.0 特性。 编译时 编译时的批注处理要比运行时处理涉及更多的操作,但功能却强大得多。为提供编译时挂钩,Java 5 引入了一个称作“apt”的新批注处理工具。它是一个用于 javac 的包装程序,其中包含一个 com.sun.mirror.* Mirror API,用于以编程方式访问通过该工具处理的代码。使用 apt 工具,您可以在编译期间发出注释、警告甚至错误。还可以生成新文本文件、二进制文件或源文件。这正是该工具的意义所在。 假设您需要一个不变的子类,即一个类似于它的超类但任何更改其值的尝试都将导致异常的类。Java 集合库中实现了该类,并且它是其他程序中一个常见的约定。遗憾的是,由于使子类与超类保持同步并且不使新的 setter 方法遗漏比较困难,因此用纯 Java 编写非常困难。 使用批注即可解决此问题。首先,我们编写一个基本的批注类型 @Immutable
import java.lang.annotation.*;

@Documented
@Target(ElementType.TYPE)
public @interface Immutable {
String value();
}

Then we can add the annotation to each immutable subclass:

public class Project {
// Content here
}

@Immutable
public class ImmutableProject { }
即使保留 ImmutableProject 为空,apt 工具也可以在编译过程中生成一个全新的 ImmutableProject.java。可以通过为 apt 工具提供一个 AnnotationProcessorFactory(返回自定义 AnnotationProcessor 实例)来控制该工具。每个 AnnotationProcessor 可以使用 Mirror 类通过工具和输出注释、警告、错误、支持文件或新源文件(@Immutable 需要新的源文件)的检查这些类。在 apt 工具完成后,它将调用 javac。 以下是一个只支持 @Immutable 并返回 ImmutableAnnotationProcessor 的基本 AnnotationProcessorFactory 实现:
import java.util.*;
import java.io.*;
import com.sun.mirror.apt.*;
import com.sun.mirror.declaration.*;
import com.sun.mirror.util.*;

public class ImmutableAnnotationProcessorFactory
implements AnnotationProcessorFactory {
public AnnotationProcessor
getProcessorFor(Set<AnnotationTypeDeclaration> atds,
AnnotationProcessorEnvironment env) {
if (!atds.isEmpty()) {
return new ImmutableAnnotationProcessor(env);
}
else {
return AnnotationProcessors.NO_OP;
}
}

public Collection<String> supportedAnnotationTypes() {
return Collections.singletonList("Immutable");
}

public Collection<String> supportedOptions() {
return Collections.emptyList();
}
}
工厂非常简单。重要之处蕴含在如下所示的 ImmutableAnnotationProcessor 类中:
import java.util.*;
import java.io.*;
import com.sun.mirror.apt.*;
import com.sun.mirror.declaration.*;
import com.sun.mirror.util.*;

public class ImmutableAnnotationProcessor implements AnnotationProcessor {
private final AnnotationProcessorEnvironment env;

ImmutableAnnotationProcessor(AnnotationProcessorEnvironment env) {
this.env = env;
}

public void process() {
DeclarationVisitor visitor =
DeclarationVisitors.getSourceOrderDeclarationScanner (
new ClassVisitor(),
DeclarationVisitors.NO_OP);
for (TypeDeclaration type :env.getSpecifiedTypeDeclarations()) {
type.accept(visitor);
}
}

private class ClassVisitor extends SimpleDeclarationVisitor {
public void visitClassDeclaration(ClassDeclaration c) {
Collection<AnnotationMirror> annotations = c.getAnnotationMirrors();
TypeDeclaration immutable = env.getTypeDeclaration("Immutable");
for (AnnotationMirror mirror :annotations) {
if (mirror.getAnnotationType().getDeclaration().equals(immutable)) {
ClassDeclaration superClass = c.getSuperclass().getDeclaration();

// Check that we found a super class other than Object
if (superClass.getSimpleName().equals("Object")) {
env.getMessager().printError(
"@Immutable annotations can only be placed on subclasses");
return;
}

String errorMessage = null;
Map<AnnotationTypeElementDeclaration,AnnotationValue> values =
mirror.getElementValues();
for (Map.Entry<AnnotationTypeElementDeclaration, AnnotationValue>
entry :values.entrySet()) {
AnnotationValue value = entry.getValue();
errorMessage = value.toString();
}

String newline = System.getProperty("line.separator");
String packageString = c.getPackage().getQualifiedName();
String newClass = c.getSimpleName();
try {
StringBuffer sourceString = new StringBuffer();
sourceString.append("package " + packageString + ";" + newline);
sourceString.append("public class " + newClass + " extends " +
superClass.getSimpleName() + " { " + newline);
Collection<MethodDeclaration> methods = superClass.getMethods();
for (MethodDeclaration m :methods) {
if (m.getSimpleName().startsWith("set")) {
Collection<Modifier> modifiers = m.getModifiers();
for(Modifier mod :modifiers) {
sourceString.append(mod + " ");
}
sourceString.append(m.getReturnType() + " ");
sourceString.append(m.getSimpleName() + "(");
Collection<ParameterDeclaration> params = m.getParameters();
int count = 0;
for (ParameterDeclaration p :params);
sourceString.append(p.getType() + " " + p.getSimpleName());
count++;
if (count != params.size()) {
sourceString.append(", ");
}
}
sourceString.append(") {" + newline);
sourceString.append("throw new RuntimeException(" +
errorMessage + ");" + newline);
sourceString.append("}" + newline);
}
}
sourceString.append("}" + newline);

System.out.println("------- GENERATED SOURCE FILE --------");
System.out.println(sourceString.toString());
System.out.println("--------------------------------------");
PrintWriter writer = env.getFiler().
createSourceFile(packageString + "."+ newClass);
writer.append(sourceString);
}
}catch(IOException e){}
env.getMessager().printError("Failed to create " + newClass +
": "+e.getMessage());
}
}
}
}
}
}
该处理器访问查找标记为 @Immutable 的类的声明,并在找到一个这样的类时查看以“set”开头的超类的方法,然后将其复制到一个新的源文件。权限、返回值和参数均被复制,而主体则被硬编码以抛出 RuntimeException。如果标记为 @Immutable 的类缺少非对象超类,则处理器逻辑将生成一个编译错误。当处理器完成并生成替换源文件后,将把控制传递给 javac。 apt 工具被安装到 javac 的旁边并共享相似的命令行选项,并添加了 sm -factory(将 apt 指向 sm AnnotationProcessorFactory 实例)和 sm -factorypath(指示在何处查找工厂的类文件)。
apt -factorypath .-factory ImmutableAnnotationProcessorFactory *.java
在编写本文时,还没有 <apt> Ant 任务,但您可以使用 <exec> 调用 apt:
<exec executable="apt">
<env key="PATH" path="${java.home}/bin"/>
<arg line="-d ${classes}"/>
<arg line="-s ${temp}"/>
<arg line="-cp ${classpath}"/>
<arg line="-factorypath ${build}"/>
<arg line="-factory org.qnot.ImmutableAnnotationProcessorFactory"/>
<arg line="${temp}/org/qnot/Project.java"/>
<arg line="${temp}/org/qnot/ImmutableProject.java"/>
<arg line="${temp}/org/qnot/Immutable.java"/>
<arg line="-nocompile"/>
</exec>
下面我将介绍最后一个问题。该基本的 @Immutable 实现有一个弱点:即假设每个 setter 方法以“set”开头。实际类有 delete()remove() 方法以及其他不需要覆盖的方法。您想到了哪些可以解决此问题的机制吗?如果您已经向每个 setter 方法中添加了批注,则恭喜您了!编写这样的批注并调整处理器来识别它可能是您进行编译时批注处理的第一个生动实验。

后续文章

在本系列的下一篇文章即最后一篇文章中,我将介绍 JSR-181 引入的元数据批注如何简化 Web 服务的编写和部署。


Jason Hunter 撰写了《Java Servlet 编程》,并与他人合著了《Java 企业最佳应用》(两者都由 O'Reilly 出版)。他是 Apache 的成员,作为 Apache 派驻 Java 团体发展进程执行理事会的代表,他缔结了 Java 开放源代码的里程碑式协议。他是 Servlets.comXQuery.com 的发起人、Apache Tomcat 的最早的贡献者、com.oreilly.servlet 库的创始人,以及负责 Servlet、JSP、JAXP 和 XQJ API 开发的专家组的成员。他是 JDOM 库的创始人之一,该 JDOM 库实现了优化的 Java 和 XML 集成。在 2003 年,他获得了 Oracle 杂志年度编辑奖。

将您的意见发送给我们

分享到:
评论

相关推荐

    JAVA2核心技术第1卷:基础知识(原书第7版)(PDF中文版)part3

    JAVA2核心技术第1卷:基础知识(原书第7版)(PDF中文版)part3(压缩包名:JAVA2核心技术第1卷:基础知识(原书第7版).part3) 其余部分可在“搜索”按钮前面的文本框内填上本资源的关键字进行搜索。 或者点击“高级搜索...

    JAVA.2核心技术.卷II:高级特性(原书第7版).part3.rar

    本书是Java技术权威指南,全面覆盖Java技术的高级主题,包括流与文件、XML、网络、数据库编程、高级Swing、高级 AWT、JavaBean构件、安全、分布式对象、脚本、编译与注解处理等,同时涉及本地化、国际化以及Java SE ...

    JAVA.2核心技术.卷II:高级特性(原书第7版).part2.rar

    本书是Java技术权威指南,全面覆盖Java技术的高级主题,包括流与文件、XML、网络、数据库编程、高级Swing、高级 AWT、JavaBean构件、安全、分布式对象、脚本、编译与注解处理等,同时涉及本地化、国际化以及Java SE ...

    AJax详解.chm

    第 3 部分: Ajax 中的高级请求和响应 第 4 部分: 利用 DOM 进行 Web 响应 第 5 部分: 操纵 DOM 第 6 部分: 建立基于 DOM 的 Web 应用程序 第 7 部分: 在请求和响应中使用 XML 第 8 部分:在请求和响应中使用 XML 第...

    Java核心技术·卷2:高级特征(原书第9版)

    英文第九版完整版,不看二手货,不解释。 Java核心技术·卷2:高级特征(原书第9版)

    JAVA.2核心技术.卷II:高级特性(原书第7版).part1.rar

    本书是Java技术权威指南,全面覆盖Java技术的高级主题,包括流与文件、XML、网络、数据库编程、高级Swing、高级 AWT、JavaBean构件、安全、分布式对象、脚本、编译与注解处理等,同时涉及本地化、国际化以及Java SE ...

    JAVA.2核心技术.卷II:高级特性(原书第7版).part5.rar

    本书是Java技术权威指南,全面覆盖Java技术的高级主题,包括流与文件、XML、网络、数据库编程、高级Swing、高级 AWT、JavaBean构件、安全、分布式对象、脚本、编译与注解处理等,同时涉及本地化、国际化以及Java SE ...

    JAVA.2核心技术.卷II:高级特性(原书第7版).part4.rar

    本书是Java技术权威指南,全面覆盖Java技术的高级主题,包括流与文件、XML、网络、数据库编程、高级Swing、高级 AWT、JavaBean构件、安全、分布式对象、脚本、编译与注解处理等,同时涉及本地化、国际化以及Java SE ...

    java错误处理:java.lang.OutOfMemoryError: Java heap space

    搜集整理关于java错误处理:java.lang.OutOfMemoryError: Java heap space java.lang.OutOfMemoryError: Java heap space 资料整理

    java.io.CharConversionException: isHexDigit处理

    java.io.CharConversionException: isHexDigit.最近项目运行出现了这个问题,刚开始就知道是编码问题,怎么改编码都不正确,在网上搜到的文章。

    深入Java虚拟机(原书第2版).pdf【附光盘内容】

    《深入Java虚拟机(原书第2版)》,原书名《Inside the Java Virtual Machine,Second Edition》,作者:【美】Bill Venners,翻译:曹晓钢、蒋靖,出版社:机械工业出版社,ISBN:7111128052,出版日期:2003 年 9 ...

    java核心技术卷二(英文版mobi格式)

    全面覆盖Java技术的高级主题,包括流与文件、XML、网络、数据库编程、高级Swing、高级 AWT、JavaBean构件、安全、分布式对象、脚本、编译与注解处理等,同时涉及本地化、国际化以及Java SE 6的内容。《JAVA核心技术...

    数据结构与问题求解Java语言

    作者采用了独特的方法将数据结构分成说明和实现两部分,并充分利用了已有的数据结构库(Java集合类API)。本书分为四个部分:第一部分讨论适合大多数应用的集合类API的一个子集,并覆盖基本的算法分析技术、递归和...

    java8-examples:Java8高级编程示例

    第 5 章:使用流处理数据 第 6 章:使用流收集数据 第 7 章:并行数据处理和性能 第 8 章:重构、测试、调试 第 9 章:默认方法 第 10 章:使用 Optional 作为 null 的更好替代品 第 11 章 CompletableFuture:可...

    JAVA核心技术第八版(上下卷)全部源码

    《Java核心技术》出版以来一直畅销不衰,深受读者青睐,每个新版本都尽可能快地跟上Java开发工具箱发展的步伐,而且每一版都重新改写了的部分内容,以便适应Java的最新特性。本版也不例外,它反遇了Java SE6的新特性...

    java一次性查询处理几百万数据解决方法

    java一次性查询处理几百万数据解决方法 几百万数据是可以处理的 暂时还没试过几千万级的数据处理

    Java入门1·2·3:一个老鸟的Java学习心得.PART3(共3个)

    第3章 Java中的基本数据类型和运算符 33 教学视频:1小时5分钟 3.1 Java中的基本数据类型 33 3.1.1 基本数据类型——编程语言中的数据原子 33 3.1.2 Java中的基本上数据类型介绍 34 3.1.3 基本数据类型值域 34 ...

    数据结构与算法分析_java语言描述

    作者采用了独特的方法将数据结构分成说明和实现两部分,并充分利用了已有的数据结构库(Java集合类API)。本书分为四个部分:第一部分讨论适合大多数应用的集合类API的一个子集,并覆盖基本的算法分析技术、递归和...

    使用Flex,Java,Json更新Mysql数据【高级篇】

    前面已经介绍如何使用Flex,java,json来更新datagrid中...所在在高级篇中我想传递的数据只是用户更新的那部分。这样效率就会明显提高了。 文章参考: http://blog.chinaunix.net/u/21684/showart_1010467.html&lt;br&gt;

Global site tag (gtag.js) - Google Analytics