查看原文
其他

Java 是如何优雅地实现接口数据校验的?

CSDN 2020-12-18

The following article is from 无敌码农 Author 无敌码农

作者 | 无敌码农  责编 | 张文
头图 | CSDN 下载自东方 IC
来源 | 无敌码农(ID:jiangqiaodege)
本篇文章给大家分享平时开发中总结的一点小技巧!
在工作中写过 Java 程序的朋友都知道,目前使用 Java 开发服务最主流的方式就是通过 Spring MVC 定义一个 Controller 层接口,并将接口请求或返回参数分别定义在一个 Java 实体类中,这样 Spring MVC 在接收到 Http 请求(POST/GET) 后,就会自动将请求报文自动映射成一个 Java 对象。这样的代码通常是这样写的:
@RestControllerpublic class OrderController {
@Autowired private OrderService orderServiceImpl;
@PostMapping("/createOrder") public CreateOrderBO validationTest(@Validated CreateOrderDTO createOrderDTO) { return orderServiceImpl.createOrder(createOrderDTO); }}
这样的代码相信大家并不陌生,但在后续的逻辑实现过程中却会遇到这样的问题:“在接收请求参数后如何实现报文对象数据值的合法性校验?”。一些同学也可能认为这并不是什么问题,因为具体某个参数字段是否为空、值的取值是否在约定范围、格式是否合法等等,在业务代码中校验就好了。例如可以在 Service实现类中对报文格式进行各种 if-else 的数据校验。
从功能上说冗余的 if-else 代码没啥毛病,但从代码的优雅性来说冗长的 if-else 代码会显得非常臃肿。接下来的内容将给大家介绍一种处理此类问题的实用方法。具体将从以下几个方面进行介绍:
  • 使用@Validated 注解实现 Controller 接口层数据直接绑定校验;

  • 扩展约束性注解实现数据取值范围的校验;

  • 更加灵活的对象数据合法性校验工具类封装;

  • 数据合法性校验结果异常统一返回处理;


Controller 接口层数据绑定校验

实际上在 Java 开发中目前普通使用的 Bean 数据校验工具是"hibernate-validator",它是一个 hibernete 独立的 jar 包,所以使用这个 jar 包并不需要一定要集成 Hibernete 框架。该jar包主要实现并扩展了 javax.validation (是一个基于 JSR-303 标准开发出来的 Bean 校验规范)接口。
由于 Spring Boot 在内部默认集成了"hibernate-validator",所以使用Spring Boot 构建的 Java 工程可以直接使用相关注解来实现 Bean 的数据校验。例如我们最常编写的 Controller 层接口参数对象,可以在定义 Bean 类时直接编写这样的代码:
@Datapublic class CreateOrderDTO {
@NotNull(message = "订单号不能为空") private String orderId; @NotNull(message = "订单金额不能为空") @Min(value = 1, message = "订单金额不能小于0") private Integer amount; @Pattern(regexp = "^1[3|4|5|7|8][0-9]{9}$", message = "用户手机号不合法") private String mobileNo; private String orderType; private String status;}
如上所示代码,我们可以使用@NotNull 注解来约束该字段必须不能为空,也可以使用@Min 注解来约束字段的最小取值,或者还可以通过@Pattern 注解来使用正则表达式来约束字段的格式(如手机号格式)等等。
以上这些注解都是“hibernate-validator”依赖包默认提供的,更多常用的注解还有很多,例如:
利用这些约束注解,我们就可以很轻松的搞定接口数据校验,而不需要在业务逻辑中编写大量的 if-else 来进行数据合法性校验。而定义好 Bean 参数对象并使用相关注解实现参数值约束后,在 Controller 层接口定义中只需要使用@Validated 注解就可以实现在接收参数后自动进行数据绑定校验了,具体代码如下:
@PostMapping("/createOrder")public CreateOrderBO validationTest(@Validated CreateOrderDTO createOrderDTO) { return orderServiceImpl.createOrder(createOrderDTO);}
如上所示,在 Controller 层中通过 Spring 提供的@Validated 注解可以自动实现数据 Bean 的绑定校验,如果数据异常则会统一抛出校验异常!

约束性注解扩展

