需要定位的网站/优化设计卷子答案
作者简介:大家好,我是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…...

论文笔记 | 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安装部署及使用 发布时间:2020-07-28 12:33:12 阅读:29442 作者:苏黎世1995 栏目:关系型数据库 活动:开发者测试专用服务器限时活动,0元免费领,库存有限,领完即止&…...

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

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

Asp.Net Core 项目中常见中间件调用顺序
常用的 AspNetCore 项目中间件有这些,调用顺序如下图所示: 最后的 Endpoint 就是最终生成响应的中间件。 Configure调用如下: 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、虚拟机 虚拟机,Virtual Machine,一台虚拟的计算机,用来执行虚拟计算机指令。分为: 系统虚拟机:如VMware&am…...

[SWPUCTF 2021 新生赛]Do_you_know_http已
打开环境 它说用WLLM浏览器打开,使用BP抓包,发送到重发器 修改User-Agent 下一步,访问a.php 这儿他说添加一个本地地址,它给了一个183.224.40.160,我用了发现没用,然后重新添加一个地址:X-Forwa…...

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

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