SpringBootBucket 4.1.1 升级指南:用好 Spring Boot 4、Java 25 与 HTTP Service Clients

2026-09-24 24 预计阅读时间: 1 分钟
来源: oschina.net AI 摘要 Original link

Disclaimer: This article is an AI-assisted summary. Read it together with the original source when precision matters. The summary may omit context, version differences, or edge cases and is not official documentation.

预计阅读时间:11 分钟

SpringBootBucket 4.1.1 将示例工程推进到 Spring Boot 4.x 技术栈。它并不只是一次依赖版本刷新:底层切换到 Spring Framework 7,开始全面利用 Java 25,并引入 HTTP Service Clients、原生 API 版本管理和 JSpecify 空安全体系。与此同时,Jackson 3.0、Tomcat 等关键组件的升级,也意味着迁移时不能只改一个版本号就结束。

这次升级真正改变了什么

Spring Boot 4.x 的价值主要体现在三个层面。

一是运行时能力。Java 25 与虚拟线程适合大量等待数据库、HTTP 或消息系统返回的阻塞式任务。虚拟线程不会自动让 CPU 密集型代码变快,但可以降低“一个请求占用一个平台线程”带来的并发成本。

二是开发模型。HTTP Service Clients 允许开发者用 Java 接口描述远程 HTTP 调用,减少重复编写 URL 拼接、响应反序列化和客户端封装代码。它适合内部服务 SDK,也便于统一加入认证、超时、日志和链路追踪。

三是接口治理。Spring Framework 7 提供原生 API 版本管理能力,版本选择可以进入框架的请求映射流程,不必在控制器中到处判断请求头。JSpecify 则把参数、返回值和字段的空值约束交给 IDE 与静态分析工具,让一部分 NPE 在编译和审查阶段暴露。

需要特别注意:这些能力不会自动修复旧项目。Jackson 3.0 可能影响自定义序列化器、模块注册和 ObjectMapper 配置;Servlet 容器升级可能暴露过滤器、连接器和请求解析行为的差异;空安全注解也只是静态契约,并不能阻止反射、外部 JSON 或错误业务数据传入 null。

用 HTTP Service Client 替代手写远程调用

下面是一个可以改造到项目中的最小示例。示例假设使用 Spring Boot 4.0.0;接入 SpringBootBucket 4.1.1 时,应把父版本替换为该工程实际管理的 Spring Boot 4.x 版本。

pom.xml:

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>4.0.0</version>
        <relativePath/>
    </parent>

    <groupId>dev.example</groupId>
    <artifactId>http-client-demo</artifactId>
    <version>0.0.1-SNAPSHOT</version>

    <properties>
        <java.version>25</java.version>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>
</project>

声明远程库存接口:

package dev.example.demo;

import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.service.annotation.GetExchange;

public interface InventoryClient {

    @GetExchange("/inventory/{sku}")
    Stock findStock(@PathVariable String sku);

    record Stock(String sku, int available) {}
}

创建接口代理:

package dev.example.demo;

import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.client.RestClient;
import org.springframework.web.client.support.RestClientAdapter;
import org.springframework.web.service.invoker.HttpServiceProxyFactory;

@Configuration
public class InventoryClientConfig {

    @Bean
    InventoryClient inventoryClient(
            @Value("${inventory.base-url}") String baseUrl) {

        RestClient restClient = RestClient.builder()
                .baseUrl(baseUrl)
                .build();

        HttpServiceProxyFactory factory = HttpServiceProxyFactory
                .builderFor(RestClientAdapter.create(restClient))
                .build();

        return factory.createClient(InventoryClient.class);
    }
}

再通过本地控制器调用它:

package dev.example.demo;

import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping("/api/stocks")
public class StockController {

    private final InventoryClient inventoryClient;

    public StockController(InventoryClient inventoryClient) {
        this.inventoryClient = inventoryClient;
    }

    @GetMapping("/{sku}")
    InventoryClient.Stock get(@PathVariable String sku) {
        return inventoryClient.findStock(sku);
    }
}

在 src/main/resources/application.yaml 中配置远程地址,并开启虚拟线程:

spring:
  threads:
    virtual:
      enabled: true

inventory:
  base-url: http://localhost:8081

运行前需要准备一个在 8081 端口提供 GET /inventory/{sku} 的测试服务,然后执行:

mvn spring-boot:run
curl http://localhost:8080/api/stocks/SKU-1001

生产项目还应为客户端补充连接超时、读取超时、认证头、指标和错误映射。虚拟线程可以提高阻塞 I/O 场景下的并发承载能力,但不会取消下游连接池、数据库连接数和限流策略的上限。

把 API 版本从业务判断提升为路由规则

过去常见的做法是在控制器中读取 X-API-Version,再用 if 分支返回不同结构。这种代码会让版本逻辑和业务逻辑纠缠在一起。Spring Framework 7 的原生版本管理可以把版本声明放到映射规则上。

可以这样实践基于请求头的版本选择;具体类名和配置方式应以项目锁定的 Spring Framework 7 小版本为准:

package dev.example.demo;

import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.ApiVersionConfigurer;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

@Configuration
public class ApiVersionConfig implements WebMvcConfigurer {

    @Override
    public void configureApiVersioning(ApiVersionConfigurer configurer) {
        configurer.useRequestHeader("X-API-Version");
    }
}

控制器可以为同一路径提供不同版本:

package dev.example.demo;

import java.util.Map;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping("/api/products")
public class ProductController {

    @GetMapping(value = "/{id}", version = "1.0")
    Map<String, Object> version1(@PathVariable long id) {
        return Map.of("id", id, "name", "Keyboard");
    }

    @GetMapping(value = "/{id}", version = "2.0")
    Map<String, Object> version2(@PathVariable long id) {
        return Map.of(
                "id", id,
                "displayName", "Keyboard",
                "status", "AVAILABLE"
        );
    }
}

调用时显式传入版本:

curl -H 'X-API-Version: 1.0' http://localhost:8080/api/products/1
curl -H 'X-API-Version: 2.0' http://localhost:8080/api/products/1

版本管理仍然需要产品层面的规则,例如默认版本是什么、旧版本保留多久、缺少版本时返回什么状态码,以及监控中如何统计仍在使用旧版本的调用方。框架解决的是路由机制,不会替团队制定兼容策略。

用 JSpecify 收紧空值边界

JSpecify 更适合从包级别开始启用。在 package-info.java 中加入 @NullMarked 后,该包内未额外标注的类型通常按非空契约理解;确实可能为空的数据则使用 @Nullable 明确标出。

@org.jspecify.annotations.NullMarked
package dev.example.demo;

业务接口可以写成:

package dev.example.demo;

import org.jspecify.annotations.Nullable;

public interface UserDirectory {

    User findRequired(long id);

    @Nullable
    User findByEmail(String email);

    record User(long id, String email) {}
}

采用时不要一次性给整个遗留代码库加上 @NullMarked。更稳妥的方式是从新模块、HTTP 边界和数据访问接口开始,根据 IDE 或静态分析结果逐步修复。还要为 JSON 缺字段、数据库空列和第三方 SDK 返回值保留运行时校验。

升级时不要漏掉这些检查

建议把 SpringBootBucket 4.1.1 当作迁移参考和能力样板,而不是直接复制所有配置。落地前至少完成以下检查:

  • 确认构建机、CI 镜像和生产运行时都提供匹配的 JDK;如果启用 Java 25 特性,避免本地与生产版本不一致。
  • 检查 Jackson 3.0 相关的自定义模块、序列化器、反序列化器和测试快照,不要假设 Jackson 2 配置可以原样工作。
  • 对过滤器、异常处理、上传下载、Cookie、代理头和连接超时做容器级回归测试。
  • 为 HTTP Service Clients 统一设置超时、认证、重试边界和可观测性;非幂等请求不要随意自动重试。
  • 为 API 版本建立弃用日期、调用量监控和兼容测试。
  • 将 JSpecify 视为静态质量工具,同时保留参数校验与运行时防御。
  • 用真实压测验证虚拟线程收益,重点观察数据库连接池、下游限流、堆内存和延迟分位数。

Spring Boot 4.x 的升级重点不只是“跑在新 JDK 上”,而是把远程调用、接口演进和空值契约纳入框架能力。先在一个边界清晰的服务中试点,再逐步推广,通常比全仓库一次性升级更容易控制风险。


相关推荐