再看参数校验

作者简介:大家好,我是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 = "所属部门不能为空")
    @Valid
    private Department department;
    
    @Data
    static 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 = "参数不能为空")
    @Valid
    private List<E> list = new LinkedList<>();

    @Override
    public int size() {
        return list.size();
    }

    @Override
    public boolean isEmpty() {
        return list.isEmpty();
    }

    @Override
    public boolean contains(Object o) {
        return list.contains(o);
    }

    @Override
    public Iterator<E> iterator() {
        return list.iterator();
    }

    @Override
    public Object[] toArray() {
        return list.toArray();
    }

    @Override
    public <T> T[] toArray(T[] a) {
        return list.toArray(a);
    }

    @Override
    public boolean add(E e) {
        return list.add(e);
    }

    @Override
    public boolean remove(Object o) {
        return list.remove(o);
    }

    @Override
    public boolean containsAll(Collection<?> c) {
        return list.containsAll(c);
    }

    @Override
    public boolean addAll(Collection<? extends E> c) {
        return list.addAll(c);
    }

    @Override
    public boolean addAll(int index, Collection<? extends E> c) {
        return list.addAll(index, c);
    }

    @Override
    public boolean removeAll(Collection<?> c) {
        return list.removeAll(c);
    }

    @Override
    public boolean retainAll(Collection<?> c) {
        return list.retainAll(c);
    }

    @Override
    public void clear() {
        list.clear();
    }

    @Override
    public E get(int index) {
        return list.get(index);
    }

    @Override
    public E set(int index, E element) {
        return list.set(index, element);
    }

    @Override
    public void add(int index, E element) {
        list.add(index, element);
    }

    @Override
    public E remove(int index) {
        return list.remove(index);
    }

    @Override
    public int indexOf(Object o) {
        return list.indexOf(o);
    }

    @Override
    public int lastIndexOf(Object o) {
        return list.lastIndexOf(o);
    }

    @Override
    public ListIterator<E> listIterator() {
        return list.listIterator();
    }

    @Override
    public ListIterator<E> listIterator(int index) {
        return list.listIterator(index);
    }

    @Override
    public 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

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

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/262697.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

如何在Linux设置JumpServer实现无公网ip远程访问管理界面

文章目录 前言1. 安装Jump server2. 本地访问jump server3. 安装 cpolar内网穿透软件4. 配置Jump server公网访问地址5. 公网远程访问Jump server6. 固定Jump server公网地址 前言 JumpServer 是广受欢迎的开源堡垒机&#xff0c;是符合 4A 规范的专业运维安全审计系统。JumpS…

逆波兰计算器的完整代码

前置知识&#xff1a; 将中缀表达式转为List方法&#xff1a; //将一个中缀表达式转成中缀表达式的List//即&#xff1a;(3042)*5-6 》[(, 30, , 42, ), *, 5, -, 6]public static List<String> toIndixExpressionList(String s) {//定义一个List&#xff0c;存放中缀表达…

YACS(上海计算机学会竞赛平台)一星级题集——水仙花指数

题目描述 定义一个正整数的十进制表示中各位数字的立方和为它的水仙花指数&#xff0c;给定一个整数 n&#xff0c;请计算它的水仙花指数。 例如 n1234 时&#xff0c;水仙花指数为 输入格式 单个整数&#xff1a;表示 n 输出格式 单个整数&#xff1a;表示 n 的水仙花指…

深⼊理解指针

1. 内存和地址 1.1 内存 在讲内存和地址之前&#xff0c;我们想有个⽣活中的案例&#xff1a; 假设有⼀栋宿舍楼&#xff0c;把你放在楼⾥&#xff0c;楼上有100个房间&#xff0c;但是房间没有编号&#xff0c;你的⼀个朋友来找你玩&#xff0c; 如果想找到你&#xff0c;就…

嵌入式中的基本定时器

学习目标 理解基本定时器的作用掌握定时器开发流程掌握基本定时器中断处理的操作流程掌握AHB和APB时钟查询方式理解周期,分频系数,周期计数,分频计数。掌握调试策略学习内容 基本定时器 只能用于定时计时操作,没有输出引脚通道的定时器,在GD32中, TIMER5和TIMER6为基本…

Ubuntu 常用命令之 reboot 命令用法介绍

&#x1f4d1;Linux/Ubuntu 常用命令归类整理 reboot命令在Ubuntu系统中用于重新启动系统。这个命令通常需要管理员权限才能执行。 reboot命令的参数如下 -f 或 --force&#xff1a;强制重启&#xff0c;不调用shutdown -r进行友好重启。-p 或 --poweroff&#xff1a;在重启…

百度地图添加查询框

<div style"display:inline;float:left;margin: 10px"><el-input style"width: 400px" placeholder"输入地点" v-model"area" class"input-with-select"keyup.enter.native"searchMap"><el-button…

随机问卷调查数据的处理(uniapp)

需求&#xff1a;问卷调查 1.返回的数据中包含单选、多选、多项文本框、单文本框、图片上传 2.需要对必填的选项进行校验 3.非必填的多项文本框内容 如果不填写 不提交 表单数据格式 res{"code": 0,"msg": null,"data": [{"executeDay&…

