当前位置: 首页 > news >正文

再看参数校验

作者简介:大家好,我是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

进群,大家一起学习,一起进步,一起对抗互联网寒冬

相关文章:

再看参数校验

作者简介&#xff1a;大家好&#xff0c;我是smart哥&#xff0c;前中兴通讯、美团架构师&#xff0c;现某互联网公司CTO 联系qq&#xff1a;184480602&#xff0c;加我进群&#xff0c;大家一起学习&#xff0c;一起进步&#xff0c;一起对抗互联网寒冬 写一个接口&#xff0c…...

计算机存储术语: 扇区,磁盘块,页

扇区(sector) 硬盘的读写以扇区为基本单位。磁盘上的每个磁道被等分为若干个弧段&#xff0c;这些弧段称之为扇区。硬盘的物理读写以扇区为基本单位。通常情况下每个扇区的大小是 512 字节。linux 下可以使用 fdisk -l 了解扇区大小&#xff1a; $ sudo /sbin/fdisk -l Disk …...

解决IDEA编译/启动报错:Abnormal build process termination

报错信息 报错信息如下&#xff1a; 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静态站点实现公网访问

&#x1f525;博客主页&#xff1a; 小羊失眠啦. &#x1f3a5;系列专栏&#xff1a;《C语言》 《数据结构》 《Linux》《Cpolar》 ❤️感谢大家点赞&#x1f44d;收藏⭐评论✍️ 前些天发现了一个巨牛的人工智能学习网站&#xff0c;通俗易懂&#xff0c;风趣幽默&#xff0c;…...

泛微e-cology XmlRpcServlet文件读取漏洞复现

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

当下流行的直播技术demo演示

nginx-http-flv-module&#xff08;更新不是很频繁&#xff09; SRS: https://ossrs.net/lts/zh-cn/&#xff08;独立官网&#xff0c;目前最新稳定版version5&#xff09; 基于SRS搭建直播demo演示&#xff1a; 一、搭建流媒体服务器 参见官网&#xff1a;https://ossrs.ne…...

Zabbix自动发现并注册已安装agent的主机

先在被监控主机上安装好zabbix-agent 然后登录zabbix网页 点击发现动作后会出现第三步 然后编辑操作&#xff0c;发现后加入到主机组群 然后编辑发现规则 然后就可以在主机列表中看到被发现的主机。...

Jtti:linux搭建开源ldap服务器的方法

搭建开源LDAP服务器是一种用于集中管理用户身份认证和授权信息的方法。在Linux系统上&#xff0c;OpenLDAP是一个流行的开源LDAP实现&#xff0c;可以用于搭建LDAP服务器。以下是搭建OpenLDAP服务器的基本步骤&#xff1a; 步骤一&#xff1a;安装OpenLDAP 安装OpenLDAP软件包&…...

Gazebo GUI模型编辑器

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

pycharm运行正常,但命令行执行提示module不存在的多种解决方式

问题描述 在执行某个测试模块时出现提示&#xff0c;显示自定义模块data不存在&#xff0c;但是在PyCharm下运行正常。错误信息如下&#xff1a; Traceback (most recent call last):File "/run/channelnterface-autocase/testcases/test_chanel_detail.py", line 2…...

GBASE南大通用GBase 8a ODBC的安装文件

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

重新配置torch1.8 cuda11.1 torchtext0.9.0虚拟Pytorch开发环境

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

【动画图解】一次理清九大排序算法!面试官问到再也不慌!

排序算法 交换排序 冒泡排序快速排序 插入排序 直接插入排序希尔排序 选择排序 简单选择排序堆排序 归并排序基数排序桶排序 一、冒泡排序 冒泡排序是一种简单的交换排序算法&#xff0c;以升序排序为例&#xff0c;其核心思想是&#xff1a; 从第一个元素开始&#xff0c…...

组播地址段及其作用

作用 组播(Multicast)传输:在发送者和每一接收者之间实现点对多点网络连接。如果一台发送者同时给多个的接收者传输相同的数据&#xff0c;也只需复制一份的相同数据包。它提高了数据传送效率。减少了骨干网络出现拥塞的可能性。 地址段 组播协议的地址在 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加速服务&#xff0c;它能够帮助游戏开发者们提升游戏的性能和稳定性。其中&#xff0c;速盾cdn的sdk游戏盾是其一项非常实用的功能&#xff0c;它能够为游戏提供更加稳定和快速的网络连接。 首先&#xff0c;让我们来了解一下什么是sdk游戏…...

理解BeEF的架构

BeEF的组件和工作原理BeEF&#xff08;The Browser Exploitation Framework&#xff09;是一款用于浏览器渗透测试和漏洞利用的强大工具。它由多个组件组成&#xff0c;这些组件协同工作以实现对受害者浏览器的控制和攻击。本文将深入探讨BeEF的各个组件和其工作原理&#xff0…...

esp32-s3训练自己的数据进行目标检测、图像分类

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

华为设备VRP基础

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

论文笔记 | ICLR 2023 WikiWhy:回答和解释因果问题

文章目录 一、前言二、主要内容三、总结🍉 CSDN 叶庭云:https://yetingyun.blog.csdn.net/ 一、前言 ICLR 2023 | Accept: notable-top-5%:《WikiWhy: Answering and Explaining Cause-and-Effect Questions》 一段话总结:WikiWhy 是一个新的 QA 数据集,围绕一个新的任务…...

LC24. 两两交换链表中的节点

