• 优雅书写Controller(参数验证+统一异常处理)


    优雅书写Controller(参数验证+统一异常处理)


    最近开发了比较多的接口,因为没有可参考的案例,所以一开始一直按照我的理解进行开发。开发多了发现自己每个结果都写了相同的代码:try() {} catch() {}, 和关于参数判空的:StringUtils.empty(xxx)。秉着饱暖思淫欲的态度,开发结束后自然想下次更加优雅的开发。因此,使用了springboot的参数验证和统一异常处理。

    一,前期数据及类准备

    1.1 统一状态码

    对于不同的返回类型,我们应该要有不同对应的状态码。接口的返回类型在统一状态码中必须存在。

    package com.lmc.common.enums;
    
    /**
     * @author lmc
     * @Description: TODO 接口API返回状态码枚举
     * @Create 2022-06-26 13:16
     * @version: 1.0
     */
    public enum ResultCodeEnum {
    
        SUCCESS(1000, "请求成功"),
    
        FAILURE(1001, "请求失败"),
    
        VALIDATE_PARAMS_ERROR(1002, "参数校验失败");
    
        private int code;
    
        private String msg;
    
        ResultCodeEnum(int code, String msg) {
            this.code = code;
            this.msg = msg;
        }
    
        /**
         * 获取code
         * @return
         */
        public int getCode() {
            return code;
        }
    
        /**
         * 获取信息
         * @return
         */
        public String getMsg() {
            return msg;
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41

    1.2 统一返回格式

    统一状态码完成后,还需要定义统一返回格式,为了前端的方便调用

    package com.lmc.common.vo;
    
    import com.fasterxml.jackson.annotation.JsonFormat;
    import com.lmc.common.enums.ResultCodeEnum;
    import lombok.Data;
    
    import java.util.Date;
    
    /**
     * @author lmc
     * @Description: TODO 接口返回结果类型
     * @Create 2022-06-26 13:13
     * @version: 1.0
     */
    @Data
    public class ResultVo {
    
        /**
         * 状态码
         */
        private int code;
        /**
         * 状态码信息
         */
        private String msg;
        /**
         * 返回描述信息(预备为调用失败的情况下提供详细的失败原因)
         */
        private String desc;
        /**
         * 返回数据
         */
        private Object data;
        /**
         * 接口调用结束时间
         */
        @JsonFormat(locale="zh", timezone="GMT+8", pattern="yyyy-MM-dd HH:mm:ss")
        private Date searchTime;
    
        public ResultVo(int code, String msg, Object data) {
            this.code = code;
            this.msg = msg;
            this.data = data;
            this.searchTime = new Date();
        }
    
        /**
         * 调用成功时返回
         * @param data
         * @return
         */
        public static ResultVo success(Object data) {
            return new ResultVo(ResultCodeEnum.SUCCESS.getCode(), ResultCodeEnum.SUCCESS.getMsg(), data);
        }
    
        /**
         * 调用失败时返回
         * @param data
         * @return
         */
        public static ResultVo fail(Object data) {
            return new ResultVo(ResultCodeEnum.FAILURE.getCode(), ResultCodeEnum.FAILURE.getMsg(), data);
        }
    
        /**
         * 调用时指定状态码
         * @param enums
         * @param data
         * @return
         */
        public static ResultVo result(ResultCodeEnum enums, Object data) {
            return new ResultVo(enums.getCode(), enums.getMsg(), data);
        }
    
        public ResultVo withDesc(String desc) {
            this.desc = desc;
            return this;
        }
    
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80

    1.3 自定义接口API异常类

    然后再自定义接口的异常类,当然也可以不用,看个人喜好

    package pers.lmc.tools2.provider.exception;
    
    import com.lmc.common.enums.ResultCodeEnum;
    
    /**
     * @author lmc
     * @Description: TODO API异常类
     * @Create 2022-06-26 18:48
     * @version: 1.0
     */
    public class ApiException extends RuntimeException{
    
        private int code;
    
        private String msg;
    
        public ApiException(String msg) {
            super(msg);
            this.code = ResultCodeEnum.FAILURE.getCode();
            this.msg = ResultCodeEnum.FAILURE.getMsg();
        }
    
        public ApiException(ResultCodeEnum enums, String msg) {
            super(msg);
            this.code = enums.getCode();
            this.msg = enums.getMsg();
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28

    1.4 参数封装类

    为了调试参数验证,还需要自定义一个参数的封装类

    package pers.lmc.tools2.provider.vo;
    
    import lombok.Data;
    
    /**
     * @author lmc
     * @Description: TODO
     * @Create 2022-06-26 13:43
     * @version: 1.0
     */
    @Data
    public class Param01Vo {
    
        private String name;
    
        private Integer age;
    
        private Short sex;
    
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20

    二,参数验证

    参数验证需要用到springboot的validation依赖

    2.1 pom.xml

    		<dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-starter-validation</artifactId>
            </dependency>
    		<!--   关于校验     -->
            <dependency>
                <groupId>com.fasterxml.jackson.core</groupId>
                <artifactId>jackson-databind</artifactId>
                <version>${jackson.version}</version>
            </dependency>
    
            <dependency>
                <groupId>com.fasterxml.jackson.core</groupId>
                <artifactId>jackson-core</artifactId>
                <version>${jackson.version}</version>
            </dependency>
    
            <dependency>
                <groupId>com.fasterxml.jackson.core</groupId>
                <artifactId>jackson-annotations</artifactId>
                <version>${jackson.version}</version>
            </dependency>
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22

    2.2 修改参数封装类

    package pers.lmc.tools2.provider.vo;
    
    import lombok.Data;
    
    import javax.validation.constraints.Max;
    import javax.validation.constraints.Min;
    import javax.validation.constraints.NotNull;
    import javax.validation.constraints.Size;
    
    /**
     * @author lmc
     * @Description: TODO
     * @Create 2022-06-26 13:43
     * @version: 1.0
     */
    @Data
    public class Param01Vo {
    
        @NotNull(message = "名称不能为空")
        @Size(min = 1, max = 50, message = "名称name长度必须是1-50个字符")
        private String name;
    
        @NotNull(message = "年龄age不能为空")
        @Min(value = 10, message = "年龄age不能低于10岁")
        @Max(value = 25, message = "年龄age不能超过25岁")
        private Integer age;
    
        @Min(value = 0, message = "性别sex只能是0和1,0=女1=男")
        @Max(value = 1, message = "性别sex只能是0和1,0=女1=男")
        private Short sex;
    
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32

    在这里对该封装类的三个参数都做了限制

    2.3 controller

    在controller中对参数做验证时,需要在类上使用注解@Validated,同时在接口的该参数也使用注解@Valid

    package pers.lmc.tools2.provider.controller;
    
    import com.lmc.common.vo.ResultVo;
    import lombok.extern.slf4j.Slf4j;
    import org.springframework.validation.annotation.Validated;
    import org.springframework.web.bind.annotation.PostMapping;
    import org.springframework.web.bind.annotation.RequestBody;
    import org.springframework.web.bind.annotation.RequestMapping;
    import org.springframework.web.bind.annotation.RestController;
    import pers.lmc.tools2.provider.vo.Param01Vo;
    
    import javax.validation.Valid;
    
    /**
     * @author lmc
     * @Description: TODO
     * @Create 2022-06-26 17:10
     * @version: 1.0
     */
    @RestController
    @Validated
    @RequestMapping("/valicate")
    @Slf4j
    public class ValicateController {
    
        @PostMapping("/add")
        public ResultVo addParam01(@Valid @RequestBody Param01Vo param01Vo) {
            log.info("执行add()方法,参数:" + param01Vo.toString());
            return ResultVo.success(param01Vo);
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31

    2.4 测试

    开发完成,准备测试,到APIPost上访问 http://localhost:9003/provider/valicate/add,带上参数:

    {
        "name":"lmc",
        "age": 22,
        "sex": 1
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5

    访问成功,返回结果如下:

    {
    	"code": 1000,
    	"msg": "请求成功",
    	"desc": null,
    	"data": {
    		"name": "lmc",
    		"age": 22,
    		"sex": 1
    	},
    	"searchTime": "2022-06-26 19:59:55"
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    如果参数输入不正确,例如:

    {
        "name":"",
        "age": 220,
        "sex": 2
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5

    得到结果如下:

    {
    	"timestamp": "2022-06-26T12:02:21.748+00:00",
    	"status": 400,
    	"error": "Bad Request",
    	"message": "",
    	"path": "/provider/valicate/add"
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    日志是这样的:

    2022-06-26 20:02:21 [http-nio-9003-exec-1] WARN  o.s.w.s.m.support.DefaultHandlerExceptionResolver - Resolved [org.springframework.web.bind.MethodArgumentNotValidException: Validation failed for argument [0] in public com.lmc.common.vo.ResultVo pers.lmc.tools2.provider.controller.ValicateController.addParam01(pers.lmc.tools2.provider.vo.Param01Vo) with 3 errors: [Field error in object 'param01Vo' on field 'sex': rejected value [2]; codes [Max.param01Vo.sex,Max.sex,Max.java.lang.Short,Max]; arguments [org.springframework.context.support.DefaultMessageSourceResolvable: codes [param01Vo.sex,sex]; arguments []; default message [sex],1]; default message [性别sex只能是0和1,0=女1=男]] [Field error in object 'param01Vo' on field 'age': rejected value [220]; codes [Max.param01Vo.age,Max.age,Max.java.lang.Integer,Max]; arguments [org.springframework.context.support.DefaultMessageSourceResolvable: codes [param01Vo.age,age]; arguments []; default message [age],25]; default message [年龄age不能超过25岁]] [Field error in object 'param01Vo' on field 'name': rejected value []; codes [Size.param01Vo.name,Size.name,Size.java.lang.String,Size]; arguments [org.springframework.context.support.DefaultMessageSourceResolvable: codes [param01Vo.name,name]; arguments []; default message [name],50,1]; default message [名称name长度必须是1-50个字符]] ]
    
    • 1

    抛出了MethodArgumentNotValidException异常。

    虽然参数错误时确实被拦截了,但格式已经和我们想要返回的不一致了。这个时候,就需要用到统一异常处理了。

    三,统一异常处理

    3.1 方法参数验证异常处理

    通过以上的问题,我们可以设置controller的统一异常处理,当出现参数验证错误时,就捕获MethodArgumentNotValidException异常,然后我们自己做处理。

    package pers.lmc.tools2.provider.aop;
    
    import com.lmc.common.enums.ResultCodeEnum;
    import com.lmc.common.vo.ResultVo;
    import lombok.extern.slf4j.Slf4j;
    import org.springframework.web.bind.MethodArgumentNotValidException;
    import org.springframework.web.bind.annotation.ExceptionHandler;
    import org.springframework.web.bind.annotation.RestControllerAdvice;
    import pers.lmc.tools2.provider.exception.ApiException;
    
    import java.util.List;
    import java.util.stream.Collectors;
    
    /**
     * @author lmc
     * @Description: TODO
     * @Create 2022-06-26 18:31
     * @version: 1.0
     */
    @Slf4j
    @RestControllerAdvice
    public class GlobalExceptionHandler {
    
        /**
         * 处理所有校验失败的异常(MethodArgumentNotValidException异常)
         * @param e
         * @return
         */
        @ExceptionHandler(value = MethodArgumentNotValidException.class)
        public ResultVo handleBindGetException(MethodArgumentNotValidException e) {
            // 获取所有异常参数
            List<String> errors = e.getBindingResult()
                    .getFieldErrors()
                    .stream()
                    .map(x -> x.getDefaultMessage())
                    .collect(Collectors.toList());
            return ResultVo.result(ResultCodeEnum.VALIDATE_PARAMS_ERROR, null).withDesc("参数校验失败:" + errors);
        }
    
        /**
         * 处理自定义APIException异常
         * @param e
         * @return
         */
        @ExceptionHandler(value = ApiException.class)
        public ResultVo handleApiException(ApiException e) {
            return ResultVo.fail(null).withDesc(e.getMessage());
        }
    
        /**
         * 处理其他异常
         * @param e
         * @return
         */
        @ExceptionHandler(value = Exception.class)
        public ResultVo handleException(Exception e) {
            log.info("执行到统一处理方法...");
            return ResultVo.fail(null).withDesc(e.getMessage());
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60

    通过以上配置,再次以非法参数传输时,会报出以下错误:

    {
    	"code": 1002,
    	"msg": "参数校验失败",
    	"desc": "参数校验失败:[性别sex只能是0和1,0=女1=男, 名称name长度必须是1-50个字符, 年龄age不能超过25岁]",
    	"data": null,
    	"searchTime": "2022-06-26 20:08:22"
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    这个时候格式已经我们想要的返回格式了。

    3.2 其他异常处理

    刚刚我们尝试的是方法的参数验证异常的处理,对于程序还可能出现的错误,配置统一异常处理后也不需要使用try{} catch() {},因为我们已经在全局异常处理类中配置了:

    	/**
         * 处理其他异常
         * @param e
         * @return
         */
        @ExceptionHandler(value = Exception.class)
        public ResultVo handleException(Exception e) {
            log.info("执行到统一处理方法...");
            return ResultVo.fail(null).withDesc(e.getMessage());
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    这个时候在程序中抛出其他异常,就会执行到这里的代码,同样返回我们想要的格式。举例如下

    修改controller接口:

        @PostMapping("/add")
        public ResultVo addParam01(@Valid @RequestBody Param01Vo param01Vo) {
            log.info("执行add()方法,参数:" + param01Vo.toString());
            int k = 1/0; // 调用该接口时执行到这里会抛出异常
            return ResultVo.success(param01Vo);
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    调用接口返回结果:

    {
    	"code": 1001,
    	"msg": "请求失败",
    	"desc": "/ by zero",
    	"data": null,
    	"searchTime": "2022-06-26 20:13:51"
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
  • 相关阅读:
    永达理简析:利用保险的“财务规划”功能维持退休后生活水平
    Parallel通过XCM与Moonbeam集成,将PARA以及DeFi用例带入Moonbeam生态
    如何在Linux中转换一个/Home目录到分区
    游戏开发25可 cocoscreate2.x升级3.x
    十九种卷积
    IB心理学生物分析模块
    硬核 阿里P8终于把(数据结构与算法经典问题解析)讲全了
    JSP基础
    企业为什么要做数字化转型,应该如何进行转型?
    蓝牙协议之配对和绑定学习笔记
  • 原文地址:https://blog.csdn.net/lmchhh/article/details/125473736