Spring Modulith:我们已经达到模块化成熟度了吗?

每日分享最新,最流行的软件开发知识与最新行业趋势,希望大家能够一键三连,多多支持,跪求关注,点赞,留言。

微服务强制执行强大的模块边界,但微服务的缺点很大。在这篇文章中,学习可管理的方法来实现相同的结果。

设计微服务的主要原因之一是它们强制执行强大的模块边界。然而,微服务的缺点是如此之大,就像砍掉你的右手来学习用左手写字一样;有更多易于管理(并且痛苦更少!)的方法来实现相同的结果。

即使自微服务热潮开始以来,一些冷静的头脑也占了上风。特别是, Spring 框架的开发人员Oliver Drotbohm长期以来一直是模块化替代方案的支持者。这个想法是保持一个整体,但围绕模块进行设计。

许多人涌向微服务,因为他们处理的应用程序类似于意大利面条拼盘。如果他们的应用程序设计得更好,微服务的吸引力就不会那么强大。

为什么要模块化?

模块化是一种减少变更对代码库影响的方法。这与设计(大型)船舶的方式非常相似。

当水不断渗入船内时,船通常会因为阿基米德推力的减小而沉没。为避免一次漏水沉船,它围绕多个水密隔间设计。如果发生泄漏,它会包含在一个隔间中。虽然它并不理想,但它可以防止船沉没,使其能够重新路由到最近的港口,在那里人们可以修理它。

模块化的工作原理类似:它在部分代码周围设置了边界。这样,更改的影响仅限于该部分,不会超出其边界。

在 Java 中,这些部分称为包。与船舶的相似之处到此为止,因为包必须协同工作才能达到预期的结果。包不能“滴水不漏”。Java 语言提供了跨包边界工作的可见性修饰符。有趣的是,最著名的一个public,允许完全交叉包。

设计遵循最小特权原则的边界需要不断努力。很可能在项目的初始开发压力或维护期间随着时间的推移,努力会滑落,边界会衰减。

我们需要一种更先进的方式来强制执行边界。

模块,模块无处不在

在 Java 的悠久历史中,“模块”一直是一种强制边界的解决方案。问题是,即使在今天,关于什么是模块的定义也有很多。

OSGi始于 2000 年,旨在提供可以在运行时安全部署和取消部署的版本化组件。它保留了 JAR 部署单元,但在其清单中添加了元数据。OSGi 很强大,但是开发 OSGi(模块的名称)很复杂。开发人员付出了更高的开发成本,而运维团队则享受了部署带来的收益。DevOps 尚未诞生;它并没有使 OSGi 像它本来应该的那样流行。

与此同时,Java 的架构师也在寻找将 JDK 模块化的途径。与 OSGI 相比,该方法要简单得多,因为它避免了部署和版本控制问题。Java 9 中引入的 Java 模块将自身限制为以下数据:名称、公共 API 以及对其他模块的依赖性。

Java 模块适用于 JDK,但由于先有鸡还是先有蛋的问题,适用于应用程序的效果就差很多了。为了对应用程序有所帮助,开发人员必须将库模块化——而不是依赖自动模块。但只有当有足够多的应用程序开发人员使用它时,库开发人员才会这样做。上次查的时候,20个常用库中只有一半是模块化的。

在构建方面,我需要引用 Maven 模块。它们允许将一个代码拆分到多个项目中。

JVM 上还有其他模块系统,但这三个是最知名的。

执行边界的尝试性方法

如上所述,微服务在开发和部署过程中提供了最终边界。在大多数情况下,它们都是矫枉过正的。另一方面,不可否认的是项目会随着时间的推移而腐烂。即使是最精美的、重视模块化的,如果不经常小心,也注定会变得一团糟。

我们需要规则来强制执行边界,并且需要像测试一样对待它们:当测试失败时,必须修复它们。同样,当一个人违反规则时,就必须改正它。ArchUnit是一种创建和执行规则的工具。一个配置规则并将它们验证为测试。不幸的是,配置非常耗时,必须不断维护才能提供价值。以下是遵循六边形架构原则的示例应用程序的片段:

