探索 Roo 的架构

工程 | Ben Alex | 2009 年 6 月 18 日 | ...

上个月我们发现了使用 Spring Roo(我们为 Java 开发人员提供的全新生产力工具)在短短几分钟内 构建一个功能齐全的企业应用程序 是多么容易。虽然许多 Java 开发人员 已经 开始 评估 Roo 帮助 节省 他们 项目 时间,但我收到了许多人提出的关于 Roo 实际工作原理的问题。在这篇博文中,我将深入探讨 Roo 的架构,包括其目标、原型替代方案、设计原理和实现细节。到最后,您将对 Roo 的运作方式及其方法为何适用于 Java 项目有很好的理解。

新的 Roo 和 STS 发布

在深入探讨 Roo 的架构细节之前,我应该简要提及我们今天发布了 Spring Roo 1.0.0.M2。这个新版本包含 数十个错误修复和次要增强,还包括:

  • 一个非常好的单元测试模拟功能(由 Rod Johnson 编写)
  • Java 和 SQL 保留字检测(因此您不再会意外地将字段命名为“from”之类的名称)
  • 能够指定您想要使用的特定 Java 版本(对 Apple 用户尤其重要)
  • 额外的 Spring Web Flow 配置(因此您现在有一个合适的流程可以玩)
  • 将动态查找器自动暴露到 Web 层
  • 改进了对 Windows 用户和非英语默认区域设置用户的支持

自我的 上一篇博文 以来,我们还发布了 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 核心与 Roo 附加组件

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 目前还附带 JPAJSP 附加组件。这两个都是我们在 Roo 1.0.0 中支持典型 Web 应用程序开发所做的实用选择。没有任何技术原因可以阻止开发 JDBCJDOJSFVelocityFreeMarker 附加组件,我们希望随着时间的推移看到这些附加组件。

由于这篇博文侧重于 Roo 的架构,我将在此结束对单个附加组件的讨论。如果您想了解有关如何使用当前 Roo 1.0.0 附加组件构建应用程序的更多信息,可以阅读我的 上一篇博文。现在,让我们深入探讨 Roo 的实际工作原理。

Roo 的设计目标

在审查任何技术时,重要的是要考虑影响其架构选择的设计目标和目的。我在我的 原始 Roo 博文 中探讨了其中一些目标,但让我们在这里更详细地重新讨论这个主题。

最重要的是,我们希望 Roo 成为 Java 开发人员的生产力解决方案。许多开发人员喜欢(或需要)使用 Java,而 Java 仍然是 全球使用最广泛的编程语言。为这一庞大的开发人员群体提供一流的生产力工具是 Roo 最根本的目标。

其次,我们希望确保消除采用 Roo 的障碍。如果人们不习惯(或根本不允许)使用它,那么拥有一个出色的生产力工具就毫无意义。具体来说,这意味着没有 锁定(即轻松移除 Roo),没有运行时部分(以及许多组织中潜在的审批障碍),没有不自然的开发技术,没有 IDE 依赖,没有许可成本,没有奇怪的依赖使其工作,没有陡峭的学习曲线,并且不牺牲速度、性能或灵活性。

第三,我们希望提供一个建立在 Java 诸多优势之上的解决方案。这些优势包括极佳的运行时性能、标准的可用性(如 JPABean ValidationRESTServlet API 等)、出色的 IDE 支持(如调试器、代码辅助、重构等)、成熟的技术、类型安全,以及庞大的现有开发人员知识、技能和经验库(不仅是 Java 本身,还有事实上的 Java 构建块,如 Spring、JSP、Hibernate 等)。

Roo 架构的替代方案

考虑到上述要求,我在 2008 年原型化了许多不同的技术,包括 JSR 269(Java 6 中的可插拔注解处理 API)、构建时源代码生成、IDE 插件、开发时字节码生成、运行时字节码生成以及高级反射方法,例如 Spring Framework AOP 的扩展。我没有原型化其他 JVM 语言,因为支撑 Roo 的主要动机是实现 Java 编程的工具。