代码随想录 class Solution {// 举例子:假设两个节点 1 -> 2// 那么 head 1; next 2; next.next null// 那么swapPairs(next.next),传入的是null,再下一次递归中直接返回null// 因此 newNode null// 所以 next.next head; > 2.next 1; 2 -> 1// head.next…...

使用redis-rds-tools 工具分析redis rds文件

redis-rdb-tools安装部署及使用 发布时间&#xff1a;2020-07-28 12:33:12 阅读&#xff1a;29442 作者&#xff1a;苏黎世1995 栏目&#xff1a;关系型数据库 活动&#xff1a;开发者测试专用服务器限时活动&#xff0c;0元免费领&#xff0c;库存有限&#xff0c;领完即止&…...

C# Onnx yolov8 plane detection

C# Onnx yolov8 plane detection 效果 模型信息 Model Properties ------------------------- date&#xff1a;2023-12-22T10:57:49.823820 author&#xff1a;Ultralytics task&#xff1a;detect license&#xff1a;AGPL-3.0 https://ultralytics.com/license version&am…...

Oracle定时任务的创建与禁用/删除

在开始操作之前&#xff0c;先从三W开始&#xff0c;即我常说的what 是什么&#xff1b;why 为什么使用&#xff1b;how 如何使用。 一、Oracle定时器是什么 Oracle定时器是一种用于在特定时间执行任务或存储过程的工具&#xff0c;可以根据需求设置不同的时间段和频率来执行…...

Asp.Net Core 项目中常见中间件调用顺序

常用的 AspNetCore 项目中间件有这些&#xff0c;调用顺序如下图所示&#xff1a; 最后的 Endpoint 就是最终生成响应的中间件。 Configure调用如下&#xff1a; public void Configure(IApplicationBuilder app, IWebHostEnvironment env){if (env.IsDevelopment()){app.UseD…...

【JVM】一、认识JVM

文章目录 1、虚拟机2、Java虚拟机3、JVM的整体结构4、Java代码的执行流程5、JVM的分类6、JVM的生命周期 1、虚拟机 虚拟机&#xff0c;Virtual Machine&#xff0c;一台虚拟的计算机&#xff0c;用来执行虚拟计算机指令。分为&#xff1a; 系统虚拟机&#xff1a;如VMware&am…...

[SWPUCTF 2021 新生赛]Do_you_know_http已

打开环境 它说用WLLM浏览器打开&#xff0c;使用BP抓包&#xff0c;发送到重发器 修改User-Agent 下一步&#xff0c;访问a.php 这儿他说添加一个本地地址&#xff0c;它给了一个183.224.40.160&#xff0c;我用了发现没用&#xff0c;然后重新添加一个地址&#xff1a;X-Forwa…...

hadoop01_完全分布式搭建

hadoop完全分布式搭建 1 完全分布式介绍 Hadoop运行模式包括&#xff1a;本地模式&#xff08;计算的数据存在Linux本地&#xff0c;在一台服务器上 自己测试&#xff09;、伪分布式模式&#xff08;和集群接轨 HDFS yarn&#xff0c;在一台服务器上执行&#xff09;、完全分…...

【每日一题】得到山形数组的最少删除次数

文章目录 Tag题目来源解题思路方法一&#xff1a;最长递增子序列 写在最后 Tag 【最长递增子序列】【数组】【2023-12-22】 题目来源 1671. 得到山形数组的最少删除次数 解题思路 方法一&#xff1a;最长递增子序列 前后缀分解 根据前后缀思想&#xff0c;以 nums[i] 为山…...

办公室装修计入什么会计科目/山西seo排名厂家

try:#code except SomeError1 as e:#codeprint(e) except SomeError2 as e:#codeprint(e) else:#code 没出错的时候执行 finally:#code 不管有没有错都会执行 也可以这样写&#xff1a;try:#code except (SomeError1,SomeError2) as e:#code 所以异常类都继承自Exception,一般放…...

二手书网站建设日程表/天津seo托管

关键词 并行开发 代码复用 关注点分离经典的MVC架构模式MVC架构模式是经典设计模式中的经典&#xff0c;是一种编程的方法论。具有高度抽象的特征&#xff0c;经典MVC用简单的定义体现出解决复杂通用问题的办法&#xff0c;只有不断思考和体会才能用来解决不同情况下程序设计所…...

最新商城系统/百度搜索关键词排名人工优化

大家好&#xff0c;我是胖虎&#xff0c;一名历经沧桑&#xff0c;看透互联网行业百态的测试从业者。今天&#xff0c;就来细说我这一路走来的经历&#xff0c;通过我的经验分享和个人思考&#xff0c;也希望给你一些有价值的启发和帮助&#xff01; 选择改变&#xff0c;跳出舒…...

学做ps的网站有哪些/东莞网站制作十年乐云seo

在类中的静态函数里面不能使用$this因为静态不用实例化就生成了内存空间&#xff0c;而类需要实例化之后才生成内存空间&#xff0c;两个内存不在一起所以不能互相访问。所以下面的写法是错误的&#xff1b;class Book extends Goods {public $anthor;public $publisher;static…...

做教育培训网站/军事最新消息

1. Group By 语句简介&#xff1a; Group By语句从英文的字面意义上理解就是“根据(by)一定的规则进行分组(Group)”。它的作用是通过一定的规则将一个数据集划分成若干个小的区域&#xff0c;然后针对若干个小区域进行数据处理。 P.S. 这里真是体会到了一个好的命名的力量&…...

菜鸟学做网站/抚顺优化seo

http://jingyan.baidu.com/article/eae07827a820b71fec5485a9.html转载于:https://www.cnblogs.com/laowengdiaodayu/p/4626347.html...