还记得 2022 年底不?当时Spring Boot 3 和 Spring Framework 6 一出来,直接给整个 Spring 生态来了个 “大换血”, 这可是自 Spring 诞生以来动静最大的一次更新。不仅把 Java 17 设为了最低要求,还把以前的 javax.* 换成了 jakarta.*,连 GraalVM 原生镜像也开始初步支持了。

转眼到 2025 年,新一代版本马上要来了:就是 Spring Boot 4 和 Spring Framework 7。

这俩版本没跑偏,接着推进生态现代化。一方面用上了最新的 Java 特性,跟 Jakarta EE 11 的适配也更紧密;另一方面还帮开发者省了不少事,而且默认就支持打造 “抗造” 的应用,不用额外折腾太多。

接下来我就跟大家捋捋这俩版本的核心亮点,再配点说明和代码例子,让大家清楚升级后能用到啥好东西。

基线升级

在说具体功能之前,得先明确新版本对 “基础依赖” 的要求,这很关键。

现在行业里用 Java 17 的还挺多,所以它依然是最低要求版本。不过官方特别建议用 Java 21 或 25,因为能用上虚拟线程这类新的 JVM 特性。关于这点,Spring 博客里有官方说明,大家想细看可以去那看。

Spring Framework 7 现在完全适配 Jakarta EE 11 了,也就是说,它依赖的技术标准都升级了:Servlet 6.1、JPA 3.2,还有 Bean Validation 3.1。

另外,Kotlin 现在支持 2.2 及以上版本了。这么一来,写协程会更顺,处理响应式代码也感觉更 “自然”,不用绕弯子。

Spring Boot 4

作为第四个大版本,Spring Boot 这次加了不少实用改进,重点提一下:性能变快了、更容易 “监控” 应用了、维护起来更省心了,连配置支持都变强了。这些改动让它作为 “现代云原生 Java 应用的基础框架”,地位更稳了。

原生镜像更给力了

Spring Boot 4 还在使劲推进 GraalVM 原生镜像的支持,现在已经完全跟 GraalVM 24 对齐了。而且 “提前编译(AOT)” 的能力也优化了 —— 编译速度更快,启动时占的内存也更少了。

举个例子,Spring Data 新增了AOT 仓库,简单说就是:通过 AOT 处理,能把查询方法转成源代码,然后跟应用一起编译。这样一来,运行效率会更高。

监控应用更方便:Micrometer 2 + OpenTelemetry

云原生应用想跑稳,“可观测性”(就是能看到应用的运行状态)特别重要。Spring Boot 3 之前就加了Spring 可观测性,这次Spring Boot 4 直接升级到 Micrometer 2,还集成了 OpenTelemetry 的启动器。这样一来,“Trace日志”、“普通日志” 和 “性能指标(metrics)” 就能无缝配合,不用再自己凑一套了。

SSL 证书快过期?现在能清楚看到了

以前看 SSL 证书状态有点麻烦,现在改进了:如果证书链里有快过期的证书,会专门新增一个 expiringChains 条目来显示它们。而且之前的 “WILL_EXPIRE_SOON” 状态没了,快过期的证书会统一标成 “VALID”。

这样一来,团队在生产环境监控 SSL 证书时,能更清楚哪些要过期了,还不会出现没必要的 “误报”,省了不少排查时间。

代码拆得更细了:模块化改造

Spring Boot 4 刚启动开发时,第一个重要目标就是把自己的代码库拆成更 “模块化” 的结构。

以前 Spring Boot 3 里,很多核心模块(比如自动配置、启动器依赖、构建工具这些)都打包在大的 “构件” 里。虽然用着方便,但有时候依赖管理会很乱,类路径扫描也慢,连原生镜像的体积都会变大。

从 Spring Boot 4 开始,团队把 “自动配置” 和 “支持代码” 拆成了更小的模块,每个模块只负责一小块功能。这么改有啥好处?

  • 构建和生成原生镜像更快:GraalVM 的 AOT 处理不用管那些没用的 “提示信息” 和 “元数据” 了。

  • 依赖管理更清爽:像 Micrometer、OpenTelemetry 这些可选的功能,不再跟核心代码打包在一起,而是单独放一个模块,想用就加,不想用也不占地方。

  • Spring 团队维护起来更轻松:模块和功能一一对应,找问题、改代码都方便,贡献代码的人也更容易上手。

对咱们开发者来说,平时写 pom.xml 或 build.gradle 可能感觉不到变化。只要用 “启动器依赖”(比如 spring-boot-starter-data-jpa),就不用改任何东西。比如要用到 JPA 和 Hibernate,直接在 pom.xml 里加这段依赖就行:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>