在“hibernate-validator”依赖 jar 包中,虽然提供了很多很方便的约束注解,但是也有不满足某些实际需要的情况,例如我们想针对参数中的某个值约定其值的枚举范围,如 orderType 订单类型只允许传“pay”、“refund”两种值,那么现有的约束注解可能就没有特别适用的了。此外,如果对这样的枚举值,我们还想在约束定义中直接匹配代码中的枚举定义,以更好地统一接口参数与业务逻辑的枚举定义。那么这种情况下,我们还可以自己扩展定义相应地约束注解逻辑。
接下来我们定义新的约束注解@EnumValue,来实现上面我们所说的效果,具体代码如下:
@Target({METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER})@Retention(RUNTIME)@Documented@Constraint(validatedBy = {EnumValueValidator.class})public @interface EnumValue {
//默认错误消息 String message() default "必须为指定值";
//支持string数组验证 String[] strValues() default {};
//支持int数组验证 int[] intValues() default {};
//支持枚举列表验证 Class<?>[] enumValues() default {};
//分组 Class<?>[] groups() default {};
//负载 Class<? extends Payload>[] payload() default {};
//指定多个时使用 @Target({FIELD, METHOD, PARAMETER, ANNOTATION_TYPE}) @Retention(RUNTIME) @Documented @interface List { EnumValue[] value(); }
/** * 校验类逻辑定义 */ class EnumValueValidator implements ConstraintValidator<EnumValue, Object> {
//字符串类型数组 private String[] strValues; //int类型数组 private int[] intValues; //枚举类 private Class<?>[] enumValues;
/** * 初始化方法 * * @param constraintAnnotation */ @Override public void initialize(EnumValue constraintAnnotation) { strValues = constraintAnnotation.strValues(); intValues = constraintAnnotation.intValues(); enumValues = constraintAnnotation.enumValues(); }
/** * 校验方法 * * @param value * @param context * @return */ @SneakyThrows @Override public boolean isValid(Object value, ConstraintValidatorContext context) { //针对字符串数组的校验匹配 if (strValues != null && strValues.length > 0) { if (value instanceof String) { for (String s : strValues) {//判断值类型是否为Integer类型 if (s.equals(value)) { return true; } } } } //针对整型数组的校验匹配 if (intValues != null && intValues.length > 0) { if (value instanceof Integer) {//判断值类型是否为Integer类型 for (Integer s : intValues) { if (s == value) { return true; } } } } //针对枚举类型的校验匹配 if (enumValues != null && enumValues.length > 0) { for (Class<?> cl : enumValues) { if (cl.isEnum()) { //枚举类验证 Object[] objs = cl.getEnumConstants(); //这里需要注意,定义枚举时,枚举值名称统一用value表示 Method method = cl.getMethod("getValue"); for (Object obj : objs) { Object code = method.invoke(obj, null); if (value.equals(code.toString())) { return true; } } } } } return false; } }}
如上所示的@EnumValue 约束注解,是一个非常实用的扩展,通过该注解我们可以实现对参数取值范围(不是大小范围)的约束,它支持对 int、string 以及 enum 三种数据类型的约束,具体使用方式如下:
/** * 定制化注解,支持参数值与指定类型数组列表值进行匹配(缺点是需要将枚举值写死在字段定义的注解中) */@EnumValue(strValues = {"pay", "refund"}, message = "订单类型错误")private String orderType;/** * 定制化注解,实现参数值与枚举列表的自动匹配校验(能更好地与实际业务开发匹配) */@EnumValue(enumValues = Status.class, message = "状态值不在指定范围")private String status;
如上所示代码,该扩展注解既可以使用 strValues intValues 属性来编程列举取值范围,也可以直接通过 enumValues 来绑定枚举定义。但是需要注意,处于通用考虑,具体枚举定义的属性的名称要统一匹配为 value、desc,例如  Status 枚举定义如下:
public enum Status { PROCESSING(1, "处理中"), SUCCESS(2, "订单已完成"); Integer value; String desc;
Status(Integer value, String desc) { this.value = value; this.desc = desc; }
public Integer getValue() { return value; }
public String getDesc() { return desc; }}
通过注解扩展,就能实现更多方便的约束性注解!

更加灵活的数据校验工具类封装

除了上面直接在 Controller 层使用@Validated 进行绑定数据校验外,在有些情况,例如你的参数对象中的某个字段是一个复合对象,或者业务层的某个方法所定义的入参对象也需要进行数据合法性校验,那么这种情况下如何实现像  Controller 层一样的校验效果呢?
需要说明在这种情况下@Validated 已经无法直接使用了,因为@Validated 注解发挥作用主要是 Spring MVC 在接收参数的过程中实现了自动数据绑定校验,而在普通的业务方法或者复合参数对象中是没有办法直接绑定校验的。这种情况下,我们可以通过定义 ValidateUtils 工具类来实现一样的校验效果,具体代码如下:
public class ValidatorUtils {
private static Validator validator = Validation.buildDefaultValidatorFactory().getValidator();
/** * bean整体校验,有不合规范,抛出第1个违规异常 */ public static void validate(Object obj, Class<?>... groups) { Set<ConstraintViolation<Object>> resultSet = validator.validate(obj, groups); if (resultSet.size() > 0) { //如果存在错误结果,则将其解析并进行拼凑后异常抛出 List<String> errorMessageList = resultSet.stream().map(o -> o.getMessage()).collect(Collectors.toList()); StringBuilder errorMessage = new StringBuilder(); errorMessageList.stream().forEach(o -> errorMessage.append(o + ";")); throw new IllegalArgumentException(errorMessage.toString()); } }}
如上所示,我们定义了一个基于"javax.validation"接口的工具类实现,这样就可以在非@Validated 直接绑定校验的场景中通过校验工具类来实现对 Bean 对象约束注解的校验处理,具体使用代码如下:
public boolean orderCheck(OrderCheckBO orderCheckBO) { //对参数对象进行数据校验 ValidatorUtils.validate(orderCheckBO); return true;}
方法入参对象则还是可以继续使用前面我们介绍的约束性注解进行约定,例如上述方法的入参对象定义如下:
@Data@Builderpublic class OrderCheckBO {
@NotNull(message = "订单号不能为空") private String orderId; @Min(value = 1, message = "订单金额不能小于0") private Integer orderAmount; @NotNull(message = "创建人不能为空") private String operator; @NotNull(message = "操作时间不能为空") private String operatorTime;}
这样在编程体验上就可以整体上保持一致!


数据合法性校验结果异常统一处理

通过前面我们所讲的各种约束注解,我们实现了对 Controller 层接口以及业务方法参数对象的统一数据校验。而为了保持校验异常处理的统一处理和错误报文统一输出,我们还可以定义通用的异常处理机制,来保证各类数据校验错误都能以统一错误格式反馈给调用方。具体代码如下:
@Slf4j@ControllerAdvicepublic class GlobalExceptionHandler { /** * 统一处理参数校验错误异常(非Spring接口数据绑定验证) * * @param response * @param e * @return */ @ExceptionHandler(BindException.class) @ResponseBody public ResponseResult<?> processValidException(HttpServletResponse response, BindException e) { response.setStatus(HttpStatus.INTERNAL_SERVER_ERROR.value()); //获取校验错误结果信息,并将信息组装 List<String> errorStringList = e.getBindingResult().getAllErrors() .stream().map(ObjectError::getDefaultMessage).collect(Collectors.toList()); String errorMessage = String.join("; ", errorStringList); response.setContentType("application/json;charset=UTF-8"); log.error(e.toString() + "_" + e.getMessage(), e); return ResponseResult.systemException(GlobalCodeEnum.GL_FAIL_9998.getCode(), errorMessage); }
/** * 统一处理参数校验错误异常 * * @param response * @param e * @return */ @ExceptionHandler(IllegalArgumentException.class) @ResponseBody public ResponseResult<?> processValidException(HttpServletResponse response, IllegalArgumentException e) { response.setStatus(HttpStatus.INTERNAL_SERVER_ERROR.value()); String errorMessage = String.join("; ", e.getMessage()); response.setContentType("application/json;charset=UTF-8"); log.error(e.toString() + "_" + e.getMessage(), e); return ResponseResult.systemException(GlobalCodeEnum.GL_FAIL_9998.getCode(), errorMessage); }
...}
如上所示,我们定义了针对前面两种数据校验方式的统一异常处理机制,这样数据校验的错误信息就能通过统一的报文格式反馈给调用端,从而实现接口数据报文的统一返回!
其中通用的接口参数对象 ResponseResult 的代码定义如下:
@Data@Builder@NoArgsConstructor@AllArgsConstructor@JsonPropertyOrder({"code", "message", "data"})public class ResponseResult<T> implements Serializable {
private static final long serialVersionUID = 1L;
/** * 返回的对象 */ @JsonInclude(JsonInclude.Include.NON_NULL) private T data; /** * 返回的编码 */ private Integer code; /** * 返回的信息 */ private String message;
/** * @param data 返回的数据 * @param <T> 返回的数据类型 * @return 响应结果 */ public static <T> ResponseResult<T> OK(T data) { return packageObject(data, GlobalCodeEnum.GL_SUCC_0); }
/** * 自定义系统异常信息 * * @param code * @param message 自定义消息 * @param <T> * @return */ public static <T> ResponseResult<T> systemException(Integer code, String message) { return packageObject(null, code, message); }}
当然,这样的统一报文格式也不仅仅只处理异常返回,正常的数据报文格式也可以通过该对象来进行统一封装!
本文内容从实用的角度给大家演示了,如何在日常工作中编写通用的数据校验逻辑,希望能对大家有所帮助。

更多精彩推荐

程序员有钱了都干什么?买豪宅,玩跑车,上太空!| 涛滔不绝

☞分库分表:TiDB,求别抢饭碗!

☞他被称为印度“ IT 大王”,富可敌国,却精打细算如守财奴

☞带你「周游世界」的 MODNet 算法

☞一文聊“图”,从图数据库到知识图谱

红帽"干掉" CentOS 8,CentOS Stream 上位

科技垄断正在朝着纵向发展
点分享点点赞点在看

    您可能也对以下帖子感兴趣

    文章有问题?点此查看未经处理的缓存