Spring

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 异常。

Spring 在运行动态地创建 Prototype Scope Bean

1、概览 本文将带你了解如何在 Spring 中使用运行时参数创建一个 Prototype Scope Bean。 在 Spring 中,有许多不同的 Bean Scope,默认的 Scope 是 Singleton(单例)。Singleton Scope 的 Bean 将始终产生相同的对象。 如果每次都需要从容器中获得一个新实例,可以使用 Pototype Scope Bean。然而,在大多数情况下,如果我们想要从一个 Singleton Bean 实例化 Pototype Bean,或者将动态参数传递给 Pototype Bean,可能会遇到一些问题。 2、使用动态参数创建 Prototype Bean 有时,我们需要在每次初始化时将动态参数作为输入来初始化 Spring Bean。通过 Spring,可以使用多种方法为 Prototype Bean 分配不同的动态参数。 首先,创建一个 Prototype Bean Employee : public class Employee { private String name; public Employee(String name) { this.name = name; } public void printName() { System.out.println(name); } } 为 Employee Prototype Bean 创建一个配置:

根据属性(Properties)动态注册 Spring Bean

1、概览 本文将带你了解如何根据自定义属性动态注册 Bean。 主要是学习 BeanDefinitionRegistryPostProcessor 接口,以及如何使用它将 Bean 添加到 Application Context 中。 2、设置 创建一个简单的 Spring Boot 应用。 首先,定义一个要动态注册的 Bean。然后,提供一个属性来决定如何注册 Bean。最后,定义一个配置类,它将根据自定义属性注册 Bean。 2.1、依赖 添加 Maven 依赖: <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter</artifactId> <version>3.2.3</version> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <version>3.2.3</version> <scope>test</scope> </dependency> 添加 spring-boot-starter 和 spring-boot-starter-test 依赖。 2.2、Bean 类 接下来,根据自定义 application properties 定义要注册的 ApiClient: public class ApiClient { private String name; private String url; private String key; // Getter、Setter 和构造函数 public String getConnectionProperties() { return "Connecting to " + name + " at " + url; } } 假设我们希望根据提供的属性使用这个 Bean 连接到不同的 API。我们不想为每个 API 创建类定义,而是希望动态地为每个 API 定义属性并注册该 Bean。

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 等编译时库为字段、构造函数、静态初始化器等创建切面。