我原型化的每种方法都或多或少存在问题,导致其被排除。每种方法都需要特殊的运行时、特殊的 IDE 插件或次优的构建步骤(或其组合)。大多数还永久性地将用户锁定在该方法中,移除过程异常困难,从而造成采用障碍,阻止许多 Java 开发人员享受所提供的生产力提升。许多方法还依赖于运行时反射技术,这会使调试变得缓慢和混乱,并且大多数几乎没有提供 IDE 集成。我还特别倾向于提供一个轻量级的命令行工具,因为我坚信这将提供比 GUI 更好的可用性体验。这些是我们没有使用上述方法的原因。

Roo 架构概述

经过大量的原型设计,我们确定了 Roo 架构,其关键要素包括:

  • 一个支持 Tab 键自动补全、上下文感知、提供提示的命令行 Shell,用户可以随时加载和退出,并支持与文本编辑器和 IDE 并发使用
  • 使用 仅具有源级别保留 (而非运行时保留) 的 @Roo* 注解
  • AspectJ 类型间声明(ITD,也称为“引入”或“混入”),用于自动维护 Java 成员(我们将在下面深入讨论 ITD)
  • 一个 元数据 模型,以方便自定义 Roo 附加组件的开发(我们还将在下面讨论元数据模型)
  • 得益于元数据模型和上述各种核心服务,具备完整的往返能力

这种架构不需要特殊的构建系统、运行时组件、IDE 插件或类似的东西。它也满足了前面提到的所有设计要求。

Roo 的秘诀

实现这一点的“新想法”是自动将 ITD 用作代码生成工件。以这种方式使用 ITD 带来了巨大的实际好处,因为它允许 Roo 生成的代码位于与开发人员编写的代码不同的 编译单元(即物理文件)中。尽管位于单独的文件中,但 ITD 在编译时会合并到同一个编译后的 .class 文件中。由于生成的类本质上与开发人员自己编写所有代码时相同,因此传统 Java 编程的所有优点(如 IDE、调试器支持、代码辅助、类型内省、类型安全等)都按您预期的方式工作。此外,由于编译后的类只是一个类文件,因此在运行时一切都完美运行。具体来说,您无需担心 反射性能、内存使用、混乱且难以调试的操作、可能需要批准和升级的额外库等问题。

将 ITD 用于代码生成令人兴奋的还有它所带来的 关注点分离。关注点分离对应用程序开发人员有利,因为他们可以安全地忽略 Roo 创建的 ITD 文件(因为开发人员知道 Roo 会管理它们)。但关注点分离对 Roo 附加组件也同样出色。附加组件的开发要容易得多,因为附加组件开发人员知道他们控制着整个 ITD 编译单元的内容。一个更微妙的好处是它提供了自动升级支持。在 Roo 的开发过程中,我们看到了许多示例,我们改进了一个附加组件,然后随后加载 Roo 的用户会自动收到升级后的 ITD。类似地,用户可以从其环境中移除附加组件,Roo 会自动移除相关的 ITD。这是一种我们发现非常有价值的极其实用和有用的技术。

ITD 的最后一个主要好处是避免锁定。正如我们稍后将看到的,ITD 本质上是普通的 Java 源文件。它们只是与所有其他源代码一起存储在您的磁盘上,这意味着开发人员可以选择不再加载 Roo,他们的项目仍然可以工作。那些想要更彻底移除的人可以使用 Eclipse AJDT 的“推入重构”等功能。这会自动将所有源代码从 ITD 移动到正确的 Java 源文件中。这意味着如果您不想再使用 Roo,只需“推入重构”您的项目,您就会得到一个完美的普通 Java 项目——就像您自己手动编写所有代码一样。这是一个绝佳的消息:

  • 想要启动项目的人可以非常轻松地做到这一点,然后移除 Roo(顺便说一句,他们也可以随时恢复使用 Roo,并且它会正常工作)
  • 希望利用 Roo 实现长期生产力提升的人可以完全放心地这样做,因为他们知道将来只需单击几下鼠标就可以非常轻松地将其移除

