为了账号安全,请及时绑定邮箱和手机立即绑定

Spring Boot与Temporal:轻松集成的经验分享

我超爱 Spring Boot

我也并不孤单。

Spring 无疑是当前最受欢迎的 Java 框架,而在今年的 SpringOne keynote 上,VMware Tanzu 部门的总经理兼副总裁普尔尼玛·帕德马纳巴西(Purnima Padmanabhan)报告称,46% 的企业工作负载使用 Spring。该部门也是 Spring 团队的主要基地。

这是46%的工作负载。这意味着当你在使用银行、保险、医疗记录、电信和公用事业的应用程序时,大约有一半的时间Spring都在发挥作用。流媒体?没错,Netflix全靠Spring。电商?没错,亚马逊也全靠Spring。

这当然有它的道理。

Spring Boot 不仅仅提供了许多非常有价值的抽象实现,比如数据库访问和其他持久化存储访问、RESTful 接口、批处理作业等等,它在其他很多方面也做得很好,比如依赖管理和打包成可执行文件。

春天是我最爱的。

所以当我开始将Temporal应用到我的书中的示例项目时,我其实根本没有考虑过其他选择——我理所当然地选择了Spring Boot。

基于服务结构的快速回顾

提醒一下,这里提到的实验中,我使用 Temporal 来增强了请求/响应风格的 web 服务的弹性,将原本位于 web 服务控制器中的业务逻辑移至工作流中。我现在有了两个 Java 应用程序——一个用于 HTTP API,另一个用于工作流。前一个应用变为了工作流客户端,在收到 HTTP 请求时启动工作流。

今天我在笔记本上做实验时,我们需要考虑这些应用最终如何在生产环境中部署,对于Java来说,这就意味着要用胖Jar包来打包。我不会详细解释胖Jar包的价值,但可以说,在当前容器化的世界里,一个单一的、自包含的、携带所有依赖的工件显然是更好的选择。

好的,所以在上面的图中,我的目标是将每个Java应用打包成jar包。实际上,我希望有一个controller.jar和一个workflow.jar。我的项目已经使用Spring Boot Maven插件将控制器打包成了一个jar包(参见pom文件)。剩下的问题是,我该如何创建包含所有依赖的worker/workflow的fat jar。

Spring Boot 和 Temporal 不兼容 — 或者说,他们能兼容吗?

作为一个 Temporal 新手,我按照这样的教程(项目仓库)构建了我的第一个 Temporal 工作者(教程)。这个项目包含了一些 pom 文件,这些文件不是为了生成 jar 文件,而是为了本地运行工作者而优化的。大多数教程用到了 Maven 的 exec 插件,这个插件能为 Java 构建类路径,然后将这个类路径传递给本地的 java 运行(顺便说一下,这是我第一次使用这个 Maven 插件,当我理解了它是怎么工作的后,我觉得它非常酷)。当然,你可以通过运行 mvn clean package 来生成一个 jar 文件,但是这个 jar 文件不是胖 jar,它只包含工作者、工作流和活动的类文件。你可以运行这个 jar 文件,但你需要自己搞定所有依赖。正如我所说,胖 jar 才是正解。

我可能提到过 😉,我使用 Spring 来应对所有 Java 相关的问题,所以我去查看了 Temporal 和 Spring Boot 相关的故事,结果得到的信息有些混乱。我发现了一些论坛帖子,如这里这里,建议不要将 Spring 和 Temporal 一起使用,但这些帖子有点旧了,像 MaximTihomir 这样的人承诺未来会在 Temporal 中内置支持 Spring。我还看到有些人已经在使用两者了,但没有找到任何关于如何操作的详细说明。

所以我稍微处理了一下,这就是要点。

Spring Boot 和 Temporal 可以一起用,只要你懂怎么弄。

记住,Spring Boot 能搞定很多事情。接下来我会讲述我是怎么用它来解决不同问题的。

我最初的目标就是实现一个fat jar。

我喜欢Spring,但我不狂热——实际上,我对于任何技术都不狂热。只有当技术解决了某个问题时,它才真正有趣,无论这技术多么酷。由于我对Spring得到的信息不一,我认为这正是看看其他选择的好机会。我想要解决的第一个问题是创建那个所谓的“胖jar”。

阴影和Maven Assembly插件 — 它们对我没用