lamda表达式(史上最全)

一、函数式接口 在jdk8中什么是函数式接口&#xff1a; 被FunctionalInterface注解修饰的。接口里边只有一个非default的方法。 满足以上2个条件的即为函数式接口&#xff0c;ps&#xff1a;即使一个接口没有FunctionalInterface修饰&#xff0c;但是满足2&#xff0c;那么这…

国产低成本Wi-Fi SoC解决方案芯片ESP8266与ESP8285对比差异

目录 ESP8266与ESP8285对比差异微信号&#xff1a;dnsj5343ESP8285简介ESP8285 主要特性Wi-Fi特性射频模块CPU特性硬件软件 ES8285 8266通用开发板 ESP8266与ESP8285对比差异 ESP8285相当于在ESP8266基础上多加了1/2MB Flash&#xff0c; ESP8285与ESP8266同用一套SDK&#xf…

异方差与多重共线性对回归问题的影响

异方差的检验 1.异方差的画图观察 2.异方差的假设检验&#xff0c;假设检验有两种&#xff0c;一般用怀特检验使用方法在ppt中&#xff0c;课程中也有实验&#xff0c;是一段代码。 异方差的解决办法 多重共线性 多重共线性可能带来的影响&#xff1a; 多重共线性的检验 多重…

德思特方案 | 德思特毫米波RIS研究测试方案:一站式助力工程师探索高频通信未来

来源&#xff1a;德思特测试测量 德思特方案 | 德思特毫米波RIS研究测试方案&#xff1a;一站式助力工程师探索高频通信未来 原文链接&#xff1a;德思特方案 | 德思特毫米波RIS研究测试方案&#xff1a;一站式助力工程师探索高频通信未来 欢迎关注虹科&#xff0c;为您提供最…

KubeSphere应用【五】发布镜像至Harbor

一、IDEA发布镜像至Docker 1.1IDEA安装Docker插件 1.2配置Docker服务器地址 1.3编写POM.XML文件 <?xml version"1.0" encoding"UTF-8"?> <project xmlns"http://maven.apache.org/POM/4.0.0" xmlns:xsi"http://www.w3.org/20…

Java学习常用实用类2

1 StringTokenizer类 字符串分析器&#xff0c;能够从一个字符串中根据指定的分隔符拆分出若干单词 StringTokenizer(String s) 使用默认分隔符集合&#xff0c;即&#xff1a;空格符、换行符、回车符、Tab符、进纸符 StringTokenizer(String s, String delim) 指定分…

cilium原理之ebpf尾调用与trace

背景 在深入剖析cilium原理之前&#xff0c;有两个关于epbf的基础内容需要先详细介绍一下&#xff1a; 1. ebpf尾调用 尾调用类似于程序之间的相互跳转&#xff0c;但它的功能更加强大。 2. trace 虽然之前使用trace_printk输出日志&#xff0c;但这个函数不能多用&#x…

数据仓库-数据治理小厂实践

一、简介 数据治理贯穿数仓中数据的整个生命周期&#xff0c;从数据的产生、加载、清洗、计算&#xff0c;再到数据展示、应用&#xff0c;每个阶段都需要对数据进行治理&#xff0c;像有些比较大的企业都是有自己的数据治理平台或者会开发一些便捷的平台&#xff0c;对于没有平…

从DevOps状态报告看技术团队的文化建设

本文源自一次内部分享&#xff0c;借由此机会又把历年的DevOps状态报告翻看了一遍&#xff0c;其实大多数时候我们对于DevOps的理解都在于流程&#xff0c;工具&#xff0c;实践这些看得见摸得着的东西&#xff0c;但就像文末的几点思考所说的那样&#xff0c;我们一直相信技术…

轴承故障诊断分类模型全家桶-最全教程

Python轴承故障诊断 (一)短时傅里叶变换STFT-CSDN博客 Python轴承故障诊断 (二)连续小波变换CWT-CSDN博客 Python轴承故障诊断 (三)经验模态分解EMD-CSDN博客 Pytorch-LSTM轴承故障一维信号分类(一)-CSDN博客 Pytorch-CNN轴承故障一维信号分类(二)-CSDN博客 Pytorch-Trans…

VM Group

在复杂方案中模块过多可能造成查看或修改方案时存在视觉混乱&#xff0c;不够直观。此时可利用Group模块进行模块整合&#xff0c;同时Group模式也兼容循环的功能&#xff0c;如下图所示。 双击Group模块可进入Group内部&#xff0c;如下图所示。 在Group模块单击 可设置输入、…

Spring Cloud+SpringBoot b2b2c:Java商城实现一件代发设置及多商家直播带货商城 免 费 搭 建

【saas云平台】打造全行业全渠道全场景的saas产品&#xff0c;为经营场景提供一体化解决方案&#xff1b;门店经营区域化、网店经营一体化&#xff0c;本地化、全方位、一站式服务&#xff0c;为多门店提供统一运营解决方案&#xff1b;提供丰富多样的营销玩法覆盖所有经营场景…