再看参数校验
作者简介:大家好,我是smart哥,前中兴通讯、美团架构师,现某互联网公司CTO
联系qq:184480602,加我进群,大家一起学习,一起进步,一起对抗互联网寒冬
写一个接口,大致就几个步骤:
- 参数校验
- 编写Service、Dao(SQL)
- Result封装返回值
- 如果是分布式,还可能涉及网关配置、服务引用等
业务代码总是变化的,没太多可说的,统一结果封装我们已经介绍过,今天我们来聊聊参数校验的琐事。
老实说,参数校验很烦!不校验不行,仔细校验吧,代码又显得非常冗余,很丑:
@PostMapping("insertUser")
public Result<Boolean> insertUser(@RequestBody User user) {if (user == null) {return Result.error(ExceptionCodeEnum.EMPTY_PARAM);}if (user.getId() == null || user.getId() <= 0) {return Result.error("id为空或小于0");}if (StringUtils.isEmpty(user.getName()) || user.getName().length() > 4) {return Result.error("姓名不符合规范");}if (user.getAge() < 18) {return Result.error("年龄不小于18");}if (StringUtils.isEmpty(user.getPhone()) || user.getPhone().length() != 11) {return Result.error("手机号码不正确");}return Result.success(userService.save(user));
}
但无论以什么方式进行参数校验,归根到底就是两种:
- 手动校验
- 自动校验
对应到实际编码的话,推荐:
- 封装ValidatorUtils
- 使用Spring Validation
其实对于上面两种方式,Spring都提供了解决方案。很多人只知道Spring Validation,却不知道简单好用的Assert。
public class SpringAssertTest {/*** Spring提供的Assert工具类,可以指定IllegalArgumentException的message** @param args*/public static void main(String[] args) {String name = "";
// Assert.hasText(name, "名字不能为空");Integer age = null;
// Assert.notNull(age, "年龄不能为空");Integer height = 180;Assert.isTrue(height > 185, "身高不能低于185");}
}
只要在全局异常处理IllegalArgumentException即可。但个人觉得还是自己封装自由度高一些,所以我们按照这个思路,写一个ValidatorUtils。
封装ValidatorUtils
封装ValidatorUtils也有两种思路:
- 校验并返回结果,调用者自行处理
- 校验失败直接抛异常
方式一:校验并返回结果,调用者自行处理
比如,方法只返回true/false:
public final class ValidatorUtils {private ValidatorUtils() {}/*** 校验id是否合法** @param id*/public static boolean isNotId(Long id) {if (id == null) {return true;}if (id < 0) {return true;}return false;}
}
调用者根据返回值自行处理(抛异常或者用Result封装):
@PostMapping("insertUser")
public Result<Boolean> insertUser(@RequestBody User user) {if (user == null) {return Result.error(ExceptionCodeEnum.EMPTY_PARAM);}// 对校验结果进行判断并返回,也可以抛异常让@RestControllerAdvice兜底if (ValidatorUtils.isNotId(user.getId())) {return Result.error("id为空或小于0");}return Result.success(userService.save(user));
}
这种方式,本质上和不封装差不多...
方式二:校验失败直接抛异常
这种形式一般会结合@RestControllerAdvice进行全局异常处理:
public final class ValidatorUtils {private ValidatorUtils() {}// 错误信息模板private static final String IS_EMPTY = "%s不能为空";private static final String LESS_THAN_ZERO = "%s不能小于0";/*** 校验参数是否为null** @param param* @param fieldName*/public static void checkNull(Object param, String fieldName) {if (param == null) {// ValidatorException是自定义异常throw new ValidatorException(String.format(IS_EMPTY, fieldName));}}/*** 校验id是否合法** @param id* @param fieldName*/public static void checkId(Long id, String fieldName) {if (id == null) {throw new ValidatorException(String.format(IS_EMPTY, fieldName));}if (id < 0) {throw new ValidatorException(String.format(LESS_THAN_ZERO, fieldName));}}
}
@PostMapping("updateUser")
public Result<Boolean> updateUser(@RequestBody User user) {// 一连串的校验ValidatorUtils.checkNull(user, "user");ValidatorUtils.checkId(user.getId(), "用户id");return Result.success(true);
}
@Slf4j
@RestControllerAdvice
public class GlobalExceptionHandler {/* 省略业务异常、运行时异常处理*//*** ValidatorUtils校验异常* @see ValidatorUtils** @param e* @return*/@ExceptionHandler(ValidatorException.class)public Result<ExceptionCodeEnum> handleValidatorException(ValidatorException e) {// 打印精确的参数错误日志,方便后端排查log.warn("参数校验异常: {}", e.getMessage(), e);// 一般来说,给客户端展示“参数错误”等泛化的错误信息即可,联调时可以返回精确的信息:e.getMessage()return Result.error(ExceptionCodeEnum.ERROR_PARAM);}
}
代码
具体选择哪种,看个人喜好啦。这里给出第二种封装形式(也可以改成第一种):
public final class ValidatorUtils {private ValidatorUtils() {}private static final String IS_EMPTY = "%s不能为空";private static final String LESS_THAN_ZERO = "%s不能小于0";private static final String LENGTH_OUT_OF_RANGE = "%s长度要在%d~%d之间";private static final String LENGTH_LESS_THAN = "%s长度不能小于%d";private static final String LENGTH_GREATER_THAN = "%s长度不能大于%d";private static final String ILLEGAL_PARAM = "%s不符合规则";// 手机号码正则,可以根据需要自行调整public static final String MOBILE = "1\\d{10}";/*** 是否为true** @param expression* @param message*/public static void isTrue(boolean expression, String message) {if (!expression) {throw new ValidatorException(message);}}/*** 校验参数是否为null** @param param* @param fieldName*/public static void checkNull(Object param, String fieldName) {if (param == null) {throw new ValidatorException(String.format(IS_EMPTY, fieldName));}}/*** 校验参数是否为null或empty** @param param* @param fieldName*/public static void checkNullOrEmpty(Object param, String fieldName) {if (param == null) {throw new ValidatorException(String.format(IS_EMPTY, fieldName));}if (param instanceof CharSequence) {if (param instanceof String && "null".equals(((String) param).toLowerCase())) {throw new ValidatorException(String.format(IS_EMPTY, fieldName));}if (isBlank((CharSequence) param)) {throw new ValidatorException(String.format(IS_EMPTY, fieldName));}}if (isCollectionsSupportType(param) && sizeIsEmpty(param)) {throw new ValidatorException(String.format(IS_EMPTY, fieldName));}}/*** 校验id是否合法** @param id* @param fieldName*/public static void checkId(Long id, String fieldName) {if (id == null) {throw new ValidatorException(String.format(IS_EMPTY, fieldName));}if (id < 0) {throw new ValidatorException(String.format(LESS_THAN_ZERO, fieldName));}}/*** 校验id是否合法** @param id* @param fieldName*/public static void checkId(Integer id, String fieldName) {if (id == null) {throw new ValidatorException(String.format(IS_EMPTY, fieldName));}if (id < 0) {throw new ValidatorException(String.format(LESS_THAN_ZERO, fieldName));}}/*** 校验参数字符串** @param param 字符串参数* @param min 最小长度* @param max 最大长度*/public static void checkLength(String param, int min, int max, String fieldName) {if (param == null || "".equals(param)) {throw new ValidatorException(String.format(IS_EMPTY, fieldName));}int length = param.length();if (length < min || length > max) {throw new ValidatorException(String.format(LENGTH_OUT_OF_RANGE, fieldName, min, max));}}/*** 校验参数字符串** @param param 字符串参数* @param min 最小长度*/public static void checkMinLength(String param, int min, String fieldName) {if (param == null || "".equals(param)) {throw new ValidatorException(String.format(IS_EMPTY, fieldName));}if (param.length() < min) {throw new ValidatorException(String.format(LENGTH_LESS_THAN, fieldName, min));}}/*** 校验参数字符串** @param param 字符串参数* @param max 最大长度*/public static void checkMaxLength(String param, int max, String fieldName) {if (param == null || "".equals(param)) {throw new ValidatorException(String.format(IS_EMPTY, fieldName));}if (param.length() > max) {throw new ValidatorException(String.format(LENGTH_GREATER_THAN, fieldName, max));}}/*** 校验手机号是否合法** @param phone 手机号*/public static void checkPhone(String phone, String fieldName) {if (phone == null || "".equals(phone)) {throw new ValidatorException(String.format(IS_EMPTY, fieldName));}boolean matches = Pattern.matches(MOBILE, phone);if (!matches) {throw new ValidatorException(String.format(ILLEGAL_PARAM, fieldName));}}// --------- private method ----------private static boolean isBlank(CharSequence cs) {int strLen;if (cs != null && (strLen = cs.length()) != 0) {for (int i = 0; i < strLen; ++i) {if (!Character.isWhitespace(cs.charAt(i))) {return false;}}}return true;}private static boolean sizeIsEmpty(final Object object) {if (object == null) {return true;} else if (object instanceof Collection<?>) {return ((Collection<?>) object).isEmpty();} else if (object instanceof Map<?, ?>) {return ((Map<?, ?>) object).isEmpty();} else if (object instanceof Object[]) {return ((Object[]) object).length == 0;} else {try {return Array.getLength(object) == 0;} catch (final IllegalArgumentException ex) {throw new IllegalArgumentException("Unsupported object type: " + object.getClass().getName());}}}private static boolean isCollectionsSupportType(Object value) {boolean isCollectionOrMap = value instanceof Collection || value instanceof Map;return isCollectionOrMap || value.getClass().isArray();}
}
@Getter
@NoArgsConstructor
public class ValidatorException extends RuntimeException {/*** 自定义业务错误码*/private Integer code;/*** 系统源异常*/private Exception originException;/*** 完整的构造函数:参数错误码+参数错误信息+源异常信息** @param code 参数错误码* @param message 参数错误信息* @param originException 系统源异常*/public ValidatorException(Integer code, String message, Exception originException) {super(message);this.code = code;this.originException = originException;}/*** 构造函数:错误枚举+源异常信息** @param codeEnum*/public ValidatorException(ExceptionCodeEnum codeEnum, Exception originException) {this(codeEnum.getCode(), codeEnum.getDesc(), originException);}/*** 构造函数:参数错误信息+源异常信息** @param message 参数错误信息* @param originException 系统源错误*/public ValidatorException(String message, Exception originException) {this(ExceptionCodeEnum.ERROR_PARAM.getCode(), message, originException);}/*** 构造函数:错误枚举** @param codeEnum 错误枚举*/public ValidatorException(ExceptionCodeEnum codeEnum) {this(codeEnum.getCode(), codeEnum.getDesc(), null);}/*** 构造函数:参数错误信息** @param message 参数错误信息*/public ValidatorException(String message) {this(ExceptionCodeEnum.ERROR_PARAM.getCode(), message, null);}
}
Spring Validation
Spring也封装了一套基于注解的参数校验逻辑,常用的有:
- @Validated
- @NotNull
- @NotBlank
- @NotEmpty
- @Positive
- @Length
- @Max
- @Min
大家可能之前听说过@Valid,它和@Validated有什么关系呢?@Valid是JSR303规定的,@Validated是Spring扩展的,@Validated相对来说功能更加强大,推荐优先使用@Validated。
SpringBoot2.3.x之前可以直接使用@Validated及@Valid,SpringBoot2.3.x以后需要额外引入依赖:
<dependency><groupId>org.hibernate</groupId><artifactId>hibernate-validator</artifactId><version>6.0.1.Final</version>
</dependency>
GET散装参数校验:ConstraintViolationException
实际开发中,如果某个GET接口只有一两个参数,可以使用“散装”的参数列表(注意类上加@Validated):
@Slf4j
@Validated
@RestController
public class UserController {@GetMapping("getUser")public Result<User> getUser(@NotNull(message = "部门id不能为空") Long departmentId,@NotNull(message = "年龄不能为空")@Max(value = 35, message = "年龄不超过35")@Min(value = 18, message = "年龄不小于18") Integer age) {return Result.success(null);}
}
如果@RestControllerAdvice没有捕获对应的异常,会返回SpringBoot默认的异常JSON:
服务端则抛出ConstraintViolationException:
这样的提示不够友好,我们可以按之前的思路,为ConstraintViolationException进行全局异常处理:
@Slf4j
@RestControllerAdvice
public class GlobalExceptionHandler {/* 省略业务异常、运行时异常等其他异常处理*//*** ConstraintViolationException异常** @param e* @return* @see ValidatorUtils*/@ExceptionHandler(ConstraintViolationException.class)public Result<ExceptionCodeEnum> handleConstraintViolationException(ConstraintViolationException e) {log.warn("参数错误: {}", e.getMessage(), e);// 一般只需返回泛化的错误信息,比如“参数错误”return Result.error(ExceptionCodeEnum.ERROR_PARAM, e.getMessage());}
}
格式觉得丑的话,可以自己调整。
GET DTO参数校验:BindException
@Data
public class User {@NotNull(message = "id不能为空")private Long id;@NotNull(message = "年龄不能为空")@Max(value = 35, message = "年龄不超过35")@Min(value = 18, message = "年龄不小于18")private Integer age;
}
@Slf4j
@RestController
public class UserController {/*** 如果都是用DTO包装参数,那么Controller可以不加@Validated(但建议还是都加上吧)* 参数列表里用@Validated或@Valid都可以** @param user* @return*/@GetMapping("getUser")public Result<User> getUser(@Validated User user) {System.out.println("进来了");return Result.success(null);}
}
你会发现,虽然参数校验确实生效了:
但是全局异常似乎没有捕获到这个异常,最终又交给了SpringBoot处理:
{"timestamp": "2021-02-08T02:57:27.025+00:00","status": 400,"error": "Bad Request","message": "","path": "/getUser"
}
这是怎么回事呢?
实际上,从GET“散装参数”变成“DTO参数”后,校验异常从ConstraintViolationException变成了BindException(见上面的截图),所以需要另外定义:
@Slf4j
@RestControllerAdvice
public class GlobalExceptionHandler {/* 省略业务异常、运行时异常等其他异常处理*//*** BindException异常** @param e* @return*/@ExceptionHandler(BindException.class)public Result<Map<String, String>> validationBindException(BindException e) {List<FieldError> fieldErrors = e.getBindingResult().getFieldErrors();String message = fieldErrors.stream().map(FieldError::getDefaultMessage).collect(Collectors.joining(" && "));log.error("参数错误: {}", message, e);return Result.error(ExceptionCodeEnum.ERROR_PARAM, message);}
}
重新请求:
{"code": 10000,"message": "id不能为空 && 年龄不小于18","data": null
}
POST参数校验:MethodArgumentNotValidException
@PostMapping("updateUser")
public Result<Boolean> updateUser(@Validated @RequestBody User user) {System.out.println("进来了");return Result.success(null);
}
和GET DTO参数校验形式上一样,但POST校验的异常又是另一种,所以全局异常处理又要加一种:
@Slf4j
@RestControllerAdvice
public class GlobalExceptionHandler {/* 省略业务异常、运行时异常等其他异常处理*//*** MethodArgumentNotValidException异常** @param e* @return*/@ExceptionHandler(MethodArgumentNotValidException.class)public Result<Map<String, String>> validationMethodArgumentNotValidException(MethodArgumentNotValidException e) {List<FieldError> fieldErrors = e.getBindingResult().getFieldErrors();String message = fieldErrors.stream().map(FieldError::getDefaultMessage).collect(Collectors.joining(" && "));log.error("参数错误: {}", message, e);return Result.error(ExceptionCodeEnum.ERROR_PARAM, message);}
}
代码
@Slf4j
@RestControllerAdvice
public class GlobalExceptionHandler {/*** 业务异常** @param* @return*/@ExceptionHandler(BizException.class)public Result<ExceptionCodeEnum> handleBizException(BizException bizException) {log.warn("业务异常:{}", bizException.getMessage(), bizException);return Result.error(bizException.getError());}/*** 运行时异常** @param e* @return*/@ExceptionHandler(RuntimeException.class)public Result<ExceptionCodeEnum> handleRunTimeException(RuntimeException e) {log.warn("运行时异常: {}", e.getMessage(), e);return Result.error(ExceptionCodeEnum.ERROR);}/*** ValidatorUtils校验异常** @param e* @return* @see ValidatorUtils*/@ExceptionHandler(ValidatorException.class)public Result<ExceptionCodeEnum> handleValidatorException(ValidatorException e) {// 打印精确的参数错误日志,方便后端排查log.warn("参数校验异常: {}", e.getMessage(), e);// 一般来说,给客户端展示泛化的错误信息即可,联调时可以返回精确的信息return Result.error(e.getMessage());}/*** ConstraintViolationException异常(散装GET参数校验)** @param e* @return* @see ValidatorUtils*/@ExceptionHandler(ConstraintViolationException.class)public Result<ExceptionCodeEnum> handleConstraintViolationException(ConstraintViolationException e) {log.warn("参数错误: {}", e.getMessage(), e);return Result.error(ExceptionCodeEnum.ERROR_PARAM, e.getMessage());}/*** BindException异常(GET DTO校验)** @param e* @return*/@ExceptionHandler(BindException.class)public Result<Map<String, String>> validationBindException(BindException e) {List<FieldError> fieldErrors = e.getBindingResult().getFieldErrors();String message = fieldErrors.stream().map(FieldError::getDefaultMessage).collect(Collectors.joining(" && "));log.error("参数错误: {}", message, e);return Result.error(ExceptionCodeEnum.ERROR_PARAM, message);}/*** MethodArgumentNotValidException异常(POST DTO校验)** @param e* @return*/@ExceptionHandler(MethodArgumentNotValidException.class)public Result<Map<String, String>> validationMethodArgumentNotValidException(MethodArgumentNotValidException e) {List<FieldError> fieldErrors = e.getBindingResult().getFieldErrors();String message = fieldErrors.stream().map(FieldError::getDefaultMessage).collect(Collectors.joining(" && "));log.error("参数错误: {}", message, e);return Result.error(ExceptionCodeEnum.ERROR_PARAM, message);}}
其他校验场景
Spring Validation还有一些校验场景,这里补充一下:
- 嵌套校验
- 分组校验
- List校验
嵌套校验
@Validated不支持嵌套校验,只能用@Valid:
@Data
public class User {@NotNull(message = "id不能为空")private Long id;@NotNull(message = "年龄不能为空")@Max(value = 35, message = "年龄不超过35")@Min(value = 18, message = "年龄不小于18")private Integer age;@NotNull(message = "所属部门不能为空")@Validprivate Department department;@Datastatic class Department {@NotNull(message = "部门编码不能为空")private Integer sn;@NotBlank(message = "部门名称不能为空")private String name;}
}
分组校验
@Data
public class User {@NotNull(message = "id不能为空", groups = {Update.class})private Long id;@NotNull(message = "年龄不能为空", groups = {Add.class, Update.class})@Max(value = 35, message = "年龄不超过35", groups = {Add.class, Update.class})@Min(value = 18, message = "年龄不小于18", groups = {Add.class, Update.class})private Integer age;public interface Add {}public interface Update {}
}
@Slf4j
@RestController
public class UserController {@PostMapping("insertUser")public Result<Boolean> insertUser(@Validated(User.Add.class) @RequestBody User user) {System.out.println("进来了");return Result.success(null);}@PostMapping("updateUser")public Result<Boolean> updateUser(@Validated(User.Update.class) @RequestBody User user) {System.out.println("进来了");return Result.success(null);}
}
有两点需要注意:
- interface Add这些接口只是做个标记,本身没有任何实际意义,可以抽取出来,作为单独的接口复用
- interface Add还可以继承Default接口
@Data
public class User {// 只在Update分组下生效@NotNull(message = "id不能为空", groups = {Update.class})private Long id;// 此时如果没执行Group,那么无论什么分组,都会校验@NotNull(message = "年龄不能为空")@Max(value = 35, message = "年龄不超过35")@Min(value = 18, message = "年龄不小于18")private Integer age;public interface Add extends Default {}public interface Update extends Default {}
}
继承Default后,除非显示指定,否则只要加了@NotNull等注解,就会起效。但显示指定Group后,就按指定的分组进行校验。比如,上面的id只会在update时校验生效。
个人不建议继承Default,一方面是理解起来比较乱,另一方是加了Default后就无法进行部分字段更新了。比如:
@PostMapping("updateUser")
public Result<Boolean> updateUser(@Validated(User.Update.class) @RequestBody User user) {System.out.println("进来了");return Result.success(null);
}
@Data
public class User {@NotNull(message = "id不能为空", groups = {Update.class})private Long id;@NotNull(message = "年龄不能为空")private Integer age;@NotBlank(message = "住址不能为空")private String address;public interface Add extends Default {}public interface Update extends Default {}
}
此时如果想更新name,就不能只传id和name了,address也要传(默认也会校验)。当然,你也可以认为一般情况下update前都会有getById(),所以更新时数据也是全量的。
List校验
Spring Validation不支持以下方式校验:
@Data
public class User {@NotNull(message = "id不能为空")private Long id;@NotNull(message = "年龄不能为空")private Integer age;
}
@PostMapping("updateBatchUser")
public Result<Boolean> updateBatchUser(@Validated @RequestBody List<User> list) {System.out.println(list);return Result.success(null);
}
即使age不填,还是进来了,说明对于List而言,@Validated根本没作用:
解决办法是,借鉴嵌套校验的模式,在List外面再包一层:
@PostMapping("updateBatchUser")
public Result<Boolean> updateBatchUser(@Validated @RequestBody ValidationList<User> userList) {System.out.println(userList);return Result.success(null);
}
public class ValidationList<E> implements List<E> {@NotEmpty(message = "参数不能为空")@Validprivate List<E> list = new LinkedList<>();@Overridepublic int size() {return list.size();}@Overridepublic boolean isEmpty() {return list.isEmpty();}@Overridepublic boolean contains(Object o) {return list.contains(o);}@Overridepublic Iterator<E> iterator() {return list.iterator();}@Overridepublic Object[] toArray() {return list.toArray();}@Overridepublic <T> T[] toArray(T[] a) {return list.toArray(a);}@Overridepublic boolean add(E e) {return list.add(e);}@Overridepublic boolean remove(Object o) {return list.remove(o);}@Overridepublic boolean containsAll(Collection<?> c) {return list.containsAll(c);}@Overridepublic boolean addAll(Collection<? extends E> c) {return list.addAll(c);}@Overridepublic boolean addAll(int index, Collection<? extends E> c) {return list.addAll(index, c);}@Overridepublic boolean removeAll(Collection<?> c) {return list.removeAll(c);}@Overridepublic boolean retainAll(Collection<?> c) {return list.retainAll(c);}@Overridepublic void clear() {list.clear();}@Overridepublic E get(int index) {return list.get(index);}@Overridepublic E set(int index, E element) {return list.set(index, element);}@Overridepublic void add(int index, E element) {list.add(index, element);}@Overridepublic E remove(int index) {return list.remove(index);}@Overridepublic int indexOf(Object o) {return list.indexOf(o);}@Overridepublic int lastIndexOf(Object o) {return list.lastIndexOf(o);}@Overridepublic ListIterator<E> listIterator() {return list.listIterator();}@Overridepublic ListIterator<E> listIterator(int index) {return list.listIterator(index);}@Overridepublic List<E> subList(int fromIndex, int toIndex) {return list.subList(fromIndex, toIndex);}public List<E> getList() {return list;}public void setList(List<E> list) {this.list = list;}}
实际开发时,建议专门建一个package存放Spring Validation相关的接口和类:
SpringValidatorUtils封装
一起来封装一个SpringValidatorUtils:
public final class SpringValidatorUtils {private SpringValidatorUtils() {}/*** 校验器*/private static final Validator validator = Validation.buildDefaultValidatorFactory().getValidator();/*** 校验参数** @param param 待校验的参数* @param groups 分组校验,比如Update.class(可以不传)* @param <T>*/public static <T> void validate(T param, Class<?>... groups) {Set<ConstraintViolation<T>> validateResult = validator.validate(param, groups);if (!CollectionUtils.isEmpty(validateResult)) {StringBuilder validateMessage = new StringBuilder();for (ConstraintViolation<T> constraintViolation : validateResult) {validateMessage.append(constraintViolation.getMessage()).append(" && ");}// 去除末尾的 &&validateMessage.delete(validateMessage.length() - 4, validateMessage.length());// 抛给全局异常处理throw new ValidatorException(validateMessage.toString());}}
}
代码很简单,做的事情本质是和@Validated是一模一样的。@Validated通过注解方式让Spring使用Validator帮我们校验,而SpringValidatorUtils则是我们从Spring那借来Validator自己校验:
@PostMapping("insertUser")
public Result<Boolean> insertUser(@RequestBody User user) {SpringValidatorUtils.validate(user);System.out.println("进来了");return Result.success(null);
}
此时不需要加@Validated。
买一送一,看看我之前一个同事封装的工具类(更加自由,调用者决定抛异常还是返回错误信息):
public final class ValidationUtils {private static final Validator DEFAULT_VALIDATOR = Validation.buildDefaultValidatorFactory().getValidator();private ValidationUtils() {}/*** 验证基于注解的对象** @param target*/public static <T> String validateReq(T target, boolean throwException) {if (null == target) {return errorProcess("校验对象不能为空", throwException);} else {Set<ConstraintViolation<T>> constraintViolations = DEFAULT_VALIDATOR.validate(target);ConstraintViolation<T> constraintViolation = Iterables.getFirst(constraintViolations, null);if (constraintViolation != null) {// 用户可以指定抛异常还是返回错误信息return errorProcess(constraintViolation.getPropertyPath() + ":" + constraintViolation.getMessage(),throwException);}}return "";}private static String errorProcess(String errorMsg, boolean throwException) {if (throwException) {throw new InvalidParameterException(errorMsg);}return errorMsg;}
}
OK,至此对Spring Validation的介绍结束。
为什么@Validated这么方便,还要封装这个工具类呢?首先,很多人搞不清楚@Validated的使用或者觉得注解很碍眼,不喜欢。其次,也是最重要的,如果你想在Service层做校验,使用SpringValidatorUtils会方便些(Service有接口和实现类,麻烦些)。当然,Service也能用注解方式校验。
参数校验就介绍到这,有更好的方式欢迎大家评论交流。我个人曾经特别喜欢Spring Validation,后来觉得其实使用工具类也蛮好,想校验啥就写啥,很细腻,不用考虑乱七八糟的分组,而Spring Validation有时需要花费很多心思在分组上,就有点本末倒置了。
最后抛出两个问题:
- 写完才发现,ValidatorUtils竟然用了static final抽取错误信息模板,然后利用String.format()拼接。会出现线程安全问题吗?
- 你知道如何设计山寨版的Spring Validation吗?(只需要实现@NotNull + ValidatorUtils,参考答案见评论区)
作者简介:大家好,我是smart哥,前中兴通讯、美团架构师,现某互联网公司CTO
进群,大家一起学习,一起进步,一起对抗互联网寒冬
相关文章:

再看参数校验
作者简介:大家好,我是smart哥,前中兴通讯、美团架构师,现某互联网公司CTO 联系qq:184480602,加我进群,大家一起学习,一起进步,一起对抗互联网寒冬 写一个接口,…...

计算机存储术语: 扇区,磁盘块,页
扇区(sector) 硬盘的读写以扇区为基本单位。磁盘上的每个磁道被等分为若干个弧段,这些弧段称之为扇区。硬盘的物理读写以扇区为基本单位。通常情况下每个扇区的大小是 512 字节。linux 下可以使用 fdisk -l 了解扇区大小: $ sudo /sbin/fdisk -l Disk …...

解决IDEA编译/启动报错:Abnormal build process termination
报错信息 报错信息如下: Abnormal build process termination: "D:\Software\Java\jdk\bin\java" -Xmx3048m -Djava.awt.headlesstrue -Djava.endorsed.dirs\"\" -Djdt.compiler.useSingleThreadtrue -Dpreload.project.path………………很纳…...

Jetpack DataStore
文章目录 Jetpack DataStore概述DataStore 对比 SP添加依赖库Preferences DataStore路径创建 Preferences DataStore获取数据保存数据修改数据删除数据清除全部数据 Proto DataStore配置AndroidStudio安装插件配置proto文件创建序列化器 创建 Proto DataStore获取数据保存数据修…...

在Portainer创建Nginx容器并部署Web静态站点实现公网访问
🔥博客主页: 小羊失眠啦. 🎥系列专栏:《C语言》 《数据结构》 《Linux》《Cpolar》 ❤️感谢大家点赞👍收藏⭐评论✍️ 前些天发现了一个巨牛的人工智能学习网站,通俗易懂,风趣幽默,…...

泛微e-cology XmlRpcServlet文件读取漏洞复现
漏洞介绍 泛微新一代移动办公平台e-cology不仅组织提供了一体化的协同工作平台,将组织事务逐渐实现全程电子化,改变传统纸质文件、实体签章的方式。泛微OA E-Cology 平台XmRpcServlet接口处存在任意文件读取漏洞,攻击者可通过该漏洞读取系统重要文件 (如数据库配置…...

当下流行的直播技术demo演示
nginx-http-flv-module(更新不是很频繁) SRS: https://ossrs.net/lts/zh-cn/(独立官网,目前最新稳定版version5) 基于SRS搭建直播demo演示: 一、搭建流媒体服务器 参见官网:https://ossrs.ne…...

Zabbix自动发现并注册已安装agent的主机
先在被监控主机上安装好zabbix-agent 然后登录zabbix网页 点击发现动作后会出现第三步 然后编辑操作,发现后加入到主机组群 然后编辑发现规则 然后就可以在主机列表中看到被发现的主机。...
Jtti:linux搭建开源ldap服务器的方法
搭建开源LDAP服务器是一种用于集中管理用户身份认证和授权信息的方法。在Linux系统上,OpenLDAP是一个流行的开源LDAP实现,可以用于搭建LDAP服务器。以下是搭建OpenLDAP服务器的基本步骤: 步骤一:安装OpenLDAP 安装OpenLDAP软件包&…...

Gazebo GUI模型编辑器
模型编辑器 现在我们将构建我们的简单机器人。我们将制作一个轮式车辆,并添加一个传感器,使我们能够让机器人跟随一个斑点(人)。 模型编辑器允许我们直接在图形用户界面 (GUI) 中构建简单的模型。对于更复…...

pycharm运行正常,但命令行执行提示module不存在的多种解决方式
问题描述 在执行某个测试模块时出现提示,显示自定义模块data不存在,但是在PyCharm下运行正常。错误信息如下: Traceback (most recent call last):File "/run/channelnterface-autocase/testcases/test_chanel_detail.py", line 2…...

GBASE南大通用GBase 8a ODBC的安装文件
GBASE南大通用GBase 8a ODBC 体系结构是基于五个组件,在下图中所示: GBase 8a ODBC 体系结构图 应用 应用是通过调用 ODBC API 实现对 GBase 数据访问的程序。应用使用标准的 ODBC 调用与驱动程序管理器通信。应用并不关心数据存储在哪里ÿ…...

重新配置torch1.8 cuda11.1 torchtext0.9.0虚拟Pytorch开发环境
这里写目录标题 起因发现选择安装cuda 11.1核对下自己的显卡是否支持下载该版本的CUDACUDA下载地址CUDA安装过程 在anaconda中创建一个虚拟环境1.以下是环境的配置过程2.查看虚拟环境列表3.激活虚拟环境 安装torch和torchtext包的过程1.输入下面这句代码,就可以直接…...

【动画图解】一次理清九大排序算法!面试官问到再也不慌!
排序算法 交换排序 冒泡排序快速排序 插入排序 直接插入排序希尔排序 选择排序 简单选择排序堆排序 归并排序基数排序桶排序 一、冒泡排序 冒泡排序是一种简单的交换排序算法,以升序排序为例,其核心思想是: 从第一个元素开始,…...
组播地址段及其作用
作用 组播(Multicast)传输:在发送者和每一接收者之间实现点对多点网络连接。如果一台发送者同时给多个的接收者传输相同的数据,也只需复制一份的相同数据包。它提高了数据传送效率。减少了骨干网络出现拥塞的可能性。 地址段 组播协议的地址在 IP 协议中属于 D 类…...

Vue+ElementUI前端添加展开收起搜索框按钮
1、搜索框添加判断 v-if"advanced" <el-form-item label"创建日期" v-if"advanced"><el-date-pickerv-model"daterangeLedat"size"small"style"width: 240px"value-format"yyyy-MM-dd"type&q…...
速盾网络:sdk游戏盾有什么作用?
速盾cdn是一款非常优秀的CDN加速服务,它能够帮助游戏开发者们提升游戏的性能和稳定性。其中,速盾cdn的sdk游戏盾是其一项非常实用的功能,它能够为游戏提供更加稳定和快速的网络连接。 首先,让我们来了解一下什么是sdk游戏…...
理解BeEF的架构
BeEF的组件和工作原理BeEF(The Browser Exploitation Framework)是一款用于浏览器渗透测试和漏洞利用的强大工具。它由多个组件组成,这些组件协同工作以实现对受害者浏览器的控制和攻击。本文将深入探讨BeEF的各个组件和其工作原理࿰…...

esp32-s3训练自己的数据进行目标检测、图像分类
esp32-s3训练自己的数据进行目标检测、图像分类 一、下载项目二、环境三、训练和导出模型四、部署模型五、存在的问题 esp-idf的安装参考我前面的文章: esp32cam和esp32-s3烧录human_face_detect实现人脸识别 一、下载项目 训练、转换模型:ModelAssist…...

华为设备VRP基础
交换机可以隔离冲突域,路由器可以隔离广播域,这两种设备在企业网络中应用越来越广泛。随着越来越多的终端接入到网络中,网络设备的负担也越来越重,这时网络设备可以通过华为专有的VRP系统来提升运行效率。通用路由平台VRP…...

基于ASP.NET+ SQL Server实现(Web)医院信息管理系统
医院信息管理系统 1. 课程设计内容 在 visual studio 2017 平台上,开发一个“医院信息管理系统”Web 程序。 2. 课程设计目的 综合运用 c#.net 知识,在 vs 2017 平台上,进行 ASP.NET 应用程序和简易网站的开发;初步熟悉开发一…...
系统设计 --- MongoDB亿级数据查询优化策略
系统设计 --- MongoDB亿级数据查询分表策略 背景Solution --- 分表 背景 使用audit log实现Audi Trail功能 Audit Trail范围: 六个月数据量: 每秒5-7条audi log,共计7千万 – 1亿条数据需要实现全文检索按照时间倒序因为license问题,不能使用ELK只能使用…...

linux arm系统烧录
1、打开瑞芯微程序 2、按住linux arm 的 recover按键 插入电源 3、当瑞芯微检测到有设备 4、松开recover按键 5、选择升级固件 6、点击固件选择本地刷机的linux arm 镜像 7、点击升级 (忘了有没有这步了 估计有) 刷机程序 和 镜像 就不提供了。要刷的时…...
Matlab | matlab常用命令总结
常用命令 一、 基础操作与环境二、 矩阵与数组操作(核心)三、 绘图与可视化四、 编程与控制流五、 符号计算 (Symbolic Math Toolbox)六、 文件与数据 I/O七、 常用函数类别重要提示这是一份 MATLAB 常用命令和功能的总结,涵盖了基础操作、矩阵运算、绘图、编程和文件处理等…...

人机融合智能 | “人智交互”跨学科新领域
本文系统地提出基于“以人为中心AI(HCAI)”理念的人-人工智能交互(人智交互)这一跨学科新领域及框架,定义人智交互领域的理念、基本理论和关键问题、方法、开发流程和参与团队等,阐述提出人智交互新领域的意义。然后,提出人智交互研究的三种新范式取向以及它们的意义。最后,总结…...

push [特殊字符] present
push 🆚 present 前言present和dismiss特点代码演示 push和pop特点代码演示 前言 在 iOS 开发中,push 和 present 是两种不同的视图控制器切换方式,它们有着显著的区别。 present和dismiss 特点 在当前控制器上方新建视图层级需要手动调用…...
C++课设:简易日历程序(支持传统节假日 + 二十四节气 + 个人纪念日管理)
名人说:路漫漫其修远兮,吾将上下而求索。—— 屈原《离骚》 创作者:Code_流苏(CSDN)(一个喜欢古诗词和编程的Coder😊) 专栏介绍:《编程项目实战》 目录 一、为什么要开发一个日历程序?1. 深入理解时间算法2. 练习面向对象设计3. 学习数据结构应用二、核心算法深度解析…...

uniapp 开发ios, xcode 提交app store connect 和 testflight内测
uniapp 中配置 配置manifest 文档:manifest.json 应用配置 | uni-app官网 hbuilderx中本地打包 下载IOS最新SDK 开发环境 | uni小程序SDK hbulderx 版本号:4.66 对应的sdk版本 4.66 两者必须一致 本地打包的资源导入到SDK 导入资源 | uni小程序SDK …...

数据结构第5章:树和二叉树完全指南(自整理详细图文笔记)
名人说:莫道桑榆晚,为霞尚满天。——刘禹锡(刘梦得,诗豪) 原创笔记:Code_流苏(CSDN)(一个喜欢古诗词和编程的Coder😊) 上一篇:《数据结构第4章 数组和广义表》…...
前端调试HTTP状态码
1xx(信息类状态码) 这类状态码表示临时响应,需要客户端继续处理请求。 100 Continue 服务器已收到请求的初始部分,客户端应继续发送剩余部分。 2xx(成功类状态码) 表示请求已成功被服务器接收、理解并处…...