spring

在 Spring 中使用 AOP 记录执行日志

1、概览 Aspect-Oriented Programming(面向切面编程,简称 AOP)是一种范式,它能让我们在整个应用中隔离事务管理或日志记录等交叉问题,而不会干扰业务逻辑。 本文将带你了解如何在 Spring 中使用 AOP 记录执行日志。 2、不使用 AOP 记录日志 通常,我们会在方法的开始和结束处输出日志。这样,就能跟踪应用的执行流程。此外,还可以捕获传递给特定方法的值及其返回值。 示例如下: public String greet(String name) { logger.debug(">> greet() - {}", name); String result = String.format("Hello %s", name); logger.debug("<< greet() - {}", result); return result; } 尽管上面的实现看起来是一个标准的解决方案,但日志语句在代码中增加了复杂性。 如果没有日志记录,只需要一行代码就可以完成: public String greet(String name) { return String.format("Hello %s", name); } 3、面向切面(AOP)编程 顾名思义,面向切面的编程(Aspect-Oriented Programming)侧重于切面,而不是对象和类。我们可以使用 AOP 编程为特定应用实现额外功能,而无需修改其当前实现。 3.1、AOP 概念 在深入学习之前,让我们先从高层次来了解一下 AOP 的基本概念。 切面(Aspect):横切关注点或我们希望在整个应用中应用的功能。 连接点(Join Point):应用流程中我们希望应用切面的点。 Advice:在特定连接点应执行的操作。 Pointcut:应在其中应用某一切面的连接点集合。 另外,Spring AOP 仅支持方法执行的连接点。可以考虑使用 AspectJ 等编译时库为字段、构造函数、静态初始化器等创建切面。

Spring Webclient 自定义 JSON 反序列化

1、概览 本文将带你了解如何在 Spring WebClient 中自定义 JSON 的反序列化(Deserialization)。 2、为什么需要自定义反序列化? Spring WebFlux 模块中的 Spring WebClient 通过 Encoder 和 Decoder 组件处理序列化和反序列化。编码器(Encoder)和解码器(Decoder)是表示读取和写入内容的接口。默认情况下,spring-core 模块提供 byte[]、ByteBuffer、DataBuffer、Resource 和 String 编码器和解码器实现。 Jackson 是一个 JSON 库,它通过 ObjectMapper 将 Java 对象序列化为 JSON,并将 JSON 字符串反序列化为 Java 对象。ObjectMapper 包含内置的配置选项,可以使用 Deserialization Feature 进行开启或关闭。 当 Jackson 库提供的默认行为无法满足我们的特定需求时,就有必要定制反序列化过程。为了在序列化和反序列化过程中修改行为,ObjectMapper 提供了一系列配置选项供我们设置。因此,我们需要将这个自定义的 ObjectMapper 注册到 Spring WebClient 中,以便在序列化和反序列化中使用。 3、如何自定义 ObjectMapper? 自定义 ObjectMapper 可以在全局应用程序级别与 WebClient 关联,也可以与特定请求关联。 以一个获取客户订单详细信息的 GET 端点为例。 OrderResponse Model 如下: { "orderId": "a1b2c3d4-e5f6-4a5b-8c9d-0123456789ab", "address": [ "123 Main St", "Apt 456", "Cityville" ], "orderNotes": [ "Special request: Handle with care", "Gift wrapping required" ], "orderDateTime": "2024-01-20T12:34:56" } 对于上述响应的一些反序列化规则如下:

Spring Boot 实现 gPRC 服务调用

1、简介 gRPC 是一个高性能的开源 RPC 框架,最初由谷歌开发。它有助于消除样板代码,并在数据中心内外连接多语言服务。该 API 基于 Protocol Buffer,它提供了一个 protoc 编译器,用于生成不同支持的语言的代码。 我们可以将 gRPC 视为 REST、SOAP 或 GraphQL 的替代品,它建立在 HTTP/2 的基础上,可以使用多路复用或流式连接等功能。 本文将带你了解如何使用 Spring Boot 实现 gRPC 服务提供者和消费者。 2、面临的问题 首先,Spring Boot 并不直接支持 gRPC。它只支持 Protocol Buffer,允许我们实现基于 protobuf 的 REST 服务。因此,需要通过使用第三方库来加入 gRPC: 平台相关的编译器:protoc 编译器是平台相关的。因此,如果在构建过程中需要生成存根(Stub),构建过程会变得更加复杂且容易出错。 依赖: 需要在 Spring Boot 应用中添加兼容的依赖。不幸的是,Java 的 protoc 添加了 javax.annotation.Generated 注解(Issues),这迫使我们需要添加一个依赖项来编译旧的 Java EE 注解库。 服务器运行时:gRPC 服务提供者需要在服务器中运行。gRPC for Java 项目提供了一个 shaded Netty,我们需要将其包含在 Spring Boot 应用中,或者用 Spring Boot 已经提供的服务器来代替。 消息传输: Spring Boot 提供了不同的客户端,如 RestClient(阻塞)或 WebClient(非阻塞),但遗憾的是,这些客户端无法配置和用于 gRPC,因为 gRPC 在阻塞和非阻塞调用中都使用了 自定义 transport 技术。 配置: 由于 gRPC 使用了自己的技术,我们需要配置属性以按照 Spring Boot 的方式对其进行配置。 3、示例项目 幸运的是,可以使用第三方 Spring Boot Starter 来应对这些挑战,例如 LogNet 或 grpc ecosystem project。这两个 Starter 都很容易整合,但后者同时支持提供者和消费者以及许多其他集成功能,因此本文选择了后者。

