乐趣区

关于java:五分钟带你了解Java是如何从容而优雅地实现接口数据校验

本篇文章给大家分享平时开发中总结的一点小技巧!在工作中写过 Java 程序的敌人都晓得,目前应用 Java 开发服务最支流的形式就是通过 Spring MVC 定义一个 Controller 层接口,并将接口申请或返回参数别离定义在一个 Java 实体类中,这样 Spring MVC 在接管到 Http 申请 (POST/GET) 后,就会主动将申请报文主动映射成一个 Java 对象。这样的代码通常是这样写的:

@RestController
public 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 类时间接编写这样的代码:

@Data
public 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
@Builder
public 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
@ControllerAdvice
public 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);
    }
}

当然,这样的对立报文格式也不仅仅只解决异样返回,失常的数据报文格式也能够通过该对象来进行对立封装!

本文内容从实用的角度给大家演示了,如何在日常工作中编写通用的数据校验逻辑,心愿能对大家有所帮忙!

写在最初

欢送大家关注我的公众号【惊涛骇浪如码】,海量 Java 相干文章,学习材料都会在外面更新,整顿的材料也会放在外面。

感觉写的还不错的就点个赞,加个关注呗!点关注,不迷路,继续更新!!!

退出移动版