Spring控制器中复用表单数据类并配置不同校验规则的实现方法
这问题我碰到过好几次,Spring的Bean Validation其实提供了现成的方案来解决这种「同一个DTO在不同接口需要不同校验规则」的场景,下面给你拆解几种实用的实现方式,按推荐程度排序:
1. 分组校验(最推荐的标准方案)
这是Bean Validation原生支持的特性,通过给校验规则标记「分组」,在接口方法中指定要启用的分组即可,代码简洁且符合规范。
步骤1:定义校验分组接口
先创建两个空接口作为分组标识(不需要任何实现):
// 对应askHim接口的校验分组 public interface AskHimGroup {} // 对应askMe接口的校验分组 public interface AskMeGroup {}
步骤2:给Query类的字段绑定分组规则
在@Pattern注解中通过groups属性指定该规则属于哪个分组:
public class Query { // 仅在askHim接口调用时生效的正则:大写字母 @Pattern( regexp = "^[A-Z]+$", groups = AskHimGroup.class, message = "askHim接口的name必须为大写字母" ) // 仅在askMe接口调用时生效的正则:小写字母 @Pattern( regexp = "^[a-z]+$", groups = AskMeGroup.class, message = "askMe接口的name必须为小写字母" ) private String name; // 别忘了添加getter和setter public String getName() { return name; } public void setName(String name) { this.name = name; } }
步骤3:在控制器方法中指定要启用的分组
注意要把原来的@Valid替换成Spring提供的@Validated,它支持分组校验:
@Controller public class TestController { @PostMapping(value = "/api/askHim.html") public void askHim(@Validated(AskHimGroup.class) @RequestBody Query q) { // do something } @PostMapping(value = "/api/askMe.html") public void askMe(@Validated(AskMeGroup.class) @RequestBody Query q) { // do something } }
2. 继承DTO类(适合字段差异较大的场景)
如果除了name字段,后续两个接口可能还需要其他不同的字段或校验规则,可以用继承的方式拆分DTO,让每个DTO职责更单一:
步骤1:定义基础DTO和子DTO
// 通用字段放到BaseQuery里 public class BaseQuery { // 比如其他通用字段:private Integer age; 等 } // askHim接口专用DTO public class AskHimQuery extends BaseQuery { @Pattern(regexp = "^[A-Z]+$", message = "name必须为大写字母") private String name; // getter和setter } // askMe接口专用DTO public class AskMeQuery extends BaseQuery { @Pattern(regexp = "^[a-z]+$", message = "name必须为小写字母") private String name; // getter和setter }
步骤2:控制器中使用对应DTO
@Controller public class TestController { @PostMapping(value = "/api/askHim.html") public void askHim(@Valid @RequestBody AskHimQuery q) { // do something } @PostMapping(value = "/api/askMe.html") public void askMe(@Valid @RequestBody AskMeQuery q) { // do something } }
这个方案的好处是每个DTO只对应自己的接口规则,缺点是如果字段多会有少量重复代码,可以用Lombok的@Getter/@Setter注解减少冗余。
3. 自定义动态校验(适合复杂场景)
如果需要根据请求的其他参数(比如请求头、路径变量)动态调整校验规则,可以自定义Validator手动处理:
步骤1:实现自定义Validator
@Component public class QueryValidator implements Validator { @Override public boolean supports(Class<?> clazz) { return Query.class.isAssignableFrom(clazz); } @Override public void validate(Object target, Errors errors) { Query query = (Query) target; String name = query.getName(); // 通过RequestContextHolder获取当前请求的路径 HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest(); String requestUri = request.getRequestURI(); if ("/api/askHim.html".equals(requestUri)) { if (!name.matches("^[A-Z]+$")) { errors.rejectValue("name", "pattern", "askHim接口的name必须为大写字母"); } } else if ("/api/askMe.html".equals(requestUri)) { if (!name.matches("^[a-z]+$")) { errors.rejectValue("name", "pattern", "askMe接口的name必须为小写字母"); } } // 可以添加其他通用校验逻辑 } }
步骤2:控制器中手动调用校验
@Controller public class TestController { @Autowired private QueryValidator queryValidator; @PostMapping(value = "/api/askHim.html") public void askHim(@RequestBody Query q, BindingResult result) { queryValidator.validate(q, result); if (result.hasErrors()) { // 处理校验错误,比如抛出异常或返回错误信息 throw new IllegalArgumentException(result.getAllErrors().get(0).getDefaultMessage()); } // do something } @PostMapping(value = "/api/askMe.html") public void askMe(@RequestBody Query q, BindingResult result) { queryValidator.validate(q, result); if (result.hasErrors()) { throw new IllegalArgumentException(result.getAllErrors().get(0).getDefaultMessage()); } // do something } }
这个方案灵活性最高,但代码量也最大,只适合特殊复杂场景,一般优先用分组校验。
内容的提问来源于stack exchange,提问作者Mr.M.
相关产品推荐
相关产品推荐

