news 2026/2/14 22:20:39

Java注解校验实战

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Java注解校验实战

一、注解校验概述

1.1 为什么需要注解校验?

在实际开发中,我们经常需要对输入数据进行校验:

java

// 传统方式:代码冗长、难以维护 public void createUser(String username, String email, Integer age) { if (username == null || username.length() < 3 || username.length() > 20) { throw new IllegalArgumentException("用户名长度必须在3-20之间"); } if (email == null || !email.matches("^[A-Za-z0-9+_.-]+@(.+)$")) { throw new IllegalArgumentException("邮箱格式不正确"); } if (age == null || age < 18 || age > 120) { throw new IllegalArgumentException("年龄必须在18-120之间"); } //... } // ✅ 注解校验:简洁、声明式、可复用 public void createUser(@Valid UserDTO userDTO) { //... }

注解校验的优势

  • 声明式:通过注解声明校验规则,代码更简洁
  • 可复用:校验逻辑可以复用,避免重复代码
  • 易维护:校验规则集中管理,易于维护
  • 标准化:遵循JSR-303/JSR-380标准
  • 国际化:支持国际化错误消息

1.2 常用校验注解

Jakarta Bean Validation提供的注解

注解

说明

示例

@NotNull

值不能为null

@NotNull String name

@NotEmpty

集合、字符串、数组不能为空

@NotEmpty List<String> items

@NotBlank

字符串不能为空白(去除首尾空格后长度>0)

@NotBlank String content

@Size(min, max)

大小必须在指定范围内

@Size(min=3, max=20) String name

@Min(value)

数值必须大于等于指定值

@Min(18) Integer age

@Max(value)

数值必须小于等于指定值

@Max(120) Integer age

@Email

必须是有效的邮箱格式

@Email String email

@Pattern(regexp)

必须匹配指定的正则表达式

@Pattern(regexp="^1[3-9]\\d{9}$") String phone

@Past

日期必须是过去的时间

@Past Date birthDate

@Future

日期必须是未来的时间

@Future Date appointmentDate

@AssertTrue

布尔值必须是true

@AssertTrue Boolean agreed

@Negative

数值必须是负数

@Negative Integer balance

@Positive

数值必须是正数

@Positive Integer amount


二、@Valid vs @Validated

2.1 核心区别

这两个注解虽然功能相似,但有关键区别:

特性

@Valid

@Validated

来源

Jakarta Bean Validation (JSR-380)

Spring Framework

位置

方法、字段、构造器参数

方法、类型、参数

嵌套校验

✅ 支持

✅ 支持

分组校验

❌ 不支持

✅ 支持

校验组序列

❌ 不支持

✅ 支持

Spring集成

需要配置

原生支持

2.2 @Valid的使用

基本用法

java@Data public class UserDTO { @NotNull(message = "用户ID不能为空") private Long id; @NotBlank(message = "用户名不能为空") @Size(min = 3, max = 20, message = "用户名长度必须在3-20之间") private String username; @Email(message = "邮箱格式不正确") @NotBlank(message = "邮箱不能为空") private String email; @Min(value = 18, message = "年龄必须大于等于18岁") @Max(value = 120, message = "年龄必须小于等于120岁") private Integer age; }

在Controller中使用

java

