领先一步
VMware 提供培训和认证,助您加速前进。
了解更多上个月,我们发现使用 Spring Roo(我们为 Java 开发者提供的新生产力工具)构建一个功能齐全的企业应用只需几分钟时间,这非常容易。虽然许多 Java 开发者已经开始评估Roo,以帮助在他们的项目上节省时间,但我收到了很多来自好奇 Roo 实际工作原理的人的提问。在这篇博客文章中,我将深入探讨 Roo 的架构,包括其目标、原型备选方案、设计理念和实现细节。读完本文,您将对 Roo 的运作机制及其方法为何对 Java 项目行之有效有一个很好的理解。
在深入探讨 Roo 架构的细节之前,我应该简要提及,我们今天发布了 Spring Roo 1.0.0.M2。此新版本包含数十个 bug 修复和次要增强功能,还包括以下特性:
自从我的上一篇博客文章以来,我们还发布了 SpringSource Tool Suite (STS) 2.1.0.M2。STS 中对 Roo 的支持持续改进,您现在甚至可以将 STS 配置为指向单独下载的 Roo 安装。对于越来越多的编写自己的 Roo 插件或仅仅希望将最新 Roo 版本与 STS 一起使用的人来说,这是个好消息。STS 中其他不错的 Roo 功能包括 CTRL + R“Roo 命令”分派、内置的 Roo Shell、用于执行集成测试或部署(包括部署到云环境!)的额外 Roo 命令等等。如果您还没有下载 STS 2.1.0.M2,我强烈建议您下载。
Roo 的核心是一套允许使用“插件”的核心服务。这些核心服务包括一个 shell、文件系统管理器、文件系统监视器、文件撤销功能、类路径抽象、抽象语法树(AST)解析和绑定、项目构建系统接口、元数据模型、进程管理、引导和实用工具服务。虽然我们稍后会间接探讨其中一些核心服务,但终端用户感兴趣的绝大多数功能都来自插件。如果没有任何插件,Roo 就只是一个复杂的控制台。
当您下载 Roo 时,我们提供了核心服务以及一系列常用插件。所有插件都可以通过 JAR 名称中出现的“addon”关键字来标识。随 Roo 提供的每个插件都是可选的,终端用户可以自由地增强现有插件或创建新插件。实际上,我们非常欢迎社区开发和分享他们认为有用的插件。
鉴于 Roo 的核心服务与用户可能希望使用的插件之间存在设计上的分离,我们在 Roo 1.0.0 中主要关注确保主流 Web 应用可以轻松高效地开发。在后续版本的 Roo 中,我们将提供越来越丰富的插件,帮助用户构建其他类型的应用。
我被问及频繁的一个方面是 Roo 对 Maven 的使用。正如我在上一篇博客文章中提到的,Roo 1.0.0 创建的项目使用 Maven。由于这种 Maven 使用是通过插件实现的,因此很容易添加对其他项目构建系统的支持。实际上,我们收到了许多关于 Ant/Ivy 支持的请求,并且 Jira 中已经有一个关于此的功能请求(ROO-91)。
类似地,Roo 目前也提供了 JPA 和 JSP 插件。这两者都是我们在 Roo 1.0.0 中为支持典型的 Web 应用开发而做出的实用选择。从技术上讲,没有任何理由阻止开发 JDBC、JDO、JSF、Velocity 和 FreeMarker 插件,我们希望随着时间的推移能看到这些插件出现。
由于这篇博客文章专注于 Roo 的架构,我在此结束对单个插件的讨论。如果您想了解更多关于如何使用当前的 Roo 1.0.0 插件构建应用的信息,可以阅读我的上一篇博客文章。现在,让我们更深入地探讨一下 Roo 的实际工作原理。
无论何时评审任何技术,考虑影响其架构选择的设计目标和目的都很重要。我在我的Roo 原创博客文章中探讨了其中一些目标,但我们在这里更详细地重新讨论一下这个话题。
最重要的是,我们希望 Roo 成为 Java 开发者的生产力解决方案。有许多开发者更喜欢(或需要)使用 Java,而且 Java 仍然是全球使用最广泛的编程语言。为这一庞大的开发者群体提供一流的生产力工具是 Roo 最基本的目标。
其次,我们希望确保消除采用 Roo 的障碍。如果人们不习惯(或根本不允许)使用一个很棒的生产力工具,那么拥有它就没有意义。具体来说,这意味着没有锁定(即容易移除 Roo),没有运行时部分(以及许多组织中潜在的审批障碍),没有不自然的开发技术,没有 IDE 依赖,没有许可成本,没有奇怪的依赖使其工作,没有陡峭的学习曲线,并且不对速度、性能或灵活性妥协。
第三,我们希望提供一个基于 Java 众多优势的解决方案。这些优势包括极佳的运行时性能、标准可用性(如 JPA、Bean Validation、REST、Servlet API 等)、出色的 IDE 支持(如调试器、代码辅助、重构等)、成熟的技术、类型安全,以及庞大的现有开发者知识、技能和经验池(不仅包括 Java 本身,还包括 Spring、JSP、Hibernate 等事实上的 Java 构建模块)。
考虑到上述要求,我在 2008 年原型化了许多不同的技术,包括 JSR 269(Java 6 中的可插拔注解处理 API)、构建时源代码生成、IDE 插件、开发时字节码生成、运行时字节码生成以及高级反射方法(如 Spring Framework AOP 的扩展)。我没有原型化其他 JVM 语言,因为 Roo 的核心动机是成为一个支持 Java 编程的工具。
我原型化的每种方法都或多或少存在问题,因此被排除。每种方法都需要特殊的运行时、特殊的 IDE 插件或次优的构建步骤(或它们的组合)。大多数方法还会永久锁定用户,移除极其困难,从而造成了采用障碍,阻止许多 Java 开发者享受所提供的生产力提升。许多方法还依赖于运行时的反射技术,这会慢且难以调试,并且大多数提供的 IDE 集成很少或根本没有。我还特别偏爱提供一个轻量级的命令行工具,因为我坚信这将比 GUI 提供更好的可用性体验。这就是我们没有使用上述方法的原因。
经过大量的原型设计,我们最终确定了 Roo 的架构,其关键要素包括:
这个架构不需要特殊的构建系统、运行时组件、IDE 插件等。它也满足了之前提到的所有设计要求。
使这成为可能的“新想法”是自动将 ITDs 用作代码生成制品。以这种方式使用 ITDs 带来了巨大的实际好处,因为它允许 Roo 生成的代码与开发者编写的代码位于不同的编译单元(即物理文件)中。尽管位于单独的文件中,但在编译时 ITDs 会被合并到同一个编译后的 .class 文件中。由于最终的类与开发者自己编写所有代码所得的类本质上相同,因此传统 Java 编程的所有优势(如 IDE、调试器支持、代码辅助、类型内省、类型安全等)都能正常工作。此外,由于编译后的类只是一个 .class 文件,一切在运行时都能完美运行。具体来说,您不必担心诸如反射性能、内存使用、令人困惑且难以调试的操作、可能需要审批和升级的额外库等问题。
将 ITDs 用于代码生成的另一个令人兴奋之处在于它实现了关注点分离。关注点分离使应用开发者受益,因为他们可以安全地忽略 Roo 创建的 ITD 文件(因为开发者知道 Roo 会管理它们)。但是,关注点分离对 Roo 插件也非常好。插件的开发变得更加容易,因为插件开发者知道他们控制着整个 ITD 编译单元的内容。一个更微妙的好处是它提供的自动升级支持。在 Roo 的开发过程中,我们看到了许多这样的例子:我们改进了一个插件,然后随后加载 Roo 的用户会自动获得一个升级后的 ITD。同样,用户可以从他们的环境中移除插件,Roo 会自动移除相关的 ITDs。这是一种我们发现极其实用和有价值的技术。
ITDs 的最后一个主要好处是避免锁定。正如我们稍后将看到的,ITDs 本质上是普通的 Java 源文件。它们只是和所有其他源代码一样存储在您的磁盘上,这意味着开发者可以选择不再加载 Roo,而他们的项目仍然可以工作。那些想要更彻底移除的人可以使用诸如 Eclipse AJDT 的“推入重构”(push in refactoring)之类的功能。它的作用是将 ITDs 中的所有源代码自动移动到正确的 Java 源文件中。这意味着如果您不想再使用 Roo,只需对您的项目进行“推入重构”,您就拥有了一个完全正常的 Java 项目——就像您自己亲手编写所有代码一样。这是个极好的消息:
Roo 使用 AspectJ 提供的 ITDs。SpringSource 是 AspectJ 的大力支持者和使用者,以下是我们认为它非常适合基于 Roo 的项目的一些原因:
让我们通过创建一个新项目来探索 Roo 的 ITD 用法和元数据模型。假设您已经安装了 Roo 1.0.0.M2,让我们为我们的项目创建一个新目录并启动 Roo
$ mkdir architecture
$ cd architecture
$ roo
收到欢迎屏幕后,输入以下命令
roo> project --topLevelPackage com.hello
roo> persistence setup --provider HIBERNATE --database HYPERSONIC_IN_MEMORY
roo> entity --name World
roo> field string name
图形界面您的屏幕将看起来像这样
现在让我们打开一个文本编辑器,查看 World.java 文件内部
package com.hello;
import javax.persistence.Entity;
import org.springframework.roo.addon.javabean.RooJavaBean;
import org.springframework.roo.addon.tostring.RooToString;
import org.springframework.roo.addon.entity.RooEntity;
@Entity
@RooJavaBean
@RooToString
@RooEntity
public class World {
private String name;
}
如所示,有几个 @Roo* 注解。这些注解包含在 Roo 插件中,指示 Roo 在需要时创建一个 ITD。@RooEntity 注解表示您希望 Roo 自动提供典型的 JPA 方法和字段(包括标识符和版本属性)。@RooJavaBean 请求为每个字段创建 getter 和 setter 方法。@RooToString 请求创建一个 toString() 方法。
Roo 创建的所有 ITDs 都遵循特定的命名约定。约定是 SimpleTypeName + "_Roo_" + AddOnSpecificKeyword + ".aj"。Roo 自动确保所有符合此格式的文件都由相关的插件妥善管理。如果某个特定关键字没有安装插件,Roo 将删除孤立的 ITD 文件。这确保您可以随时更改插件配置,而无需手动清理。
让我们看看 World_Roo_ToString.aj ITD 的内部
package com.hello;
privileged aspect World_Roo_ToString {
public String World.toString() {
StringBuilder sb = new StringBuilder();
sb.append("id: ").append(getId()).append(", ");
sb.append("version: ").append(getVersion()).append(", ");
sb.append("name: ").append(getName());
return sb.toString();
}
}
正如您所见,一个 ITD 看起来就像一个普通的 Java 源文件。只有一个区别:在方法签名中,“toString()”方法名之前有一个“World.”前缀。这指示 AspectJ 在编译时将 toString() 方法引入 World.class 文件。正如您所见,即使您之前从未接触过 ITDs,它们也非常简单。特别是,不需要任何切入点(pointcuts)。
让我们编辑 World.java 文件并向其中添加另一个字段
private String comment;
如果您让 Roo 保持运行,只要您保存 World.java,您就会注意到它会立即修改 World_Roo_JavaBean.aj 和 World_Roo_ToString.aj 文件。这是因为 Roo 监控文件系统,检测您在 Roo Shell 之外进行的任何更改,例如通过您偏好的 IDE。如果您愿意,您也可以使用 Roo 的“add field string”命令。
如果您没有运行 Roo,下次加载它时,会执行一个自动的启动时扫描。这包括在相关插件升级时自动升级任何现有的 ITDs(甚至在插件不再存在时删除 ITD)。重点是这一切都是自动且自然地发生,您无需担心遵循关于 Roo 何时必须运行或如何更改文件等的特殊规则和限制。
所有 @Roo* 注解都允许您控制使用的成员名称,并允许您自己提供成员。让我们编辑 World.java 文件,并将 @RooToString 注解更改为
@RooToString(toStringMethod="rooIsFun")
如果您现在查看 World_Roo_ToString.aj 文件,您会看到方法名已自动更改
package com.hello;
privileged aspect World_Roo_ToString {
public String World.rooIsFun() {
StringBuilder sb = new StringBuilder();
sb.append("id: ").append(getId()).append(", ");
sb.append("version: ").append(getVersion()).append(", ");
sb.append("comment: ").append(getComment()).append(", ");
sb.append("name: ").append(getName());
return sb.toString();
}
}
假设您不喜欢 Roo 的 toString() 方法(现在是 rooIsFun(),请记住!)。您有两种方法可以移除它。您可以在 World.java 文件中删除或注释掉 @RooToString 注解,或者您可以直接在 World.java 中提供您自己的 rooIsFun() 方法。您可以随意尝试这两种技术。在这两种情况下,您都会看到 Roo 自动删除了 World_Roo_ToString.aj 文件,因为它检测到您不再需要 Roo 为您提供该方法了。这反映了 Roo 的方法:您始终拥有完全的控制权,并且没有任何意外。
虽然您不需要了解 Roo 的内部工作原理就可以简单地使用 Roo,但好奇的读者可能想知道 World_Roo_ToString.aj 文件怎么会知道存在 getId()、getVersion()、getComment() 和 getName() 方法。考虑到这些方法甚至不在 World.java 文件中,这尤其有趣。让我们进一步探讨这一点。
在 Roo Shell 中,输入以下命令
roo> metadata for type --type com.hello.World
结果屏幕应该类似于
这是对 Roo 内部表示的 World.java 类型的总结。它是通过对 World.java 文件进行 AST 解析和绑定构建的。您可能已经注意到列出了下游依赖项。这些代表了其他元数据项,它们希望在 World.java 元数据发生变化时收到通知。插件通常会监听其他元数据项的变化,然后相应地修改 ITDs(或 XML 文件或 JSPs 等)。
您可以通过键入“metadata trace 1”,然后更改 World.java 文件来观察元数据事件通知的发生。通知消息将类似于以下内容
在结束对 Roo 元数据模型的介绍之前,我要指出的是,Roo 不需要将元数据保留在内存中。这确保了非常大的项目仍然可以使用 Roo,而不会耗尽内存。Roo 会自动跟踪缓存统计信息以及单个插件的运行时配置文件。内存充足的系统将享受到自动的LRU缓存。如果您对 LRU 缓存统计信息感到好奇,可以通过“metadata status”命令查看(请注意缓存命中率令人满意地高)。
roo> metadata status
2: org.springframework.roo.addon.configurable.ConfigurableMetadata
5: org.springframework.roo.addon.javabean.JavaBeanMetadata
8: org.springframework.roo.addon.finder.FinderMetadata
35: org.springframework.roo.addon.plural.PluralMetadata
53: org.springframework.roo.addon.beaninfo.BeanInfoMetadata
64: org.springframework.roo.addon.entity.EntityMetadata
124: org.springframework.roo.addon.tostring.ToStringMetadata
862: org.springframework.roo.process.manager.internal.DefaultFileManager
[DefaultMetadataService@6030f9 providers = 14, validGets = 369, cachePuts = 17, cacheHits = 352, cacheMisses = 17, cacheEvictions = 0, cacheCurrentSize = 6, cacheMaximumSize = 1000]
我希望您发现关于 Roo 工作原理的讨论很有趣。我们已经看到 Roo 使用 ITDs 为 Java 开发者实现了可持续的生产力提升。我们探讨了 Roo 的 ITD 方法的优势,并深入了解了它的实际工作原理,包括如何自定义它们,它们如何在元数据层面运作,以及它们的生命周期如何透明且自动地与插件升级关联。我们还讨论了 ITDs 如何在提供成熟且经过验证的关注点分离的同时,避免锁定、运行时影响以及在大型实际项目中重要的其他微妙问题。最后,我们回顾了 Roo 的元数据系统,并探讨了它的一些事件通知、类型内省和可伸缩性特性。
我们期待支持社区参与 Roo 并开发新的插件。我们邀请您试用 Roo,并且非常欢迎您的反馈、bug 报告、功能建议和评论。希望您喜欢使用Roo。