Spring Boot 微服务编码风格指南和最佳实践

文奇摄于世界尽头州立公园

通过多年来使用 Spring Boot 微服务,我编制了一份编码风格指南和最佳实践列表。这份清单并不全面,但我希望您能找到一两点可以借鉴的地方,无论您是新手还是经验丰富的 Spring Boot 微服务开发人员。

· 1. 模块化项目结构 · 2. 项目依赖标准 · 3. 日志规则 · 4. 自动配置 · 5. 保持控制器简单和干净 · 6. 将服务集中在业务逻辑上 · 7. 支持构造函数注入而不是@Autowired · 8. 全局异常处理 · 9. 开放 API 规范代码文档 · 10. Bean 验证 · 11. 在运行时注入外部配置 · 12. 公开健康检查端点 · 13. 额外的依赖开销 · 14. 使用 Lombok ·15. 应用 Checkstyle · 16. 事件驱动编程的 Dapr · 17. Schema-Per-Service 数据库设计 · 18. Liquibase 用于数据库模式变更管理 · 19. Resilience4j for Circuit Breaker · 20. 前端后端 (BFF) 作为 API 网关 · 21. 消费者驱动的合同测试协议 · 22. 用于单元测试的 JUnit 5 · 23. 用于行为驱动开发 (BDD) 的 Cucumber · 24. 用于测试覆盖的JaCoCo · 25. 尽早并经常自动化 DevSecOps

1. 模块化项目结构

建议创建模块化的项目结构,服务类保存在服务模块中,持久化类保存在持久化模块中,等等。创建模块化代码库可以更好地扩展,并且更容易为开发人员导航。示例项目结构如下:

  • config:所有模块的 Spring 配置类

  • dapr-components: dapr 组件文件,例如用于 pubsub,如果您的应用程序使用 Dapr。

  • 数据:域数据或值对象

  • 持久性:数据库的实体和存储库

  • qa:集成/功能测试,BDD

  • 休息控制器:REST端点

  • 服务:业务逻辑

在模块内部,建议遵循以下封装结构:

  • src/main/java — 包含 Java 源代码包和类

  • src/main/resources — 包含非 Java 资源,例如属性文件和 Spring 配置

  • src/test/java — 包含测试源代码包和类

  • src/test/resources — 包含非 Java 资源,例如属性文件和 Spring 配置

  • 避免使用默认包。确保所有内容(包括入口点)都位于命名良好的包中。这是为了避免与布线和元件扫描相关的意外。

2. 项目依赖标准

  • 将不在 Spring Boot BOM 中的第三方依赖库的版本号放在 pom 文件的 部分中,这样可以更轻松地升级和测试新版本。

  • 务必包括所有插件的版本号。

  • 不要指定作为 Spring Boot 材料清单一部分的依赖库的版本号。

  • 在同一项目中不要有直接依赖库或传递依赖库的混合版本。例如,slf4j 1.5 和 slf4j 1.6 不能一起工作,因此我们需要禁止项目使用混合依赖版本构建。一种确定的方法是运行mvn dependency:tree以识别依赖库的冲突版本。

3. 记录规则

  • 永远不要使用 System.out

  • 永远不要使用 System.err

  • 始终使用 SLF4J(在类级别用 注释@Slf4j)

  • 始终使用 Logback(与 Spring Boot 一起提供)

  • 不要使用 Apache Commons Logging (JCL) aka Jakarta Commons Logging

  • 不要使用 Java Util Logging (JUL)

SLF4J(小号imple大号ogging ˚F acade为Ĵ AVA)是优选的。SLF4J 不是日志框架的实现,它是所有日志框架的抽象。日志抽象总是比日志框架更可取。如果我们使用日志抽象,特别是 SLF4J,我们可以在部署时迁移到我们需要的任何日志框架,而无需选择单一依赖项。

观察下图以更好地理解。

Spring Boot 附带 logback 作为其默认日志记录框架。

使用Spring Boot开发应用,我们只需要@Slf4j在类级别添加注解,简单干净!不再:

private static final Logger LOG = LoggerFactory.getLogger(Class.class).

4. 自动配置