真正的变化在 “底层”:JPA 的自动配置、Hibernate 的集成、还有校验相关的配置,现在都在不同的小模块里了。这样框架在运行时或者 AOT 编译时,能更精准地加载需要的配置,不浪费资源。

新增 @ConfigurationPropertiesSource 注解:跨模块配置更省心

为了让模块化更好用,这次还加了个新注解 @ConfigurationPropertiesSource。注意哦,它不会改变 “运行时配置属性怎么绑定”,主要是在 “构建阶段” 给 spring-boot-configuration-processor(配置处理器)提个醒。

平时配置处理器给 @ConfigurationProperties 类生成 “元数据” 时,只会从这个类所在的模块里找信息。但在模块化项目里,有时候会用到其他模块的 “嵌套类型” 或者 “基类”,而这些模块的源代码在构建时可能拿不到 —— 这样生成的元数据就会不完整,比如少了属性描述或者默认值。

现在给类加个 @ConfigurationPropertiesSource 注解,就能告诉处理器:“就算这个类没标 @ConfigurationProperties,也得给它生成完整的元数据”。简单说,以后跨模块开发时,再也不用操心元数据缺失的问题了,处理器会自动搞定。

Spring Framework 7

Spring Framework 7 这次既加了很多用户盼了好久的功能,还在测试、API 设计、核心基础功能上做了不少细节优化。一方面让框架更 “现代”,另一方面也帮咱们少写很多重复的模板代码。

测试功能更灵活了

Spring 在测试时会用 “上下文缓存”—— 就是为了平衡 “测试速度” 和 “环境隔离”(避免不同测试互相影响)。关于这个缓存的细节、可能踩的坑,还有怎么解决,大家可以看这篇文章。

这次 Spring Framework 7 加了个 “测试上下文暂停” 的功能。以前跑长时间的集成测试,就算测试闲着没干活,也会占着资源;现在不一样了,Spring 能把缓存里的 “上下文” 暂停,要用的时候再恢复 —— 这样能省内存,跑大量测试的时候速度也会变快。比如处理 JMS 监听器容器、定时任务这些场景,这个功能特别有用。

另外,还新增了一个RestTestClient,用它测试 REST 接口特别方便 —— 用法跟 WebTestClient 差不多,但不用额外装 “响应式基础设施”。

给大家看个例子,就能明白它多简单了:

@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
class HelloWorldApiIntegrationTest {

    RestTestClient client;

    @BeforeEach
    void setUp(WebApplicationContext context) {
        client = RestTestClient.bindToApplicationContext(context)
            .build();
    }

    @Test
    void shouldFetchHelloV1() {
        client.get()
            .uri("/api/v1/hello")
            .exchange()
            .expectStatus()
            .isOk()
            .expectHeader()
            .contentTypeCompatibleWith(MediaType.TEXT_PLAIN)
            .expectBody(String.class)
            .consumeWith(message -> assertThat(message.getResponseBody()).containsIgnoringCase("hello"));
    }

}

API 版本控制

很多人一直想要的 “API 版本控制”,这次终于成了框架的一级功能,不用再自己瞎折腾了。

以前咱们要搞 API 版本,得自己想办法:比如在 URL 里加 /v1/,或者自定义请求头、改媒体类型。现在框架原生就支持了,直接给 @GetMapping 加个 version 属性就行,看例子:

@RestController
@RequestMapping("/hello")
public class HelloWorldController {

    // 版本1的接口,返回“Hello World”
    @GetMapping(version = "1", produces = MediaType.TEXT_PLAIN_VALUE)
    public String sayHelloV1() {
        return "Hello World";
    }

    // 版本2的接口,返回“Hi World”
    @GetMapping(version = "2", produces = MediaType.TEXT_PLAIN_VALUE)
    public String sayHelloV2() {
        return "Hi World";
    }
 
}

也可以在控制器级别统一设版本,比如下面这个 “版本 3” 的控制器:

@RestController
@RequestMapping(path = "/hello", version = "3")
public class HelloWorldV3Controller {

    // 不用再给方法加version了,默认用控制器的版本3
    @GetMapping(produces = MediaType.TEXT_PLAIN_VALUE)
    public String sayHello() {
        return "Hey World";
    }

}

不过得配置一下 “版本怎么映射到请求上”,有四种方式可选:

  • 路径映射:比如 /api/v1/hello 和 /api/v2/hello

  • 查询参数:比如 /hello?version=1 和 /hello?version=2

  • 请求头:比如请求头里加 X-API-Version: 1 或 2

  • 媒体类型头:比如请求头 Accept: application/json; version=1

