Spring

Spring 实现两级缓存

1、概览 缓存数据意味着我们的应用无需访问速度较慢的存储层,从而提高了性能和响应速度。我们可以使用任何内存实现库(如 Caffeine)来实现缓存。 虽然这样做可以提高数据检索的性能,但如果应用部署在多个副本上,那么实例之间就无法共享缓存。为了解决这个问题,可以引入一个分布式缓存层,所有实例都可以访问它。 本文将带你了解如何在 Spring 中使用 Spring 的缓存支持(spring-cache)实现两级缓存,以及在本地缓存层缓存失效时如何调用分布式缓存层。 2、示例 Spring Boot 应用 创建一个简单的应用,调用数据库获取一些数据。 2.1、Maven 依赖 首先,添加 spring-boot-starter-web 依赖: <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> <version>3.1.5</version> </dependency> 2.2、实现 Service 实现一个 Spring Service,从 Repository 中获取数据。 首先,创建 Customer 实体类: public class Customer implements Serializable { private String id; private String name; private String email; // 标准 Getter / Setter } 然后,实现 CustomerService 类和 getCustomer 方法: @Service public class CustomerService { private final CustomerRepository customerRepository; public Customer getCustomer(String id) { return customerRepository.

在 Servlet Filter 中自动装配 Spring Bean

1、简介 Servlet Filter(过滤器)为拦截和处理传入的请求提供了强大的机制。 本文将会带你了解在 Servlet Filter 中无缝获取 Spring Bean 的各种方法,这种需求在 Spring Web 应用中很常见。 2、Servlet Filter 中 @Autowired 的限制 Spring 的依赖注入机制 @Autowired,是一种方便的方式来将依赖注入到由 Spring 管理的组件中。但它无法与 Servlet Filter 完美配合。这是因为 Servlet Filter 是由 Servlet 容器初始化的,通常是在 Spring 的 ApplicationContext 完全加载和初始化之前 。 因此,当容器实例化 Servlet Filter 时 ,Spring Context 可能尚未可用,从而导致在尝试使用 @Autowired 注解时 ,依赖为 null 或未初始化。 3、项目设置 创建一个通用的 LoggingService,它将自动装配到 Filter 中: @Service public class LoggingService { private final Logger logger = LoggerFactory.getLogger(this.getClass()); public void log(String message,String url){ logger.info("Logging Request {} for URI : {}",message,url); } } 然后,创建 Filter,拦截传入的 HTTP 请求,并使用 LoggingService 依赖记录 HTTP 方法和 URI 的详细信息:

在 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.