自动配置是 Spring Boot 的一部分, 我欠钱别人要起诉我该怎么办它使我们的代码简单地工作。自动配置尝试根据我们添加的 jar 依赖项自动配置我们的 Spring 应用程序。例如,如果 H2 数据库在类路径上,并且我们没有手动配置任何数据库连接 bean,那么 Spring Boot 会自动配置一个 H2 内存数据库。我们需要通过将@EnableAutoConfiguration或@SpringBootApplication注释添加到我们的配置类之一来选择加入自动配置。通常建议我们只将一个或另一个注释添加到我们的主要 Configuration 类中。绝对要利用 Spring Boot 的自动配置,而不是手动注入 bean 或拦截任何请求。

5. 保持控制器简单和干净

控制器的工作是协调和委托,而不是执行实际的业务逻辑。关键做法:

  • 控制器应该是无状态的。默认情况下,控制器是单例的,给它们任何状态都会导致大量问题。

  • 控制器不应执行业务逻辑,而应依赖委托。

  • 控制器应该处理应用程序的 HTTP 层。这不应传递给服务。

  • 控制器应该面向用例或业务能力。

6. 专注于业务逻辑的服务

围绕业务能力/领域/用例构建我们的服务。具有称为AccountService、UserService 之类的服务的应用程序比具有DatabaseService、ValidationService等的应用程序更容易处理。通常控制器和服务之间存在一对一的映射。

7. 支持构造函数注入而不是@Autowired

  • 构造函数注入保持代码干净,不允许我们在 bean 之间创建循环依赖。

  • 构造函数注入可以更轻松地在单元测试中实例化和测试类,而无需配置应用程序上下文。

  • 依赖注入的最佳实践是使用 Project Lombok:声明接口类型的最终属性,并使用 Project Lombok 的@RequiredArgsConstructor. 无需为该类硬编码构造函数。样本:

https://medium.com/media/ea175297e63704595f3685641bbbd588

8. 全局异常处理

Spring Boot 提供了两种主要的全局异常处理方式:

  • 使用HandlerExceptionResolver定义我们的全球的异常处理策略。

  • 用 注释我们的控制器@ExceptionHandler。如果我们想在某些情况下具体化,这会很有用。下面的例子:

https://medium.com/media/1ce5aecef7e4edc35bf216e20bdc56a7

9.开放API规范代码文档

建议使用springdoc-openapi库自动生成 API 文档。它会自动生成 JSON/YAML 和 HTML 格式 API 的文档。本文档可以通过使用 swagger-api 注释的注释来完成。该库支持:

  • 开放API 3

  • Spring-boot(v1 和 v2)

  • JSR-303,专为@NotNull,@Min,@Max,和@Size。

  • Swagger-ui

  • OAuth 2

在我们的 pom 文件中添加以下依赖项以开始在我们的 API 代码中使用 openapi 注释。

https://medium.com/media/ffe824de26b16a0143bb84121daef3d7

示例注释:

10. Bean 验证

使用 bean 验证注释来处理我们代码中的验证逻辑。它使代码干净。我们不必在代码中包含详细的验证逻辑。如果我们找不到任何现有的 bean 验证注释来满足我们的验证需求,我们总是可以创建自定义验证注释,@CurrencyCode如下所示。

https://medium.com/media/96279dcdf51f2febac9197100ce9fdeb

验证约束@CurrencyCode可以实现如下:

https://medium.com/media/4581f5c88d9a144890b2374a3de06ff2

11. 运行时外部配置注入

利用 Spring Config Server 在运行时外部化应用程序配置文件。这减少了代码的依赖性,因此我们不必为配置更改而修改代码,这通常在 Prod 环境和较低环境之间变化。可以动态更改外部化配置,而无需重新打包源代码。

图片来源:www.canchito-dev.com

12. 公开健康检查端点

使用spring-boot-starter-actuator暴露我们的应用程序的健康检查端点。

  • Actuator 主要用于公开有关正在运行的应用程序的操作信息——健康、指标、信息、转储、环境等。它使用 HTTP 端点或 JMX bean 使我们能够与其交互。

  • 要启用所有端点: *management.endpoints.web.exposure.include=**

  • 默认情况下,所有 Actuator 端点(见下面的列表)都放在/actuator路径下,可以被management.endpoints.web.base-path.

图片来源:baeldung.com

13. 额外的依赖开销

