SpringBoot11-Spring Validation讲解
一、为什么要使用校验(Validation)
输入可靠性:在请求一进系统边界(Controller)就拦截非法数据,避免脏数据深入业务层。
安全与合规:隐藏/拒绝敏感或越权字段,减少注入与越权风险。
开发效率:声明式注解代替大量 if-else 判空与格式判断。
一致的错误返回:统一错误格式,前后端对齐更顺滑。
可测试可维护:规则集中在 DTO/注解上,变更时定位清晰。
下面给你两个真实场景,能直观感受到为什么需要 Validation:
场景 1:用户注册接口
如果没有校验:
@PostMapping("/register")
public UserVO register(@RequestBody UserDTO dto) {// dto.username 可能是空字符串// dto.email 可能是 "aaa"// dto.password 可能是 null// 业务逻辑里必须写一堆 if 判断,否则脏数据会直接进数据库
}
可能出现:
用户名为空或长度为 1;
邮箱不是有效格式;
密码为空或太短;
数据存进数据库后出错或以后出 bug。
用 Spring Validation:
public class UserDTO {@NotBlank(message = "用户名不能为空")@Size(min = 3, max = 20)private String username;@NotBlank @Emailprivate String email;@NotBlank @Size(min = 8)private String password;
}@PostMapping("/register")
public UserVO register(@Valid @RequestBody UserDTO dto) { ... }
当用户提交:
{ "username": "", "email": "abc", "password": "123" }
直接被框架拦截,返回:
{"code": "VALIDATION_ERROR","errors": [{"field": "username", "message": "用户名不能为空"},{"field": "email", "message": "必须是一个有效的电子邮件地址"},{"field": "password", "message": "长度必须至少为8"}]
}
开发者无需写大量 if-else,接口统一返回错误,数据库不会出现垃圾数据。
场景 2:修改用户资料(部分更新)
创建新用户时:
username
必填。修改资料时:
id
必填,但username
可以不改。
用 分组校验:
public interface Create {}
public interface Update {}public class UserUpsertDTO {@NotNull(groups = Update.class) // 更新必须提供idprivate Long id;@NotBlank(groups = Create.class)private String username;@Email private String email;
}@PostMapping
public void create(@Validated(Create.class) @RequestBody UserUpsertDTO dto) { }@PutMapping
public void update(@Validated(Update.class) @RequestBody UserUpsertDTO dto) { }
调用创建接口缺少
username
会报错;调用更新接口缺少
id
会报错;规则可复用,避免在代码里写大量条件判断。
💡总结
没有 Validation:需要手动写 if 判断,代码冗余、易漏、难维护。
有 Validation:只需在字段上加注解 + 全局异常处理,就能在请求入口自动拦截不合法数据,统一返回错误信息。
对开发效率、数据安全和系统健壮性都有明显提升。
不要相信前端传来的参数是正确的,一定要做后台的表单校验!
二、开发步骤
2-1、添加依赖
<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-validation</artifactId>
</dependency>
2-2、编写请求 DTO(带注解)
// Boot 3.x
import jakarta.validation.constraints.*;
import java.time.LocalDate;
import java.util.List;public class UserCreateDTO {@NotBlank(message = "用户名不能为空")@Size(min = 3, max = 20, message = "用户名长度应在 {min}-{max} 之间")private String username;@Email(message = "邮箱格式不正确")@NotBlank(message = "邮箱不能为空")private String email;@NotBlank@Size(min = 8, message = "密码至少 {min} 位")private String password;@Past(message = "生日必须是过去的日期")private LocalDate birthday;@Valid // 级联到嵌套对象@NotNullprivate AddressDTO address;@Valid // 校验集合中每个元素private List<@NotBlank(message = "标签不可为空字符串") String> tags;// getters/setters...
}class AddressDTO {@NotBlank private String city;@NotBlank private String street;
}
【注意】:
@NotBlank
仅适用于字符串;@NotEmpty
适用于集合/数组/字符串;@NotNull
只校验非 null。基本类型(如
int
)不能为 null;要可空就用包装类型(如Integer
)。
2-3、在 Controller 中启用校验
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
import jakarta.validation.Valid;
import jakarta.validation.constraints.Positive;@RestController
@RequestMapping("/users")
@Validated // 启用方法级与参数级校验(如 PathVariable/RequestParam)
public class UserController {@PostMappingpublic UserVO create(@Valid @RequestBody UserCreateDTO dto) {// dto 已按注解校验,失败会抛 MethodArgumentNotValidExceptionreturn userService.create(dto);}@GetMapping("/{id}")public UserVO getById(@Positive @PathVariable Long id) {// 对路径变量做约束(需类上有 @Validated)return userService.getById(id);}
}
三、@Valid
vs @Validated
@Valid
:标准注解,级联/嵌套对象、请求体校验首选。@Validated
:Spring 扩展,启用分组与方法级(参数/返回值)校验;通常加在 类级 或 方法参数 上。
可以把 @Valid
和 @Validated
理解成两个“触发校验的开关”,但它们的能力范围不一样。
这里说的 “触发校验的开关”,指的是:
Spring 本身并不会自动去检查你写在 Bean 上的 @NotNull @Size …
这些注解;
只有在某个地方加上 @Valid
或 @Validated
时,Spring 才会调用底层的 Bean Validation 引擎(Hibernate Validator)去做校验。这就是“触发器”或者“开关”的意思。
所以,底层实现:
Spring 本身不做校验逻辑,它只是提供集成支持,真正做校验的是 Hibernate Validator(这是 JSR 380 的一个官方实现)。
注解 | 来自 | 主要用途 |
---|---|---|
@Valid | JSR-303 / Bean Validation 标准(jakarta.validation.Valid 或 javax.validation.Valid ) | 触发 标准 Bean 校验,支持 级联校验(嵌套对象)。 |
@Validated | Spring 自家扩展(org.springframework.validation.annotation.Validated ) | 除了触发 Bean 校验,还支持分组校验和方法级参数/返回值校验。 |
在 Spring Controller 中进行输入校验
通常我们在 Spring 中实现 REST Controller 时,需要验证客户端传入的输入数据。我们可以验证的内容主要有三类:
请求体(request body)
路径变量(path variables)
查询参数(query parameters)--URL 上的查询参数
比如一个用户注册接口,你希望:
请求体里的 JSON 数据(request body)必须符合要求;
路径里的变量(path variables)不能乱填;
URL 上的查询参数(query parameters)也要合法。
常见的 3 种输入校验位置:
Request Body —— 客户端发来的 JSON 或表单数据,比如注册时的用户信息。
Path Variable —— URL 里的动态参数,比如
/users/{id}
中的id
。Query Parameter —— URL 问号后面的参数,比如
?page=1&size=10
。
示例1:请求体校验——嵌套对象
In the example, each user has an id, name, and address.
Name cannot be an empty string nor null and address cannot be null. Address class has street and city that cannot be empty nor null and postcode with size between 4 and 6 characters.
注解 允许 null 允许 "" 空串 允许只包含空格 @NotNull
❌ 不允许 ✅ 允许 ✅ 允许 @NotEmpty
❌ 不允许 ❌ 不允许 ✅ 允许 @NotBlank
❌ 不允许 ❌ 不允许 ❌ 不允许(会 trim)
编写controller:
postman发送请求:
校验通过,明显嵌套的Address的校验没有生效!
解决方式:@Valid嵌套校验
此时,postman原来的请求会报错:
示例2:请求参数(Request Parameters)和路径变量(Path Variables)的校验
对请求参数和路径变量的校验方式与之前校验 Java 对象不同。因为路径变量和请求参数通常是原始类型(primitive types),而不是 Java Bean。
因此,我们不能像以前那样在类的字段上加注解,而是需要直接在 Controller 方法的参数上添加约束注解。
例如:在路径变量 postcode
上使用 @Size
注解限制字符串长度。
此外,我们需要在 Controller 类上添加 @Validated
注解,这样 Spring 才能在方法参数上识别并执行这些校验注解。
时候我们不是接收一个对象,而是通过 URL 路径变量 或 请求参数来接值,比如:
GET /users/123?postcode=510000
这里的 123
和 510000
是基本类型(如 String
、int
),不是 Java Bean。
如果你想对这些值做校验,比如要求 postcode 长度必须是 6 位,就不能在类字段上加注解了,只能直接在方法参数上加:
⚠️ 关键点:
方法参数也能加校验注解(如
@Size
、@NotBlank
等),但要在类上加@Validated
才会生效。如果不加
@Validated
,这些注解不会触发校验,也不会自动抛出异常。方法参数校验常用于:
路径变量
@PathVariable
查询参数
@RequestParam
甚至方法里的普通参数
总结:
对象校验:
@Valid
+ Bean 字段注解(常用于@RequestBody
)。基本类型参数校验:注解加在方法参数上 + 在类上加
@Validated
。没有 @Validated → 校验不会触发。
如果我们传入的 路径变量(Path Variable)长度比允许的短,校验会失败,并抛出
ConstraintViolationException
,而不是像校验请求体(request body)时那样抛出MethodArgumentNotValidException
。Spring 对
ConstraintViolationException
没有默认的异常处理器,所以默认会返回 500 Internal Server Error。
如果我们想要像以前一样返回 400 Bad Request,保持返回格式的统一,就需要自己编写自定义异常处理器。
对于 请求参数(Request Parameters) 的校验,工作原理和路径变量是一样的。
示例3:验证分组(Validation Groups)
当我们希望在不同的场景或不同的目的下触发校验,但又想复用同一个被校验的对象时,可以使用 校验分组(Validation Groups)。
如果查看
@NotEmpty
注解的源码,你会发现它(和其它任何校验注解一样)都有一个名为groups
的属性。
有时候,我们有一个同样的 Java Bean,但在不同的场景下,对字段的校验规则不一样。例如:
假设我们有一个用户类
public class User {@NotEmptyprivate String username;@NotEmptyprivate String password;@NotEmptyprivate String email;
}
场景1:注册
需要校验username
、password
、email
都不能为空。场景2:登录
只需要校验username
和password
,email
不需要。
如果不用分组,通常我们要写两个不同的类,很麻烦。
有了 Validation Groups,就可以在一个类里定义不同场景的分组。
🏷️ 定义分组接口
public interface RegisterGroup {}
public interface LoginGroup {}
🏷️ 在 Bean 字段上指定分组
public class User {@NotEmpty(groups = {RegisterGroup.class, LoginGroup.class})private String username;@NotEmpty(groups = {RegisterGroup.class, LoginGroup.class})private String password;@NotEmpty(groups = RegisterGroup.class) // 注册时必填,但登录不需要private String email;
}
🏷️ 在 Controller 中指定分组
@RestController
public class UserController {// 注册时使用 RegisterGroup@PostMapping("/register")public void register(@Validated(RegisterGroup.class) @RequestBody User user) {// 注册逻辑}// 登录时使用 LoginGroup@PostMapping("/login")public void login(@Validated(LoginGroup.class) @RequestBody User user) {// 登录逻辑}
}
这样:
调
/register
接口时,三个字段都要校验;调
/login
接口时,只校验username
和password
,不会校验email
。
🔹关键点总结
groups 是每个校验注解自带的属性,默认是
Default.class
。定义分组接口(只是个空接口,用来做标记)。
在字段上用
groups = {...}
指定属于哪些分组。在 Controller 方法上用
@Validated(SomeGroup.class)
指定要触发哪一组校验。如果没有指定分组,默认会用
Default.class
组。
简单理解:
Validation Groups 就是让你用同一个对象,按不同“场景”触发不同的校验规则,避免为了不同接口写很多重复的 Java Bean。
小结:
下面是这段内容的 翻译 + 通俗易懂讲解:
🔹英文原文
Key takeaways
@Valid
comes from Java Validation API
@Validated
comes from Spring Framework Validation, it is a variant of@Valid
with support for validation groups
@Valid
use on method parameters and fields, don’t forget to use it on complex objects if they need to be validated
@Validated
use on methods and method parameters when using validation groups, use on classes to support method parameter constraint validations
🔹中文翻译
重点总结:
@Valid
来自 Java 标准的 Bean Validation API(JSR-303/JSR-380)。@Validated
来自 Spring Framework,它是@Valid
的扩展版本,支持 校验分组(Validation Groups)。@Valid
常用于方法参数和字段上,如果你的方法参数是复杂对象(比如@RequestBody User user
),别忘了加上@Valid
来触发校验。@Validated
常用于方法、方法参数,特别是需要分组校验时使用;也可以加在类上,用于支持方法参数(如 PathVariable、RequestParam)的约束校验。
🔹通俗易懂解释
@Valid
= Java 标准版属于 JSR-303 规范,不依赖 Spring,也能在其他框架用。
主要用来校验对象里的字段,比如接收前端传过来的 JSON。
常见写法:
public void addUser(@Valid @RequestBody User user) { ... }
@Validated
= Spring 增强版Spring 在
@Valid
的基础上增加了 分组校验 和 方法级参数校验 的支持。如果你用到了 Validation Groups,就必须用
@Validated
。如果要校验 PathVariable 或 RequestParam,要在类上加
@Validated
才生效。常见写法:
@RestController @Validated // 支持方法参数校验 public class UserController {@GetMapping("/users/{id}")public void getUser(@PathVariable @Min(1) Long id) { ... }@PostMapping("/register")public void register(@Validated(RegisterGroup.class) @RequestBody User user) { ... } }
简单对比
| 功能 | @Valid | @Validated |
|------|--------|-----------|
| 来源 | JSR-303 标准 | Spring 框架 |
| 校验分组 | ❌ 不支持 | ✅ 支持 |
| 方法参数(PathVariable、RequestParam)校验 | ❌ 不能单独触发 | ✅ 可用 |
| 常用场景 |@RequestBody
Java Bean | 分组校验、路径参数校验 |
⚡ 一句话记忆
只要是复杂对象(RequestBody)用
@Valid
就够了。如果要分组校验或校验方法参数(PathVariable、RequestParam),用
@Validated
。
四、其他常用的校验方式
4-1、返回值的校验
Bean Validation 不只是能校验输入参数,也能用来校验 方法的返回值(Return Value Validation)。
🔹1. 原理与支持
Bean Validation 2.0(JSR-380) 开始支持方法级别约束(Method-level constraints),包括:
参数(Parameters)校验
返回值(Return Value)校验
Spring Boot 已经集成 Hibernate Validator,因此可以直接使用。
🔹2. 如何写返回值校验
🏷️ 示例 1:用在方法返回值上
import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Size;
import org.springframework.validation.annotation.Validated;
import org.springframework.stereotype.Service;@Service
@Validated // ⚠️ 必须加在类上,方法级约束才会生效
public class UserService {@NotNull@Size(min = 2, message = "返回的用户名至少要2个字符")public String getUserName() {return ""; // 这里返回空字符串会触发校验异常}
}
⚠️ 关键点:
@Validated
要加在类上,否则返回值约束不会生效。方法上的约束注解(如
@NotNull
、@Size
)直接标记在返回值类型上。
🏷️ 示例 2:Controller 返回值校验
import jakarta.validation.Valid;
import jakarta.validation.constraints.NotEmpty;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;@RestController
@Validated
public class UserController {@GetMapping("/user")@NotEmpty(message = "返回值不能为空")public String getUser() {return ""; // 会抛出 ConstraintViolationException}
}
当返回值不符合约束条件时,Spring 会抛出
ConstraintViolationException
。
⚡ 总结
可以用 Bean Validation 注解约束返回值,包括 Controller 和 Service。
⚠️ 必须在类上加
@Validated
才会触发。❌ Spring 默认返回 500,需要自定义异常处理器把
ConstraintViolationException
转为 400 或自定义响应格式。
4-2、自定义约束(如强密码、跨字段)
在 Spring(Hibernate Validator)里你可以自定义校验注解,用于字段、方法参数,甚至整个对象的“跨字段校验”。
做法很固定:定义注解 → 写校验器 →(可选)配置消息文件(国家化) → 使用。
一、最常见:字段级自定义注解
下面以“密码强度校验”为例:@StrongPassword
1) 定义注解
package com.example.validation;import jakarta.validation.Constraint;
import jakarta.validation.Payload;
import java.lang.annotation.*;
import static java.lang.annotation.ElementType.*;
import static java.lang.annotation.RetentionPolicy.RUNTIME;@Documented
@Target({ FIELD, PARAMETER }) // 用在字段或方法参数上
@Retention(RUNTIME)
@Constraint(validatedBy = StrongPasswordValidator.class) // 指定校验器
public @interface StrongPassword {String message() default "{password.weak}"; // 支持国际化占位符Class<?>[] groups() default {};Class<? extends Payload>[] payload() default {};// 也可以暴露参数,供校验器读取int min() default 8;boolean requireUppercase() default true;boolean requireNumber() default true;boolean requireSpecial() default true;
}
2) 编写校验器
package com.example.validation;import jakarta.validation.ConstraintValidator;
import jakarta.validation.ConstraintValidatorContext;public class StrongPasswordValidator implements ConstraintValidator<StrongPassword, String> {private int min;private boolean requireUppercase;private boolean requireNumber;private boolean requireSpecial;@Overridepublic void initialize(StrongPassword anno) {// 读取注解上的参数this.min = anno.min();this.requireUppercase = anno.requireUppercase();this.requireNumber = anno.requireNumber();this.requireSpecial = anno.requireSpecial();}@Overridepublic boolean isValid(String value, ConstraintValidatorContext ctx) {if (value == null) return true; // 为空交给 @NotNull 管if (value.length() < min) return false;if (requireUppercase && !value.matches(".*[A-Z].*")) return false;if (requireNumber && !value.matches(".*\\d.*")) return false;if (requireSpecial && !value.matches(".*[^\\w\\s].*")) return false;return true;}
}
在 Spring Boot 中,这个校验器可以直接使用(Boot 默认启用 Hibernate Validator 并能注入 Spring Bean;若你需要在校验器里用服务/DAO,可把校验器
@Component
并直接@Autowired
)。📌 官方建议
自定义约束的校验器一般只处理非空的值;
空值让@NotNull
、@NotEmpty
、@NotBlank
等专门的注解去判断。
3) 国际化消息(可选)
src/main/resources/ValidationMessages.properties
password.weak=密码不够安全:至少{min}位,且包含大写字母、数字和特殊字符
4) 使用
public class RegisterDTO {@NotNull(message = "密码不能为空")@StrongPassword(min = 10, message = "密码至少10位并包含多种字符")private String password;// getters/setters...
}
组合使用:空值交给@NotNull
二、跨字段校验(类级别)
比如“确认密码必须等于密码”:@FieldsMatch(first="password", second="confirmPassword")
1) 注解
@Target({ TYPE })
@Retention(RUNTIME)
@Documented
@Constraint(validatedBy = FieldsMatchValidator.class)
public @interface FieldsMatch {String message() default "{fields.not.match}";Class<?>[] groups() default {};Class<? extends Payload>[] payload() default {};String first();String second();
}
2) 校验器
import jakarta.validation.ConstraintValidator;
import jakarta.validation.ConstraintValidatorContext;
import java.beans.Introspector;
import java.beans.PropertyDescriptor;public class FieldsMatchValidator implements ConstraintValidator<FieldsMatch, Object> {private String first;private String second;@Overridepublic void initialize(FieldsMatch anno) {this.first = anno.first();this.second = anno.second();}@Overridepublic boolean isValid(Object value, ConstraintValidatorContext ctx) {try {Object v1 = readProperty(value, first);Object v2 = readProperty(value, second);return v1 == null ? v2 == null : v1.equals(v2);} catch (Exception e) {// 读取失败按不通过处理,或返回 true 并记录日志视需求return false;}}private Object readProperty(Object bean, String name) throws Exception {for (PropertyDescriptor pd : Introspector.getBeanInfo(bean.getClass()).getPropertyDescriptors()) {if (pd.getName().equals(name)) {return pd.getReadMethod().invoke(bean);}}return null;}
}
3) 使用
@FieldsMatch(first = "password", second = "confirmPassword", message = "两次输入的密码不一致")
public class RegisterDTO {@jakarta.validation.constraints.NotBlankprivate String password;@jakarta.validation.constraints.NotBlankprivate String confirmPassword;
}
三、方法返回值/参数也可用
在 类上标 @Validated
后,方法参数/返回值上的自定义注解同样生效:
@Service
@org.springframework.validation.annotation.Validated
public class UserService {public void updatePassword(@StrongPassword String newPwd) { ... }@StrongPasswordpublic String generateTempPassword() { return "abc"; } // 将会校验返回值
}
四、和校验分组一起用(可选)
自定义注解天然支持 groups
:
public interface CreateGroup {}
public interface UpdateGroup {}public class UserDTO {@StrongPassword(groups = CreateGroup.class)private String password;
}
Controller:
@PostMapping("/users")
public void create(@Validated(CreateGroup.class) @RequestBody UserDTO dto) { ... }
五、组合注解(合成/复合约束)
可以把多个现成约束“打包”为一个注解:
@Documented
@Target({ FIELD, PARAMETER })
@Retention(RUNTIME)
@Constraint(validatedBy = {}) // 没有自定义校验器
@jakarta.validation.ReportAsSingleViolation
@jakarta.validation.constraints.NotBlank
@jakarta.validation.constraints.Email
public @interface NonEmptyEmail {String message() default "邮箱格式不正确";Class<?>[] groups() default {};Class<? extends Payload>[] payload() default {};
}
五、国际化与消息定制
1. 什么是国际化(i18n)与消息定制
国际化 (Internationalization, i18n):
让系统根据不同语言或地区,显示对应的提示信息。
在 Spring + Bean Validation 中,校验失败后显示的默认信息(比如 must not be blank)通常是英文。
如果你希望对用户显示中文、马来语或其他语言,就要做国际化消息定制。消息定制:
你可以为不同的字段和校验规则指定自定义的提示文字,而不是使用 Hibernate Validator 内置的默认消息。
2. 消息配置文件
通常在 src/main/resources
下创建文件:
ValidationMessages.properties // 默认语言
ValidationMessages_zh_CN.properties // 中文
ValidationMessages_en_US.properties // 英文
如果只需要中文,也可以只保留 ValidationMessages.properties
。
例子:
# ---- ValidationMessages.properties ----
userCreateDTO.username.NotBlank=用户名必填
Size.userCreateDTO.username=用户名长度应在 {min}-{max} 之间
含义:
userCreateDTO.username.NotBlank
对应
UserCreateDTO
类中字段username
上的@NotBlank
注解。当这个字段校验失败(为空或空白字符)时,显示
"用户名必填"
。
Size.userCreateDTO.username
对应同一字段上
@Size(min=, max=)
注解。当长度不符合
min
和max
时,显示"用户名长度应在 {min}-{max} 之间"
。{min}
、{max}
是注解里的参数占位符,会被实际数值替换。
3. 规则:
{注解名}.{类名(首字母小写)}.{字段名}
比如:NotBlank.userCreateDTO.username
或者:Size.userCreateDTO.username
对应的 Java Bean
public class UserCreateDTO {@NotBlank@Size(min = 3, max = 20)private String username;
}
对应的 Controller
@RestController
public class UserController {@PostMapping("/user")public void createUser(@Valid @RequestBody UserCreateDTO dto) {// 如果 username 为空或长度不符,会自动返回上面定义的中文提示}
}
4. 默认与覆盖
如果只写
@NotBlank(message="用户名不能为空")
,直接在注解上指定信息,会覆盖国际化配置。如果注解上不写
message
,就会去 ValidationMessages.properties 找:精确匹配:
NotBlank.userCreateDTO.username
如果没有精确匹配,退回到通用的
NotBlank.message
(框架默认的英文)
5. 国际化切换
如果项目中启用了 Spring MVC 的国际化支持(LocaleResolver
),
当请求头里带
Accept-Language: zh-CN
→ 使用ValidationMessages_zh_CN.properties
当带
Accept-Language: en-US
→ 使用ValidationMessages_en_US.properties
总结
功能 | 说明 |
---|---|
自定义消息 | 在注解上直接 message = "xxx" 或在 ValidationMessages.properties 定义 |
国际化 | 用不同语言的 ValidationMessages_xx.properties 文件 |
占位符 | {min} 、{max} 、{value} 等会被注解参数自动替换 |
查找顺序 | 注解名.类名.字段名 → 注解名.message (通用默认) |
六、快速对照表
@NotNull
:值必须有;不检查空串/空集合。@NotEmpty
:非 null 且 size>0(字符串/集合/数组)。@NotBlank
:非 null 且去空白后长度>0(仅字符串)。@Size(min,max)
:长度/大小范围(字符串/集合/数组)。数值:
@Min/@Max/@Positive/@Negative/@Digits/@DecimalMin/@DecimalMax
。时间:
@Past/@Future/...
(基于系统时钟)。格式:
@Email/@Pattern 内部编写正则校验
。逻辑:
@AssertTrue/@AssertFalse
(多用于类内衍生校验)。
七、全局异常处理器
1. 为什么需要全局异常处理
在 Web 项目中,请求处理可能会抛出各种异常,例如:
参数校验不通过 (
MethodArgumentNotValidException
)数据库访问错误 (
DataAccessException
)自定义业务异常(如
BusinessException
)运行时错误(
NullPointerException
、IllegalArgumentException
)
如果不统一处理,这些异常会直接冒泡到前端,可能返回堆栈信息(安全风险)或不友好的 500 错误页面。
全局异常处理器可以统一拦截异常,返回结构化的响应(JSON),提高用户体验和代码维护性。
2. Spring Boot 提供的核心机制
Spring Boot 使用 Spring MVC 的异常处理机制:
@ControllerAdvice
表示一个全局的控制器增强类,可以拦截项目中所有被
@Controller
/@RestController
注解的类。可处理异常、数据绑定、全局数据预处理等。
@ExceptionHandler
标注在方法上,声明该方法处理指定类型的异常。
可以放在单个 Controller 中(只处理该类异常),也可以放在
@ControllerAdvice
类中(全局处理)。
返回值处理
如果是
@RestControllerAdvice
(等同于@ControllerAdvice + @ResponseBody
),默认返回 JSON。如果是
@ControllerAdvice
,则返回视图。
3、代码实现
当然也可以写的详细一点,不同的异常,不同的处理:
@RestControllerAdvice
public class GlobalExceptionHandler {@ExceptionHandler(MethodArgumentNotValidException.class)public ApiResponse<Void> handleMethodArgNotValid(MethodArgumentNotValidException ex) {return ApiResponse.error(400, ex.getBindingResult().getFieldError().getDefaultMessage());}@ExceptionHandler(BindException.class)public ApiResponse<Void> handleBindException(BindException ex) {return ApiResponse.error(400, ex.getBindingResult().getFieldError().getDefaultMessage());}@ExceptionHandler(ConstraintViolationException.class)public ApiResponse<Void> handleConstraintViolation(ConstraintViolationException ex) {return ApiResponse.error(400, ex.getMessage());}@ExceptionHandler(Exception.class)public ApiResponse<Void> handleOther(Exception ex) {return ApiResponse.error(500, "系统异常");}
}
返回格式:
八、自定义异常(回顾)
8-1、常见的异常处理方法
如果我们不自己定义业务异常类,Spring Boot 里常用的异常处理方式主要有以下几类。它们基本可以满足“异常→统一返回”的需求,只是可扩展性和可读性不如自定义异常。
1、直接使用 JDK 或 Spring 自带异常+ 全局异常处理器
最简单的做法是 抛出通用异常(如 IllegalArgumentException
、IllegalStateException
、RuntimeException
):
然后在全局异常处理器里捕获:
优点
简单,直接用现成异常类;
没有额外类的维护成本。
缺点
错误码不统一;
异常类型难以表达具体业务含义(
IllegalArgumentException
既可以是“用户名存在”,也可能是“年龄无效”);项目大了难以管理。
2、使用 Spring MVC 自带异常
Spring 本身已经内置了一些异常类型,可在全局异常处理器里直接处理,例如:
异常类 | 场景 |
---|---|
MethodArgumentNotValidException | @RequestBody 参数校验失败 |
BindException | 表单/Query 参数绑定失败 |
MissingServletRequestParameterException | 缺少请求参数 |
HttpRequestMethodNotSupportedException | 请求方法不支持(GET 调 POST 接口) |
HttpMediaTypeNotSupportedException | Content-Type 不支持 |
HttpMessageNotReadableException | 请求体 JSON 解析错误 |
AccessDeniedException | Spring Security 鉴权失败 |
3、Controller 层直接 try...catch
优点
局部处理,灵活度高;
小项目或临时需求时可以快速实现。
缺点
每个接口都要写重复代码;
无法全局统一管理,维护成本高。
8-2、自定义异常
1. 为什么要自定义异常
在实际开发中,系统异常类型很多,直接抛出 RuntimeException
/ Exception
太混乱:
无法区分业务错误(如“用户名已存在”)和系统错误(如空指针、数据库连接失败)。
无法返回明确的错误码和友好的提示信息。
无法统一日志记录与前端响应格式。
通过 自定义异常,我们可以:
封装 业务状态码、错误信息、可选的附加数据。
结合全局异常处理器,输出统一的 JSON 格式响应。
提高可维护性和可读性。
其中最常见的确是 自定义 code 和 message
2、编写规则
/*** 业务异常:用于表示可预期的业务逻辑错误*/
public class BusinessException extends RuntimeException {private int code; // 错误码public BusinessException(int code, String message) {super(message);this.code = code;}public int getCode() {return code;}
}
继承
RuntimeException
(Spring 事务回滚默认只对 RuntimeException 生效)。增加 错误码字段,便于前端识别。
示例:
全局异常处理器中处理自定义异常:
使用:
测试: