五分钟带你了解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的接口数据校验的文章就介绍到这了,更多相关Java 接口数据校验内容请搜索我们以前的文章或继续浏览下面的相关文章希望大家以后多多支持我们!

(0)

相关推荐

  • 浅谈Java 三种方式实现接口校验

    本文介绍了Java 三种方式实现接口校验,主要包括AOP,MVC拦截器,分享给大家,具体如下: 方法一:AOP 代码如下定义一个权限注解 package com.thinkgem.jeesite.common.annotation; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import j

  • 五分钟带你了解Java的接口数据校验

    本篇文章给大家分享平时开发中总结的一点小技巧!在工作中写过Java程序的朋友都知道,目前使用Java开发服务最主流的方式就是通过Spring MVC定义一个Controller层接口,并将接口请求或返回参数分别定义在一个Java实体类中,这样Spring MVC在接收到Http请求(POST/GET)后,就会自动将请求报文自动映射成一个Java对象.这样的代码通常是这样写的: @RestController public class OrderController { @Autowired pr

  • 五分钟带你搞懂python 迭代器与生成器

    前言 大家周末好,今天给大家带来的是Python当中生成器和迭代器的使用. 我当初第一次学到迭代器和生成器的时候,并没有太在意,只是觉得这是一种新的获取数据的方法.对于获取数据的方法而言,我们会一种就足够了.但是在我后来Python的使用以及TensorFlow等学习使用当中,我发现很多地方都用到了迭代器和生成器,或者是直接使用,或者是借鉴了思路.今天就让我们仔细来看看,它们到底是怎么回事. 迭代器 我们先从迭代器开始入手,迭代器并不是Python独有的概念,在C++和Java当中都有itera

  • 五分钟带你快速学习Spring IOC

    目录 一.IOC底层原理: 1.IOC过程 2.IOC接口 二.ioc容器bean管理xml方式(创建对象和set注入) 三.ioc操作bean管理(基于xml方式) 四.IOC操作Bean管理(xml注入其他类型属性) 五.IOC操作Bean管理(注入属性-外部bean,内部bean,级联) 六.IOC操作Bean管理(xml注入集合属性) 七.IOC操作Bean管理(FactoryBean) 八.IOC操作Bean管理(bean作用域) 九.IOC操作Bean管理(bean生命周期) 十.I

  • 10分钟带你理解Java中的反射

    一.简介 Java 反射是可以让我们在运行时获取类的方法.属性.父类.接口等类的内部信息的机制.也就是说,反射本质上是一个"反着来"的过程.我们通过new创建一个类的实例时,实际上是由Java虚拟机根据这个类的Class对象在运行时构建出来的,而反射是通过一个类的Class对象来获取它的定义信息,从而我们可以访问到它的属性.方法,知道这个类的父类.实现了哪些接口等信息. 二.Class类 我们知道使用javac能够将.java文件编译为.class文件,这个.class文件包含了我们对

  • 10分钟带你理解Java中的弱引用

    前言 本文尝试从What.Why.How这三个角度来探索Java中的弱引用,帮助大家理解Java中弱引用的定义.基本使用场景和使用方法. 一. What--什么是弱引用? Java中的弱引用具体指的是java.lang.ref.WeakReference<T>类,我们首先来看一下官方文档对它做的说明: 弱引用对象的存在不会阻止它所指向的对象被垃圾回收器回收.弱引用最常见的用途是实现规范映射(canonicalizing mappings,比如哈希表). 假设垃圾收集器在某个时间点决定一个对象是

  • 一篇文章带你了解Java基础-接口

    目录 Java基础知识(接口) 接口 接口的定义 接口和抽象的区别 接口的格式 接口中的主要成分 接口的案例 接口与接口的关系 JDK 8之后的接口新增方法 总结 Java基础知识(接口) 接口 Java接口是一系列方法的声明,是一些方法特征的集合,一个接口只有方法的特征没有方法的实现,因此这些方法可以在不同的地方被不同的类实现,而这些实现可以具有不同的行为(功能). 接口的定义 接口: 在JAVA编程语言中是一个抽象类型,是抽象方法的集合,接口通常以interface来声明.一个类通过继承接口

  • 五分钟带你搞懂MySQL索引下推

    目录 什么是索引下推 索引下推优化的原理 索引下推的具体实践 没有使用ICP 使用ICP 索引下推使用条件 相关系统参数 总结 如果你在面试中,听到MySQL5.6"."索引优化" 之类的词语,你就要立马get到,这个问的是"索引下推". 什么是索引下推 索引下推(Index Condition Pushdown,简称ICP),是MySQL5.6版本的新特性,它能减少回表查询次数,提高查询效率. 索引下推优化的原理 我们先简单了解一下MySQL大概的架构:

  • 五分钟带你快速了解vue的常用实例方法

    目录 前言 一.vue实例方法和实例数据 1.vm.$set 2.vm.$delete 3.vm.$watch 二.实例方法和事件 1.vm.$on 2.vm.$emit 3.vm.$once 4.vm.$off 三.实例方法和生命周期 1.vm.$mount 2.vm.$destroy 3.vm.$nextTick 总结 前言 在了解vue的常用的实例方法之前,我们应该先要了解其常用的实例属性,你能了解到的vue实例属性有哪些呢?小编在这里就列举了几个常用的vue实例的属性.大家可以一起参考学

  • 五分钟教你手写 SpringBoot 本地事务管理实现

    白菜Java自习室 涵盖核心知识 1. SpringBoot 事务 一直在用 SpringBoot 中的 @Transactional 来做事务管理,但是很少没想过 SpringBoot 是如何实现事务管理的,今天从源码入手,看看 @Transactional 是如何实现事务的,最后我们结合源码的理解,自己动手写一个类似的注解来实现事务管理,帮助我们加深理解. 1.1. 事务的隔离级别 事务为什么需要隔离级别呢?这是因为在并发事务情况下,如果没有隔离级别会导致如下问题: 脏读 (Dirty Re

  • 五分钟解锁springboot admin监控新技巧

    最近这一个月由于项目进度紧张,将近一个月没有动静.分享一下最近体会的springboot监控的一些心得体会,供一些规模不是很大的团队做一些监控. 适用场景: 1.项目规模不大 2.用户量不是很大.并发要求不强 3.无专门运维力量 4.精致的团队规模 对于一些常规的项目,或者企业职责分工不是非常明确的单位来说.往往一个系统从需求到设计,开发,测试到最终上线,运维.往往80%的任务由开发团队来完成.由此,开发人员除了要实现系统的功能,还要为客户进行问题咨询答疑以及生产问题解决. 试想,一个应用上线后

随机推荐