教程

在 Spring Context 中重新初始化 Singleton Bean

1、概览 在本教程中,我们将了解如何在运行时重新初始化 Spring Context 中的 Singleton Bean。 默认情况下,Singleton Scope Bean 不会在应用生命周期中重新初始化。不过,有时可能需要重新创建 Bean,例如在需要更新属性时。我们将介绍几种实现此目的的方法。 2、示例 一个小示例。创建一个 Bean,从配置文件中读取属性保存在内存中。如果配置文件中的属性值被修改,那么 Bean 就要重新加载配置。 2.1、Singleton Bean 首先创建 ConfigManager 类: @Service("ConfigManager") public class ConfigManager { private static final Log LOG = LogFactory.getLog(ConfigManager.class); private Map<String, Object> config; private final String filePath; public ConfigManager(@Value("${config.file.path}") String filePath) { this.filePath = filePath; initConfigs(); } private void initConfigs() { Properties properties = new Properties(); try { properties.load(Files.newInputStream(Paths.get(filePath))); } catch (IOException e) { LOG.error("Error loading configuration:", e); } config = new HashMap<>(); for (Map.

Spring Cloud Gateway 根据客户端 IP 限制请求速率

1、简介 在本教程中,我们将学习如何在 Spring Cloud Gateway 中根据客户端的实际 IP 地址来限制请求速率。 简而言之,我们将在路由上设置 RequestRateLimiter Filter,然后配置网关根据 IP 地址来限制客户端的请求。 2、路由配置 首先,我们需要配置 Spring Cloud Gateway 以对特定路由进行速率限制。为此,我们将使用由 spring-boot-starter-data-redis-reactive 实现的经典 令牌桶(Token Bucket) Rate Limiter。简而言之, Rate Limiter 创建一个带有唯一 Key 的 Bucket,该 Key 用于标识 Bucket 自身,并具有固定的初始 Token 容量,随时间自动生成 Token。然后,对于每个请求,Rate Limiter 会获取其关联的 Bucket,并试图减少其 Bucket 中的一个 Token。如果 Bucket 中的 Token 数量不足,将拒绝传入的请求。 在分布式系统中,我们希望所有系统对同一客户端的请求都采用相同的限速策略。所以,我们使用分布式缓存 Redis 来存储 Bucket。在本例中,我们预先配置了一个 Redis 实例。 接下来,配置一个带有 Rate Limiter 的路由。监听 /example 端点,并将请求转发至 http://example.org: @Bean public RouteLocator myRoutes(RouteLocatorBuilder builder) { return builder.routes() .route("requestratelimiter_route", p -> p .

在 HttpServletRequest 中添加自定义请求头

HTTP 请求头一般都是由客户端设置,服务端获取,用以处理业务。但是偶尔也有一些需求,需要服务端对一些请求,添加自定义的请求头。例如:统一为所有请求添加一个名为 X-Requested-Id 的请求头,用于跟踪不同请求。 Servlet 中的请求对象 jakarta.servlet.http.HttpServletRequest 并未提供设置 Header 的方法。 但是,但是通过一些方法,可以实现。本教程将会教你如何在 Spring Boot 应用中添加自定义请求头到 HttpServletRequest。 思路 既然可以从 HttpServletRequest 获取到 Header,那它一定是把 Header 存储在了某个地方。我们只要找到 HttpServletRequest 存储 Header 的容器,就可以对其进行添加、编辑、删除操作。 不同 HttpServletRequest 的实现中,对 Header 的存储方式不一定一样。目前,在 Spring Boot 中最流行的 Servlet 实现就是 Tomcat 和 Undertow,所以下文将会针对这两个服务器进行讲解。 示例应用 创建一个 Spring Boot 应用。我们要在 Filter 中为所有请求添加一个 X-Requested-Id 请求头,并在 Controller 中获取并返回。 Controller 如下: import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; import jakarta.servlet.http.HttpServletRequest; @RestController @RequestMapping("/demo") public class DemoController { @GetMapping public String getHeaders (HttpServletRequest request) { // 获取 X-Requested-Id Header 值 String requestedId = request.

在 Springdoc 文档中配置身份认证,以访问需要认证的的端点

1、概览 Springdoc-OpenAPI 是一个为 Spring Boot 应用程序自动生成 API 文档的框架。它实现了 OpenAPI 3 规范,使用它,通过 UI 界面就可以与 API 进行交互,非常方便。 在本教程中,我们将学习如何使用 Spring Security 通过表单登录和 Basic Authentication 来认证 springdoc 中的端点访问。 2、项目设置 创建一个 Spring Boot Web 应用程序,该应用程序将通过 Spring Security 保护 API,并使用 Springdoc 生成文档。 2.1、依赖 首先添加 springdoc-openapi-ui,它整合了 Swagger-UI,用于提供 UI 界面: http://localhost:8080/swagger-ui.html 其次,添加 springdoc-openapi-security,用于为 Spring Security 提供支持: <dependency> <groupId>org.springdoc</groupId> <artifactId>springdoc-openapi-ui</artifactId> <version>1.6.13</version> </dependency> <dependency> <groupId>org.springdoc</groupId> <artifactId>springdoc-openapi-security</artifactId> <version>1.6.13</version> </dependency> 2.2、示例 API 创建一个示例 REST Controller,Springdoc 会为其生成文档。 此外,我们将通过 Swagger-UI 来演示与 FooController 中受保护(需要认证)的端点进行交互。 @RestController @RequestMapping(value = "foos", produces = MediaType.

使用 Webclient 以流式下载大文件

1、简介 在本教程中,我们将学习如何使用 WebClient 从服务器以流式下载一个大文件。我们通过创建一个简单的 Controller 和两个客户端进行演示。最后,我们将了解如何以及何时使用 Spring 的 DataBuffer 和 DataBufferUtils 工具类。 2、服务器 创建一个可以下载文件的简单 Controller。 首先,构建一个 FileSystemResource,传递一个文件路径,然后将其封装为 ResponseEntity 的 body: @RestController @RequestMapping("/large-file") public class LargeFileController { @GetMapping ResponseEntity<Resource> get() { return ResponseEntity.ok() .body(new FileSystemResource(Paths.get("/tmp/large.dat"))); } } 其次,我们需要生成下载所用的示例文件,文件内容并不重要,所以我们使用 fallocate 在磁盘上生成一个指定大小的“空”内容文件。如下: fallocate -l 128M /tmp/large.dat 然后就可以开始编写客户端了。 3、WebClient 使用 ExchangeStrategies 处理大文件 先用一个简单而有限的 WebClient 下载文件。使用 ExchangeStrategies 来提高 exchange() 操作的可用内存限制。这样,就能操作更多字节,但仍受限于 JVM 可用的最大内存。 使用 bodyToMono() 从服务器获取 Mono<byte[]>: public class LimitedFileDownloadWebClient { public static long fetch(WebClient client, String destination) { Mono<byte[]> mono = client.

Spring Boot 启动时 “jar中没有主清单属性” 异常

1、概览 当我们在执行 Spring Boot JAR 文件时候,可能会遇到 “jar中没有主清单属性”(no main manifest attribute)错误。这是因为我们在 MANIFEST.MF 文件中缺少了 Main-Class 元数据属性的声明,该文件位于 META-INF 文件夹下。 在本教程中,我们将重点介绍造成这一问题的原因以及如何解决。 TL;DR 出现这个问题的原因,八成是你忘记在 pom.xml 中添加 spring-boot-maven-plugin 插件了,添加即可。如下: <build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> </plugin> </plugins> </build> 2、问题的出现 一般来说,从 Spring Initializr 获取 pom.xml 不会有任何问题。不过,如果我们是手动创建项目,自己在 pom.xml 中添加 spring-boot-starter-parent 依赖,可能会遇到这个问题。 我们可以尝试以 mvn clean package 的方式来构建应用,以重现这个问题: $ mvn clean package 运行 jar 时,我们会遇到错误: $ java -jar target\spring-boot-artifacts-2.jar no main manifest attribute, in target\spring-boot-artifacts-2.jar 在此示例中,MANIFEST.MF 文件的内容为: Manifest-Version: 1.0 Archiver-Version: Plexus Archiver Created-By: Apache Maven 3.

对 RestTemplate 上的 URI 变量进行编码

1、概览 我们经常遇到的一个编码问题是,URI 变量中包含一个加号(+)。例如,如果我们的 URI 变量值为 http://localhost:8080/api/v1/plus+sign,那么加号将被编码为空格,这可能会导致意外的服务器响应。 在本教程中,我们将学习如何在 Spring 的 RestTemplate 上对 URI 变量进行编码。 让我们来看看解决这个问题的几种方法。 2、项目设置 创建一个使用 RestTemplate 进行 API 调用的小项目。 2.1、Spring Web 依赖 在 pom.xml 中添加 Spring Web Starter 依赖: <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> 或者,可以使用 Spring Initializr 生成项目并添加依赖。 2.2、RestTemplate Bean 创建一个 RestTemplate Bean: @Configuration public class RestTemplateConfig { @Bean public RestTemplate restTemplate() { return new RestTemplate(); } } 3、 调用 API 创建一个 service 类,调用公共 API http://httpbin.org/get。 API 会返回一个包含请求参数的 JSON 响应。例如,如果我们在浏览器上访问 URL https://httpbin.

在 Spring Boot 中使用 Undertow 作为嵌入式服务器

Spring Boot 默认使用 Tomcat 作为嵌入式 Servlet 服务器,它是由 Apache 软件基金会下 Jakarta 项目开发的 Servlet 容器,被广泛用于部署和运行 Java Web 应用程序。特点是轻量级、易于安装和配置,并且具有良好的可扩展性和性能 本文将会介绍另一款优秀的 Servlet 容器,Undertow。以及如何在 Spring Boot 中用它来替换 Tomcat 作为嵌入式服务器。 Undertow Undertow 是一个轻量级的、高性能的 Java Web 服务器,由 JBoss 开发并开源。它是基于非阻塞(non-blocking)的I/O模型,具有低资源消耗和高并发处理能力。 Undertown 的优势如下: 支持 HTTP/2:Undertow 开箱即支持 HTTP/2,无需重写启动类路径。 支持 HTTP Upgrade:允许通过 HTTP 端口复用多种协议。 支持 Web Socket:Undertow 提供对 Web Sockets 的全面支持,包括 JSR-356 支持。 Servlet 4.0:Undertow 支持 Servlet 4.0,包括对嵌入式 Servlet 的支持。还可以在同一部署中混合使用 Servlet 和原生 undertow 非阻塞 handler。 可嵌入式:只需几行代码,即可将 Undertow 嵌入应用程序或独立运行。 灵活性:Undertow 通过链式 handler 进行配置,可以根据需求灵活地添加功能。 在很多场景的测试下, Undertow 的性能都高于 Tomcat。天生适合作为 Spring Boot 应用的嵌入式服务器!

在 Spring 应用中实现 Kafka Consumer 重试消费

1、概览 本文将会带你学习在 Spring 应用中实现 Kafka Consumer 重试消费的 2 种方式,及其优缺点。 关于如何在 Spring 中整合 Kafka 的细节,请参阅 这里。 2、项目设置 创建一个新的 Spring Boot 项目,并添加 spring-kafka 依赖: <dependency> <groupId>org.springframework.kafka</groupId> <artifactId>spring-kafka</artifactId> <version>3.0.1</version> </dependency> 创建一个对象: public class Greeting { private String msg; private String name; // 构造函数、get、set 方法省略 } 3、Kafka Consumer Kafka Consumer(消费者)是从 Kafka 集群中读取数据的客户端应用程序。它订阅一个或多个 topic,并消费已发布的消息。Producer (生产者)向 topic 发送消息,topic 是存储和发布记录的类别名称。topic 被分为多个分区,以便横向扩展。每个分区都是一个不可更改的消息序列。 Consumer 可以通过指定偏移量(即消息在分区中的位置)来读取特定分区中的消息。Ack(确认)是消费者发送给 Kafka broker 的消息,表示它已成功处理了一条记录。一旦 ACK 被发送,消费者偏移量(consumer offset)将会被更新。 这将确保消息已被消费,并且不会再次传递给当前 Listener。 3.1、Ack 模式 Ack 模式决定了 broker 何时更新消费者偏移量(consumer offset)。

将 byte[] 转换为 MultipartFile

1、概览 MutlipartFile 是 Spring 提供的一个接口,用于接收 multipart 请求中的文件参数。Spring 没有为其提供任何默认实现,但提供了一个用于测试的实现。 在本教程中,我们将学习如何将字节数组转换为 MultipartFile。 2、实现 MultipartFile 接口 让我们创建自己的 MultipartFile 接口实现,并封装传入的字节数组: public class CustomMultipartFile implements MultipartFile { private byte[] input; @Override public String getName() { return null; } @Override public String getOriginalFilename() { return null; } @Override public String getContentType() { return null; } // 下一个代码段中定义了接口的其他方法 } 我们在类中定义了一个 byte[] 属性,存储传入的字节数组。接口中定义的其他几个描述性方法,可以根据需求来实现。这里默认返回 null。 剩余的几个接口方法是必须要实现的,如下: public class CustomMultipartFile implements MultipartFile { // 前一段儿代码省略... @Override public boolean isEmpty() { return input == null || input.