不要在我们的应用程序中添加不必要的依赖项,这会减慢启动过程,以及其他依赖项问题或运行时问题。确保添加正确版本的依赖项,不要在我们的应用程序中保留同一依赖项库的多个版本。减少依赖项的大小,使我们的应用程序运行得更快。

14.使用龙目岛

Lombok 是一个简洁的库,它提供了许多功能来减少代码重复性和样板代码。使用 Lombok 将 java 字节码自动生成到我们的 .class 文件中并消除代码冗长:

  • 使用@Data,@NoArgsConstructor,@AllArgsConstructor摆脱我们的实体和值对象的getter / setter方法和构造函数,以及Java样板代码,如toString(),equals()和hashCode()。

  • 使用 Builder 模式替换我们代码中的工厂模式或编组器

实体类中使用的龙目岛Customer:

用于构造对象的构建器模式:

15. 应用 Checkstyle

Checkstyle 是一种用于软件开发的静态代码分析工具,用于检查 Java 源代码是否符合指定的编码规则。要将 checkstyle 应用于我们的代码,请checkstyle.xml在项目根目录下创建,然后在我们的根 pom.xml 文件中添加以下插件。在下面的例子中,配置文件是checkstyle.xml. 执行部分中提到的目标检查要求插件在构建的验证阶段运行,并在发生违反编码标准时强制构建失败。如果我们运行该mvn clean install命令,它将扫描文件中是否存在违规,如果发现任何违规,构建将失败。

https://medium.com/media/b6bdf74fe3bc55a77ab9aaf6f167eda8

16. 用于事件驱动编程的 Dapr

消息传递是一种重要的技术,可以在松散耦合的微服务之间实现灵活的通信模式。作为分布式运行时,Dapr 为开发人员设计事件驱动的微服务应用程序提供了内置的消息支持。有关如何配置 Dapr 发布/订阅组件、如何订阅主题和将事件发布到主题的分步指南,请参阅使用 Dapr进行事件驱动编程。

17. Schema-Per-Service 数据库设计

尝试尝试“每个服务的数据库服务器”方法,结果发现由于内存不足错误,几乎不可能在本地 docker 容器中运行多个应用程序,每个应用程序都有自己的数据库服务器。

我推荐“ schema-per-service ”方法,因为它的开销很低。使用“schema-per-service”,我们将为每个应用程序创建一个数据库,例如 MS SQL 服务器和新的数据库架构。每个应用程序都有一个该应用程序专用的数据库架构。它使所有权更加清晰。使用这种方法,我们必须将数据库设置提取到它自己的组件中,除了它的应用程序代码,以允许数据库和应用程序部署独立。下面的示例数据库组件文件结构。

  • create-database.sh 位于“aks”(用于 Azure Kubernetes 服务)文件夹下,用于为 Azure SQL 中的应用程序创建数据库服务器和新数据库架构。

  • 位于“本地”文件夹下的文件列表用于为本地容器中的应用程序创建数据库服务器和新数据库模式。

注意:这只是一般性建议。根据应用程序及其在生产中的使用情况,由于其高吞吐量,它可能需要拥有自己的数据库服务器。

18. 用于数据库架构变更管理的 Liquibase

Liquibase 让我们能够以匹配我们的应用程序代码的速度跟踪、版本和部署数据库代码。因此,我们可以更轻松地协作,更快地交付。Liquibase 实现了数据库模式更新自动化,消除了开发人员和 DBA 之间耗时的来回。为了实现 Liquibase,我们首先将 Liquibase 依赖添加到我们的项目 pom 中:

https://medium.com/media/1dc84d8eef04cd9d785cfac55875cf34

然后我们在应用程序的配置 yml 文件中启用 Liquibase,同时指定 master 更改日志文件的路径,以通知应用程序在哪里查找 master 更改日志:

https://medium.com/media/744b0bd4cca13db7042cd86c1bb30cef

使用更改集填充更改日志文件以管理数据库架构更改。Liquibase 更改日志文件可以分组到持久性子模块中,例如:

使用 Liquibase 时的最佳实践:

  • 组织变更日志:创建一个没有实际变更集但包含其他变更日志的主变更日志文件。这样做允许我们在不同的变更日志文件中组织我们的变更集。

  • One Change per ChangeSet:每个 changeSet只有一个更改,因为这样可以在应用 changeSet 失败时更容易回滚。

  • 不要修改变更集:一旦执行了变更集,就不要修改它。相反,如果需要对现有变更集应用的更改进行修改,请添加新的变更集。Liquibase 会跟踪它已经执行的变更集的校验和。如果已经运行的变更集被修改,默认情况下 Liquibase 将无法再次运行该变更集。

  • ChangeSet Id:Liquibase 允许我们为 changeSet 设置一个描述性名称。最好使用唯一的描述性名称作为 changeSetId 而不是使用序列号。

  • 参考数据管理:使用 Liquibase 填充应用程序需要的参考数据和代码表。这样做允许一起部署它需要的应用程序和配置数据。Liquibase 提供了 changeType loadUpdateData 来支持这一点。

  • 使用前提条件:具有变更集的前提条件。他们确保 Liquibase 在应用更改之前检查数据库状态。

  • 测试迁移:确保我们始终测试我们在本地编写的迁移,然后再将它们应用到实际环境中。始终使用 Liquibase 在非生产或生产环境中运行数据库迁移,而不是手动执行数据库更改。

19. 断路器的 Resilience4j

Resilience4j 断路器模式为我们的 REST 客户端提供容错能力。当远程服务关闭时,它可以帮助我们防止一连串的故障。在多次尝试失败后,我们可以认为该服务不可用/过载并急切地拒绝所有后续请求。这样,我们可以为可能失败的调用节省系统资源。要为断路器实现 Resilience4j,请按照以下步骤操作:

  • spring-cloud-starter-circuitbreaker-resilience4j在项目 pom 中添加依赖库:

https://medium.com/media/fe0a8076d97528744770dad53ecace66

  • 在应用程序的 yml 文件中定义断路器属性的配置。

  • 在 REST 服务 impl 类中,注入CircuitBreakerRegistry并查找CircuitBreakerfrom CircuitBreakerRegistry,用断路器装饰 REST 客户端调用,并执行它。

  • 对于没有返回类型的方法,使用 CheckedRunnable.

20. 前端后端 (BFF) 作为 API 网关

BFF 是 API 网关模式的一种变体。BFF 不是在 UI 层和后端微服务之间只有一个 API 网关,而是为不同类型的客户端提供专用网关。例如,移动 UI 的数据消耗可能与浏览器的数据消耗不同。在这种情况下,为了更好地表示数据,可以使用两个 BFF。

图片来源:微服务设计模式:API 网关、前端后端 (BFF) | TSH.io

BFF的优势:

  • 关注点分离——前端需求将与后端关注点分离。这更易于维护。Web 团队不需要在代码合并上与移动团队争吵。

  • 更易于维护和修改 API — 客户端应用程序对 API 结构的了解较少,这将使其对这些 API 中的更改更具弹性。

  • 前端更好的错误处理——BFF 可以映射出需要显示给用户的错误,而不是直接返回服务器发送的错误。这将改善用户体验。

  • 更好的安全性- 可以隐藏某些敏感信息,并且在向前端发送响应时可以省略前端的不必要数据。抽象将使攻击者更难瞄准应用程序。

21. 消费者驱动的合同测试协议

契约测试是一种测试集成点的技术,它通过隔离检查每个应用程序来确保它发送或接收的消息符合“契约”中记录的共享理解。我们使用Pact以合同的形式定义消费者对给定提供者的期望。对于使用消息队列的应用程序,此“消息”将是进入队列的消息。在实践中,实现契约测试(以及 Pact 的方式)的一种常见方法是检查对我们的测试替身的所有调用是否返回与对实际应用程序的调用相同的结果。有关如何编写 Pact 测试的详细信息,请参阅Consumer Driven Contract Testing with Pact。

22. 用于单元测试的 JUnit 5

JUnit 5 是 JUnit 4 框架的模块化和现代版本。鉴于与 JUnit 4 相比有许多优势,我建议使用 JUnit 5 在您的微服务中编写单元测试。我们中的许多人都熟悉 JUnit 4,有关 JUnit 4 和 JUnit 5 之间某些更改的迁移指南,请参阅JUnit 迁移指南从 JUnit 4 到 JUnit 5。

23. 行为驱动开发 (BDD) 的黄瓜

