再看参数校验

news2024/11/17 1:28:20
作者简介:大家好,我是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

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

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

redis基本用法学习(字符串类型基本操作)

字符串类型是redis支持的最简单的数据类型&#xff0c;同时最简单的键值对类型也是key和value都是单个字符串&#xff0c;本质上就是字符串之间的相互映射&#xff0c;redis官网String类型简介页面提到可以用于缓存HTML片段或页面内容。   redis支持设置/获取单个键值对&…

行为型设计模式(一)模版方法模式 迭代器模式

模板方法模式 Template 1、什么是模版方法模式 模版方法模式定义了一个算法的骨架&#xff0c;它将其中一些步骤的实现推迟到子类里面&#xff0c;使得子类可以在不改变算法结构的情况下重新定义算法中的某些步骤。 2、为什么使用模版方法模式 封装不变部分&#xff1a;模版…

2024 年 8 个顶级开源 LLM(大语言模型)

如果没有所谓的大型语言模型&#xff08;LLM&#xff09;&#xff0c;当前的生成式人工智能革命就不可能实现。LLM 基于 transformers&#xff08;一种强大的神经架构&#xff09;是用于建模和处理人类语言的 AI 系统。它们之所以被称为“大”&#xff0c;是因为它们有数亿甚至…

Axure的案例演示

增删改查&#xff1a; 在中继器里面展示照片

第三讲GNSS相关时间系统和转换 第四讲观测值的产生和分类 | GNSS(RTK)课程学习笔记day2

说明&#xff1a;以下笔记来自计算机视觉life吴桐老师课程&#xff1a;从零掌握GNSS、RTK定位[链接]&#xff0c;从零掌握RTKLIB[链接]。非原创&#xff01;且笔记仅供自身与大家学习使用&#xff0c;无利益目的。 第三讲 GNSS相关时间系统和转换 GPS卫星的位置在时间过程中是…

SpringCloud源码探析(十二)-基于SpringBoot开发自定义中间件

1.概述 中间件是一种介于操作系统和应用软件之间&#xff0c;为应用软件提供服务功能的软件&#xff0c;按功能划分有消息中间件&#xff08;Kafka、RocketMQ&#xff09;、通信中间件&#xff08;RPC通信中间件&#xff0c;dubbo等&#xff09;&#xff0c;应用服务器等。中间…

等保测评主要保护哪些方面的安全?

等保测评是经公安部认证的具有资质的测评机构&#xff0c;依据国家信息安全等级保护规范规定&#xff0c;受有关单位委托&#xff0c;按照有关管理规范和技术标准&#xff0c;对信息系统安全等级保护状况进行检测评估的活动。那么企业做等保“保”的是什么呢&#xff1f; 等保主…

《空气质量持续改善行动计划》发布,汉威科技助力蓝天保卫战

近日&#xff0c;国务院印发《空气质量持续改善行动计划》&#xff0c;这是继2013年“大气十条”之后的第三个国家层面的保卫蓝天行动计划。 计划要求协同推进降碳、减污、扩绿、增长&#xff0c;以改善空气质量为核心&#xff0c;以减少重污染天气和解决人民群众身边的突出大…

深入理解 HTTP 和 HTTPS:提升你的网站安全性(上)

&#x1f90d; 前端开发工程师&#xff08;主业&#xff09;、技术博主&#xff08;副业&#xff09;、已过CET6 &#x1f368; 阿珊和她的猫_CSDN个人主页 &#x1f560; 牛客高级专题作者、在牛客打造高质量专栏《前端面试必备》 &#x1f35a; 蓝桥云课签约作者、已在蓝桥云…

Android Studio问题解决:Gradle Download 下载超时 Connect reset

文章目录 一、遇到问题二、解决办法 一、遇到问题 Gradle Download下载超时Sync了很多次&#xff0c;一直失败 二、解决办法 手动通过gradle网站下载 https://gradle.org/releases/可能也会出现超时&#xff0c;最好开个VPN软件会比较快。 下载好的软件&#xff0c;放到本机的…

自定义IDEA代码补全插件

