Spring

使用 Spring WebClient 和 WireMock 进行集成测试

1、简介 Spring WebClient 是一款非阻塞、响应式的 HTTP 客户端,而 WireMock 是一个强大的用于模拟基于 HTTP 的 API 的工具。 2、依赖和示例 首先,需要在 Spring Boot 项目中添加必要的依赖。 在 pom.xml 中添加 spring-boot-starter-flux(WebClient) 和 spring-cloud-starter-wiremock(WireMock Server)依赖: <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-webflux</artifactId> </dependency> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-contract-wiremock</artifactId> <version>4.1.2</version> <scope>test</scope> </dependency> 假设,我们的应用需要调用外部天气 API,以获取给定城市的天气数据。 定义 WeatherData POJO: public class WeatherData { private String city; private int temperature; private String description; // 构造函数、Getter/Setter 方法省略 我们要使用 WebClient 和 WireMock 进行集成测试,以测试这个功能。 3、使用 WireMock API 进行集成测试 首先用 WireMock 和 WebClient 设置 Spring Boot 测试类:

如何控制 Spring Bean 的加载顺序?

先说结论,使用 @Order 注解或者是实现 Ordered 接口并不能控制 Bean 的加载顺序。 一、@Order 注解和 Ordered 接口 在 Spring 框架中,@Order 是一个非常实用的元注解,它位于 spring-core 包下,主要用于控制某些特定上下文(Context)中组件的执行顺序或排序,但它并不直接控制 Bean 的初始化顺序。 1.1、用途 @Order 注解或者是 Ordered 接口,常见的用途主要是两种: 定义执行顺序:当多个组件(如 Interceptor、Filter、Listrner 等)需要按照特定的顺序执行时,@Order 注解可以用来指定这些组件的执行优先级。数值越小,优先级越高,相应的组件会更早被执行或被放置在集合的前面(@Order 注解接受一个整数值,这个值可以是负数、零或正数。Spring 框架提供了 Ordered.HIGHEST_PRECEDENCE(默认最低优先级)和 Ordered.LOWEST_PRECEDENCE(默认最高优先级)常量,分别对应于 Integer.MIN_VALUE 和 Integer.MAX_VALUE,可以方便地设定优先级。 集合排序:当相同类型的组件被自动装配到一个集合中时,@Order 注解会影响它们在这个集合中的排列顺序。 1.2、使用场景 经典的使用场景如下。 拦截器的排序 在 Spring MVC 中,可以使用 @Order 来控制拦截器(Interceptor)的执行顺序。 Spring Security Filter 在 Spring Security 中,过滤器链(Filter Chain)的顺序通过 @Order 来定义,确保正确的安全处理流程。 // HttpSecurity 的 performBuild 方法 @Override protected DefaultSecurityFilterChain performBuild() { // 对 Filter 进行排序 this.

Spring 异常 “No Multipart Boundary Was Found”

1、简介 本文将带你了解在 Spring 中处理文件上传(Multipart)请求时出现异常:“No Multipart Boundary Was Found” 的原因,以及解决办法。 2、理解 Multipart 请求 简而言之,Multipart 请求是一种 HTTP 请求,它在一条消息的请求体中传输一种或多种不同的数据。请求体被分成多个部分,请求中的每个部分都可能代表不同的文件或数据。 通常使用它来传输或上传文件、交换电子邮件、流媒体或提交 HTML 表单,并使用 Content-Type 标头来指明在请求中发送的数据类型。 来看看 Multipart 请求需要设置哪些值。 2.1、主类型 主类型(顶级类型)指定了我们发送的内容的主要类别。如果我们在单个 HTTP 请求中提交多种数据类型,则需要将 Content-Type Header 值设置为 multipart。 2.2、子类型 除了顶级类型外,Content-Type Header 值还包含一个强制的子类型。子类型值提供了有关数据格式的附加信息。 在不同的 RFC 中定义了多种 multipart 子类型。例如 multipart/mixed、multipart/alternative、multipart/related 和 multipart/form-data(常用)。 由于我们在一个请求中封装了多种不同的数据类型,因此需要一个额外的参数来分隔 multipart 消息的不同部分:即,boundary 参数。 2.3、Boundary 参数 Boundary 指令(参数)是 multipart Content-Type 的强制值,它指定了封装边界。 根据 RFC 1341 的定义,封装(Boundary)边界是由两个连字符(“–”)后跟 Content-Type Header 中的 boundary 值组成的分隔线。它用于分隔 HTTP 请求体中的各个部分(即,子请求体)。 来看一个实际的案例,Web 浏览器请求包含两个 Part。通常情况下,Content-Type Header 信息如下:

在 Spring 中实现 Bulk 和 Batch API

1、概览 尽管标准的 REST API 可以满足大多数常见需求,但在处理批量(Bulk)或批处理(Batch)操作时,基于 REST 的架构风格存在一些限制。 本文将带你了解如何在微服务中应用 Bulk 和 Batch 操作,以及如何实现一些自定义的面向 “写” 的 Bulk 和 Batch API。 2、 Bulk 和 Batch API 简介 Bulk 和 Batch 操作这两个术语经常被互换使用。不过,两者之间还是有硬性区别的。 通常,Bulk(批量)操作是指对同一类型的多个条目执行相同的操作。一种简单的方法是为每个请求调用相同的 API 来执行批量操作。这种方法可能太慢,而且会浪费资源。相反,我们可以在一次往返中处理多个条目。 我们可以通过在一次调用中对同一类型的多个条目执行相同的操作来实现 Bulk(批量)操作。这种对条目集合进行操作的方式可减少整体延迟并提高应用性能。要实现 Bulk(批量)操作,我们可以重用用于于单个条目的现有端点,或者为 Bulk(批量)方法创建一个单独的路由。 Batch(批处理)操作通常是指对多个资源类型执行不同的操作。Batch API 是在一次调用中对多个资源执行多个操作的集合。这些资源操作可能没有任何连贯性。每个请求路由可以是独立的,互相之间没有依赖关系。 简而言之,“Batch” 一词是指批量处理不同的请求。 目前,对于实现 Bulk 或 Batch 操作来说,缺乏明确定义的标准或规范。此外,许多流行的框架,如 Spring,没有内置的 Bulk(批量)操作支持。 尽管如此,在本文中,我们将使用常见的 REST 架构来实现自定义 Bulk 和 Batch 操作的方法。 3、Spring 示例应用 假设我们需要构建一个简单的微服务,同时支持 Bulk 和 Batch 操作。 3.1、Maven 依赖 首先,添加 spring-boot-starter-web 和 spring-boot-starter-validation 依赖: <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> <version>3.

再谈谈 Spring 中的循环依赖

一、循环依赖 1.1、什么是循环依赖 首先,什么是循环依赖?这个其实好理解,就是两个 Bean 互相依赖,类似下面这样: @Service public class AService { @Autowired BService bService; } @Service public class BService { @Autowired AService aService; } AService 和 BService 互相依赖: 这个应该很好理解。 1.2、循环依赖的类型 一般来说,循环依赖有三种不同的形态,上面 1.1 小节是其中一种。 另外两种分别是三者依赖,如下图: 这种循环依赖一般隐藏比较深,不易发觉。 还有自我依赖,如下图: 一般来说,如果我们的代码中出现了循环依赖,则说明我们的代码在设计的过程中可能存在问题,我们应该尽量避免循环依赖的发生。不过一旦发生了循环依赖,Spring 默认也帮我们处理好了,当然这并不能说明循环依赖这种代码就没问题。实际上在目前最新版的 Spring 中,循环依赖是要额外开启的,如果不额外配置,发生了循环依赖就直接报错了。 另外,Spring 并不能处理所有的循环依赖,后面会和大家进行分析。 二、循环依赖解决思路 2.1、解决思路 那么对于循环依赖该如何解决呢?其实很简单,加入一个缓存就可以了。 来看下面这张图: 如上图所示,引入了一个缓存池。 当我们需要创建 AService 的实例的时候,会首先通过 Java 反射创建出来一个原始的 AService,这个原始 AService 可以简单理解为刚刚 new 出来(实际是刚刚通过反射创建出来)还没设置任何属性的 AService,此时,我们把这个 AService 先存入到一个缓存池中。 接下来我们就需要给 AService 的属性设置值了,同时还要处理 AService 的依赖,这时我们发现 AService 依赖 BService,那么就去创建 BService 对象,结果创建 BService 的时候,发现 BService 依赖 AService,那么此时就先从缓存池中取出来 AService 先用着,然后继续 BService 创建的后续流程,直到 BService 创建完成后,将之赋值给 AService,此时 AService 和 BService 就都创建完成了。

Spring 注入具有多个实现类的接口

1、简介 本文将带你了解如何在 Spring Boot 中自动装配具有多个实现的接口,以及一些常见用例。这是一个强大的功能,允许开发人员动态地将接口的不同实现注入到组件中。 2、默认行为 默认情况下,当接口存在多个实现并试图将该接口自动装配到组件中时,会遇到异常:“required a single bean, but X were found”。原因很简单: Spring 不知道我们想在该组件中注入哪个实现。幸运的是,Spring 提供了多种工具,可以明确要注入的实现。 3、@Qualifier 通过 @Qualifier 注解,我们可以在接口的多个实现组件中指定要自动装配的 Bean。我们可以将其应用到组件本身,为其自定义名称: @Service @Qualifier("goodServiceA-custom-name") public class GoodServiceA implements GoodService { // 实现 } 然后,用 @Qualifier 对参数进行注解,以指定想要的实现: @Autowired public SimpleQualifierController( @Qualifier("goodServiceA-custom-name") GoodService niceServiceA, @Qualifier("goodServiceB") GoodService niceServiceB, GoodService goodServiceC ) { this.goodServiceA = niceServiceA; this.goodServiceB = niceServiceB; this.goodServiceC = goodServiceC; } 如上,使用自定义 Qualifier 自动装配了 GoodServiceA。 而,GoodServiceB 实现却没有自定义 Qualifier: @Service public class GoodServiceB implements GoodService { // 实现 } 这种情况下,通过类名对组件进行了自动装配。这种自动装配的限定符(Qualifier)使用驼峰形式,例如,如果类名是 MyAwesomeClass,那么 myAwesomeClass 就是一个有效的限定符。

Spring WebClient 中的 exchange() 和 retrieve() 方法

1、概览 WebClient 是一个简化 HTTP 请求执行过程的接口。与 RestTemplate 不同,它是一个响应式非阻塞客户端,可以消费和操作 HTTP 响应。虽然它被设计为非阻塞型,但也可用于阻塞型场景。 本文将带你了解 WebClient 接口中的关键方法,包括 retrieve()、exchangeToMono() 和 exchangeToFlux(),以及它们之间的差异。 2、示例项目设置 首先,创建一个 Spring Boot 应用,在 pom.xml 中添加 spring-boot-starter-webflux 依赖: <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-webflux</artifactId> <version>3.2.4</version> </dependency> 该依赖提供了 WebClient 接口,用于执行 HTTP 请求。 另外,来看看 https://jsonplaceholder.typicode.com/users/1 请求的 GET 响应示例: { "id": 1, "name": "Leanne Graham", // ... } 创建一个名为 User 的 POJO 类: class User { private int id; private String name; // 构造函数、Getter、Setter 方法省略 } 来自 JSONPlaceholder API 的 JSON 响应将被反序列化并映射到 User 类的实例。

在 Spring 测试中禁用 @EnableScheduling

1、简介 本文将带你了解如何测试启用了定时任务(@EnableScheduling)的 Spring 应用,以及如何在测试过程中禁用定时任务。 2、示例 首先来看一个示例,假设我们有一个系统,允许公司的代表向客户发送通知。其中一些通知是时间敏感的,应该立即发送,但有些通知应该等到下一个工作日再发送。因此,我们需要一个机制来定期尝试发送这些通知: public class DelayedNotificationScheduler { private NotificationService notificationService; @Scheduled(fixedDelayString = "${notification.send.out.delay}", initialDelayString = "${notification.send.out.initial.delay}") public void attemptSendingOutDelayedNotifications() { notificationService.sendOutDelayedNotifications(); } } attemptSendingOutDelayedNotifications() 方法上添加了 @Scheduled 注解。当 initialDelayString 配置的时间过去后,该方法将被首次调用。执行结束后,Spring 会在 fixedDelayString 参数配置的时间过后再次调用该方法。该方法本身将实际逻辑委托给了 NotificationService。 当然,我们还需要开启调度功能。为此,需要在 @Configuration 类上添加 @EnableScheduling 注解。 3、集成测试中定时任务的问题 首先,为通知应用编写一个基本的集成测试: @SpringBootTest( classes = { ApplicationConfig.class, SchedulerTestConfiguration.class }, properties = { "notification.send.out.delay: 10", "notification.send.out.initial.delay: 0" } ) public class DelayedNotificationSchedulerIntegrationTest { @Autowired private Clock testClock; @Autowired private NotificationRepository repository; @Autowired private DelayedNotificationScheduler scheduler; @Test public void whenTimeIsOverNotificationSendOutTime_thenItShouldBeSent() { ZonedDateTime fiveMinutesAgo = ZonedDateTime.

如何测试 Spring Application Event?

1、概览 本文将带你了解如何测试 Spring Application Event,以及如何使用 Spring Modulith 的测试库。 2、Application Event Spring 提供了 Application Event(观察者设计模式),允许组件在保持松散耦合的同时相互通信。我们可以使用 ApplicationEventPublisher Bean 发布内部事件,这些事件都是纯 Java 对象,所有已注册的监听器(Listener)都会收到通知。 例如,当成功创建 Order 时,OrderService 组件可以发布 OrderCompletedEvent: @Service public class OrderService { private final ApplicationEventPublisher eventPublisher; // 构造函数 public void placeOrder(String customerId, String... productIds) { Order order = new Order(customerId, Arrays.asList(productIds)); // 验证和下单的业务逻辑 OrderCompletedEvent event = new OrderCompletedEvent(savedOrder.id(), savedOrder.customerId(), savedOrder.timestamp()); eventPublisher.publishEvent(event); } } 把 OrderCompletedEvent 对象作为应用事件发布,不同模块中监听这些事件的组件会收到通知。 假设 LoyaltyPointsService 会对这些事件做出反应,以奖励下单客户积分。要实现这一点,可以使用 Spring 的 @EventListener 注解:

@Transactional 能和 @Async 一起用吗?

1、简介 本文将带你了解 Spring 中 @Transactional 和 @Async 注解之间的兼容性。 2、 了解 @Transactional 和 @Async @Transactional 注解是 Spring 提供的声明式事务注解。可以让多个业务方法在同一个事务中执行,只有所有方法都正常执行完毕后事务才会提交。如果任何一个方法在调用过程中抛出了异常,那么事务就会回滚。 @Async 注解用于执行异步任务,如果从一个线程调用 @Async 方法或类,Spring 会使用另一个线程来运行该方法,从而提高执行效率。 在有些情况下,我们需要在代码中同时使用 @Transactional 和 @Async 来保业务数据的一致性以及性能。 3、@Transactional 能和 @Async 一起用吗? 异步 和 事务 如果使用不当,可能会带来数据不一致等问题。 关于这一点,需要充分了解 Spring 的事务上下文和上下文之间的数据传播。 3.1、创建示例应用 本文使用银行的转账功能来说明事务和异步代码的使用。简而言之,是一个转账的场景,从一个账户中扣除资金并将其添加到另一个账户。 我们可以把它想象成数据库操作,比如 select 相关账户并 update 其资金余额: public void transfer(Long depositorId, Long favoredId, BigDecimal amount) { Account depositorAccount = accountRepository.findById(depositorId) .orElseThrow(IllegalArgumentException::new); Account favoredAccount = accountRepository.findById(favoredId) .orElseThrow(IllegalArgumentException::new); depositorAccount.setBalance(depositorAccount.getBalance().subtract(amount)); favoredAccount.setBalance(favoredAccount.getBalance().add(amount)); accountRepository.save(depositorAccount); accountRepository.save(favoredAccount); } 首先使用 findById() 查找相关账户,如果给定 ID 的账户不存在,则抛出 IllegalArgumentException 异常。