Roo 使用 AspectJ 提供的 ITD。SpringSource 是 AspectJ 的大力支持者和用户,以下是我们认为它非常适合基于 Roo 的项目的一些原因:

  • AspectJ 是一个活跃的项目,拥有庞大的社区
  • AspectJ 成熟、可靠且健壮,其起源可追溯到 2001 年的 PARC
  • AspectJ 得到 Maven、Ant 和 IDE 等主流技术的广泛支持
  • 使用 AspectJ 提供了现有的 IDE 支持,而我们无需编写额外的插件
  • 我们的研究表明,大约一半的 Spring 用户无论如何都在使用 AspectJ
  • AspectJ 不在运行时使用(需要 AspectJ 运行时 JAR,但自 Spring 2.0 以来,这一直是 Spring Framework 的依赖项,因此使用 Spring 2.0 及以上版本的组织已经批准了它)
  • AspectJ 在构建时操作,因此确保了 Java 的性能和 永久代空间 不受影响
  • Roo 的 ITD 使用模式是自动、透明的,不需要用户具备任何 AspectJ(或 ITD)知识、技能或经验
  • 如果开发人员希望使用 AspectJ,则允许采用更高级的编程模式,例如 领域驱动设计 (DDD) 和 强制性切面
  • SpringSource 聘用了 AspectJ(Andy Clement)和 AJDT(Andrew Eisenberg)的当前领导者,以及备受推崇的 AspectJ 专家(如 Ramnivas Laddard 和 Adrian Colyer),因此我们知道我们拥有丰富的内部技能,可以确保 AspectJ 与 Roo 完美配合
  • SpringSource 的许多其他经过生产验证的技术也基于或支持 AspectJ,包括 Spring FrameworkSpring SecuritySpringSource Application Management SuiteSpringSource dm ServerSpringSource tc Server 等等

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 

您的屏幕将如下所示:

first-commands

现在让我们打开一个文本编辑器,看看 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 创建的所有 ITD 都遵循特定的命名约定。约定是 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 文件中。正如你所见,即使你以前从未遇到过 ITD,它们也极其简单。特别是,不需要任何切入点。

让我们编辑 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,那么下次加载它时,将执行自动启动时扫描。这包括如果相关附加组件已升级,则自动升级任何现有的 ITD(甚至在附加组件不再存在时删除 ITD)。重点是所有这一切都是自动且自然发生的,您无需担心遵循关于 Roo 何时必须运行或如何更改文件等特殊规则和约束。

自定义 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

结果屏幕应类似于:

metadata

这总结了 Roo 对 World.java 类型的内部表示。它是通过对 World.java 文件进行 AST 解析和绑定构建的。您可能已经注意到列出了下游依赖项。这些代表了其他元数据项,如果 World.java 元数据发生任何更改,它们希望收到通知。附加组件通常会监听其他元数据项的更改,然后相应地修改 ITD(或 XML 文件或 JSP 等)。

您可以通过输入“metadata trace 1”然后更改 World.java 文件来观察元数据事件通知的发生。通知消息将类似于以下内容:

tracing

在结束对 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 使用 ITD 为 Java 开发人员实现可持续的生产力提升。我们研究了 Roo ITD 方法的优点,并深入了解了它的实际工作原理,包括如何自定义它们,它们如何在元数据级别操作以及它们的生命周期如何透明地自动链接到附加组件升级。我们还讨论了 ITD 如何提供成熟且经过验证的关注点分离,同时避免了锁定、运行时影响以及在大型实际项目中很重要的其他细微问题。最后,我们回顾了 Roo 的元数据系统,并探讨了它的一些事件通知、类型内省和可扩展性功能。

我们期待支持社区参与 Roo 并开发新的附加组件。我们邀请您 试用 Roo,我们非常欢迎您的 反馈错误报告功能想法评论。希望您喜欢使用 Roo

获取 Spring 新闻通讯

通过 Spring 新闻通讯保持联系

订阅

领先一步

VMware 提供培训和认证,助您加速进步。

了解更多

获得支持

Tanzu Spring 提供 OpenJDK™、Spring 和 Apache Tomcat® 的支持和二进制文件,只需一份简单的订阅。

了解更多

即将举行的活动

查看 Spring 社区所有即将举行的活动。

查看所有