目标&#xff1a; 对于项目中的静态方法&#xff08;主要是各种工具类里的静态方法&#xff09;&#xff0c;可以在输入方法名时直接提示相关的静态方法&#xff0c;选中后自动补全代码&#xff0c;并导入静态类。 设计&#xff1a; 初步构想&#xff0c;用户选择要导入的文…

得物-Golang-记一次线上服务的内存泄露排查

1.出现内存泄漏 1.1 事发现场 在风和日丽的一天&#xff0c;本人正看着需求、敲着代码&#xff0c;展望美好的未来。突然收到一条内存使用率过高的告警。 1.2 证人证词 告警的这个项目&#xff0c;老代码是python的&#xff0c;最近一直在go化。随着go化率不断上升&#xff…

nodejs+vue+微信小程序+python+PHP协同过滤算法的电影推荐系统-计算机毕业设计推荐python

信息数据的处理完全依赖人工进行操作&#xff0c;会耗费大量的人工成本&#xff0c;特别是面对大量的数据信息时&#xff0c;传统人工操作不仅不能对数据的出错率进行保证&#xff0c; 所以电子化信息管理的出现就能缓解以及改变传统人工方式面临的处境&#xff0c;一方面可以确…

管理类联考——数学——真题篇——按题型分类——充分性判断题——蒙猜D

先看目录&#xff0c;除了2018年比较怪&#xff0c;其他最多2个D&#xff08;数学只有两个弟弟&#xff0c;一个大弟&#xff0c;一个小弟&#xff09; 文章目录 2023真题&#xff08;2023-16&#xff09;-D 2022真题&#xff08;2022-21&#xff09;-D-分析选项⇒是否等价⇒是…

pycharm中如何去除波浪线的设置

pycharm中&#xff0c;碰到恼人的红绿波浪线&#xff0c;打开’file-settings’&#xff0c;然后&#xff0c;参照如图设置&#xff0c;去除’effects’选项&#xff1a;

AWS 知识二:AWS同一个VPC下的ubuntu实例通过ldapsearch命令查询目录用户信息

前言&#xff1a; 前提&#xff1a;需要完成我的AWS 知识一创建一个成功运行的目录。 主要两个重要&#xff1a;1.本地windows如何通过SSH的方式连接到Ubuntu实例 2.ldapsearch命令的构成 一 &#xff0c;启动一个新的Ubuntu实例 1.创建一个ubuntu实例 具体创建实例步骤我就不…

深入了解常见的应用层网络协议

目录 1. HTTP协议 1.1. 工作原理 1.2. 应用场景 1.3. 安全性考虑 2. SMTP协议 2.1. 工作原理 2.2. 应用场景 2.3. 安全性考虑 3. FTP协议 3.1. 工作原理 3.2. 应用场景 3.3. 安全性考虑 4. DNS协议 4.1. 工作原理 4.2. 应用场景 4.3. 安全性考虑 5. 安全性考虑…

用23种设计模式打造一个cocos creator的游戏框架----(二十一)组合模式

1、模式标准 模式名称&#xff1a;组合模式 模式分类&#xff1a;结构型 模式意图&#xff1a;将对象组合成树型结构以表示“部分-整体”的层次结构。Composite 使得用户对单个对象和组合对象的使用具有一致性。 结构图&#xff1a; 适用于&#xff1a; 1、想表示对象的部分…

『OPEN3D』1.5.4 动手实现点云八叉树(OctoTree)最近邻

本专栏地址: https://blog.csdn.net/qq_41366026/category_12186023.html?spm=1001.2014.3001.5482 在二维和三维空间中,我们可以采用四叉树(Quad tree)和八叉树(Octree)这两种特定的数据结构来处理空间分割。这些树形结构可以看作是K-d树在不同维度下的扩展。…

C7练习题答案

一、单项选择题(本大题共20小题,每小题2分,共40分。在每小题给出的四个备选项中,选出一个正确的答案,并将所选项前的字母填写在答题纸的相应位置上。) 以下不是 C 语言的特点的是BA. C 简洁,紧凑 B.不能够编制出功能复杂的程序 C. C语言可以直接对硬件进行操作 D. 语言 C 语言…