转自 http://www.ibm.com/developerworks/cn/java/j-lombok/java
什么时候以及如何为自定义代码生成扩展 Lomboknode
Alex Ruiz 在本文中介绍了 Project Lombok,探讨了它的一些独特的编程特点,包括注释驱动代码生成,以及简洁、紧凑、可读的代码。而后,他会提示你们关注 Lombok 更有价值的用途:利用自定义 AST(Abstract Syntax Tree,抽象语法树)转换来对其进行扩展。扩展 Lombok 使得您能够生成本身的项目或者域特定样板代码,可是,这也确实须要大量的工做。最后 Alex 提供了一些技巧,就是经过简化流程的关键步骤,以及一个自由使用的 JavaBeans 自定义扩展。ios
即便对于保守的 Java™ 开发人员来讲,冗长的语法也是 Java 语言编程的一个弱点。虽然有时可经过采用 Groovy 之类的新语言来避免冗长,可是,不少时候采用 Java 编程是最适合的,有时甚至就是这样要求的。那么您可能会想要尝试 Project Lombok,它是个开源的、用于 Java 平台的代码生成库。git
Lombok 能够方便地减小 Java 应用程序中样板文件的代码量,这样,您就不须要编码大量的 Java 语法。可是使 Lombok 如此贴心的不仅是语法,它是一种独特的代码生成方法,可以开启全部 Java 开发可能性。程序员
在 本文中,我将介绍 Project Lombok,并说明其优越之处,尽管并不完美,但丰富了 Java 开发人员的工具箱。我将为你们提供对 Lombok 的概述,包括它的工做方式以及它最适用的场景,并简单罗列其优缺点。接下来,我将为你们介绍一个最有用,但也很复杂的 Lombok 用例:将其扩展为一个自定义代码基。这多是您本身的代码或者现有的 Java 模板,它还不属于 Lombok 库的一部分。不管哪一种方式,文章的后续部分将侧重于扩展 Lombok 的技巧与窍门,包括肯定是否值得在 Lombok API 上花费时间,或者是否可以为您特定的应用程序更好地编写样本文件。编程
所包括的示例代码(见 下载)扩展 Lombok 来生成 JavaBeans 样板代码。这在 Apache 2.0 环境下许可无偿使用。安全
也 许选用 Lombok 而不是其余代码生成工具的主要缘由就是 Lombok 不只生成 Java 源或者比特代码:它会经过在编译阶段修改其结构来转换抽象语法树(AST)。AST 表明已解析源代码的树,它由编译器建立,与 XML 文件的 DOM 树模型相似。经过修改(或转换)AST,Lombok 可对源代码进行修剪,来避免膨胀,这与纯文本代码生成不一样。Lombok 所生成的代码对于同一编译单元的类是可见的,这不一样于带库的直接字符编码操做,好比 CGLib 或者 ASM。app
Lombok 支持多个触发代码生成的机制,包括了很是流行的 Java 注释。利用 Java 注释,开发人员可以修改已注释的类,这是常规 Java 触发流程所禁止的。eclipse
关于 Lombok 使用的例子,可参考清单 1 中的类:ide
public class Person { private String firstName; private String lastName; private int age; }
向代码中增长 equals
、hashCode
、以及 toString
实施并不困难,只是单调乏味而容易出错。您可采用 Eclipse 之类的现代 Java IDE 来自动生成主要的样本代码,可是,那只是部分解决方案。这是节省了时间与精力,但将以牺牲可读性与可理解性为代价,由于样本代码一般会向应用程序源增长干扰词。
然而,Lombok 有一个很智能的方法来解决样板代码问题。以 清单 1 为例,可经过为 Person.java
类增长 @Data
注释,来方便地生成所需的方法。图 1 展现了 Lombok 在 Eclipse 中的代码生成。在大纲视图中,能够看到在编译类中展现了所生成的方法,同时源文件仍处于样文件以外。
Lombok 支持流行的 Java 编译器 javac 以及 Eclipse Compiler for Java(ECJ)。尽管这两个编译器产生相似的输出,可是他们的实现却彻底不一样。结果是, Lombok 自带两套注释处理程序(挂接到 Lombok 中的代码以及包含的代码生成逻辑):每一个编译器一个。幸运的是,这是透明的,所以,做为用户,咱们仅需面对一套 Java 注释。
Lombok 还提供与 Eclipse 的紧密集成:保存 Java 文件会自动触发 Lombok 的代码生成(没有明显的延迟)并更新 Eclipse 的大纲视图来展现所生产的成员,如 图 1 所示。
对于想要 了解内部状况的开发人员,Lombok delombok
工具将为您提供指导,可经过 Maven 或者 Ant 命令行访问。Delombok 获取经过 Lombok 转换的代码,并依据它来生成普通的 Java 源文件。“已被 delombok 处理过” 的代码将会包含以前由 Lombok 所完成的转换,格式为普通文本。例如,若是将 delombok
应用到 图 1 的代码中,您将可以看到,equals
、hashCode
、以及 toString
已被实施。
在选择 Lombok 并准备在项目中进行应用以前,您应当知道它有一些限制。其中两个主要的方面是:
@SneakyThrows
转换就是个明显的例子。它容许不在方法定义中声明所检查的异常,而将其扔掉,如同它们是未经检查的异常: // normally, we would need to declare that this method throws Exception @SneakyThrows public void doSomething() { throw new Exception(); }
@GenerateGetter
将可以比当前注释 @Getter
更好地交流意图。除了这些 Lombok 相关问题以外,还有一些有关 Eclipse 集成的问题。在大多数状况下,这是因为 Eclipse 不了解 Lombok 代码生成状况所形成的:
NullPointerException
。问题的缘由如今还不清楚。关闭并从新打开 Eclipse 一般就能解决此问题。getName
的代码,Eclipse 调试工具会跳到字段 name
的注释 @Getter
。除此以外,当 Lombok 出现时,Eclipse 调试工具会向日常同样工做。总的说来,这些问题能够绕过,并且从此其中大部分问题可能会被 Lombok 与 Eclipse dev 团队所解决。可是,最好对所要应用的技术有所了解。这能够随时向工具箱中增长新的工具。
Lombok 生成大部分公共 Java 样本代码,包括 getters、setters、equals
、以及 hashCode
,仅举几个例子。这个颇有用,但有时您还须要生成本身的样本代码。例如,Lombok 还不支持一些公共编码模式,好比 JavaBeans。在有些状况下,您可能还须要生成指定给项目或者域的代码。
关 于扩展的最佳用例就是在项目早期阶段,利用新的代码模式来进行原型设计与试验。这些代码模式会愈来愈成熟,所以,Lombok 会使其变动或者加强实施变得很简单:仅需修改注释处理程序(挂接到 Lombok 中来生成代码的那部分代码段)并编译。全部基本代码将被自动更新(除非在所生成代码中的公共约定有变化,致使编译出错)一旦这些代码模式肯定了,就能够选 择 delombok
代码。所以,您就可使用常规 Java 源了。
为扩展 Lombok,须要识别或者建立将触发 Lombok 代码生成的注释。接下来,将须要为所肯定的每一个注释编写注释处理程序。注释处理程序 是实现一对 Lombok 接口以及转换逻辑的类 — aka 代码生成。
如下部分包含了一些建议,从项目设置到测试,这些在建立本身的 AST 转换时可能会颇有用。其中还包括了一些代码示例,演示了用于支持 JavaBeans 的功能性 Lombok 扩展。后续文章将深刻介绍。
正 如我前面所提到的,Lombok 当前支持公共代码模式,但并不能彻底涵盖,包括 JavaBeans。为了演示 Lombok 扩展,我编写了一个用于生成 JavaBeans 全程(plumbing)代码的很是简单的项目。除了展现如何利用自定义注释处理程序来为 javac 与 ECJ 扩展 Lombok,本项目还打包了一些颇有用的工具(好比用于每一个编译器的字段与方法构建程序),这些工具使得整个流程更清晰、更简单。
我采用了 Eclipse 3.6(Helios)以及用于版本 0.10-BETA2 的 Lombok git
库的快照。代码包含了生成 JavaBean “绑定” setters 的。附加的 zip 文件(见 下载 部分)包含如下内容:
@GenerateBoundSetter
与 @GenerateJavaBean
PropertyChangeSupport
字段的生成)附加的代码 具备完整的功能,并已得到 Apache 2.0 下的许可。可从 GitHub(见 参考资料)得到升级版本的代码。此处有一个有关代码功能的快速浏览可寻找灵感。
若是在清单 3 中采用个人触发处理程序编写代码,Lombok 将会生成相似清单 4 中的代码:
@GenerateJavaBean public class Person { @GenerateBoundSetter private String firstName; }
public class Person { public static final String PROP_FIRST_NAME = "firstName"; private String firstName; private PropertyChangeSupport propertySupport = new PropertyChangeSupport(this); public void addPropertyChangeListener(PropertyChangeListener listener) { propertySupport.addPropertyChangeListener(listener); } public void removePropertyChangeListener(PropertyChangeListener listener) { propertySupport.removePropertyChangeListener(listener); } public void setFirstName(String value) { String oldValue = firstName; firstName = value; propertySupport.firePropertyChange(PROP_FIRST_NAME, oldValue, firstName); } }
参阅包含在 示例代码 中的 readme.txt 文件,来了解如何从示例代码的构建文件生成 Eclipse 项目。
以 个人观点看,任何 Lombok 扩展都须要同时支持 javac 与 ECJ,至少如今是这样。Javac 是 Ant 与 Maven 之类的构建工具所默认采用的编译器。然而,在写这篇文章的时候,在与 Lombok 一块儿使用时,Eclipse 能提供最流畅的编码体验。同时支持两个编译器,对于提升开发人员的生产效率是相当重要的。
Javac 与 ECJ 采用相似的 AST 结构。不幸的是,他们的部署彻底不一样,这使得您不得不为每一个注释编写两个注释处理程序,一个用于 javac 另外一个用于 ECJ。有个好消息是 Lombok 团队已经开始了统一 AST API 的相关工做,这将最终实现了在采用两个编译器时,只须要为每一个注释编写一个注释处理程序(见 参考资料)。
接下来须要了解将要处理的事情,对此,最好是去查看源代码。
Lombok 在 javac 与 ECJ 中采用非公共 API 来实现其智能的代码生成技术。由于代码将被插入到 Lombok 中,因此即便没有相同的 API,也应当拥有相似的 API。
非 公共 API 的主要问题是缺乏文档与可靠性。幸运的是,据 Lombok 团队说,他们尚未遇到有关新版本 Eclipse(当 Java 7 发布之后咱们就有机会看到)的兼容性问题。目前,缺少文档是不得不处理的最大的问题。此外,即便有很好的文档,学习两个不一样编译器的 API 确实是个艰苦并耗时的任务。咱们须要的是一个有关 javac 与 ECJ 的 “快速而实用的指南” — 其中一些超出了本文的范围。
有 一个好消息是,Lombok 团队已经完成了大量关于利用 javac 与 ECJ 生成 AST 节点的相关文档工做。强烈建议您阅读一下他们的代码。他们提供了最通用的用例:好比变量声明,方法实施等。阅读 Lombok 的源代码是学习 javac 与 ECJ 的 API 的最快捷的方法。清单 5 展现了 Lombok 所拥有的源代码的示例:
/* final int PRIME = 31; */ { if (!fields.isEmpty() || callSuper) { statements.append(maker.VarDef(maker.Modifiers(Flags.FINAL), primeName, maker.TypeIdent(Javac.getCTCint(TypeTags.class, "INT")), maker.Literal(31))); } }
正如您所见,Lombok 团队已经记录了什么块产生什么。下一次须要生成本地变量的声明时,您能够回到此源,并以此为参考。
不要仅限于阅读 Lombok 的 .java 文件。Lombok 开发人员已经提供了用于设置于构建项目以及用于测试注释处理程序的指针。如下部分会介绍这些主题的更多细节。
若是尝试在项目中自动化依赖管理,那么就很难返回手动方式。Java 体系中有多个构建工具来提供依赖管理,包括 Ivy 与 Maven(见 参考资料)。然而,当建立 Lombok 扩展时,选择范围缩小为一个,而且它是 Ivy。
选 择 Ivy 的理由之一是全部必要的依赖,例如 javac,都位于 Maven 的中心库中 — 这就排除了 Maven。另外一个理由是 Ivy 支持 Maven 库中所没有的管理依赖。能够很方便地指定下载依赖的连接。这一配置须要自定义 ivysettings.xml 配置文件,这个比较简单。
Ivy 位于 Ant 之上,提供对于构建的依赖管理。Lombok 团队采用他们本身开发的 Ivy 的优化版本,ivyplusplus(见 参考资料)。这一 Ivy 扩展提供了一些有用的 Ant 目标(targets),好比从一系列依赖中建立 Eclipse 与 IntelliJ 项目文件。
要设置 Lombok 扩展项目须要以下文件:
您没必要作重复的工做。为节省时间与精力,可关注一下来自 Lombok 的构建文件,或者来自本文 附加资源 与其余所需的内容。
正如前面所提到的,Lombok 的注释不只是元数据,它还能很好地完成通讯任务。它们应当指出它们负责触发一些类型的代码生成。所以,我强烈建议您将全部 Lombok 相关的注释放到 “Generate” 前面。在本文的 源代码 中,我已对触发 JavaBeans 相关源代码 @GenerateBoundSetter
与 @GenerateJavaBean
的注释命名。这一命名规则至少给不熟悉基本代码的开发人员一个线索,即在构建环境中存在生成代码的处理过程。
在扩展 Lombok 时,文档很重要。文档注释处理程序将有益于 AST 转换的维护者,而文档注释将有益于其用户。
采用 javac 或 ECJ API 的代码阅读或了解起来并不繁琐。即便其生成最简单的 Java 代码,也是复杂与耗时的。文档记录注释处理程序会减轻您和您团队的维护工做。关于文档记录问题,我发现如下内容颇有用:
/** * Instructs lombok to generate the necessary code to make an annotated Java * class a JavaBean. * <p> * For example, given this class: * * <pre> * @GenerateJavaBean * public class Person { * * } * </pre> * our lombok annotation handler (for both javac and eclipse) will generate * the AST nodes that correspond to this code: * * <pre> * public class Person { * * private PropertyChangeSupport propertySupport * = new PropertyChangeSupport(this); * * public void addPropertyChangeListener(PropertyChangeListener l) { * propertySupport.addPropertyChangeListener(l); * } * * public void removePropertyChangeListener(PropertyChangeListener l) { * propertySupport.removePropertyChangeListener(l); * } * } * </pre> * </p> * * @author Alex Ruiz */
// public void setFirstName(String value) { // final String oldValue = firstName; // firstName = value; // propertySupport.firePropertyChange(PROP_FIRST_NAME, oldValue, // firstName); // } JCVariableDecl fieldDecl = (JCVariableDecl) fieldNode.get(); long mods = toJavacModifier(accessLevel) | (fieldDecl.mods.flags & STATIC); TreeMaker treeMaker = fieldNode.getTreeMaker(); List<JCAnnotation> nonNulls = findAnnotations(fieldNode, NON_NULL_PATTERN); return newMethod().withModifiers(mods) .withName(setterName) .withReturnType(treeMaker.Type(voidType())) .withParameters(parameters(nonNulls, fieldNode)) .withBody(body(propertyNameFieldName, fieldNode)) .buildWith(fieldNode);
增长一个与咱们在注释处理程序中所采用注释相相似的类级别 Javadoc 注释(在 清单 6 中),有助于注释用户知道并理解当他们使用这些注释是所发生的状况。
若是决定同时支持 javac 与 ECJ,这一提示将颇有用。当拥有两套注释处理程序时,任何错误修正、变动、或者增长都应当对两套(或分支)同时应用。分支越相似,变动就会越快越安全。这种类似性必须同时出如今包级别与文件级别。
包级别一致性:越多越好,每一个分支(javac 与 ECJ)应当具备同等数量的类,采用相同的名字,如图 2 所示:
文件级别一致性:由于这两个分支可能或多或少具备相似数量的类,具备相似的名字,具备相同名字的每对文件中的注释必须尽可能相似:字段、方法计数、方法名字等等,应当都基本相同。清单 8 展现了用于 javac 和 ECJ 的 generatePropertySupportField
方法。请注意,即便对于不一样 AST API,这些方法的实现也是很是类似的。
// javac private void generatePropertyChangeSupportField(JavacNode typeNode) { if (fieldAlreadyExists(PROPERTY_SUPPORT_FIELD_NAME, typeNode)) return; JCExpression exprForThis = chainDots(typeNode.getTreeMaker(), typeNode, "this"); JCVariableDecl fieldDecl = newField().ofType(PropertyChangeSupport.class) .withName(PROPERTY_SUPPORT_FIELD_NAME) .withModifiers(PRIVATE | FINAL) .withArgs(exprForThis) .buildWith(typeNode); injectField(typeNode, fieldDecl); } // ECJ private void generatePropertyChangeSupportField(EclipseNode typeNode) { if (fieldAlreadyExists(PROPERTY_SUPPORT_FIELD_NAME, typeNode)) return; Expression exprForThis = referenceForThis(typeNode.get()); FieldDeclaration fieldDecl = newField().ofType(PropertyChangeSupport.class) .withName(PROPERTY_SUPPORT_FIELD_NAME) .withModifiers(PRIVATE | FINAL) .withArgs(exprForThis) .buildWith(typeNode); injectField(typeNode, fieldDecl); }
测试自定义 AST 转换比您想象的更容易,这要感谢 Lombok 所提供的测试基础设施。为说明测试 AST 转换有多容易,咱们来看一下清单 9 中的 JUnit 测试用例:
import static lombok.DirectoryRunner.Compiler.ECJ; import java.io.File; import lombok.*; import lombok.DirectoryRunner.Compiler; import lombok.DirectoryRunner.TestParams; import org.junit.runner.RunWith; /** * @author Alex Ruiz */ @RunWith(DirectoryRunner.class) public class TestWithEcj implements TestParams { @Override public Compiler getCompiler() { return ECJ; } @Override public boolean printErrors() { return true; } @Override public File getBeforeDirectory() { return new File("test/transform/resource/before"); } @Override public File getAfterDirectory() { return new File("test/transform/resource/after-ecj"); } @Override public File getMessagesDirectory() { return new File("test/transform/resource/messages-ecj"); } }
该测试工做或多或少有点相似下面的状况:
getBeforeDirectory
指定的文件夹中的全部 Java 文件,采用由 getCompiler
与 Lombok 指定的编译器。delombok
建立了已编译类的文本表示。getAfterDirectory
指定的文件夹中的文件。这些文件包含所指望的已编译类的内容。测试将这些文件的内容与在[第 2 步]中所获取的文件进行对比。对比的文件必须具备相同的名字。getMessagesDirectory
中指定的文件夹中读取文件。这些文件包含了所指望的编译器消息(警告与错误)。测试将这些文件的内容与编译过程当中所展现的实际值相对比,若是编译 Java 文件则不须要消息文件,不存在所指望的消息。经过名字来匹配。例如,若是编译 CompleteJavaBean.java
时有指望的编译器消息,则包含此类消息的文件应当命名为 CompleteJavaBean.java.messages
。如您所见,这是一个有很大不一样但颇有效的测试注释处理程序的方法:
我 所描述的测试在验证注释处理程序生成所指望的代码过程当中颇有用。然而,还须要测试所生成代码真的完成了您所指望的任务。要验证所生成代码特性的正确性,需 要编写采用您的 AST 转换的 Java 类,而后编写测试来检查所生成代码的特性。要像代码是您所编写的那样进行测试。
编译并返回那些测试的最简单方法是采用 Ant,这意味着利用 javac 来编译。由于已经测试并了解了采用 ECJ 所生成代码是正确的,因此没必要在 Eclipse 内部(这会使设置严重复杂化)运行这些测试。
我已在本文示例代码中(见 下载)包含了用于 javac 与 ECJ 注释处理程序的测试。
Project Lombok 是简化冗长 Java 代码的有效工具。它经过以不寻常的智能方法使用 Java 注释与编译 API 来实现这一目的。与其余工具同样,它并不完美。实现获益(代码简洁化)是要付代价的:Java 代码失去了其 WYSIWYG 风格,并且,开发者失去了一些喜好的 IDE 功能。在向工具箱中增长 Lombok 以前必定要考虑好它的利弊,肯定所得是否大于所失。
如 果决定采用 Lombok,那就可能会但愿对其进行扩展,来生成本身的样板代码。目前,虽然扩展 Lombok 并不简单,但它是可行的。本文提供了一些关于扩展 Lombok 的指导,并描述了如何进行操做。花费时间与经从来进行 Lombok 扩展,仍是手工建立样板代码,这二者那个更划算您本身决定。