在上述一些论坛帖子中,有人报告同时使用了这两个 Maven 插件,并且我非常轻松地使用了maven shade 插件构建了一个 fat jar。但是运行 jar 文件时却不起作用——在启动 worker 厂房时崩溃了。我在网上搜索了一番,找到了一篇帖子,里面有一些关于 Spring 成功的报告,也有人建议不要使用 shade 插件。所以我没有再继续研究。

然后我试了下maven assembly plugin,结果和之前一样——jar文件构建成功,但当我运行它时,在启动worker工厂的过程中就崩溃了。

Spring Boot Maven 插件 — 总之,就是好用

看了上面链接中的论坛帖子后,受到一些评论的启发,我决定试一下Spring Boot。我曾用Spring Boot Maven插件构建过许多jar文件,所以我直接在pom中添加了以下内容。

<构建>  
    <插件>  
        <插件>  
            <groupId>org.springframework.boot</groupId>  
            <artifactId>spring-boot-maven-plugin</artifactId>  
        </插件>  
    </插件>  
</构建>

没错,我把从时间示例应用程序中借用的所有其他插件都移除了,换成了这个插件。然后我运行了这个。

执行 mvn clean package spring-boot:repackage 命令

然后就这样。

运行以下命令:```
java -jar target/connectionposts-workflow-2.0-SNAPSHOT.jar


就这样,我的程序跑起来了!我有提到过我喜欢Spring框架吗?

简单说明:在上面的 Maven 命令中,你看到的 `spring-boot:repackage` 部分 — 这告诉 Maven 让 Spring 打包成包含所有依赖的 jar 包。如果没有这一步,它将仅生成仅包含该项目类的 jar 文件。如果在你的 pom.xml 中也有以下配置,则 Spring Boot 的重新打包会自动执行。父 pom 已设置了一些默认目标。
<parent>  
    <groupId>org.springframework.boot</groupId>  
    <artifactId>spring-boot-starter-parent</artifactId>  
    <version>3.3.4</version>  
    <relativePath/>  
</parent>
此段XML定义了spring-boot-starter-parent的父依赖信息,包括其groupId、artifactId、版本号以及相对路径。

搞定啦——我成功构建了一个fat jar,利用Spring。

不过你知道吗?

# 我还没用过Spring和Temporal一起工作

那这么说并不完全正确。在我的工作者中,更确切地说是在我的活动中,我已经使用了Spring框架来发起HTTP请求,使用了诸如[RestTemplate(RestTemplate)](https://github.com/cdavisafc/cloudnativepatterns/blob/main/cloudnative-statelessness-temporal/cloudnative-connectionposts/workflow/src/main/java/com/corneliadavis/cloudnative/connectionsposts/workflow/PostAggregatorActivitiesImpl.java#L34)和[ResponseEntity(ResponseEntity)](https://github.com/cdavisafc/cloudnativepatterns/blob/main/cloudnative-statelessness-temporal/cloudnative-connectionposts/workflow/src/main/java/com/corneliadavis/cloudnative/connectionsposts/workflow/PostAggregatorActivitiesImpl.java#L36)这样的工具,但我的应用并不是一个Spring Boot应用。你知道吗?实际上,它的运行非常顺畅。我真的需要将这个应用转换成Spring Boot应用吗?

嗯,我又得解决一个问题了。

就像每个好开发者一样,我最初硬编码了几个值——特别是针对下游服务的URL。我需要提醒你,我是在第5章的代码基础上构建的,而那时还没有处理应用配置或服务发现的问题——因此我通过`application.properties`文件引入这些值。但我之前已经通过Spring Boot将这些值注入到了控制器中。

所以要解决的下一个问题是:如何将属性文件中的值导入工作流或活动代码中。

当然,Spring Boot 并不是唯一的方法来完成这样的任务,但它做得很棒。鉴于我想要真正理解 Spring 和 Temporal 的愿望,我觉得这是个深入学习的好机会。这也就意味着我需要将我的工作应用转换成一个 Spring Boot 应用。

# 将我的工作流程变成一个Spring Boot项目

Spring Boot 可以搞定很多事情。它提供了很棒的编程抽象,比如我之前使用的那些用来发送 HTTP 请求的抽象。它还有很棒的打包工具,比如我用来打包胖jar 的 Spring Boot Maven插件。

但Spring Boot 提供最有用的功能之一是对象的生命周期管理,或者说,Spring 称之为 bean。虽然不是所有的类实例/对象都需要是 bean,Spring bean 可以从依赖注入、自动装配以及通过属性文件进行配置等多种特性中受益。

但允许框架来管理对象的生命周期正是之前提到的帖子链接[帖子链接](https://community.temporal.io/t/temporal-with-springboot/2734)所警告的,因为Temporal本身控制工作流的生命周期,并更希望控制注入这些实例的内容。

经过一番挖掘、阅读和实验,我得出如下结论:Spring Boot 可以管理 Workers 和 Activities 的生命周期,但不能管理工作流。这一点很重要。

在最终实现中,我为我的工作者创建了一个Spring Boot应用程序,但没有为我的工作流及活动创建Spring Bean。使用标准的Spring Boot应用程序结构,我创建了一个 `[PostAggregatorWorkerConfig](https://github.com/cdavisafc/cloudnativepatterns/blob/main/cloudnative-statelessness-temporal/cloudnative-connectionposts/workflow/src/main/java/com/corneliadavis/cloudnative/connectionsposts/workflow/PostAggregatorWorkerConfig.java)` 类,在其中定义了工作者的Bean(还有一些其他的内容);这个配置基本上实现了之前在工作者应用程序内部直接创建的功能。然后 `[PostAggregatorWorker](https://github.com/cdavisafc/cloudnativepatterns/blob/main/cloudnative-statelessness-temporal/cloudnative-connectionposts/workflow/src/main/java/com/corneliadavis/cloudnative/connectionsposts/workflow/PostAggregatorWorker.java)` 的主方法只是启动了配置类中定义的Bean组成的Spring容器。作为其中的一部分,工作者被创建并开始监听Temporal服务器上的适当任务队列。

当应用程序配置被定义为一个Spring bean时,我们可以注入包括应用属性在内的值,我使用了Spring `@Value` 注解来实现。如这里所示[此处](https://github.com/cdavisafc/cloudnativepatterns/blob/main/cloudnative-statelessness-temporal/cloudnative-connectionposts/workflow/src/main/java/com/corneliadavis/cloudnative/connectionsposts/workflow/PostAggregatorWorkerConfig.java#L46),我将这些注解放在传递给用于生成worker的Spring配置类方法的参数上——Spring Boot会自动为我们注入这些参数。

@Bean
public Worker 工人(工人工厂 factory,
@Value("${connectionpostscontroller.connectionsUrl}") String 连接URL,
@Value("${connectionpostscontroller.postsUrl}") String 帖子URL,
@Value("${connectionpostscontroller.usersUrl}") String 用户URL,
@Value("${INSTANCE_IP}") String ip,
@Value("${INSTANCE_PORT}") String p) {
...


注释:此处的“Worker”保持英文原名,因为它是特定的类名;变量名根据语境进行了中文翻译,以增加代码的可读性。

在该方法中,我们创建了 `PostAggregatorActivitiesImpl` 的实例,并传入了这些参数,以便使用。

worker.registerActivitiesImplementations(
new 后处理聚合活动实现(connectionsUrl, postsUrl, usersUrl, ip, p));



定义在`[PostAggregatorActivitiesImpl](https://github.com/cdavisafc/cloudnativepatterns/blob/main/cloudnative-statelessness-temporal/cloudnative-connectionposts/workflow/src/main/java/com/corneliadavis/cloudnative/connectionsposts/workflow/PostAggregatorActivitiesImpl.java)`中的活动现在有了调用下游服务所需的 URL。

以下图展示了我刚才提到的配置。

![](https://imgapi.imooc.com/676375b9094e0b5114000864.jpg)

硬编码的网址终于消失了!🙌

总结

Java 和 Spring Boot 仍然十分流行——事实上,可以说在大型企业中它们占据了主导地位,我有一些轶事证据表明,公共部门的情况也差不多,那里的软件越来越多地是定制开发的([例子](https://18f.gsa.gov/2024/10/08/18f-year-of-launches/))。随着 Temporal 越来越受欢迎,它与 Spring Boot 结合使用的方式需要被充分理解并掌握,必要时还需要进行微调。这就是我在探索和这篇博客中想要达到的目的——深入了解。好消息是,它们配合得非常不错。

最终,我的项目在多个方面借助了Spring Boot。

* 它使用抽象来简化发出 REST 请求的代码,特别是通过使用像 `RestTemplate` 和 `ResponseEntity` 这样的类。
* 它使用 Spring Boot Maven 插件来创建包含所有依赖项的 fat jar,以便我的工作流程应用可以包含所有依赖项。
* 而且将我的 worker 作为 Spring Boot 应用程序来设置,使我能够利用熟悉的机制将配置和属性值注入到应用中。

关于Spring Boot和Temporal不兼容的传言是不正确的。实际上,让我回到之前提到的更紧密集成的承诺,在这方面已经取得了一些进展。Temporal的Java SDK项目中有两个与Spring Boot相关的模块([starter](https://github.com/temporalio/sdk-java/tree/c8a27ce9073164141fc1f08ac4c80456f48b3c1d/temporal-spring-boot-starter),[autoconfigure](https://github.com/temporalio/sdk-java/tree/c8a27ce9073164141fc1f08ac4c80456f48b3c1d/temporal-spring-boot-autoconfigure))。这两个模块在今年六月已经不再标记为“alpha”版本,但它们仍然处于公测状态。我只粗略地看过,因为我想要亲自理解它们之间的关系,而不是依赖额外的抽象来为我做事情。随着这些进展的发展,我相信我们将会得到一些我之前提到的微调。我一定会仔细研究并给出反馈!

我想最后说两件事。

## **学到的一件事**

这是我第一次使用Spring为非Spring应用构建fat jar,效果真的很棒!Spring Boot Maven插件在这方面做得非常出色,特别是内置了一个Java类加载器,让fat jar变得可执行。

## **有需要帮忙的地方**

你们在这里看到过我对Temporal工作流中各个部分的生命周期管理的评论。据所有报告和个人经验,Temporal实际上只管理工作流生命周期,允许工作者和活动等由其他机制来管理生命周期。这可以是你通过代码自己来实现的,也可以通过使用类似Spring Boot这样的工具来实现,就像我在这里做的那样。

我希望得到帮助的是理解Temporal如何管理工作流的生命周期细节。你能给我一些文档吗?序列图或其它类型的图?甚至是视频也行。那太棒了。非常感谢你的帮助。

最后,一些参考。

这些大多数都有链接,可以在帖子中找到,但为了方便访问。

我的样品应用程序

* 包含三个微服务在内的[完整项目](https://github.com/cdavisafc/cloudnativepatterns/tree/main/cloudnative-statelessness-temporal)
* 这是在[帖子聚合服务](https://github.com/cdavisafc/cloudnativepatterns/tree/main/cloudnative-statelessness-temporal/cloudnative-connectionposts)中我应用了Temporal的地方
* 在该服务中,这是[工作流实现](https://github.com/cdavisafc/cloudnativepatterns/tree/main/cloudnative-statelessness-temporal/cloudnative-connectionposts/workflow)的部分
* (注意,我之前写过一篇[博客](https://medium.com/applying-temporal-to-request-response-microservices-4312ad59b165),简要介绍了我是如何组织这个web服务的repo的)

通过我的研究,我发现了一些其他样本,它们给了我一些如何继续的线索。

* 一个由 Tihomir 编写的简洁 [示例应用程序](https://github.com/tsurdilo/activities-component/tree/main),展示了 Spring Boot 与 Temporal 的结合。尽管它的结构与我使用的稍有不同,但它依然很有帮助。
* 一个更加 [全面的项目](https://github.com/temporalio/spring-boot-demo),展示了许多超出基础内容的功能和特性。

与时间相关的Spring Boot集成组件;这些位于java-sdk仓库中:

* [starter](https://github.com/temporalio/sdk-java/tree/c8a27ce9073164141fc1f08ac4c80456f48b3c1d/temporal-spring-boot-starter "Temporal Spring Boot Starter源码")
* [autoconfigure](https://github.com/temporalio/sdk-java/tree/c8a27ce9073164141fc1f08ac4c80456f48b3c1d/temporal-spring-boot-autoconfigure "Temporal Spring Boot Autoconfigure源码")

![](https://imgapi.imooc.com/676375ba09daa2a407250258.jpg)
点击查看更多内容
TA 点赞

若觉得本文不错,就分享一下吧!

评论

作者其他优质文章

正在加载中
  • 推荐
  • 评论
  • 收藏
  • 共同学习,写下你的评论
感谢您的支持,我会继续努力的~
扫码打赏,你说多少就多少
赞赏金额会直接到老师账户
支付方式
打开微信扫一扫,即可进行扫码打赏哦
今天注册有机会得

100积分直接送

付费专栏免费学

大额优惠券免费领

立即参与 放弃机会
意见反馈 帮助中心 APP下载
官方微信

举报

0/150
提交
取消