Java 響應(yīng)式編程之Spring WebFlux+Reactor 實戰(zhàn)攻略
在傳統(tǒng)的Java Web開發(fā)中,Spring MVC基于Servlet API構(gòu)建,采用的是同步阻塞的I/O模型。這種模型在高并發(fā)、高IO等待的場景下會面臨性能瓶頸——每個請求都需要占用一個獨立的線程,大量線程的創(chuàng)建和切換會消耗過多的系統(tǒng)資源。為了解決這一問題,響應(yīng)式編程應(yīng)運而生。
本文將帶你深入了解Java響應(yīng)式編程的核心,重點講解Spring WebFlux框架與Reactor響應(yīng)式庫的實戰(zhàn)用法,通過大量可運行的示例代碼,幫助你快速上手并理解響應(yīng)式編程的精髓。
一、響應(yīng)式編程核心概念
在開始實戰(zhàn)之前,我們先明確幾個響應(yīng)式編程的核心概念,為后續(xù)學(xué)習(xí)打下基礎(chǔ)。
1.1 什么是響應(yīng)式編程?
響應(yīng)式編程是一種基于數(shù)據(jù)流(Data Stream)和變化傳播(Propagation of Change)的編程范式。簡單來說,就是當(dāng)數(shù)據(jù)發(fā)生變化時,系統(tǒng)會自動響應(yīng)這些變化,而無需主動輪詢或等待。
其核心特性包括:
- 非阻塞:操作不會阻塞當(dāng)前線程,而是在任務(wù)完成后通過回調(diào)函數(shù)通知結(jié)果,極大提升了線程利用率。
- 異步:任務(wù)的執(zhí)行與結(jié)果的處理不在同一時間線,避免了線程的閑置等待。
- 數(shù)據(jù)流驅(qū)動:一切操作圍繞數(shù)據(jù)流展開,支持對數(shù)據(jù)流的過濾、轉(zhuǎn)換、合并等多種操作。
- 背壓(Backpressure):消費者可以告知生產(chǎn)者自己的處理能力,避免生產(chǎn)者發(fā)送數(shù)據(jù)過快導(dǎo)致消費者過載,這是響應(yīng)式編程區(qū)別于傳統(tǒng)異步編程的關(guān)鍵特性。
1.2 Reactor:Java響應(yīng)式編程的基石
Reactor是Spring官方推薦的響應(yīng)式編程庫,也是Spring WebFlux的底層依賴。它實現(xiàn)了Reactive Streams規(guī)范,提供了兩個核心的數(shù)據(jù)流類型:Mono和Flux,用于處理01個元素和0N個元素的場景。
Reactive Streams是一個行業(yè)標(biāo)準(zhǔn),定義了響應(yīng)式數(shù)據(jù)流的發(fā)布者(Publisher)、訂閱者(Subscriber)、訂閱(Subscription)和處理器(Processor)四個核心接口,目的是實現(xiàn)不同響應(yīng)式庫之間的互操作性。
1.2.1 Mono與Flux的區(qū)別
| 類型 | 描述 | 適用場景 |
|---|---|---|
| Mono | 表示0或1個元素的異步序列,可能成功完成或失敗 | 查詢單個對象(如根據(jù)ID查詢用戶)、執(zhí)行無返回值的操作(如插入數(shù)據(jù)) |
| Flux | 表示0到N個元素的異步序列,支持背壓機(jī)制 | 查詢列表數(shù)據(jù)(如分頁查詢用戶)、處理流式數(shù)據(jù)(如日志流、消息流) |
二、環(huán)境搭建:Spring WebFlux+Reactor入門
接下來我們搭建一個基礎(chǔ)的Spring WebFlux項目,體驗響應(yīng)式編程的基本用法。
2.1 項目依賴配置(Maven)
創(chuàng)建一個Maven項目,在pom.xml中添加以下依賴:
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.7.15</version>
<relativePath/>
</parent>
<dependencies>
<!-- Spring WebFlux 核心依賴 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-webflux</artifactId>
</dependency>
<!-- Reactor 測試依賴 -->
<dependency>
<groupId>io.projectreactor</groupId>
<artifactId>reactor-test</artifactId>
<scope>test</scope>
</dependency>
<!-- Lombok 簡化代碼 -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<!-- Spring Boot 測試依賴 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>說明:spring-boot-starter-webflux已經(jīng)包含了Reactor的核心依賴(reactor-core),無需額外引入。
2.2 啟動類編寫
創(chuàng)建Spring Boot啟動類,與Spring MVC的啟動類類似,只需添加@SpringBootApplication注解:
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class WebFluxReactorApplication {
public static void main(String[] args) {
SpringApplication.run(WebFluxReactorApplication.class, args);
}
}三、Reactor核心實戰(zhàn):Mono與Flux操作
Reactor提供了豐富的操作符(Operator)用于處理Mono和Flux數(shù)據(jù)流,本節(jié)通過示例講解常用操作。
3.1 數(shù)據(jù)流的創(chuàng)建
常用的創(chuàng)建方法:just()、fromIterable()、empty()、error()等。
import io.projectreactor.core.publisher.Flux;
import io.projectreactor.core.publisher.Mono;
import org.junit.Test;
public class ReactorCreateTest {
// 測試Flux創(chuàng)建
@Test
public void testFluxCreate() {
// 1. 創(chuàng)建包含多個元素的Flux
Flux<String> flux1 = Flux.just("Java", "Spring", "WebFlux");
flux1.subscribe(System.out::println); // 訂閱并打印元素
System.out.println("=====");
// 2. 從集合創(chuàng)建Flux
List<Integer> list = Arrays.asList(1, 2, 3, 4);
Flux<Integer> flux2 = Flux.fromIterable(list);
flux2.subscribe(num -> System.out.println("數(shù)字:" + num));
System.out.println("=====");
// 3. 創(chuàng)建空Flux(會觸發(fā)onComplete回調(diào))
Flux<Void> flux3 = Flux.empty();
flux3.subscribe(
null,
error -> System.err.println("錯誤:" + error),
() -> System.out.println("flux3 完成")
);
}
// 測試Mono創(chuàng)建
@Test
public void testMonoCreate() {
// 1. 創(chuàng)建包含單個元素的Mono
Mono<String> mono1 = Mono.just("Hello Reactor");
mono1.subscribe(System.out::println);
System.out.println("=====");
// 2. 創(chuàng)建空Mono
Mono<String> mono2 = Mono.empty();
mono2.subscribe(
str -> System.out.println("元素:" + str),
error -> System.err.println("錯誤:" + error),
() -> System.out.println("mono2 完成")
);
System.out.println("=====");
// 3. 創(chuàng)建包含錯誤的Mono
Mono<String> mono3 = Mono.error(new RuntimeException("測試錯誤"));
mono3.subscribe(
str -> System.out.println("元素:" + str),
error -> System.err.println("錯誤:" + error.getMessage()),
() -> System.out.println("mono3 完成")
);
}
}運行測試方法,可以看到不同創(chuàng)建方式的輸出結(jié)果。注意:Reactor的數(shù)據(jù)流只有在被訂閱(subscribe)后才會開始執(zhí)行,這就是“惰性求值”特性。
3.2 常用操作符實戰(zhàn)
操作符用于對數(shù)據(jù)流進(jìn)行轉(zhuǎn)換、過濾、聚合等處理,以下是最常用的操作符示例。
3.2.1 轉(zhuǎn)換操作:map與flatMap
map:同步轉(zhuǎn)換,將一個元素轉(zhuǎn)換為另一個元素;flatMap:異步轉(zhuǎn)換,將一個元素轉(zhuǎn)換為一個新的數(shù)據(jù)流(Mono/Flux),并合并這些數(shù)據(jù)流。
import io.projectreactor.core.publisher.Flux;
import org.junit.Test;
public class ReactorMapTest {
@Test
public void testMap() {
// map:將字符串轉(zhuǎn)換為其長度
Flux<String> flux = Flux.just("Java", "Spring", "WebFlux");
flux.map(String::length)
.subscribe(length -> System.out.println("字符串長度:" + length));
}
@Test
public void testFlatMap() {
// flatMap:將每個數(shù)字轉(zhuǎn)換為一個新的Flux(0到該數(shù)字),并合并所有Flux
Flux<Integer> flux = Flux.just(2, 3);
flux.flatMap(num -> Flux.range(0, num))
.subscribe(System.out::println);
// 輸出:0,1,0,1,2
}
}3.2.2 過濾操作:filter與take
filter:根據(jù)條件過濾元素;take:獲取前N個元素。
import io.projectreactor.core.publisher.Flux;
import org.junit.Test;
public class ReactorFilterTest {
@Test
public void testFilter() {
// 過濾出偶數(shù)
Flux<Integer> flux = Flux.range(1, 10); // 生成1到10的數(shù)字
flux.filter(num -> num % 2 == 0)
.subscribe(even -> System.out.println("偶數(shù):" + even));
}
@Test
public void testTake() {
// 獲取前3個元素
Flux<String> flux = Flux.just("A", "B", "C", "D", "E");
flux.take(3)
.subscribe(str -> System.out.println("前3個元素:" + str));
}
}3.2.3 聚合操作:reduce與collectList
reduce:將數(shù)據(jù)流中的元素聚合為一個值;collectList:將數(shù)據(jù)流中的所有元素收集到一個List中,返回Mono。
import io.projectreactor.core.publisher.Flux;
import org.junit.Test;
public class ReactorAggregateTest {
@Test
public void testReduce() {
// 計算1到10的和
Flux<Integer> flux = Flux.range(1, 10);
flux.reduce((a, b) -> a + b)
.subscribe(sum -> System.out.println("1到10的和:" + sum));
}
@Test
public void testCollectList() {
// 將元素收集到List中
Flux<String> flux = Flux.just("Java", "Spring", "WebFlux");
flux.collectList()
.subscribe(list -> System.out.println("元素列表:" + list));
}
}四、Spring WebFlux實戰(zhàn):響應(yīng)式Web開發(fā)
Spring WebFlux支持兩種編程模型:基于注解的編程模型(類似Spring MVC)和基于函數(shù)式的編程模型。本節(jié)分別講解兩種模型的實戰(zhàn)用法。
4.1 基于注解的編程模型(常用)
這種模型與Spring MVC非常相似,使用@RestController、@GetMapping、@PostMapping等注解,上手成本低。
4.1.1 實體類定義
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@NoArgsConstructor
@AllArgsConstructor
public class User {
private Long id;
private String name;
private Integer age;
}4.1.2 模擬數(shù)據(jù)服務(wù)
創(chuàng)建UserService,模擬數(shù)據(jù)庫操作:
import io.projectreactor.core.publisher.Flux;
import io.projectreactor.core.publisher.Mono;
import org.springframework.stereotype.Service;
import java.util.HashMap;
import java.util.Map;
@Service
public class UserService {
// 模擬數(shù)據(jù)庫
private static final Map<Long, User> USER_MAP = new HashMap<>();
static {
USER_MAP.put(1L, new User(1L, "張三", 20));
USER_MAP.put(2L, new User(2L, "李四", 25));
USER_MAP.put(3L, new User(3L, "王五", 30));
}
// 查詢所有用戶
public Flux<User> findAll() {
return Flux.fromIterable(USER_MAP.values());
}
// 根據(jù)ID查詢用戶
public Mono<User> findById(Long id) {
return Mono.justOrEmpty(USER_MAP.get(id));
}
// 添加用戶
public Mono<User> addUser(User user) {
// 生成唯一ID(簡化處理)
Long id = USER_MAP.keySet().stream().max(Long::compare).orElse(0L) + 1;
user.setId(id);
USER_MAP.put(id, user);
return Mono.just(user);
}
// 更新用戶
public Mono<User> updateUser(Long id, User user) {
if (!USER_MAP.containsKey(id)) {
return Mono.error(new RuntimeException("用戶不存在"));
}
user.setId(id);
USER_MAP.put(id, user);
return Mono.just(user);
}
// 刪除用戶
public Mono<Void> deleteUser(Long id) {
USER_MAP.remove(id);
return Mono.empty(); // 無返回值,用Mono<Void>表示
}
}4.1.3 控制器編寫
import io.projectreactor.core.publisher.Flux;
import io.projectreactor.core.publisher.Mono;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
@RestController
@RequestMapping("/users")
public class UserController {
@Autowired
private UserService userService;
// 查詢所有用戶
@GetMapping
public Flux<User> findAll() {
return userService.findAll();
}
// 根據(jù)ID查詢用戶
@GetMapping("/{id}")
public Mono<User> findById(@PathVariable Long id) {
return userService.findById(id);
}
// 添加用戶
@PostMapping
public Mono<User> addUser(@RequestBody Mono<User> userMono) {
// 接收請求體為Mono<User>,通過flatMap調(diào)用服務(wù)方法
return userMono.flatMap(userService::addUser);
}
// 更新用戶
@PutMapping("/{id}")
public Mono<User> updateUser(@PathVariable Long id, @RequestBody Mono<User> userMono) {
return userMono.flatMap(user -> userService.updateUser(id, user));
}
// 刪除用戶
@DeleteMapping("/{id}")
public Mono<Void> deleteUser(@PathVariable Long id) {
return userService.deleteUser(id);
}
}4.1.4 接口測試
啟動項目后,使用Postman或curl測試接口:
- 查詢所有用戶:GET http://localhost:8080/users → 返回所有用戶的JSON數(shù)組
- 根據(jù)ID查詢:GET http://localhost:8080/users/1 → 返回張三的信息
- 添加用戶:POST http://localhost:8080/users,請求體{“name”:“趙六”,“age”:35} → 返回添加后的用戶信息
- 更新用戶:PUT http://localhost:8080/users/4,請求體{“name”:“趙六更新”,“age”:36} → 返回更新后的用戶信息
- 刪除用戶:DELETE http://localhost:8080/users/4 → 無返回值,狀態(tài)碼200
4.2 基于函數(shù)式的編程模型
Spring WebFlux的函數(shù)式模型基于RouterFunction和HandlerFunction,更符合響應(yīng)式編程的函數(shù)式風(fēng)格,適用于簡單的API場景。
4.2.1 編寫處理器(Handler)
處理器負(fù)責(zé)處理具體的請求邏輯,類似控制器中的方法:
import io.projectreactor.core.publisher.Flux;
import io.projectreactor.core.publisher.Mono;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.MediaType;
import org.springframework.stereotype.Component;
import org.springframework.web.reactive.function.server.ServerRequest;
import org.springframework.web.reactive.function.server.ServerResponse;
import static org.springframework.web.reactive.function.BodyInserters.fromValue;
@Component
public class UserHandler {
@Autowired
private UserService userService;
// 查詢所有用戶
public Mono<ServerResponse> findAll(ServerRequest request) {
Flux<User> userFlux = userService.findAll();
// 返回JSON數(shù)組,媒體類型為APPLICATION_JSON
return ServerResponse.ok()
.contentType(MediaType.APPLICATION_JSON)
.body(userFlux, User.class);
}
// 根據(jù)ID查詢用戶
public Mono<ServerResponse> findById(ServerRequest request) {
Long id = Long.valueOf(request.pathVariable("id"));
Mono<User> userMono = userService.findById(id);
// 如果用戶存在,返回200;不存在,返回404
return userMono.flatMap(user -> ServerResponse.ok()
.contentType(MediaType.APPLICATION_JSON)
.body(fromValue(user)))
.switchIfEmpty(ServerResponse.notFound().build());
}
// 添加用戶
public Mono<ServerResponse> addUser(ServerRequest request) {
Mono<User> userMono = request.bodyToMono(User.class);
return userMono.flatMap(user -> ServerResponse.ok()
.contentType(MediaType.APPLICATION_JSON)
.body(userService.addUser(user), User.class));
}
}4.2.2 編寫路由配置(Router)
路由配置用于將請求路徑映射到對應(yīng)的處理器方法:
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.reactive.function.server.RouterFunction;
import org.springframework.web.reactive.function.server.RouterFunctions;
import org.springframework.web.reactive.function.server.ServerResponse;
import static org.springframework.web.reactive.function.server.RequestPredicates.*;
@Configuration
public class UserRouter {
@Bean
public RouterFunction<ServerResponse> userRoutes(UserHandler userHandler) {
return RouterFunctions
.route(GET("/func/users"), userHandler::findAll)
.andRoute(GET("/func/users/{id}"), userHandler::findById)
.andRoute(POST("/func/users"), userHandler::addUser);
}
}4.2.3 測試函數(shù)式接口
啟動項目后,測試函數(shù)式接口:
- 查詢所有用戶:GET http://localhost:8080/func/users
- 根據(jù)ID查詢:GET http://localhost:8080/func/users/1
- 添加用戶:POST http://localhost:8080/func/users,請求體{“name”:“孫七”,“age”:40}
五、關(guān)鍵拓展知識點
掌握以下拓展知識點,能幫助你更好地應(yīng)對實際開發(fā)中的問題。
5.1 背壓機(jī)制詳解
背壓是響應(yīng)式編程的核心特性之一,用于解決“生產(chǎn)者速度大于消費者處理速度”的問題。Reactor通過Subscription接口實現(xiàn)背壓:消費者通過request(n)方法告知生產(chǎn)者自己最多能處理n個元素,生產(chǎn)者根據(jù)這個需求發(fā)送數(shù)據(jù)。
import io.projectreactor.core.publisher.Flux;
import org.junit.Test;
public class BackpressureTest {
@Test
public void testBackpressure() {
Flux.range(1, 100) // 生產(chǎn)者生成1到100的數(shù)字
.subscribe(
num -> {
System.out.println("處理元素:" + num);
try {
Thread.sleep(100); // 模擬消費者處理耗時
} catch (InterruptedException e) {
e.printStackTrace();
}
},
error -> System.err.println("錯誤:" + error),
() -> System.out.println("處理完成"),
subscription -> subscription.request(2) // 初始請求2個元素
);
try {
Thread.sleep(10000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}上述代碼中,消費者初始請求2個元素,處理完后會自動請求后續(xù)元素(Reactor的默認(rèn)行為),避免了數(shù)據(jù)堆積。
5.2 異常處理策略
響應(yīng)式數(shù)據(jù)流中的異常會終止數(shù)據(jù)流,Reactor提供了多種異常處理操作符:
- onErrorReturn:當(dāng)發(fā)生異常時,返回一個默認(rèn)值;
- onErrorResume:當(dāng)發(fā)生異常時,切換到一個新的數(shù)據(jù)流;
- retry:當(dāng)發(fā)生異常時,重試指定次數(shù)。
import io.projectreactor.core.publisher.Mono;
import org.junit.Test;
public class ErrorHandleTest {
@Test
public void testOnErrorReturn() {
Mono.error(new RuntimeException("測試異常"))
.onErrorReturn("默認(rèn)值") // 發(fā)生異常時返回默認(rèn)值
.subscribe(System.out::println); // 輸出:默認(rèn)值
}
@Test
public void testOnErrorResume() {
Mono.error(new RuntimeException("測試異常"))
.onErrorResume(error -> Mono.just("從異常中恢復(fù)")) // 切換到新的Mono
.subscribe(System.out::println); // 輸出:從異常中恢復(fù)
}
@Test
public void testRetry() {
Mono<Integer> mono = Mono.defer(() -> {
System.out.println("執(zhí)行方法");
return Math.random() > 0.5 ? Mono.just(1) : Mono.error(new RuntimeException("隨機(jī)異常"));
});
mono.retry(2) // 最多重試2次
.subscribe(
num -> System.out.println("成功:" + num),
error -> System.err.println("最終失?。? + error)
);
}
}5.3 Spring WebFlux與Spring MVC的對比
| 特性 | Spring MVC | Spring WebFlux |
|---|---|---|
| 編程模型 | 基于注解(@Controller等) | 注解模型 + 函數(shù)式模型 |
| IO模型 | 同步阻塞IO | 異步非阻塞IO |
| 底層依賴 | Servlet API | Reactor + Netty(默認(rèn))/Servlet 3.1+ |
| 適用場景 | 普通Web應(yīng)用,CPU密集型場景 | 高并發(fā)、高IO等待場景(如API網(wǎng)關(guān)、消息處理) |
| 數(shù)據(jù)訪問 | JPA、MyBatis(同步) | R2DBC(響應(yīng)式關(guān)系型數(shù)據(jù)庫)、MongoDB Reactive等 |
六、總結(jié)
本文從響應(yīng)式編程的核心概念出發(fā),詳細(xì)講解了Reactor庫的Mono與Flux操作,以及Spring WebFlux的兩種編程模型(注解式和函數(shù)式),并通過完整的實戰(zhàn)示例幫助你快速上手。同時,我們還拓展了背壓機(jī)制、異常處理等關(guān)鍵知識點,以及Spring WebFlux與Spring MVC的區(qū)別。
響應(yīng)式編程的核心價值在于提升高并發(fā)場景下的系統(tǒng)吞吐量和資源利用率,但它也帶來了一定的學(xué)習(xí)成本,需要轉(zhuǎn)變傳統(tǒng)的同步編程思維。建議在實際項目中,根據(jù)業(yè)務(wù)場景選擇合適的技術(shù)框架:如果是高并發(fā)、高IO等待的場景,Spring WebFlux是不錯的選擇;如果是普通Web應(yīng)用,Spring MVC依然是更成熟、更易維護(hù)的方案。
后續(xù)可以進(jìn)一步學(xué)習(xí)響應(yīng)式數(shù)據(jù)訪問(如R2DBC、MongoDB Reactive)、Spring Cloud Gateway(基于WebFlux的API網(wǎng)關(guān))等內(nèi)容,深入挖掘響應(yīng)式編程的潛力。
到此這篇關(guān)于Java 響應(yīng)式編程之Spring WebFlux+Reactor 實戰(zhàn)攻略的文章就介紹到這了,更多相關(guān)Spring WebFlux Reactor 實戰(zhàn)內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
java編程創(chuàng)建型設(shè)計模式工廠方法模式示例詳解
這篇文章主要為大家介紹了java編程創(chuàng)建型設(shè)計模式之工廠方法模式的創(chuàng)建及案例詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助2022-02-02
Springboot整合camunda+mysql的集成流程分析
本文介紹基于mysql數(shù)據(jù)庫,如何實現(xiàn)camunda與springboot的集成,如何實現(xiàn)基于springboot運行camunda開源流程引擎,本文分步驟圖文相結(jié)合給大家介紹的非常詳細(xì),需要的朋友參考下吧2021-06-06
mybatis-plus的分頁攔截器(PaginationInterceptor)分頁失敗問題及解決
這篇文章主要介紹了mybatis-plus的分頁攔截器(PaginationInterceptor)分頁失敗問題及解決,具有很好的參考價值,希望對大家有所幫助,如有錯誤或未考慮完全的地方,望不吝賜教2025-05-05
使用Spring Boot Maven插件的詳細(xì)方法
這篇文章主要介紹了如何使用Spring Boot Maven插件,本文給大家介紹的非常詳細(xì),對大家的學(xué)習(xí)或工作具有一定的參考借鑒價值,需要的朋友可以參考下2023-05-05
Java實戰(zhàn)之超市收銀管理系統(tǒng)的實現(xiàn)
這篇文章主要介紹了如何利用Java實現(xiàn)超市收銀管理系統(tǒng),文中采用的技術(shù)有:Spring、SpringMVC、MyBatis、ThymeLeaf等,需要的可以參考一下2022-03-03
maven創(chuàng)建spark項目的pom.xml文件配置demo
這篇文章主要為大家介紹了maven創(chuàng)建spark項目的pom.xml文件配置demo,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2023-05-05
Spring Validation數(shù)據(jù)校驗詳解
SpringValidation是Spring框架的數(shù)據(jù)校驗抽象層,支持注解、分組、嵌套校驗及國際化,默認(rèn)集成HibernateValidator,可自定義校驗器滿足復(fù)雜需求,適用于Web參數(shù)校驗和業(yè)務(wù)邏輯驗證2025-09-09

