Spring

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); } 如果任务的每次执行都是独立的,则应使用该选项。

Spring Profile 指南

1、概览 本文将带你了解 Spring 中的 Profile(配置文件),这是 Spring 的核心功能之一。可以把 Bean 配置在不同的 Profile,例如:dev、test、prod。然后,可以在不同的环境中激活指定的 Profile,以便只加载当前环境所需的 Bean。 2、在 Bean 上使用 @Profile 从简单的开始,看看如何使用 @Profile 注解将 Bean 映射到特定的 Profile。 该注解接受一个(或多个) Profile 名称。 考虑一个场景:有一个 Bean,它只能在开发过程中激活,不能在生产过程中部署。 用 dev Profile 注解该 Bean,它只会在开发环境中被加载到容器。在生产环境中, dev Profile 不会被激活: @Component @Profile("dev") public class DevDatasourceConfig Profile 名称也可以用 NOT 运算符作为前缀,如 !dev,以将其从 Profile 排除。 在如下示例中,只有当 dev Profile 未激活时,组件才会被激活: @Component @Profile("!dev") public class DevDatasourceConfig 3、在 XML 中声明 Profile Profile 也可以用 XML 配置。<beans> 标签有一个 profile 属性,该属性包含以逗号分隔的 Profile 值: <beans profile="dev"> <bean id="devDatasourceConfig" class="org.

Spring Bean 的生命周期

“讲一讲 Spring Bean 的生命周期”,这算是一道非常经典的面试题了! 如果没有研究过 Spring 源码,单纯去背面试题,这个问题也是可以回答出来的,但是单纯的背缺乏理解,而且面试一紧张,就容易背岔了。 在前面的文章中,给大家分析了 Spring 中 Bean 的创建是在 createBean 方法中完成的。在该方法中,真正干活的实际上是 doCreateBean 方法,具体位置在 AbstractAutowireCapableBeanFactory#doCreateBean,大家在面试时候常被问到的 Spring Bean 的生命周期,实际上就是问 doCreateBean 方法的执行逻辑。 doCreateBean 方法整体上来说,干了四件事: Bean 的实例化。 Bean 属性填充。 Bean 初始化。 Bean 销毁方法注册。 这里大家注意区分 实例化 和 初始化 两个方法,实例化是指通过反射创建出来 Bean 实例的过程,而初始化则是调用一些回调函数进行 Bean 的一些预处理。 1、实例化 // Instantiate the bean. BeanWrapper instanceWrapper = null; if (mbd.isSingleton()) { instanceWrapper = this.factoryBeanInstanceCache.remove(beanName); } if (instanceWrapper == null) { instanceWrapper = createBeanInstance(beanName, mbd, args); } Object bean = instanceWrapper.