充分利用 Java 的元数据,第 3 部分:高级处理
2007-03-10 21:25
357 查看
充分利用 Java 的元数据,第 3 部分:高级处理
作者:Jason Hunter
了解一些用于在运行时(甚至是编译时)处理批注以及更改程序行为的技术和机制。
在本系列文章(共四篇)的第一篇文章中,我介绍了 Java 的新元数据工具以及内置的批注类型 @Override、@Deprecated 和 @SuppressWarning。在第二篇文章中,我介绍了如何编写自定义批注类型并使用 java.lang.annotation 中的元批注控制批注行为。在这第三篇文章中,我将演示用于在运行时(甚至是编译时)处理批注并更改程序行为的技术和机制。
运行时批注处理的价值
能够在运行时与批注交互可以提供重要的价值。设想一个为利用批注而构建的下一代测试工具。此类工具可以运行标记为 @Test 的方法 — 没有用于区分测试方法与支持方法的方法名掩饰。通过对 @Test 批注使用参数,每个测试都可以按逻辑分组,可以控制它所依赖的测试并可以接受各种测试用例参数。(这样的测试工具并非仅是一种假想,实际上您在 beust.com/testng 中就可以找到这样一个工具。)
在 J2EE 5.0 环境(其中批注驱动的“资源注入”似乎成为了标准操作过程)中,这些可能性继续存在。使用资源注入,容器可以将值“注入”到其受管理对象特殊批注的变量中。例如,如果 servlet 需要一个数据源,则 J2SE 1.4 中的模型将从 JNDI 中提取资源:
以上代码不但非常复杂,而且为了使资源可用于 JNDI 查找,该 servlet 必须在其单独的 web.xml 部署描述符中声明一个 <resource-ref> 条目:
而在提供了资源注入功能的 J2SE 5.0 环境中,该 servlet 只需在代码中为它的要求设置批注,并在执行代码前使数据源能够引用“注入的资源”:
尤其值得称道的是,批注本身用作部署描述符,从而不必在 web.xml 部署描述符中声明一个单独的条目。@Resource 批注可以接受指示资源名、类型、验证以及作用域等值的参数。即使没有参数,也可以方便的推断出这些项。
JSR-181(用于 Java 的 Web 服务元数据)现已完成,我们将看到为指导容器部署 Web 服务而广泛使用的批注。批注正逐渐成为定义容器与受管理的对象之间协议的原始材料。我将在本系列的第四篇文章即最后一篇文章中介绍 JSR-181 Web 服务。
运行时反射
Java 使用反射公开批注并使程序能够在运行时更改它们的行为。Java 在 J2SE 1.2 中引入了基本反射,并且在 J2SE 5.0 中添加了 AnnotatedElement 和 Annotation 接口来支持批注。这两个接口使您可以定位批注,并在您获得批注的句柄后进行调用以检索它的参数。
新的 AnnotatedElement 接口位于 java.lang.reflect 程序包中并由 Class、Constructor、Field、Method 和 Package 等类实现:
getAnnotations() 方法返回附加到给定元素的所有批注。getDeclaredAnnotations() 方法与该方法相似,但只返回在该位置经过特殊声明的批注,而非 @Inherited 批注。
getAnnotation() 方法接受一个类类型,返回该类型的批注。该方法使用范型,因此将把返回的值相应地进行隐式转换。无论将何种类型的类传递给该方法,它均返回此类型。isAnnotationPresent() 方法使您可以查看批注是否存在而不必检索批注。它也使用范型强制它所接受的 Class 类型必须是实现 Annotation 的类。
每个批注类型自动实现 java.lang.annotation.Annotation 接口。这在您使用 @interface 关键字声明一个新批注类型时将在后台实施此接口。使用 AnnotatedElement 上的方法,您可以获取任何 Annotation 类型。例如,以下代码提取前一篇文章中的 UnfinishedDemo 的 @Unfinished 批注并请求它的优先级:
注意,范型使接口声明显得有点凌乱,但使功能代码却非常雅致!此外,还有一点比较有意义的是,用于声明批注参数的已被删除的方法最终成为被调用来检索其值的方法。
以下示例演示了如何对 UnfinishedDemo 类所有未完成的部分执行完整的“转储”。
下面我们将逐步介绍此代码。main() 方法请求有关 UnfinishedDemo 类、它的程序包、它的构造函数及其每个方法的信息转储。dump() 方法有两个重载的变体形式。第一个变体使用 Java 的新 foreach 循环接受一个数组,并对数组中的每一项调用 dump()。第二个变体接受一项并执行批量操作。
起主要作用 dump() 方法先检查 Unfinished 批注是否存在。如果不存在则不显示任何内容,因此它将返回。如果存在,它将使用 getAnnotation() 获取批注,并获取它的值、优先级以及所有者列表,然后将这些值打印到控制台。它使用 List 包装该数组,作为一种打印数组的简单方法。
输出如下所示:
一个基本的 @Test 工具
我在前面指出了 @Test 批注驱动的测试工具的可能性。以下是 java.sun.com/j2se/1.5.0/docs/guide/language/annotations.html 中的基本示例。
我们将保持批注本身的简单性。它不接受参数,一直持续存在到运行时,并只能应用于方法:
我们将指定这样一个约定:如果在执行时未抛出异常,则任何标记为 @Test 的方法将通过,而如果异常传播到调用者,则该方法将失败。因此,以下简单示例包含一个成功的方法、一个失败的方法以及一个未测试的方法:
可以将测试工具实现为如下所示的简单代码:
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:
即使保留 ImmutableProject 为空,apt 工具也可以在编译过程中生成一个全新的 ImmutableProject.java。可以通过为 apt 工具提供一个 AnnotationProcessorFactory(返回自定义 AnnotationProcessor 实例)来控制该工具。每个 AnnotationProcessor 可以使用 Mirror 类通过工具和输出注释、警告、错误、支持文件或新源文件(@Immutable 需要新的源文件)的检查这些类。在 apt 工具完成后,它将调用 javac。
以下是一个只支持 @Immutable 并返回 ImmutableAnnotationProcessor 的基本 AnnotationProcessorFactory 实现:
工厂非常简单。重要之处蕴含在如下所示的 ImmutableAnnotationProcessor 类中:
该处理器访问查找标记为 @Immutable 的类的声明,并在找到一个这样的类时查看以“set”开头的超类的方法,然后将其复制到一个新的源文件。权限、返回值和参数均被复制,而主体则被硬编码以抛出 RuntimeException。如果标记为 @Immutable 的类缺少非对象超类,则处理器逻辑将生成一个编译错误。当处理器完成并生成替换源文件后,将把控制传递给 javac。
apt 工具被安装到 javac 的旁边并共享相似的命令行选项,并添加了 sm -factory(将 apt 指向 sm AnnotationProcessorFactory 实例)和 sm -factorypath(指示在何处查找工厂的类文件)。
在编写本文时,还没有 <apt> Ant 任务,但您可以使用 <exec> 调用 apt:
下面我将介绍最后一个问题。该基本的 @Immutable 实现有一个弱点:即假设每个 setter 方法以“set”开头。实际类有 delete() 和 remove() 方法以及其他不需要覆盖的方法。您想到了哪些可以解决此问题的机制吗?如果您已经向每个 setter 方法中添加了批注,则恭喜您了!编写这样的批注并调整处理器来识别它可能是您进行编译时批注处理的第一个生动实验。
后续文章
在本系列的下一篇文章即最后一篇文章中,我将介绍 JSR-181 引入的元数据批注如何简化 Web 服务的编写和部署。
作者: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 中添加了 AnnotatedElement 和 Annotation 接口来支持批注。这两个接口使您可以定位批注,并在您获得批注的句柄后进行调用以检索它的参数。
新的 AnnotatedElement 接口位于 java.lang.reflect 程序包中并由 Class、Constructor、Field、Method 和 Package 等类实现:
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 服务的编写和部署。
相关文章推荐
- 充分利用 Java 的元数据,第 3 部分:高级处理
- 充分利用 Java 的元数据,第 3 部分:高级处理
- 充分利用 Java 的元数据,第 2 部分:自定义批注
- 充分利用 Java 的元数据,第 2 部分:自定义批注
- 充分利用 Java 的元数据,第 2 部分:自定义批注
- java面试⑦高级部分
- Tiger 中的注释,第 1 部分: 向 Java 代码中添加元数据
- Java高级部分容器重点总结下
- JSF 2 简介,第 3 部分: 事件处理、JavaScript 和 Ajax
- Java并发编程高级篇(四):运行多个任务并处理第一个结果
- Android 项目中部分Java文件报红,不影响运行处理
- JSF 2 简介,第 3 部分: 事件处理、JavaScript 和 Ajax
- 解决axis2处理java.util.Date类型对象时丢弃时间部分的问题
- Java高级技术第三章——Java处理时间的方法,详解Date,DateFormat,Calendar类
- Eclipse 中的 JFace 数据绑定,第 3 部分: 使用高级功能(2/19)
- Eclipse 中的 JFace 数据绑定,第 3 部分: 使用高级功能(14/19)
- Spring 事务管理高级应用难点剖析: 第 3 部分
- Java Web 高性能开发,第 3 部分: 网站优化实战
- 掌握 Ajax,第 3 部分: Ajax 中的高级请求和响应
- 掌握 Ajax,第 3 部分: Ajax 中的高级请求和响应