首页
关于这个博客
Search
1
Java 实现Google 账号单点登录(OAuth 2.0)全流程解析
822 阅读
2
Spring AI 无法获取大模型深度思考内容?解决方案来了
360 阅读
3
EasyExcel 实战:导出带图片的 Excel 完整方案
169 阅读
4
微信小程序实现页面返回前确认弹窗:兼容左上角返回与右滑返回
155 阅读
5
服务器遭遇 XMRig 挖矿程序入侵排查与清理全记录
153 阅读
Java 核心
框架与中间件
数据库技术
开发工具与效率
问题排查与踩坑记录
程序员成长与思考
前端
登录
Search
标签搜索
java虚拟机
JVM
保姆级教程
Java
Spring AI
SpringBoot
Spring
WebFlux
Nginx
Spring Retry
EasyExcel
流式输出
WebSocket
JustAuth
sso
google
单点登录
源码解析
Tool
图片导出
Luca Ju
累计撰写
39
篇文章
累计收到
1
条评论
首页
栏目
Java 核心
框架与中间件
数据库技术
开发工具与效率
问题排查与踩坑记录
程序员成长与思考
前端
页面
关于这个博客
搜索到
1
篇与
的结果
2026-02-03
Jakarta Validation 优雅实现参数校验:从基础使用到自定义扩展
在后端开发中,参数校验是保障接口安全性和数据合法性的核心环节,硬编码的if-else校验逻辑不仅繁琐冗余,还会让代码可读性大打折扣。Jakarta Validation(原Java Validation)为我们提供了一套轻量、优雅的注解式参数校验方案,通过标准化的注解即可实现各类参数校验规则,大幅简化开发流程。本文将从基础注解使用、实战场景落地、统一异常处理到自定义校验注解,全方位讲解Jakarta Validation的使用技巧,结合实际业务代码示例,让你快速上手并灵活运用到项目中。一、核心校验注解速查Jakarta Validation提供了一系列开箱即用的校验注解,覆盖空值、长度、格式、数值、日期等绝大多数日常校验场景,核心注解及功能如下表所示,可直接作为开发速查手册:注解适用类型核心功能@NotNull所有类型字段值不能为null@NotBlank字符串不能为null,且去除首尾空格后长度大于0@NotEmpty字符串/集合/数组不能为null,且长度/元素个数大于0@Size(min, max)字符串/集合/数组长度/元素个数在[min, max]范围内@Pattern(regexp)字符串必须匹配指定的正则表达式@Email字符串必须符合合法的邮箱格式(支持自定义正则)@Min(value)数值类型数值必须大于等于value@Max(value)数值类型数值必须小于等于value@Positive数值类型必须为正数(大于0)@Negative数值类型必须为负数(小于0)@PositiveOrZero数值类型必须为正数或0@NegativeOrZero数值类型必须为负数或0@Future日期/时间类型必须是未来的时间@FutureOrPresent日期/时间类型必须是未来或当前时间@Past日期/时间类型必须是过去的时间@PastOrPresent日期/时间类型必须是过去或当前时间注解使用小技巧@NotBlank/@NotEmpty/@NotNull 区分:字符串优先用@NotBlank(过滤空白字符),集合/数组用@NotEmpty,非字符串非集合类型用@NotNull;注解组合使用:实际业务中可组合多个注解,如用户账号需同时满足「非空、正则匹配、长度限制」;默认提示语自定义:所有注解都支持message属性,用于自定义校验失败的提示信息,贴合业务场景。二、实战场景落地:三种核心使用方式Jakarta Validation的注解可根据参数传递方式灵活使用,核心分为「简单参数直接注解」「实体对象属性注解」两种核心场景,后者是项目中最常用的方式。场景1:GET请求简单参数,直接注解参数对于GET请求的URL拼接参数(如/user/get?name=test&id=1),可直接在接口方法的参数前添加校验注解,适用于参数数量少的简单场景。/** * 根据用户名和ID查询用户 * @param name 用户名(不能为空) * @param id 用户ID(必须为正数) * @return 用户信息 */ @GetMapping("/get") public CommonResult<UserVO> getUser( @NotBlank(message = "用户名不能为空") String name, @Positive(message = "用户ID必须为正数") Long id ) { UserVO user = userService.getByNameAndId(name, id); return CommonResult.success(user); }场景2:POST请求实体参数,注解+@Valid 触发校验对于POST/PUT请求,参数通常封装为实体对象(如新增/编辑用户的入参),只需在实体的属性上添加校验注解,并在接口方法的实体参数前添加@Valid(或@Validated)注解,即可触发整体校验逻辑。这是项目中最常用的方式,适合复杂参数的校验场景。步骤1:实体类添加校验注解@Data @Schema(description = "新增用户请求参数") public class UserSaveReqVO { @Schema(description = "用户编号(编辑时传,新增时不传)", example = "1024") private Long id; @Schema(description = "用户账号", requiredMode = Schema.RequiredMode.REQUIRED, example = "yudao") @NotBlank(message = "用户账号不能为空") @Pattern(regexp = "^[a-zA-Z0-9]+$", message = "用户账号仅支持数字、字母组成") @Size(min = 4, max = 30, message = "用户账号长度为4-30个字符") private String username; @Schema(description = "用户密码", requiredMode = Schema.RequiredMode.REQUIRED, example = "123456a") @NotBlank(message = "用户密码不能为空") @Size(min = 6, max = 20, message = "用户密码长度为6-20个字符") private String password; @Schema(description = "用户年龄", example = "25") @Min(value = 18, message = "用户年龄不能小于18岁") @Max(value = 60, message = "用户年龄不能大于60岁") private Integer age; @Schema(description = "邮箱", example = "test@example.com") @NotBlank(message = "邮箱不能为空") @Email(message = "邮箱格式不合法") private String email; }步骤2:接口方法添加@Valid 触发校验@PostMapping("/create") @Operation(summary = "新增用户") public CommonResult<Long> createUser(@Valid @RequestBody UserSaveReqVO reqVO) { Long userId = userService.createUser(reqVO); return CommonResult.success(userId); }关键区别:@Valid vs @Validated@Valid:属于JSR-380标准注解,支持嵌套实体校验(如实体中包含另一个实体属性);@Validated:属于Spring扩展注解,支持分组校验(如新增和编辑用户时,同一实体的校验规则不同),可替代@Valid使用。嵌套实体校验示例:如果UserSaveReqVO中包含AddressVO实体属性,只需在AddressVO属性上添加@Valid+自身属性注解,即可触发嵌套校验。@Data public class UserSaveReqVO { // 其他属性... @Schema(description = "用户地址") @Valid // 触发嵌套校验 @NotNull(message = "用户地址不能为空") private AddressVO address; } @Data public class AddressVO { @NotBlank(message = "省不能为空") private String province; @NotBlank(message = "市不能为空") private String city; }三、全局异常拦截:统一处理校验失败结果当参数校验失败时,Jakarta Validation会自动抛出异常,不同场景抛出的异常类型不同:简单参数校验失败:抛出ConstraintViolationException;实体对象校验失败:抛出MethodArgumentNotValidException。为了让前端能接收到统一格式的错误返回,我们需要在项目中添加全局异常处理器,拦截这些校验异常,封装成统一的返回结果。全局异常处理器实现结合Spring Boot的@RestControllerAdvice和@ExceptionHandler实现全局异常拦截,统一返回格式(如包含错误码、错误信息的CommonResult)。@RestControllerAdvice @Slf4j public class GlobalExceptionHandler { /** * 处理实体对象参数校验失败异常(@Valid + 实体注解) */ @ExceptionHandler(MethodArgumentNotValidException.class) public CommonResult<?> handleMethodArgumentNotValid(MethodArgumentNotValidException ex) { log.warn("参数校验失败:{}", ex.getMessage()); // 获取校验失败的第一条错误信息 String errorMsg = getFirstValidErrorMessage(ex.getBindingResult()); return CommonResult.error(HttpStatus.BAD_REQUEST.value(), "请求参数不正确:" + errorMsg); } /** * 处理简单参数校验失败异常(直接注解参数) */ @ExceptionHandler(ConstraintViolationException.class) public CommonResult<?> handleConstraintViolation(ConstraintViolationException ex) { log.warn("参数校验失败:{}", ex.getMessage()); // 获取校验失败的第一条错误信息 String errorMsg = ex.getConstraintViolations().stream() .map(ConstraintViolation::getMessage) .findFirst() .orElse("参数校验失败"); return CommonResult.error(HttpStatus.BAD_REQUEST.value(), "请求参数不正确:" + errorMsg); } /** * 提取BindingResult中的第一条错误信息 */ private String getFirstValidErrorMessage(BindingResult bindingResult) { // 优先获取字段级别的错误 if (bindingResult.hasFieldErrors()) { return bindingResult.getFieldErrors().get(0).getDefaultMessage(); } // 无字段错误则获取全局错误 if (bindingResult.hasGlobalErrors()) { return bindingResult.getGlobalErrors().get(0).getDefaultMessage(); } return "参数校验失败"; } }异常处理小技巧返回第一条错误信息:避免返回所有错误信息导致前端展示混乱,优先返回第一条校验失败的信息;统一错误码:参数校验失败统一使用400(BAD_REQUEST)错误码,符合HTTP协议规范;日志记录:记录异常日志便于问题排查,但无需打印完整堆栈(非运行时异常,属于业务异常)。四、高级扩展:自定义校验注解Jakarta Validation提供的默认注解无法覆盖所有业务场景(如「参数必须为指定枚举值」「手机号格式校验」「身份证号校验」),此时可通过自定义校验注解实现个性化的校验规则,步骤固定且可复用。实战示例:实现「参数必须为指定枚举值」的自定义注解以最常见的「参数必须是枚举中的某个值」为例,实现自定义注解@InEnum,支持校验参数是否为指定枚举的有效值。步骤1:定义自定义注解通过@Constraint指定校验器实现类,注解的属性可传递自定义参数(如枚举类),同时指定注解的适用目标(字段、参数等)。@Target({ ElementType.METHOD, ElementType.FIELD, ElementType.PARAMETER, ElementType.ANNOTATION_TYPE }) @Retention(RetentionPolicy.RUNTIME) @Documented // 指定校验器实现类:InEnumValidator @Constraint(validatedBy = {InEnumValidator.class}) public @interface InEnum { /** * 校验失败的提示语 */ String message() default "必须为指定枚举值:{value}"; /** * 分组校验(可省略,默认空) */ Class<?>[] groups() default {}; /** * 负载(可省略,默认空) */ Class<? extends Payload>[] payload() default {}; /** * 目标枚举类(必须实现ArrayValuable接口,提供枚举值数组) */ Class<? extends ArrayValuable<?>> value(); } /** * 枚举值获取接口,所有需要被@InEnum校验的枚举需实现此接口 * @param <T> 枚举值类型 */ public interface ArrayValuable<T> { /** * 获取枚举的所有值数组 */ T[] array(); }步骤2:实现注解校验器实现ConstraintValidator<A, T>接口,其中A为自定义注解,T为被校验的参数类型,重写initialize(初始化注解参数)和isValid(核心校验逻辑)方法。@Slf4j public class InEnumValidator implements ConstraintValidator<InEnum, Object> { /** * 枚举的有效值集合 */ private List<?> validValues; /** * 初始化:获取注解中指定的枚举类,提取其有效值 */ @Override public void initialize(InEnum annotation) { Class<? extends ArrayValuable<?>> enumClass = annotation.value(); // 获取枚举的所有实例 ArrayValuable<?>[] enumConstants = enumClass.getEnumConstants(); if (ArrayUtil.isEmpty(enumConstants)) { this.validValues = Collections.emptyList(); return; } // 提取枚举的有效值数组,转为List方便判断 this.validValues = Arrays.asList(enumConstants[0].array()); } /** * 核心校验逻辑 * @param value 被校验的参数值 * @param context 校验上下文 * @return true=校验通过,false=校验失败 */ @Override public boolean isValid(Object value, ConstraintValidatorContext context) { // 1. 参数为null时,默认校验通过(如需非空,可配合@NotNull注解) if (value == null) { return true; } // 2. 参数值在枚举有效值集合中,校验通过 if (validValues.contains(value)) { return true; } // 3. 校验失败,自定义提示语(替换{value}为实际枚举有效值) context.disableDefaultConstraintViolation(); context.buildConstraintViolationWithTemplate( context.getDefaultConstraintMessageTemplate() .replace("{value}", validValues.toString()) ).addConstraintViolation(); return false; } }步骤3:枚举实现接口并使用自定义注解让目标枚举实现ArrayValuable接口,提供有效值数组,然后在实体/参数上添加@InEnum注解即可。/** * 性别枚举 */ public enum GenderEnum implements ArrayValuable<Integer> { MALE(1, "男"), FEMALE(2, "女"); private final Integer code; private final String name; GenderEnum(Integer code, String name) { this.code = code; this.name = name; } @Override public Integer[] array() { // 返回枚举的有效值数组 return new Integer[]{MALE.getCode(), FEMALE.getCode()}; } public Integer getCode() { return code; } } // 在实体中使用@InEnum注解 @Data public class UserSaveReqVO { // 其他属性... @Schema(description = "性别(1=男,2=女)", example = "1") @NotNull(message = "性别不能为空") @InEnum(value = GenderEnum.class, message = "性别必须为{value}") private Integer gender; }自定义注解开发通用规范注解属性规范:必须包含message/groups/payload三个基础属性(符合Jakarta Validation标准);空值处理:校验器中默认对null放行,如需非空可配合@NotNull注解,解耦「非空校验」和「业务规则校验」;提示语自定义:通过context.disableDefaultConstraintViolation()禁用默认提示语,实现动态替换(如替换枚举有效值);可复用性:自定义注解应设计为通用型(如手机号、身份证号校验注解),可在项目中全局复用。五、实用进阶技巧1. 分组校验:同一实体不同场景不同校验规则实际开发中,新增和编辑用户时,同一实体的校验规则可能不同(如新增时id无需传,编辑时id必须传),可通过分组校验实现,基于@Validated的分组属性。步骤1:定义分组标识接口/** * 校验分组 - 新增 */ public interface AddGroup { } /** * 校验分组 - 编辑 */ public interface EditGroup { }步骤2:实体注解指定分组@Data public class UserSaveReqVO { @Schema(description = "用户编号", example = "1024") @NotNull(message = "用户ID不能为空", groups = EditGroup.class) // 仅编辑时校验id非空 private Long id; @NotBlank(message = "用户账号不能为空", groups = {AddGroup.class, EditGroup.class}) // 新增+编辑都校验 private String username; }步骤3:接口指定分组触发校验// 新增用户:使用AddGroup分组 @PostMapping("/create") public CommonResult<Long> createUser(@Validated(AddGroup.class) @RequestBody UserSaveReqVO reqVO) { return CommonResult.success(userService.createUser(reqVO)); } // 编辑用户:使用EditGroup分组 @PutMapping("/edit") public CommonResult<Boolean> editUser(@Validated(EditGroup.class) @RequestBody UserSaveReqVO reqVO) { return CommonResult.success(userService.editUser(reqVO)); }六、总结Jakarta Validation通过注解式编程让参数校验从繁琐的硬编码中解放出来,实现了「校验规则和业务逻辑的解耦」,让代码更简洁、优雅、易维护。合理使用Jakarta Validation,不仅能提升开发效率,还能让接口的参数校验更规范、更健壮,为项目的稳定性提供保障。
2026年02月03日
1 阅读
0 评论
0 点赞