下面是 “路径映射” 的配置例子,大家可以参考:

@Configuration
public class ApiConfig implements WebMvcConfigurer {

    @Override
    public void configureApiVersioning(ApiVersionConfigurer configurer) {
        // 用路径的第1段作为版本(比如/v1/里的1)
        configurer.usePathSegment(1);
    }

    @Override
    public void configurePathMatch(PathMatchConfigurer configurer) {
        // 给所有@RestController的接口加前缀/api/v{version}
        configurer.addPathPrefix("/api/v{version}", HandlerTypePredicate.forAnnotation(RestController.class));
    }

}

配置完之后,Spring 会自动识别请求里的版本,对应到正确的接口。这样迭代 API 的时候,就不用担心影响老用户了。

@HttpServiceClient:写 HTTP 客户端更简单

还有个好用的功能:声明式 HTTP 客户端。灵感来自 Feign,但更轻量,而且跟 Spring 融得更紧。

以前用 Spring,要调用其他服务的 HTTP 接口,得给 HttpInterface 创建代理,想搞智能点还得自己写代码。比如这个仓库里,就有个自定义的 @HttpClient 注解和 Bean 注册器的例子(Spring Framework 7 还优化了 Bean 注册器,这篇文章有说)。

现在不用自己写了,框架直接给了 @HttpServiceClient 注解,开箱即用。看个例子就懂了:

// 给这个接口加注解,指定服务名叫“christmasJoy”
@HttpServiceClient("christmasJoy")
public interface ChristmasJoyClient {

    // 用@GetExchange指定请求地址,相当于发GET请求到/greetings?random
    @GetExchange("/greetings?random")
    String getRandomGreeting();

}

然后要配置一下 “扫描这个接口” 和 “服务的基础地址”:

@Configuration
// 导入自定义的注册器
@Import(HttpClientConfig.HelloWorldClientHttpServiceRegistrar.class)
public class HttpClientConfig {

    // 自定义注册器,负责扫描并注册HttpServiceClient接口
    static class HelloWorldClientHttpServiceRegistrar extends AbstractClientHttpServiceRegistrar {

        @Override
        protected void registerHttpServices(GroupRegistry registry, AnnotationMetadata metadata) {
            // 扫描com.baeldung.spring.mvc包下的HttpServiceClient接口
            findAndRegisterHttpServiceClients(registry, List.of("com.baeldung.spring.mvc"));
        }
    }

    // 配置“christmasJoy”这个服务的基础地址
    @Bean
    RestClientHttpServiceGroupConfigurer christmasJoyServiceGroupConfigurer() {
        return groups -> {
            groups.filterByName("christmasJoy") // 找到名叫christmasJoy的服务组
                .forEachClient((group, clientBuilder) -> {
                    // 设置基础URL
                    clientBuilder.baseUrl("https://christmasjoy.dev/api");
                });
        };
    }

}

配置完之后,这个 ChristmasJoyClient 就能像普通 Bean 一样注入到其他组件里用了,比如下面这个控制器:

@RestController
@RequestMapping(path = "/hello", version = "4")
@RequiredArgsConstructor // Lombok注解,自动生成构造器
public class HelloWorldV4Controller {

    // 注入ChristmasJoyClient
    private final ChristmasJoyClient christmasJoy;

    @GetMapping(produces = MediaType.TEXT_PLAIN_VALUE)
    public String sayHello() {
        // 调用客户端的方法,获取随机问候语
        return this.christmasJoy.getRandomGreeting();
    }

}

弹性能力内置:加个注解就有重试、限流

Spring Retry 虽然在生态里好多年了,但一直像个 “外挂”,不是框架核心功能。这次 Spring Framework 7 直接把 “弹性能力”(比如重试、限流)做成内置的了—— 给 Spring 组件的方法加个注解,就能实现重试逻辑或者并发限制,特别方便。

看个例子,还是刚才的 ChristmasJoyClient,加两个注解就行:

@HttpServiceClient("christmasJoy")
public interface ChristmasJoyClient {

    @GetExchange("/greetings?random")
    // 重试:最多试3次,第一次等100ms,之后每次等的时间翻倍,最多等1000ms
    @Retryable(maxAttempts = 3, delay = 100, multiplier = 2, maxDelay = 1000)
    // 并发限制:最多3个请求同时调用
    @ConcurrencyLimit(3)
    String getRandomGreeting();

}