HexagonalArchitecture.boundedContext(“io.reflectoring.buckpal.account”)
.withDomainLayer(“domain”)
.withAdaptersLayer(“adapter”)
.incoming(“in.web”)
.outgoing(“out.persistence”)
.and()
.withApplicationLayer(“application”)
.services(“service”)
.incomingPorts(“port.in”)
.outgoingPorts(“port.out”)
.and()
.withConfiguration(“configuration”)
.check(new ClassFileImporter()
.importPackages(“io.reflectoring.buckpal..”));

请注意,HexagonalArchitecture该类是基于 ArchUnit API 的定制 DSL 门面。

总的来说,ArchUnit 总比没有好,但只是勉强如此。它的主要好处是通过测试实现自动化。如果可以自动推断架构规则,将会有显着改善。这就是 Spring Modulith 项目背后的想法。

弹簧模块

Spring Modulith 允许:

  • 记录项目包之间的关系
  • 限制某些关系
  • 在测试期间测试限制
  • 它要求一个人的应用程序使用 Spring 框架:它利用后者对前者的理解,通过DI组装获得。

    默认情况下,Modulith 模块是一个与
    SpringBootApplication-annotated 类位于同一级别的包。

    |_ ch.frankel.blog
    |_ DummyApplication // 1
    |_ packagex // 2
    | |_ subpackagex // 3
    |_ packagey // 2
    |_ packagez // 2
    |_ subpackagez // 3

  • #1:应用类
  • #2:模块化模块
  • #3:不是模块
  • 默认情况下,一个模块可以访问任何其他模块的内容,但不能访问其他模块的子包。

    C4

    var modules = ApplicationModules.of(DummyApplication.class);
    new Documenter(modules).writeModulesAsPlantUml();

    要在模块访问常规包时中断构建,请verify()在测试中调用该方法。

    var modules = ApplicationModules.of(DummyApplication.class).verify();

    一个可以玩的样本

    我创建了一个示例应用程序来玩:它模拟在线商店的主页。主页是使用 Thymeleaf 在服务器端生成的,并显示目录项和新闻源。后者也可以通过客户端调用的 HTTP API 访问(我懒得写代码)。项目显示有价格,因此需要定价服务。

    每个功能 – 页面、目录、新闻源和定价 – 都位于一个包中,该包被视为一个 Spring 模块。Spring Modulith 的文档功能生成以下内容:

    让我们检查一下定价功能的设计:

    目前的设计有两个问题:

  • PricingRepository可以在模块外访问
  • 泄漏PricingServiceJPAPricing实体
  • 我们将通过封装不应公开的类型来修复设计。我们将Pricing和PricingRepositorytypes 移动到模块的internal子文件夹中pricing:

    如果我们调用该verify()方法,它会抛出并中断构建,因为Pricing无法从pricing模块外部访问:

    纯文本1个模块“home”依赖于模块“定价”中的非公开类型
    ch.frankel.blog.pricing.internal.Pricing!

    让我们通过以下更改来解决违规问题:

    结论

    通过尝试示例应用程序,我确实喜欢 Spring Modulith。

    我可以看到两个突出的用例:记录现有应用程序和保持设计“干净”。后者避免了应用程序随时间的“腐烂”效应。这样,我们可以保持设计的预期并避免意大利面条效应。

    锦上添花:当我们需要将一个或多个功能切入他们的部署单元时,这很棒。这将是一个非常直接的举措,不会浪费时间来理清依赖关系。Spring Modulith 提供了一个巨大的好处:将每个有影响力的架构决策推迟到最后一刻

    感谢 Oliver Drotbohm 的审阅。

    您可以在 GitHub 上找到源代码。

    走得更远

  • 介绍 Spring 模块
  • 快速开始
  • 参考文档
  • 声明:本站部分文章及图片源自用户投稿,如本站任何资料有侵权请您尽早请联系jinwei@zod.com.cn进行处理,非常感谢!

    上一篇 2022年10月15日
    下一篇 2022年10月15日

    相关推荐