在 Spring Boot 中把 YAML 属性绑定到 Map

1、概览 本文将带你了解如何在 Spring Boot 中把 YAML 属性注入到 Map。 2、Spring 中的 YAML 文件 使用 YAML 文件存储外部配置数据是 Spring 开发人员的常见做法。Spring 支持使用 YAML 作为 Properties 的替代。 Spring 底层使用 SnakeYAML 来解析 YAML。 话不多说,来看看典型的 YAML 文件是什么样的: server: port: 8090 application: name: myapplication url: http://myapplication.com 如你所见,YAML 文件不言自明,而且更易于人阅读。YAML 提供了一种精美而简洁的方式来存储层次化的配置数据。 默认情况下,Spring Boot 会在应用启动时从 application.properties 或 application.yml 中读取配置属性。不过,可以使用 @PropertySource 来加载自定义 YAML 文件。 熟悉了 YAML 文件后,来看看如何在 Spring Boot 中将 YAML 属性注入到 Map 中。 3、将 YAML 属性注入到 Map 通过 @ConfigurationProperties 注解,Spring Boot 可轻松地将配置文件中的外部属性直接注入 Java 对象。

Spring 配置 Kafka 死信队列

1、简介 本文将带你了解如何在 Spring 中为 Apache Kafka 配置死信队列。 2、死信队列 死信队列(Dead Letter Queue,DLQ)用于存储由于各种原因无法正确处理的消息,例如间歇性系统故障、无效的消息模式或损坏的内容。这些消息可以稍后从 DLQ 中移除,以进行分析或重新处理。 下图是 DLQ 机制的简化流程: 通常情况下,使用 DLQ 是一个不错的主意,但也有一些情况下应该避免使用 DLQ。例如,在对消息的精确顺序很重要的队列中,不建议使用 DLQ,因为重新处理 DLQ 消息会打乱消息的到达顺序。 3、Spring Kafka 中的死信队列 在 Spring Kafka 中,与 DLQ 概念相对应的是死信 Topic(DLT)。 接下来,我们通过一个简单的支付系统来介绍 DLT 应该如何使用。 3.1、Model 类 从 Model 类开始: public class Payment { private String reference; private BigDecimal amount; private Currency currency; // Get、Set 方法省略 } 再实现一个用于创建事件的方法: static Payment createPayment(String reference) { Payment payment = new Payment(); payment.setAmount(BigDecimal.valueOf(71)); payment.

Spring 事务最佳实践

概览 本文将带你了解各种 Spring 事务的最佳实践,以保证底层业务的数据完整性。 数据完整性至关重要。如果没有适当的事务处理,应用就很容易出现竞赛条件,从而给底层业务带来可怕的后果。 模拟竞赛条件 以一个实际问题为例,说明在构建基于 Spring 的应用时,应该如何处理事务。 使用以下 Service 层和 Dao 层组件来实现转账服务: 使用最简单的 Dao 层实现来说明不按业务要求处理事务会发生什么情况: @Repository @Transactional(readOnly = true) public interface AccountRepository extends JpaRepository<Account, Long> { @Query(value = """ SELECT balance FROM account WHERE iban = :iban """, nativeQuery = true) long getBalance(@Param("iban") String iban); @Query(value = """ UPDATE account SET balance = balance + :cents WHERE iban = :iban """, nativeQuery = true) @Modifying @Transactional int addBalance(@Param("iban") String iban, @Param("cents") long cents); } getBalance 和 addBalance 方法都使用 Spring 的 @Query 注解来定义原生 SQL 查询,以检索或者修改用户的账户余额。

配置 Spring 以接收和返回 XML 格式的数据

1、概览 虽然 JSON 是 RESTful 服务的事实标准,但在某些情况下,可能需要使用 XML。例如:老掉牙的银行 API 就是通过 XML 进行交互的。 Spring 通过 Jackson XML 提供了一种简单的方法来支持 XML 端点。 2、依赖 第一步是添加 依赖。注意 spring-boot-starter-web Starter 默认不包含支持 XML 的库。需要手动添加: <dependency> <groupId>com.fasterxml.jackson.dataformat</groupId> <artifactId>jackson-dataformat-xml</artifactId> </dependency> 另外,也可以使用 JAXB 来实现,但总的来说,JAXB 更啰嗦,而且 API 没有 Jackson 那么优雅好用。不过,如果使用的是 Java 8,JAXB 库与实现都位于 javax 包中,因此无需在应用中添加任何其他依赖。 在 Java 9 开始的版本中,javax 包被移动并更名为 jakarta,因此 JAXB 需要额外的 依赖: <dependency> <groupId>jakarta.xml.bind</groupId> <artifactId>jakarta.xml.bind-api</artifactId> <version>4.0.0</version> </dependency> 另外,它需要一个运行时实现来处理 XML Mapper,这可能会导致其他的问题。 3、端点 由于 JSON 是 Spring REST Controller 的默认格式,因此需要在端点上明确配置 “消费” 和 “生产” 的数据类型是 XML:

在 Spring Boot 中记录完整的请求体和响应体日志

完整的请求日志对于 故障排查 和 审计 来说极其重要。通过查看日志,可以检查数据的准确性、参数的传递方式以及服务器返回的数据。 由于 Socket 流不能重读,所以需要一种实现来把读取和写入的数据缓存起来,并且可以多次重复读取缓存的内容。 Spring 提供 2 个可重复读取请求、响应的 Wrapper 工具类: ContentCachingRequestWrapper ContentCachingResponseWrapper 通过类名不难看出,这是典型的装饰者设计模式。它俩的作用就是把读取到的 请求体 和写出的 响应体 都缓存起来,并且提供了访问缓存数据的 API。 创建 RequestLogFilter 创建 RequestLogFilter 继承 HttpFilter,以记录完整的请求和响应日志。 package cn.springdoc.demo.web.filter; import java.io.IOException; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.web.util.ContentCachingRequestWrapper; import org.springframework.web.util.ContentCachingResponseWrapper; import jakarta.servlet.FilterChain; import jakarta.servlet.ServletException; import jakarta.servlet.http.HttpFilter; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; /** * 记录请求日志 */ public class RequestLogFilter extends HttpFilter { static final Logger log = LoggerFactory.getLogger(RequestLogFilter.class); /** * */ private static final long serialVersionUID = 8991118181953196532L; @Override protected void doFilter(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException { // Wrapper 封装 Request 和 Response ContentCachingRequestWrapper cachingRequest = new ContentCachingRequestWrapper(request); ContentCachingResponseWrapper cachingResponse = new ContentCachingResponseWrapper(response); // 继续执行请求链 chain.

在 Spring 应用中为 REST API 实现异常处理

1、概览 本文将地带你了解如何在 Spring 中为 REST API 实现异常处理。 在 Spring 3.2 之前,在 Spring MVC 应用中处理异常的两种主要方法是 HandlerExceptionResolver 或 @ExceptionHandler 注解。这两种方法都有一些明显的缺点。 自 3.2 以来,可以使用 @ControllerAdvice 注解来解决前两种解决方案的局限性,并促进整个应用中统一的异常处理。 Spring 5 引入了 ResponseStatusException 类,一种在 REST API 中进行基本错误处理的快速方法。 所有这些都有一个共同点:它们都很好地处理了关注点的分离。应用通常可以抛出异常来表示某种失败,然后再单独进行处理。 2、解决方案 1:Controller 级的 @ExceptionHandler 第一种解决方案适用于 @Controller 层面。定义一个处理异常的方法,并用 @ExceptionHandler 进行注解: public class FooController{ //... @ExceptionHandler({ CustomException1.class, CustomException2.class }) public void handleException() { // } } 这种方法有一个很大的缺点:@ExceptionHandler 注解方法仅对特定 Controller 有效,而不是对整个应用全局有效。当然,可以将其添加到每个 Controller 中,但这并不适合作为通用的异常处理机制。 也可以通过让所有 Controller 都继承一个 Base Controller 类来绕过这一限制。 然而,对于某些原因无法实现上述方法的应用来说,这种解决方案可能会成为一个问题。例如,Controller 可能已经从另一个 Base 类继承而来,而该 Base 类可能在另一个 Jar 中或不可直接修改,或者 Controller 本身不可直接修改。

Spring 中的 @Scheduled 注解

1、概览 本文将带你了解如何使用 Spring @Scheduled 注解来配置和调度定时任务。 使用 @Scheduled 对方法进行注解时,需要遵循如下简单的规则: 方法的返回类型通常应为 void(如果不是,返回值将被忽略) 方法不应有任何参数 2、启用定时调度 可以在配置类上使用 @EnableScheduling 注解来启用 Spring 中的定时任务和 @Scheduled 注解的支持: @Configuration @EnableScheduling public class SpringConfig { ... } 也可以在 XML 中启用,如下: <task:annotation-driven> 3、以固定延迟调度任务 配置一个任务,使其在固定延迟后运行: @Scheduled(fixedDelay = 1000) public void scheduleFixedDelayTask() { System.out.println( "Fixed delay task - " + System.currentTimeMillis() / 1000); } 如上,上一次执行结束与下一次执行开始之间的持续时间是固定的。任务会一直等待到前一个任务结束。 在必须确保上一次执行完成后再次运行的情况下,应使用此选项。 4、以固定频率调度任务 在固定的时间间隔内执行一项任务: @Scheduled(fixedRate = 1000) public void scheduleFixedRateTask() { System.out.println( "Fixed rate task - " + System.currentTimeMillis() / 1000); } 如果任务的每次执行都是独立的,则应使用该选项。