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

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

[[356232]]

本文转载自微信公众号「无敌码农」,作者无敌码农。转载本文请联系无敌码农公众号。

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

  1. @RestController 
  2. public class OrderController { 
  3.  
  4.     @Autowired 
  5.     private OrderService orderServiceImpl; 
  6.  
  7.     @PostMapping("/createOrder"
  8.     public CreateOrderBO validationTest(@Validated CreateOrderDTO createOrderDTO) { 
  9.         return orderServiceImpl.createOrder(createOrderDTO); 
  10.     } 

这样的代码相信大家并不陌生,但在后续的逻辑实现过程中却会遇到这样的问题:“在接收请求参数后如何实现报文对象数据值的合法性校验?”。一些同学也可能认为这并不是什么问题,因为具体某个参数字段是否为空、值的取值是否在约定范围、格式是否合法等等,在业务代码中校验就好了。例如可以在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类时直接编写这样的代码:

  1. @Data 
  2. public class CreateOrderDTO { 
  3.  
  4.     @NotNull(message = "订单号不能为空"
  5.     private String orderId; 
  6.     @NotNull(message = "订单金额不能为空"
  7.     @Min(value = 1, message = "订单金额不能小于0"
  8.     private Integer amount; 
  9.     @Pattern(regexp = "^1[3|4|5|7|8][0-9]{9}$", message = "用户手机号不合法"
  10.     private String mobileNo; 
  11.     private String orderType; 
  12.     private String status; 

如上所示代码,我们可以使用@NotNull注解来约束该字段必须不能为空,也可以使用@Min注解来约束字段的最小取值,或者还可以通过@Pattern注解来使用正则表达式来约束字段的格式(如手机号格式)等等。

以上这些注解都是“hibernate-validator”依赖包默认提供的,更多常用的注解还有很多,例如:

利用这些约束注解,我们就可以很轻松的搞定接口数据校验,而不需要在业务逻辑中编写大量的if-else来进行数据合法性校验。而定义好Bean参数对象并使用相关注解实现参数值约束后,在Controller层接口定义中只需要使用@Validated注解就可以实现在接收参数后自动进行数据绑定校验了,具体代码如下:

  1. @PostMapping("/createOrder"
  2. public CreateOrderBO validationTest(@Validated CreateOrderDTO createOrderDTO) { 
  3.     return orderServiceImpl.createOrder(createOrderDTO); 

如上所示,在Controller层中通过Spring提供的@Validated注解可以自动实现数据Bean的绑定校验,如果数据异常则会统一抛出校验异常!

约束性注解扩展

在“hibernate-validator”依赖jar包中,虽然提供了很多很方便的约束注解,但是也有不满足某些实际需要的情况,例如我们想针对参数中的某个值约定其值的枚举范围,如orderType订单类型只允许传“pay”、“refund”两种值,那么现有的约束注解可能就没有特别适用的了。此外,如果对这样的枚举值,我们还想在约束定义中直接匹配代码中的枚举定义,以更好地统一接口参数与业务逻辑的枚举定义。那么这种情况下,我们还可以自己扩展定义相应地约束注解逻辑。

接下来我们定义新的约束注解@EnumValue,来实现上面我们所说的效果,具体代码如下:

  1. @Target({METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER}) 
  2. @Retention(RUNTIME) 
  3. @Documented 
  4. @Constraint(validatedBy = {EnumValueValidator.class}) 
  5. public @interface EnumValue { 
  6.  
  7.     //默认错误消息 
  8.     String message() default "必须为指定值"
  9.  
  10.     //支持string数组验证 
  11.     String[] strValues() default {}; 
  12.  
  13.     //支持int数组验证 
  14.     int[] intValues() default {}; 
  15.  
  16.     //支持枚举列表验证 
  17.     Class<?>[] enumValues() default {}; 
  18.  
  19.     //分组 
  20.     Class<?>[] groups() default {}; 
  21.  
  22.     //负载 
  23.     Class<? extends Payload>[] payload() default {}; 
  24.  
  25.     //指定多个时使用 
  26.     @Target({FIELD, METHOD, PARAMETER, ANNOTATION_TYPE}) 
  27.     @Retention(RUNTIME) 
  28.     @Documented 
  29.     @interface List { 
  30.         EnumValue[] value(); 
  31.     } 
  32.  
  33.     /** 
  34.      * 校验类逻辑定义 
  35.      */ 
  36.     class EnumValueValidator implements ConstraintValidator<EnumValue, Object> { 
  37.  
  38.         //字符串类型数组 
  39.         private String[] strValues; 
  40.         //int类型数组 
  41.         private int[] intValues; 
  42.         //枚举类 
  43.         private Class<?>[] enumValues; 
  44.  
  45.         /** 
  46.          * 初始化方法 
  47.          * 
  48.          * @param constraintAnnotation 
  49.          */ 
  50.         @Override 
  51.         public void initialize(EnumValue constraintAnnotation) { 
  52.             strValues = constraintAnnotation.strValues(); 
  53.             intValues = constraintAnnotation.intValues(); 
  54.             enumValues = constraintAnnotation.enumValues(); 
  55.         } 
  56.  
  57.         /** 
  58.          * 校验方法 
  59.          * 
  60.          * @param value 
  61.          * @param context 
  62.          * @return 
  63.          */ 
  64.         @SneakyThrows 
  65.         @Override 
  66.         public boolean isValid(Object value, ConstraintValidatorContext context) { 
  67.             //针对字符串数组的校验匹配 
  68.             if (strValues != null && strValues.length > 0) { 
  69.                 if (value instanceof String) { 
  70.                     for (String s : strValues) {//判断值类型是否为Integer类型 
  71.                         if (s.equals(value)) { 
  72.                             return true
  73.                         } 
  74.                     } 
  75.                 } 
  76.             } 
  77.             //针对整型数组的校验匹配 
  78.             if (intValues != null && intValues.length > 0) { 
  79.                 if (value instanceof Integer) {//判断值类型是否为Integer类型 
  80.                     for (Integer s : intValues) { 
  81.                         if (s == value) { 
  82.                             return true
  83.                         } 
  84.                     } 
  85.                 } 
  86.             } 
  87.             //针对枚举类型的校验匹配 
  88.             if (enumValues != null && enumValues.length > 0) { 
  89.                 for (Class<?> cl : enumValues) { 
  90.                     if (cl.isEnum()) { 
  91.                         //枚举类验证 
  92.                         Object[] objs = cl.getEnumConstants(); 
  93.                         //这里需要注意,定义枚举时,枚举值名称统一用value表示 
  94.                         Method method = cl.getMethod("getValue"); 
  95.                         for (Object obj : objs) { 
  96.                             Object code = method.invoke(obj, null); 
  97.                             if (value.equals(code.toString())) { 
  98.                                 return true
  99.                             } 
  100.                         } 
  101.                     } 
  102.                 } 
  103.             } 
  104.             return false
  105.         } 
  106.     } 

如上所示的@EnumValue约束注解,是一个非常实用的扩展,通过该注解我们可以实现对参数取值范围(不是大小范围)的约束,它支持对int、string以及enum三种数据类型的约束,具体使用方式如下:

  1. /** 
  2.  * 定制化注解,支持参数值与指定类型数组列表值进行匹配(缺点是需要将枚举值写死在字段定义的注解中) 
  3.  */ 
  4. @EnumValue(strValues = {"pay""refund"}, message = "订单类型错误"
  5. private String orderType; 
  6. /** 
  7.  * 定制化注解,实现参数值与枚举列表的自动匹配校验(能更好地与实际业务开发匹配) 
  8.  */ 
  9. @EnumValue(enumValues = Status.class, message = "状态值不在指定范围"
  10. private String status; 

如上所示代码,该扩展注解既可以使用strValues或intValues属性来编程列举取值范围,也可以直接通过enumValues来绑定枚举定义。但是需要注意,处于通用考虑,具体枚举定义的属性的名称要统一匹配为value、desc,例如Status枚举定义如下:

  1. public enum Status { 
  2.     PROCESSING(1, "处理中"), 
  3.     SUCCESS(2, "订单已完成"); 
  4.     Integer value; 
  5.     String desc
  6.  
  7.     Status(Integer value, String desc) { 
  8.         this.value = value; 
  9.         this.desc = desc
  10.     } 
  11.  
  12.     public Integer getValue() { 
  13.         return value; 
  14.     } 
  15.  
  16.     public String getDesc() { 
  17.         return desc
  18.     } 

通过注解扩展,就能实现更多方便的约束性注解!

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

除了上面直接在Controller层使用@Validated进行绑定数据校验外,在有些情况,例如你的参数对象中的某个字段是一个复合对象,或者业务层的某个方法所定义的入参对象也需要进行数据合法性校验,那么这种情况下如何实现像Controller层一样的校验效果呢?

需要说明在这种情况下@Validated已经无法直接使用了,因为@Validated注解发挥作用主要是Spring MVC在接收参数的过程中实现了自动数据绑定校验,而在普通的业务方法或者复合参数对象中是没有办法直接绑定校验的。这种情况下,我们可以通过定义ValidateUtils工具类来实现一样的校验效果,具体代码如下:

  1. public class ValidatorUtils { 
  2.  
  3.     private static Validator validator = Validation.buildDefaultValidatorFactory().getValidator(); 
  4.  
  5.     /** 
  6.      * bean整体校验,有不合规范,抛出第1个违规异常 
  7.      */ 
  8.     public static void validate(Object obj, Class<?>... groups) { 
  9.         Set<ConstraintViolation<Object>> resultSet = validator.validate(obj, groups); 
  10.         if (resultSet.size() > 0) { 
  11.             //如果存在错误结果,则将其解析并进行拼凑后异常抛出 
  12.             List<String> errorMessageList = resultSet.stream().map(o -> o.getMessage()).collect(Collectors.toList()); 
  13.             StringBuilder errorMessage = new StringBuilder(); 
  14.             errorMessageList.stream().forEach(o -> errorMessage.append(o + ";")); 
  15.             throw new IllegalArgumentException(errorMessage.toString()); 
  16.         } 
  17.     } 

如上所示,我们定义了一个基于"javax.validation"接口的工具类实现,这样就可以在非@Validated直接绑定校验的场景中通过校验工具类来实现对Bean对象约束注解的校验处理,具体使用代码如下:

  1. public boolean orderCheck(OrderCheckBO orderCheckBO) { 
  2.     //对参数对象进行数据校验 
  3.     ValidatorUtils.validate(orderCheckBO); 
  4.     return true

而方法入参对象则还是可以继续使用前面我们介绍的约束性注解进行约定,例如上述方法的入参对象定义如下:

  1. @Data 
  2. @Builder 
  3. public class OrderCheckBO { 
  4.  
  5.     @NotNull(message = "订单号不能为空"
  6.     private String orderId; 
  7.     @Min(value = 1, message = "订单金额不能小于0"
  8.     private Integer orderAmount; 
  9.     @NotNull(message = "创建人不能为空"
  10.     private String operator; 
  11.     @NotNull(message = "操作时间不能为空"
  12.     private String operatorTime; 

这样在编程体验上就可以整体上保持一致!

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

通过前面我们所讲的各种约束注解,我们实现了对Controller层接口以及业务方法参数对象的统一数据校验。而为了保持校验异常处理的统一处理和错误报文统一输出,我们还可以定义通用的异常处理机制,来保证各类数据校验错误都能以统一错误格式反馈给调用方。具体代码如下:

  1. @Slf4j 
  2. @ControllerAdvice 
  3. public class GlobalExceptionHandler { 
  4.     /** 
  5.      * 统一处理参数校验错误异常(非Spring接口数据绑定验证) 
  6.      * 
  7.      * @param response 
  8.      * @param e 
  9.      * @return 
  10.      */ 
  11.     @ExceptionHandler(BindException.class) 
  12.     @ResponseBody 
  13.     public ResponseResult<?> processValidException(HttpServletResponse response, BindException e) { 
  14.         response.setStatus(HttpStatus.INTERNAL_SERVER_ERROR.value()); 
  15.         //获取校验错误结果信息,并将信息组装 
  16.         List<String> errorStringList = e.getBindingResult().getAllErrors() 
  17.                 .stream().map(ObjectError::getDefaultMessage).collect(Collectors.toList()); 
  18.         String errorMessage = String.join("; ", errorStringList); 
  19.         response.setContentType("application/json;charset=UTF-8"); 
  20.         log.error(e.toString() + "_" + e.getMessage(), e); 
  21.         return ResponseResult.systemException(GlobalCodeEnum.GL_FAIL_9998.getCode(), 
  22.                 errorMessage); 
  23.     } 
  24.  
  25.     /** 
  26.      * 统一处理参数校验错误异常 
  27.      * 
  28.      * @param response 
  29.      * @param e 
  30.      * @return 
  31.      */ 
  32.     @ExceptionHandler(IllegalArgumentException.class) 
  33.     @ResponseBody 
  34.     public ResponseResult<?> processValidException(HttpServletResponse response, IllegalArgumentException e) { 
  35.         response.setStatus(HttpStatus.INTERNAL_SERVER_ERROR.value()); 
  36.         String errorMessage = String.join("; ", e.getMessage()); 
  37.         response.setContentType("application/json;charset=UTF-8"); 
  38.         log.error(e.toString() + "_" + e.getMessage(), e); 
  39.         return ResponseResult.systemException(GlobalCodeEnum.GL_FAIL_9998.getCode(), 
  40.                 errorMessage); 
  41.     } 
  42.  
  43.     ... 

如上所示,我们定义了针对前面两种数据校验方式的统一异常处理机制,这样数据校验的错误信息就能通过统一的报文格式反馈给调用端,从而实现接口数据报文的统一返回!

其中通用的接口参数对象ResponseResult的代码定义如下:

  1. @Data 
  2. @Builder 
  3. @NoArgsConstructor 
  4. @AllArgsConstructor 
  5. @JsonPropertyOrder({"code""message""data"}) 
  6. public class ResponseResult<T> implements Serializable { 
  7.  
  8.     private static final long serialVersionUID = 1L; 
  9.  
  10.     /** 
  11.      * 返回的对象 
  12.      */ 
  13.     @JsonInclude(JsonInclude.Include.NON_NULL) 
  14.     private T data; 
  15.     /** 
  16.      * 返回的编码 
  17.      */ 
  18.     private Integer code; 
  19.     /** 
  20.      * 返回的信息 
  21.      */ 
  22.     private String message; 
  23.  
  24.     /** 
  25.      * @param data 返回的数据 
  26.      * @param <T>  返回的数据类型 
  27.      * @return 响应结果 
  28.      */ 
  29.     public static <T> ResponseResult<T> OK(T data) { 
  30.         return packageObject(data, GlobalCodeEnum.GL_SUCC_0); 
  31.     } 
  32.  
  33.     /** 
  34.      * 自定义系统异常信息 
  35.      * 
  36.      * @param code 
  37.      * @param message 自定义消息 
  38.      * @param <T> 
  39.      * @return 
  40.      */ 
  41.     public static <T> ResponseResult<T> systemException(Integer code, String message) { 
  42.         return packageObject(null, code, message); 
  43.     } 

当然,这样的统一报文格式也不仅仅只处理异常返回,正常的数据报文格式也可以通过该对象来进行统一封装!

本文内容从实用的角度给大家演示了,如何在日常工作中编写通用的数据校验逻辑.

原文链接:https://mp.weixin.qq.com/s/9kKIDZYB7bR7jiC5vj6qMg

 

责任编辑:武晓燕 来源: 无敌码农
相关推荐

2023-03-28 08:07:12

2021-05-12 22:07:43

并发编排任务

2023-06-06 08:51:06

2020-07-07 07:33:12

Java单元集成

2021-03-24 10:20:50

Fonts前端代码

2020-11-05 18:30:32

接口测试

2024-11-13 16:37:00

Java线程池

2020-09-25 11:30:20

Java判空代码

2017-10-20 12:59:05

数据分层数据建设数据仓库

2024-06-05 09:17:31

Python数据清洗开发

2022-08-03 07:07:10

Spring数据封装框架

2020-02-05 14:05:21

Java技术数组

2020-04-10 10:22:12

Java判空编程语言

2020-03-26 11:04:00

Linux命令光标

2021-01-18 13:17:04

鸿蒙HarmonyOSAPP

2022-05-13 21:20:23

组件库样式选择器

2023-05-12 14:14:00

Java线程中断

2021-01-28 14:53:19

PHP编码开发

2022-05-24 06:07:48

JShack用户代码

2024-04-24 12:34:08

Spring事务编程
点赞
收藏

51CTO技术栈公众号