探索 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 Core 与 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 本身,还在诸如 Spring、JSP、Hibernate 等事实上的 Java 构建块中)。

Roo 架构的替代方案

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

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

Roo 架构摘要

经过大量的原型设计,我们得出了 Roo 架构,其关键要素是

  • 一个支持制表符补全、上下文感知、提供提示的命令行 shell,用户可以随时加载和退出,并支持与文本编辑器和 IDE 并发使用
  • 使用 @Roo* 注释,这些注释仅具有源级保留(不是运行时保留)
  • AspectJ跨类型声明(ITD,也称为“引入”或“mixin”),用于自动维护的 Java 成员(我们将在下面深入讨论 ITD)
  • 一个元数据模型,以方便开发自定义 Roo 加载项(我们还将在下面讨论元数据模型)
  • 完整的往返功能,这得益于元数据模型和上面提到的各种核心服务

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

Roo 的秘诀

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

使用 ITD 进行代码生成令人兴奋的另一个方面是它带来的**关注点分离**。关注点分离 有利于应用程序开发人员,因为他们可以安全地忽略 Roo 创建的 ITD 文件(因为开发人员知道 Roo 会管理它们)。但是,关注点分离对于 Roo 插件也非常有用。插件的开发变得更加容易,因为插件开发人员知道他们控制着整个 ITD 编译单元的内容。一个更微妙的好处是它带来的自动升级支持。在 Roo 的开发过程中,我们看到了很多例子,我们改进了插件,然后随后加载 Roo 的用户会收到自动升级的 ITD。类似地,用户可以从他们的环境中删除插件,并且相关的 ITD 将由 Roo 自动删除。这是一个极其实用且有用的技术,我们发现它非常宝贵。

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 Framework 自 Spring 2.0 以来一直依赖的,因此使用 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 上或支持 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 的“添加字段字符串”命令。

如果您没有让 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 社区中所有即将举行的活动。

查看全部