不过要注意:这些注解默认是不生效的,得在某个配置类上加 @EnableResilientMethods 才能启用。

这样一来,不用再依赖 Resilience4j 这类额外库(当然,想集成也能正常用),就能实现弹性功能,省了不少配置功夫。而且还能在运行时验证这些策略有没有生效,不用担心注解白加了。

 

支持多个 TaskDecorator:异步任务能加多个 “钩子”

以前用 Spring 的时候,想自定义异步任务的执行逻辑,顶多只能在 ThreadPoolTaskExecutor 上挂一个 TaskDecorator。比如把安全上下文(SecurityContext)或者日志里的 MDC 信息传到异步线程里,靠它还能实现。但要是想同时搞好几件事,比如:又传上下文又打日志,就得自己手动写个 “复合装饰器”,特别麻烦。

好在从 Spring Framework 7 开始,咱们能在应用上下文里声明多个 TaskDecorator Bean 了!Spring 会自动把它们串成一个 “装饰链”,按 Bean 定义的顺序或者 @Order 注解标的顺序,一个接一个生效。

举个实际例子,咱们有个异步的事件监听器:

@Component
@Slf4j
public class HelloWorldEventLogger {

    @Async // 标了这个就是异步执行
    @EventListener
    void logHelloWorldEvent(HelloWorldEvent event) {
        log.info("Hello World Event: {}", event.message());
    }

}

要是想给这个异步任务加个简单日志(记录开始结束),再算个执行时间,不用写复杂代码,直接注册两个 TaskDecorator Bean 就行:

@Configuration
@Slf4j
public class TaskDecoratorConfiguration {

    // 第一个装饰器:打任务开始/结束日志,@Order标了2
    @Bean
    @Order(2)
    TaskDecorator loggingTaskConfigurator() {
        return runnable -> () -> {
            log.info("Running Task: {}", runnable); // 任务开始日志
            try {
                runnable.run(); // 执行实际任务
            } finally {
                log.info("Finished Task: {}", runnable); // 任务结束日志
            }
        };
    }

    // 第二个装饰器:算执行时间,@Order标了1(比上面先执行)
    @Bean
    @Order(1)
    TaskDecorator measuringTaskConfigurator() {
        return runnable -> () -> {
            final var ts1 = System.currentTimeMillis(); // 开始时间
            try {
                runnable.run(); // 执行实际任务
            } finally {
                final var ts2 = System.currentTimeMillis(); // 结束时间
                log.info("Finished within {}ms (Task: {})", ts2 - ts1, runnable); // 输出耗时
            }
        };
    }

}

最后日志会输出这样的内容:

Running Task: com.baeldung.spring.mvc.TaskDecoratorConfiguration$$Lambda/0x00000ff0014325f8@57e8609
Hello World Event: "Happy Christmas"
Finished within 0ms (Task: java.util.concurrent.FutureTask@bb978d6[Completed normally])
Finished Task: com.baeldung.spring.mvc.TaskDecoratorConfiguration$$Lambda/0x00000ff0014325f8@57e8609

这波改进是真省心,不用再写一堆重复的复合装饰器代码了,想给异步任务加多个功能(比如日志、计时、权限校验),直接加 Bean 就行,特别方便。

用 JSpecify 搞定空安全,终于不混乱了

之前 Java 生态里的空值注解随处可见(@Nonnull、@Nullable、@NotNull 等),现在 Spring Framework 7 干脆定了标准:用 JSpecify,示例长这样:

@Configuration
public class ApiConfig implements WebMvcConfigurer {

    @Override
    public void configureApiVersioning(@NonNull ApiVersionConfigurer configurer) {
        configurer.usePathSegment(1);
    }

}

这改进了 IDE 工具和 Kotlin 互操作性,减少了大型代码库中 NullPointerException 的风险。

总结

Spring Boot 4 和 Spring Framework 7 真不是 “小打小闹的更新”,而是 Spring 特意朝着 Java 开发的 “现代化、模块化、云原生” 方向迈的一大步:

  • 有了 API 版本控制和弹性注解,应用既能轻松升级,又能扛住各种异常

  • 靠 JSpecify 的空安全和 Kotlin 支持,运行时出错能少很多。

  • 声明式 HTTP 客户端,服务之间调用不用写一堆请求代码了。

  • 原生镜像支持和可观测性工具加强,往云环境上部署更顺了。

跟所有大版本升级一样,关键是早点拿项目测。尤其是依赖升级和旧 API 替换这两块,早发现问题早解决。不过话说回来,升级后开发效率变高、性能变好、维护也省心,这点麻烦还是值得的。