SpringBoot中注解参数校验的实战指南
作者:悟空码字
这篇文章主要为大家详细介绍了SpringBoot中注解参数校验的相关知识,文中的示例代码讲解详细,具有一定的借鉴价值,感兴趣的小伙伴可以了解下
一、参数校验:程序员的“防杠精神器”
假如你的API像个热情的饭店服务员,用户说“随便来点吃的”,你就真给他上了盘空气——这可不妙!参数校验就像是那个会耐心问“要辣的还是不辣的?要牛肉还是鸡肉?”的细心服务员,确保不闹出“我要咖啡你却给我上了杯洗脚水”的尴尬。
SpringBoot的注解校验就像给你的方法参数请了个私人保镖,专门拦截那些不靠谱的输入。没有它?用户传个null过来,你的程序可能就会表演“当场崩溃”的绝活。
二、详细步骤:给代码戴上“紧箍咒”
第1步:先来点“开胃菜”——添加依赖
<!-- pom.xml里加入这个,就像泡面加卤蛋,标配! -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
第2步:创建个“相亲简历”DTO类
import javax.validation.constraints.*;
import java.util.Date;
import java.util.List;
/**
* 用户注册DTO - 比相亲网站的个人资料要求还严格
*/
public class UserRegisterDTO {
@NotBlank(message = "用户名不能为空,难道您是无名氏?")
@Size(min = 2, max = 20, message = "用户名长度在2-20之间,太短没存在感,太长记不住")
private String username;
@Email(message = "邮箱格式不对,这可不是在写情书,随便写写就行")
private String email;
@Pattern(regexp = "^(?=.*[A-Za-z])(?=.*\\d)[A-Za-z\\d]{8,}$",
message = "密码至少8位,包含字母和数字,别再用123456了!")
private String password;
@Min(value = 18, message = "未满18岁?小朋友先去写作业")
@Max(value = 120, message = "超过120岁?您是老神仙吧")
private Integer age;
@NotNull(message = "手机号必须填,不然外卖到了找谁?")
private String phone;
@AssertTrue(message = "必须接受协议,虽然可能没人看")
private Boolean acceptedAgreement;
@Future(message = "预约时间必须是未来,时光机还没发明呢")
private Date appointmentTime;
@Size(min = 1, max = 3, message = "最多选3个爱好,您是想成为全能超人吗?")
private List<String> hobbies;
// 此处省略getter和setter,但它们确实存在,我发誓!
// 用Lombok的@Data也行,但今天咱们保持纯洁的Java关系
// 自定义校验注解示例
@ValidGender
private String gender;
}
第3步:自定义校验注解——打造专属“安检仪”
import javax.validation.Constraint;
import javax.validation.Payload;
import java.lang.annotation.*;
/**
* 性别校验注解 - 咱们思想很开放,但数据要规范
*/
@Target({ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
@Constraint(validatedBy = GenderValidator.class)
public @interface ValidGender {
String message() default "性别必须是男、女或保密,您这是来自火星吗?";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
}
/**
* 性别校验器 - 严肃的判官
*/
public class GenderValidator implements ConstraintValidator<ValidGender, String> {
private static final Set<String> VALID_GENDERS =
new HashSet<>(Arrays.asList("男", "女", "保密"));
@Override
public boolean isValid(String value, ConstraintValidatorContext context) {
if (value == null) {
return true; // 用@NotNull管非空,咱们只管格式
}
return VALID_GENDERS.contains(value);
}
}
第4步:控制器里使用——给API装上“安检门”
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
import javax.validation.Valid;
import javax.validation.constraints.NotBlank;
@RestController
@RequestMapping("/api/users")
@Validated // 这个注解让方法参数校验生效,就像给方法吃了“严格丸”
public class UserController {
/**
* 注册用户 - 参数校验比丈母娘挑女婿还严格
*/
@PostMapping("/register")
public Result register(@RequestBody @Valid UserRegisterDTO userDTO) {
// 如果参数校验失败,根本走不到这里
// 就像考试不及格,进不了下一轮面试
return Result.success("注册成功,恭喜通过严格审查!");
}
/**
* 方法参数校验 - 连路径变量都不放过
*/
@GetMapping("/{id}")
public Result getUser(
@PathVariable @Min(value = 1, message = "ID必须大于0,您这是要找空气用户吗?") Long id,
@RequestParam @NotBlank(message = "令牌不能为空,您这是想蒙混过关?") String token) {
return Result.success("找到了用户ID: " + id);
}
/**
* 分组校验 - 根据不同场景使用不同规则
* 就像上班穿正装,在家穿睡衣,场合要分清
*/
@PostMapping("/update")
public Result updateUser(@RequestBody @Validated(UserUpdateGroup.class) UserUpdateDTO dto) {
return Result.success("更新成功");
}
}
// 分组接口定义
interface UserUpdateGroup {}
interface UserCreateGroup {}
第5步:全局异常处理——优雅的“救火队员”
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import org.springframework.validation.FieldError;
/**
* 全局异常处理器 - 专业收拾校验失败的烂摊子
*/
@RestControllerAdvice
public class GlobalExceptionHandler {
/**
* 处理参数校验异常 - 把技术语言翻译成人话
*/
@ExceptionHandler(MethodArgumentNotValidException.class)
public Result handleValidationException(MethodArgumentNotValidException ex) {
// 收集所有错误信息,就像收集考试错题
Map<String, String> errors = new HashMap<>();
ex.getBindingResult().getAllErrors().forEach(error -> {
String fieldName = ((FieldError) error).getField();
String errorMessage = error.getDefaultMessage();
errors.put(fieldName, errorMessage);
});
return Result.error(400, "参数校验失败", errors)
.setMessage("您提交的数据有点小问题,请检查后再试哦~");
}
/**
* 处理ConstraintViolationException - 方法参数校验失败
*/
@ExceptionHandler(ConstraintViolationException.class)
public Result handleConstraintViolationException(ConstraintViolationException ex) {
List<String> errors = ex.getConstraintViolations().stream()
.map(violation -> violation.getMessage())
.collect(Collectors.toList());
return Result.error(400, "参数不合法", errors);
}
}
/**
* 统一返回结果 - 给前端一个标准的“成绩单”
*/
@Data
@AllArgsConstructor
@NoArgsConstructor
public class Result<T> {
private Integer code;
private String message;
private T data;
private Long timestamp = System.currentTimeMillis();
public static <T> Result<T> success(T data) {
return new Result<>(200, "成功", data);
}
public static <T> Result<T> error(Integer code, String message, T data) {
return new Result<>(code, message, data);
}
public Result<T> setMessage(String message) {
this.message = message;
return this;
}
}
第6步:进阶玩法——嵌套校验和集合校验
/**
* 订单DTO - 俄罗斯套娃式的校验
*/
public class OrderDTO {
@NotNull(message = "订单信息不能为空")
@Valid // 这个注解让嵌套校验生效,就像班主任检查每个学生的作业
private UserDTO user;
@Valid // 集合也要逐个校验,一个都别想逃
private List<@Valid OrderItemDTO> items;
@Valid
private AddressDTO address;
}
/**
* 地址DTO - 精确到门牌号
*/
public class AddressDTO {
@NotBlank(message = "省份不能空,您这是要寄到外太空?")
private String province;
@NotBlank(message = "城市不能空")
private String city;
@Size(min = 5, max = 100, message = "详细地址5-100字,说清楚点,快递员会感谢您")
private String detail;
}
三、测试一下:看看“保镖”工作认不认真
// 测试Controller - 专门捣乱看系统反应
@SpringBootTest
@AutoConfigureMockMvc
class UserControllerTest {
@Autowired
private MockMvc mockMvc;
@Test
void testRegisterWithInvalidData() throws Exception {
String invalidUserJson = """
{
"username": "A", // 太短了!
"email": "not-an-email", // 这不是邮箱
"password": "123", // 太弱了
"age": 10, // 未成年!
"phone": null, // 空值
"acceptedAgreement": false, // 不同意协议
"appointmentTime": "2020-01-01", // 过去的时间
"hobbies": ["吃饭", "睡觉", "打豆豆", "刷手机", "发呆"] // 爱好太多
}
""";
mockMvc.perform(MockMvcRequestBuilders.post("/api/users/register")
.contentType(MediaType.APPLICATION_JSON)
.content(invalidUserJson))
.andExpect(status().isBadRequest()) // 应该返回400
.andExpect(jsonPath("$.code").value(400))
.andExpect(jsonPath("$.data").exists()) // 错误详情
.andDo(print()); // 打印响应,看看“保镖”怎么怼你
}
}
四、性能优化小贴士
/**
* 校验配置 - 让校验既严格又高效
*/
@Configuration
public class ValidationConfig {
@Bean
public Validator validator() {
ValidatorFactory factory = Validation.buildDefaultValidatorFactory();
Validator validator = factory.getValidator();
// 可以在这里配置一些自定义设置
// 比如缓存校验器,避免重复创建
return validator;
}
/**
* 快速失败模式 - 发现一个错误就立即返回
* 就像考试发现第一题错了就交卷(不建议真人尝试)
*/
@Bean
public Validator fastFailValidator() {
return Validation.byDefaultProvider()
.configure()
.addProperty("hibernate.validator.fail_fast", "true")
.buildValidatorFactory()
.getValidator();
}
}
总结:参数校验的“人生哲理”
为什么需要参数校验?
- 防止GIGO(垃圾进,垃圾出)——输入决定输出质量
- 安全第一:很多安全漏洞都源于不可信的输入
- 用户体验:早发现错误,早提示用户,别让用户猜谜
注解校验的优点:
- 声明式:像贴标签一样简单,告别一堆if-else
- 集中管理:规则在实体类上一目了然
- 易于维护:改注解就能改规则,不用翻业务代码
- 丰富内置:Spring提供了几十种注解,总有一款适合你
最佳实践建议:
- 在DTO层做校验,保持业务层纯洁
- 错误消息要友好,说人话,别甩技术术语
- 区分必填和非必填字段,别要求用户填宇宙
- 复杂逻辑用自定义校验器,别硬塞到一个注解里
- 记得处理异常,给前端统一的错误格式
总结: 参数校验就像给你的代码请了个:
- 门卫大爷:不合格的一律不让进
- 语文老师:检查格式对不对,内容全不全
- 健身教练:严格要求,不容马虎
- 相声演员:出错时还能用幽默的方式告诉你
严谨的程序员对待输入就像猫奴对待猫主子,既要有爱,也要有规矩! 你的API会因为良好的参数校验而变得更加健壮、安全、用户友好。
以上就是SpringBoot中注解参数校验的实战指南的详细内容,更多关于SpringBoot注解参数校验的资料请关注脚本之家其它相关文章!
