基础概念
一、引入依赖
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-validation</artifactId> </dependency>验证注解都在javax.validation.constraints这个包下
二、定义自定义注解 @XlsxFile
项目中的注解定义在XlsxFile.java
@Documented @Target({ ElementType.FIELD, ElementType.PARAMETER }) @Retention(RetentionPolicy.RUNTIME) @Constraint(validatedBy = XlsxFileValidator.class) public @interface XlsxFile { String message() default "请上传有效的 Excel 文件"; long maxSize() default 5 * 1024 * 1024; Class<?>[] groups() default {}; Class<? extends Payload>[] payload() default {}; }
@Constraint(validatedBy = XlsxFileValidator.class)
这是自定义校验注解最关键的配置。
当遇到@XlsxFile时,使用XlsxFileValidator执行具体校验。
注解负责声明规则,Validator 负责实现规则。
message、groups和payload
一个标准的 Bean Validation 约束注解通常需要提供:
String message() default "..."; Class<?>[] groups() default {}; Class<? extends Payload>[] payload() default {};它们的作用分别是:
message:默认校验失败信息;groups:校验分组,可以针对不同业务场景执行不同校验;payload:携带额外元数据,普通项目中较少直接使用。
三、实现自定义校验器
校验器位于XlsxFileValidator.java
public class XlsxFileValidator implements ConstraintValidator<XlsxFile, MultipartFile> { private long maxSize; @Override public void initialize(XlsxFile annotation) { this.maxSize = annotation.maxSize(); } @Override public boolean isValid( MultipartFile file, ConstraintValidatorContext context) { if (file == null || file.isEmpty()) { return fail(context, "请选择要上传的 Excel 文件"); } if (file.getSize() > maxSize) { return fail(context, "Excel 文件不能超过 5 MB"); } String filename = file.getOriginalFilename(); if (filename == null || !filename.toLowerCase(Locale.ROOT) .endsWith(".xlsx")) { return fail(context, "仅支持 .xlsx 文件"); } return true; } private boolean fail( ConstraintValidatorContext context, String message) { context.disableDefaultConstraintViolation(); context.buildConstraintViolationWithTemplate(message) .addConstraintViolation(); return false; } }1.ConstraintValidator的两个泛型
ConstraintValidator<XlsxFile, MultipartFile>第一个泛型表示对应的注解类型:
XlsxFile第二个泛型表示被校验的数据类型:
MultipartFile因此,这个校验器用于校验标注了@XlsxFile的MultipartFile数据。
2.initialize()方法
@Override public void initialize(XlsxFile annotation) { this.maxSize = annotation.maxSize(); }该方法用于读取注解上的配置。
例如:
@XlsxFile(maxSize = 10 * 1024 * 1024)那么annotation.maxSize()得到的就是 10 MB。
3.isValid()方法
isValid()是真正执行校验的方法:
@Override public boolean isValid( MultipartFile file, ConstraintValidatorContext context) { }返回值含义:
- 返回
true:校验通过;- 返回
false:校验失败。
当前项目依次校验了文件是否为空、文件大小以及文件后缀。
4. 自定义错误信息
如果直接返回false,框架会使用注解的默认message。
项目中希望不同错误返回不同提示,因此使用:
context.disableDefaultConstraintViolation(); context.buildConstraintViolationWithTemplate(message) .addConstraintViolation();这样便可以分别返回:
请选择要上传的 Excel 文件
Excel 文件不能超过 5 MB
仅支持 .xlsx 文件
四、在 DTO 中使用注解
项目中的请求对象是UserImportRequest.java
public class UserImportRequest { @XlsxFile(maxSize = 5 * 1024 * 1024) private MultipartFile file; public MultipartFile getFile() { return file; } public void setFile(MultipartFile file) { this.file = file; } }六、使用@Valid触发校验
仅仅在字段上添加@XlsxFile还不够,还需要在 Controller 参数前添加@Valid:
@PostMapping( value = "/import", consumes = MediaType.MULTIPART_FORM_DATA_VALUE ) public ImportResult importUsers( @Valid @ModelAttribute UserImportRequest request) { MultipartFile file = request.getFile(); try (InputStream inputStream = file.getInputStream()) { return userService.importUsers(inputStream); } catch (IOException exception) { throw new UserImportException( 0, "文件读取失败", List.of(new ImportError( 0, "file", "无法读取上传的文件" )) ); } }其中:
@ModelAttribute:将multipart/form-data请求参数绑定到 DTO;@Valid:触发 DTO 字段上的 Bean Validation 校验;- 校验通过后才会进入方法主体;
- 校验失败时会抛出参数校验异常。
这里很容易忘记@Valid。如果没有它,字段上的@XlsxFile通常不会自动执行。
七、统一处理校验异常
项目通过GlobalExceptionHandler.java统一处理参数校验异常:
@ExceptionHandler(MethodArgumentNotValidException.class) public ResponseEntity<ImportResult> handleValidationException( MethodArgumentNotValidException exception) { List<ImportError> errors = exception .getBindingResult() .getFieldErrors() .stream() .map(error -> new ImportError( 0, error.getField(), error.getDefaultMessage() )) .toList(); ImportResult result = ImportResult.failure( 0, "文件校验失败", errors ); return ResponseEntity.badRequest().body(result); }这里通过:
exception.getBindingResult().getFieldErrors()获取所有字段错误,再转换成项目统一的ImportError对象。
这样Controller不需要编写大量if/else,客户端收到的异常结构也更加统一。
八、学习总结
自定义注解这种声明式写法更加直观,也更容易复用和维护。
自定义校验注解的通用实现步骤可以总结为:
- 引入Validation依赖;
- 创建自定义注解;
- 使用
@Constraint绑定校验器;- 实现
ConstraintValidator;- 在 DTO 字段上使用注解;
- 通过
@Valid触发校验;- 使用全局异常处理器统一返回错误。
这套模式不只适用于 Excel 文件,还可以扩展到身份证号、手机号、枚举值、日期范围、业务编码以及多个字段之间的关联校验。