@RestController @RequestMapping("/api/users") public class UserController { // 使用@Valid触发校验 @PostMapping public ResponseEntity<String> createUser(@Valid @RequestBody UserDTO userDTO) { // 如果校验失败,会自动抛出MethodArgumentNotValidException return ResponseEntity.ok("用户创建成功"); } }

嵌套校验

java

@Data public class OrderDTO { @NotNull(message = "订单ID不能为空") private Long orderId; @Valid // 关键:必须使用@Valid才能触发嵌套对象的校验 @NotNull(message = "用户信息不能为空") private UserDTO user; @Valid @NotEmpty(message = "订单项不能为空") private List<OrderItemDTO> items; } @Data public class OrderItemDTO { @NotNull(message = "商品ID不能为空") private Long productId; @Min(value = 1, message = "数量必须大于0") private Integer quantity; }

2.3 @Validated的使用

基本用法

java

@Service @Validated // 类级别添加@Validated,启用方法参数校验 public class UserService { // 简单参数校验 public void updateUser( @NotNull(message = "用户ID不能为空") Long id, @NotBlank(message = "用户名不能为空") String username) { // 业务逻辑... } // 对象校验 public void createUser(@Valid UserDTO userDTO) { // 业务逻辑... } }

分组校验(@Validated独有):

java

public interface CreateGroup {} public interface UpdateGroup {} @Data public class UserDTO { @Null(groups = CreateGroup.class, message = "创建时ID必须为空") @NotNull(groups = UpdateGroup.class, message = "更新时ID不能为空") private Long id; @NotBlank(groups = {CreateGroup.class, UpdateGroup.class}) private String username; } @RestController @RequestMapping("/api/users") public class UserController { @PostMapping public ResponseEntity<String> create( @Validated(CreateGroup.class) @RequestBody UserDTO userDTO) { return ResponseEntity.ok("创建成功"); } @PutMapping public ResponseEntity<String> update( @Validated(UpdateGroup.class) @RequestBody UserDTO userDTO) { return ResponseEntity.ok("更新成功"); } }

2.4 选择建议

选择决策树

less

是否需要分组校验? ├─ 是 → 使用 @Validated └─ 否 → 是否在Controller中? ├─ 是 → 两者都可以,推荐 @Valid └─ 否 → 使用 @Validated

最佳实践