Cucumber 是一种遵循行为驱动开发 (BDD) 和动态文档原则的测试自动化工具。黄瓜是用简单的英文文本写的,叫做小黄瓜。它被定义为输入、行动和结果的情景。始终通过隔离特定于功能的流程来测试我们的应用程序。通过利用 Cucumber 或规范测试,通过特定于特征的流来隔离流或特征。单独创建测试以将我们的所有模块集成到同一应用程序中,并仅测试这些模块,并将它们作为我们 CICD 管道的一部分进行自动化。有关如何编写 Cucumber 测试的分步指南,请参阅使用 Cucumber进行BDD 测试。

24. 用于测试覆盖的 JaCoCo

JaCoCo 是一个免费的 Java 代码覆盖率库,用于测量在自动化测试期间执行了多少行代码。要使用 JaCoCo 跟踪测试覆盖率,请jacoco-maven-plugin在项目根pom.xml 中定义以下内容。它定义了目标prepare-agent,report和check。规则在check目标中定义,以确保我们的测试代码覆盖率满足为代码中的包、行、类、分支和方法定义的阈值。我建议将所有这些阈值定义为 90%,以确保可靠的单元测试覆盖率。未达到阈值的测试覆盖率将导致构建失败。target\site\jacoco\index.html当构建由于测试覆盖而失败时,请查阅位于子模块下的 JaCoCo 报告,以深入了解失败的测试覆盖。

https://medium.com/media/9e512ddf82ee664af603bff71e724b54

25. 尽早并经常自动化 DevSecOps

DevSecOps、开发、安全和运营,在软件开发生命周期的每个阶段自动集成安全。DevSecOps 将应用程序和基础设施安全无缝集成到敏捷和 DevOps 流程和工具中。当安全问题出现时,它会更容易、更快、更便宜地解决这些安全问题。有几种方法可以尽早将 DevSecOps 纳入我们的开发周期:

  • 依赖漏洞检查:确保我们应用程序的整个依赖树中没有已知漏洞很重要。我们推荐OWASP Dependency Check作为一种工具来检查我们的应用程序及其依赖项的漏洞。为了实现它,我们将 maven 插件添加dependency-check-maven到我们的项目 pom.xml 中。然后通过mvn clean install或在其正常构建过程中运行应用程序mvn verify。找报告dependency-check-report.html在项目模块或子模块的目标目录下。对于在我们的应用检查中可以忽略的已知漏洞,我们可以通过此插件中的配置来抑制它。对于不可忽视的漏洞,我建议的解决方法是将这些依赖项更新到项目根 pom 中的最新版本,这会覆盖那些易受攻击的库的过时版本。如果这些库的最新版本仍然存在相同的漏洞问题,请联系库供应商以提醒他们并要求修复。关于这个插件的更多使用细节,请参考dependency-check-maven – Usage。

https://medium.com/media/c12c2a48ecf8fff759f44a14e2822fa0

  • Docker 镜像漏洞检查:Docker 镜像安全扫描是整个 Docker 安全的基础部分。它是防御容器镜像中安全漏洞或不安全代码的主要手段。我推荐Trivy。Trivy 是一个简单而全面的漏洞扫描器,用于容器和其他工件。要扫描特定的trivy image .docker镜像,请运行:Trivy 将生成表格格式的输出。

  • 渗透测试:OWASP ZAP安全工具是一个代理,它在运行时对我们的应用程序执行渗透测试。OWASP ZAP 使用两种方法来查找漏洞:Spider 和 Active Scan。Spider 工具从 URL 种子开始,它将访问和解析每个响应,识别超链接并将它们添加到列表中。然后它将访问这些新找到的 URL 并递归地继续,为我们的 Web 应用程序创建一个 URL 映射。主动扫描工具将根据潜在漏洞列表自动测试所选目标。它为我们提供了一份报告,显示我们的 Web 应用程序可利用的位置,以及有关漏洞的详细信息。

  • 升级到最新版本:安全性是我们应该将应用程序依赖项升级到最新版本的主要原因之一。记得经常升级。

就这样吧!我的客户服务微服务实施了许多此类最佳实践。随意检查一下。我的客户服务微服务的 github 存储库是https://github.com/wenqiglantz/customer-service.git。

你可能感兴趣的:(spring,单元测试,java)