本篇文章给大家分享平时开发中总结的一点小技巧!在工作中写过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 {        //字符串类型数组        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> resultSet = validator.validate(obj, groups);        if (resultSet.size() > 0) {            //如果存在错误结果,则将其解析并进行拼凑后异常抛出            List 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 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 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   返回的数据类型     * @return 响应结果     */    public static  ResponseResult OK(T data) {        return packageObject(data, GlobalCodeEnum.GL_SUCC_0);    }    /**     * 自定义系统异常信息     *     * @param code     * @param message 自定义消息     * @param      * @return     */    public static  ResponseResult systemException(Integer code, String message) {        return packageObject(null, code, message);    }}

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

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

写在最后

欢迎大家关注我的公众号【风平浪静如码】,海量Java相关文章,学习资料都会在里面更新,整理的资料也会放在里面。

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

c语言实现java接口_五分钟带你了解Java是如何从容而优雅地实现接口数据校验相关推荐

  1. 五分钟学会python函数_五分钟带你搞懂python 迭代器与生成器

    前言 大家周末好,今天给大家带来的是Python当中生成器和迭代器的使用. 我当初第一次学到迭代器和生成器的时候,并没有太在意,只是觉得这是一种新的获取数据的方法.对于获取数据的方法而言,我们会一种就 ...

  2. 语言叮叮消息接口_五分钟学后端技术:如何学习Java工程师必知必会的消息队列...

    原创声明 本文作者:黄小斜 转载请务必在文章开头注明出处和作者. 什么是消息队列 "RabbitMQ?""Kafka?""RocketMQ?" ...

  3. Java并发插件_五分钟,轻松掌握Java并发编程!

    Java作为最流行的编程语言之一,随着 Java 8的到来,越来越多的人开始学习,并深入研究!下面将介绍 Java并发编程,让开发者在最短的时间里掌握并发编程. 1. 并发 1.1. 什么是并发? 并 ...

  4. uml类图例子_五分钟带你读懂UML类图

    目录 先看下面这张类图,包括了UML类图的基本图示法 1.类的表示 2.接口的表示 3.继承关系 4.实现接口 5.关联关系 6.聚合关系 7.合成(组合)关系 8.依赖关系 先看下面这张类图,包括了 ...

  5. druid seata 配置_五分钟带你了解Seata分布式事务

    1.Seata介绍 Seata是由阿里中间件团队发起的开源项目 Fescar,后更名为Seata,它是一个是开源的分布式事务框架. 传统2PC的问题在Seata中得到了解决,它通过对本地关系数据库的分 ...

  6. c++ map用法_Python专题——五分钟带你了解map、reduce和filter

    点击上方蓝字,和我一起学技术.今天是Python专题第6篇文章,给大家介绍的是Python当中三个非常神奇的方法:map.reduce和filter.不知道大家看到map和reduce的时候有没有什么 ...

  7. 还在动手画棋盘 ?20分钟带你用Java写一个井字棋!

    还在动手画棋盘 ?20分钟带你用Java写一个井字棋! 文章目录 还在动手画棋盘 ?20分钟带你用Java写一个井字棋! 前言 设计过程 1.创建窗体类MyGameWindow 2.创建窗体的构造器 ...

  8. c++ stack 遍历_五分钟C语言数据结构 之 二叉树后序遍历(非递归很重要)

    五分钟C语言实现常见数据结构 今天的内容分享的是二叉树后序遍历 DP问题,欢迎关注 动态规划一篇就够了 全网最详细, 逐步理解, 万字总结 - Johngo的文章 - 知乎 https://zhuan ...

  9. lvds接口屏线安装图解_五分钟让你学会液晶拼接屏安装方法

    液晶拼接屏成为目前比较热门的大屏显示系统,技术比较成熟,清晰度高.维护方便,性价比也还不错,市场需求量较大.但是液晶拼接屏安装方法很多人并不了解,下面小编就来简单介绍一下.以46寸液晶拼接屏指导安装为 ...

最新文章

  1. WebClient UI和Tomcat的启动器
  2. 有关Android线程的学习
  3. 用C语言编程实现矩形波信号,信号与系统综合设计性实验
  4. pytorch 常用问题解决
  5. cisco路由器针对进行ip限速
  6. linux c fopen open 互相转换 文件指针 到 文件描述符 FILE* 转 FD
  7. Lambda方法推导(method references)
  8. python包接口,Typetalk聊天API的python接口包
  9. Asp.Net ajax 面向对象类型系统2 - 使用事件
  10. 前端自适应----单位rem
  11. 【重难点】【Java基础 04】值传递和引用传递、序列化和反序列化
  12. Ubuntu 图形界面入门
  13. 摩托罗拉E2卸载MPKG程序的简单方法
  14. 蚁群算法原理以及应用
  15. USB协议详解第29讲(USB设备状态及数据交互条件)
  16. macos 废纸篓强制删除文件文件夹
  17. 【Win10如何给桌面文件夹自定义图标】怎么改应用图标1.回收站,垃圾桶图标改为pop猫+2.如何把icon图标打包成dll然后在修改文件夹图标时在该dll中选择,用Resource Hacker
  18. 一、OpenAI ChatGPT 注册使用
  19. 高德地图商户标注平台上线 免费标注线下店铺
  20. 计算机数据采集管理系统的结构和功能,生产数据采集系统结构、功能及特点

热门文章

  1. 怎么查看这个历史最大连接session数
  2. iterator的使用和封个问题
  3. Unity3D第三人称摄像机控制脚本
  4. Tomcat6 内存和线程配置
  5. 解决mac上mariadb不能远程访问的问题
  6. 苹果小圆点怎么关闭_苹果手机连按2下屏幕,就能自动截图,不知道的来学一学...
  7. 【Python自学】万文字,学习框架+思维整理,入门就是这么简单
  8. 简单介绍python装饰器
  9. Vue 应用 Sass、Scss、Less 和 Stylus
  10. Lora模块项目01:简述物联网的各大无线通信技术