  1. Controller层:使用 @Valid(简洁、够用)
  2. Service层:使用 @Validated(支持方法参数校验)
  3. 需要分组:必须使用 @Validated
  4. 嵌套对象:在嵌套对象字段上添加 @Valid

三、校验组(Validation Groups)

3.1 为什么需要校验组?

不同场景下,同一对象的校验规则可能不同:

java

体验AI代码助手

代码解读

复制代码

// 场景1:新增用户 // - id为空(由数据库生成) // - username必填 // - password必填 // 场景2:更新用户 // - id必填(根据id更新) // - username可选 // - password可选(不修改则不传)

3.2 定义校验组

java

/** * 校验组定义 */ public interface ValidationGroups { // 新增操作 interface Create {} // 更新操作 interface Update {} // 删除操作 interface Delete {} // 默认组(不指定group时使用) interface Default {} }

3.3 在实体类中使用分组

java

@Data public class UserDTO { // 创建时ID必须为空,更新时ID不能为空 @Null(groups = ValidationGroups.Create.class, message = "创建用户时ID必须为空") @NotNull(groups = {ValidationGroups.Update.class, ValidationGroups.Delete.class}, message = "更新/删除用户时ID不能为空") private Long id; @NotBlank(groups = {ValidationGroups.Create.class, ValidationGroups.Update.class}, message = "用户名不能为空") @Size(min = 3, max = 20, groups = {ValidationGroups.Create.class, ValidationGroups.Update.class}, message = "用户名长度必须在3-20之间") private String username; @Email(groups = ValidationGroups.Create.class, message = "邮箱格式不正确") @NotBlank(groups = ValidationGroups.Create.class, message = "邮箱不能为空") private String email; // 创建时密码必填,更新时可选 @NotBlank(groups = ValidationGroups.Create.class, message = "密码不能为空") @Size(min = 6, max = 20, groups = ValidationGroups.Create.class, message = "密码长度必须在6-20之间") private String password; @NotNull(groups = ValidationGroups.Create.class, message = "年龄不能为空") @Min(value = 18, groups = ValidationGroups.Create.class, message = "年龄必须大于等于18岁") private Integer age; }

3.4 使用校验组

java

@RestController @RequestMapping("/api/users") public class UserController { @PostMapping public ResponseEntity<?> create( @Validated(ValidationGroups.Create.class) @RequestBody UserDTO userDTO) { // 只校验Create组中定义的规则 return ResponseEntity.ok("创建成功"); } @PutMapping("/{id}") public ResponseEntity<?> update( @PathVariable Long id, @Validated(ValidationGroups.Update.class) @RequestBody UserDTO userDTO) { // 只校验Update组中定义的规则 return ResponseEntity.ok("更新成功"); } @DeleteMapping("/{id}") public ResponseEntity<?> delete( @PathVariable Long id, @Validated(ValidationGroups.Delete.class) @RequestBody UserDTO userDTO) { // 只校验Delete组中定义的规则 return ResponseEntity.ok("删除成功"); } }

3.5 组序列(Group Sequence)

控制校验组的执行顺序,默认按照定义的顺序依次校验:

java

@GroupSequence({CreateGroup.class, UpdateGroup.class, Default.class}) public interface OrderedGroup { } @RestController public class UserController { @PostMapping public ResponseEntity<?> create( @Validated(OrderedGroup.class) @RequestBody UserDTO userDTO) { return ResponseEntity.ok("创建成功"); } }

注意:一旦某个组校验失败,后续组不会再执行。


四、自定义校验注解

4.1 自定义注解的应用场景

当内置注解无法满足需求时,可以创建自定义校验注解:

  • 手机号校验:@PhoneNumber
  • 身份证号校验:@IdCard
  • 枚举值校验:@EnumValue
  • 字段互斥:@FieldMatch
  • 密码强度:@StrongPassword

4.2 实现手机号校验注解

第一步:定义注解

java

@Target({ElementType.FIELD, ElementType.PARAMETER}) @Retention(RetentionPolicy.RUNTIME) @Constraint(validatedBy = PhoneNumberValidator.class) @Documented public @interface PhoneNumber { // 必须的三个属性 String message() default "手机号格式不正确"; Class<?>[] groups() default {}; Class<? extends Payload>[] payload() default {}; // 自定义属性:是否支持国际化号码 boolean international() default false; // 自定义属性:支持的国家代码 String[] countryCodes() default {"+86"}; }

第二步:实现校验器

java

public class PhoneNumberValidator implements ConstraintValidator<PhoneNumber, String> { private boolean international; private String[] countryCodes; // 中国大陆手机号正则 private static final String CHINA_PHONE_PATTERN = "^1[3-9]\\d{9}$"; @Override public void initialize(PhoneNumber constraintAnnotation) { this.international = constraintAnnotation.international(); this.countryCodes = constraintAnnotation.countryCodes(); } @Override public boolean isValid(String value, ConstraintValidatorContext context) { // null值由@NotNull处理 if (value == null) { return true; } // 国际号码校验 if (international) { return validateInternational(value); } // 中国手机号校验 return value.matches(CHINA_PHONE_PATTERN); } private boolean validateInternational(String phone) { // 简单的国际号码校验逻辑 for (String code : countryCodes) { if (phone.startsWith(code)) { String number = phone.substring(code.length()); return number.matches("^\\d{6,15}$"); } } return false; } }

第三步:使用注解

java

@Data public class UserDTO { @PhoneNumber(message = "手机号格式不正确") private String mobile; @PhoneNumber(international = true, countryCodes = {"+86", "+1", "+44"}, message = "国际手机号格式不正确") private String internationalPhone; }

4.3 实现密码强度校验

注解定义

java

@Target({ElementType.FIELD, ElementType.PARAMETER}) @Retention(RetentionPolicy.RUNTIME) @Constraint(validatedBy = StrongPasswordValidator.class) @Documented public @interface StrongPassword { String message() default "密码强度不足"; Class<?>[] groups() default {}; Class<? extends Payload>[] payload() default {}; // 最小长度 int minLength() default 8; // 是否需要大写字母 boolean requireUppercase() default true; // 是否需要小写字母 boolean requireLowercase() default true; // 是否需要数字 boolean requireDigit() default true; // 是否需要特殊字符 boolean requireSpecialChar() default true; }

校验器实现

java

public class StrongPasswordValidator implements ConstraintValidator<StrongPassword, String> { private int minLength; private boolean requireUppercase; private boolean requireLowercase; private boolean requireDigit; private boolean requireSpecialChar; private static final Pattern UPPERCASE_PATTERN = Pattern.compile("[A-Z]"); private static final Pattern LOWERCASE_PATTERN = Pattern.compile("[a-z]"); private static final Pattern DIGIT_PATTERN = Pattern.compile("\\d"); private static final Pattern SPECIAL_CHAR_PATTERN = Pattern.compile("[!@#$%^&*()_+\\-=\\[\\]{};':\"\\\\|,.<>\\/?]"); @Override public void initialize(StrongPassword constraintAnnotation) { this.minLength = constraintAnnotation.minLength(); this.requireUppercase = constraintAnnotation.requireUppercase(); this.requireLowercase = constraintAnnotation.requireLowercase(); this.requireDigit = constraintAnnotation.requireDigit(); this.requireSpecialChar = constraintAnnotation.requireSpecialChar(); } @Override public boolean isValid(String password, ConstraintValidatorContext context) { if (password == null) { return true; } if (password.length() < minLength) { return false; } if (requireUppercase && !UPPERCASE_PATTERN.matcher(password).find()) { return false; } if (requireLowercase && !LOWERCASE_PATTERN.matcher(password).find()) { return false; } if (requireDigit && !DIGIT_PATTERN.matcher(password).find()) { return false; } if (requireSpecialChar && !SPECIAL_CHAR_PATTERN.matcher(password).find()) { return false; } return true; } }

4.4 跨字段校验

实现"密码"和"确认密码"必须一致的校验:

注解定义

java

@Target({ElementType.TYPE}) @Retention(RetentionPolicy.RUNTIME) @Constraint(validatedBy = FieldMatchValidator.class) @Documented public @interface FieldMatch { String message() default "字段值不匹配"; Class<?>[] groups() default {}; Class<? extends Payload>[] payload() default {}; // 第一个字段名 String first(); // 第二个字段名 String second(); }

校验器实现

java

public class FieldMatchValidator implements ConstraintValidator<FieldMatch, Object> { private String firstFieldName; private String secondFieldName; @Override public void initialize(FieldMatch constraintAnnotation) { this.firstFieldName = constraintAnnotation.first(); this.secondFieldName = constraintAnnotation.second(); } @Override public boolean isValid(Object value, ConstraintValidatorContext context) { if (value == null) { return true; } try { Field firstField = value.getClass().getDeclaredField(firstFieldName); firstField.setAccessible(true); Object firstValue = firstField.get(value); Field secondField = value.getClass().getDeclaredField(secondFieldName); secondField.setAccessible(true); Object secondValue = secondField.get(value); return Objects.equals(firstValue, secondValue); } catch (Exception e) { return false; } } }

使用示例

java

@Data @FieldMatch(first = "password", second = "confirmPassword", message = "两次输入的密码不一致") public class RegisterRequest { private String username; private String password; private String confirmPassword; }


五、生产环境实战

5.1 统一异常处理

在生产环境中,需要统一处理校验异常:

java

@RestControllerAdvice public class GlobalExceptionHandler { /** * 处理 @Valid 触发的校验异常 */ @ExceptionHandler(MethodArgumentNotValidException.class) public ResponseEntity<ErrorResponse> handleValidationException( MethodArgumentNotValidException ex) { List<String> errors = ex.getBindingResult() .getFieldErrors() .stream() .map(error -> error.getField() + ": " + error.getDefaultMessage()) .collect(Collectors.toList()); ErrorResponse response = ErrorResponse.builder() .code(400) .message("参数校验失败") .errors(errors) .timestamp(LocalDateTime.now()) .build(); return ResponseEntity.badRequest().body(response); } /** * 处理 @Validated 触发的校验异常 */ @ExceptionHandler(ConstraintViolationException.class) public ResponseEntity<ErrorResponse> handleConstraintViolation( ConstraintViolationException ex) { List<String> errors = ex.getConstraintViolations() .stream() .map(violation -> violation.getPropertyPath() + ": " + violation.getMessage()) .collect(Collectors.toList()); ErrorResponse response = ErrorResponse.builder() .code(400) .message("参数校验失败") .errors(errors) .timestamp(LocalDateTime.now()) .build(); return ResponseEntity.badRequest().body(response); } /** * 处理请求参数绑定异常 */ @ExceptionHandler(BindException.class) public ResponseEntity<ErrorResponse> handleBindException(BindException ex) { List<String> errors = ex.getBindingResult() .getFieldErrors() .stream() .map(error -> error.getField() + ": " + error.getDefaultMessage()) .collect(Collectors.toList()); ErrorResponse response = ErrorResponse.builder() .code(400) .message("参数绑定失败") .errors(errors) .timestamp(LocalDateTime.now()) .build(); return ResponseEntity.badRequest().body(response); } } @Data @Builder class ErrorResponse { private Integer code; private String message; private List<String> errors; private LocalDateTime timestamp; }

5.2 快速失败机制

默认情况下,Bean Validation会校验所有约束并返回所有错误。如果需要在第一个错误时就停止:

java

@Configuration public class ValidationConfig { @Bean public Validator validator() { ValidatorFactory factory = Validation.byDefaultProvider() .configure() .failFast(true) // 启用快速失败 .buildValidatorFactory(); return factory.getValidator(); } @Bean public MethodValidationPostProcessor methodValidationPostProcessor() { MethodValidationPostProcessor processor = new MethodValidationPostProcessor(); processor.setValidator(validator()); return processor; } }

5.4 手动触发校验

在Service层手动触发校验:

java

@Service @RequiredArgsConstructor public class UserService { private final Validator validator; public void createUser(UserDTO userDTO) { // 手动校验 Set<ConstraintViolation<UserDTO>> violations = validator.validate(userDTO, Default.class); if (!violations.isEmpty()) { throw new ConstraintViolationException(violations); } // 业务逻辑... } }


六、最佳实践

6.1 设计原则

  1. 单一职责:每个注解只负责一个校验规则
  2. 组合使用:多个简单注解组合成复杂规则
  3. 错误消息清晰:提供具体、可操作的错误提示
  4. 分组管理:使用校验组区分不同场景
  5. 自定义注解:复杂业务逻辑创建自定义注解

6.2 性能优化

  1. 避免过度校验:只校验必要的数据
  2. 校验顺序:将简单的校验放在前面
  3. 缓存Validator:Validator实例可以复用
  4. 异步校验:对于复杂校验,考虑异步处理

七、总结

本文系统地介绍了Java注解校验的核心概念和实践:

  1. @Valid vs @Validated:理解两者的区别和适用场景
  2. 校验组:使用分组管理不同场景的校验规则
  3. 自定义注解:创建符合业务需求的校验注解
  4. 生产实践:异常处理、国际化、性能优化
版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/2/5 4:25:39

DDS与模拟滤波结合的波形发生器设计实践指南

DDS与模拟滤波结合的波形发生器设计实践指南从一个常见问题说起&#xff1a;为什么我的DDS输出“看起来像噪声”&#xff1f;你有没有遇到过这样的情况&#xff1a;明明配置好了AD9833或AD9850&#xff0c;目标频率是1 MHz正弦波&#xff0c;结果用示波器一看——信号确实出来了…

作者头像 李华
网站建设 2026/1/29 22:12:04

I2C协议与工业总线协同控制:完整示例

I2C与工业总线如何“搭伙干活”&#xff1f;一个真实工程案例讲透多总线协同你有没有遇到过这样的场景&#xff1a;现场一堆传感器要接&#xff0c;温度、电压、日志存储、PWM控制……全扔给PLC&#xff1f;线越拉越多&#xff0c;端子排密密麻麻&#xff0c;改个点就得停机半天…

作者头像 李华
网站建设 2026/1/29 20:57:04

AI表情识别实战:用通义千问2.5-7B-Instruct快速搭建应用

AI表情识别实战&#xff1a;用通义千问2.5-7B-Instruct快速搭建应用 随着多模态大模型的快速发展&#xff0c;AI在图像理解与语义生成方面的融合能力显著增强。通义千问2.5-7B-Instruct作为阿里云于2024年9月发布的中等体量全能型模型&#xff0c;不仅具备强大的语言理解和生成…

作者头像 李华
网站建设 2026/2/6 11:22:48

从零开始学大模型:通义千问2.5-7B-Instruct入门指南

从零开始学大模型&#xff1a;通义千问2.5-7B-Instruct入门指南 1. 学习目标与背景介绍 随着大语言模型技术的快速发展&#xff0c;越来越多开发者希望在本地或私有环境中部署和使用高性能开源模型。通义千问2.5-7B-Instruct作为阿里云于2024年9月发布的中等体量全能型模型&a…

作者头像 李华
网站建设 2026/2/11 4:35:54

实测通义千问2.5-7B-Instruct:多模态对话效果惊艳

实测通义千问2.5-7B-Instruct&#xff1a;多模态对话效果惊艳 1. 引言 随着大模型技术的持续演进&#xff0c;中等参数量级&#xff08;7B~13B&#xff09;的模型正逐渐成为实际落地应用的主流选择。这类模型在性能、资源消耗和推理速度之间实现了良好平衡&#xff0c;尤其适…

作者头像 李华