Spring Boot 统一参数校验、统一异常、统一响应,这才是优雅的处理方式!

前言

本篇主要要介绍的就是 controller层的处理,一个完整的后端请求由4部分组成:

  1. 接口地址(也就是URL地址)
  2. 请求方式(一般就是get、set,当然还有put、delete)
  3. 请求数据(request,有head跟body)
  4. 响应数据(response)

本篇将解决以下3个问题:

  1. 当接收到请求时,如何优雅的校验参数
  2. 返回响应数据该如何统一的进行处理
  3. 接收到请求,处理业务逻辑时抛出了异常又该如何处理

一、Controller层参数接收(太基础了,可以跳过)

常见的请求就分为 getpost2种

@RestController
@RequestMapping("/product/product-info")
public class ProductInfoController {

    @Autowired
    ProductInfoService productInfoService;

    @GetMapping("/findById")
    public ProductInfoQueryVo findById(Integer id) {
     ...

    }

    @PostMapping("/page")
    public IPage findPage(Page page, ProductInfoQueryVo vo) {
     ...

    }
}

1、 @RestController:之前解释过, @RestController = @Controller + ResponseBody。加上这个注解,springboot就会吧这个类当成 controller进行处理,然后把所有返回的参数放到 ResponseBody

推荐一个 Spring Boot 基础实战教程:
https://github.com/javastacks/spring-boot-best-practice

2、 @RequestMapping:请求的前缀,也就是所有该 Controller下的请求都需要加上 /product/product-info的前缀

3、 @GetMapping("/findById"):标志这是一个 get请求,并且需要通过 /findById地址才可以访问到

4、 @PostMapping("/page"):同理,表示是个 post请求

5、 参数:至于参数部分,只需要写上 ProductInfoQueryVo,前端过来的 json请求便会通过映射赋值到对应的对象中,例如请求这么写, productId就会自动被映射到 vo对应的属性当中

size : 1
current : 1

productId : 1
productName : 泡脚

二、统一状态码

1. 返回格式

为了跟 前端妹妹打好关系,我们通常需要对后端返回的数据进行包装一下,增加一下 状态码状态信息,这样前端妹妹接收到数据就可以根据不同的 状态码,判断 响应数据状态,是否成功是否异常进行不同的显示。

当然这让你拥有了更多跟前端妹妹的交流机会,假设我们约定了 1000就是成功的意思

如果你不封装,那么返回的数据是这样子的

{
  "productId": 1,
  "productName": "泡脚",
  "productPrice": 100.00,
  "productDescription": "中药泡脚加按摩",
  "productStatus": 0,
}

经过封装以后时这样子的

{
  "code": 1000,
  "msg": "请求成功",
  "data": {
    "productId": 1,
    "productName": "泡脚",
    "productPrice": 100.00,
    "productDescription": "中药泡脚加按摩",
    "productStatus": 0,
  }
}

2. 封装ResultVo

这些状态码肯定都是要预先编好的,怎么编呢?写个常量 1000?还是直接写死 1000?要这么写就真的书白读的了,写 状态码当然是用枚举拉

1、首先先定义一个 状态码的接口,所有 状态码都需要实现它,有了标准才好做事

public interface StatusCode {
    public int getCode();
    public String getMsg();
}

2、然后去找前端妹妹,跟他约定好状态码(这可能是你们唯一的约定了)枚举类嘛,当然不能有 setter方法了,因此我们不能在用 @Data注解了,我们要用 @Getter

@Getter
public enum ResultCode implements StatusCode{
    SUCCESS(1000, "请求成功"),
    FAILED(1001, "请求失败"),
    VALIDATE_ERROR(1002, "参数校验失败"),
    RESPONSE_PACK_ERROR(1003, "response返回包装失败");

    private int code;
    private String msg;

    ResultCode(int code, String msg) {
        this.code = code;
        this.msg = msg;
    }
}

3、写好枚举类,就开始写 ResultVo包装类了,我们预设了几种默认的方法,比如成功的话就默认传入 object就可以了,我们自动包装成 success

@Data
public class ResultVo {
    // 状态码
    private int code;

    // 状态信息
    private String msg;

    // 返回对象
    private Object data;

    // 手动设置返回vo
    public ResultVo(int code, String msg, Object data) {
        this.code = code;
        this.msg = msg;
        this.data = data;
    }

    // 默认返回成功状态码,数据对象
    public ResultVo(Object data) {
        this.code = ResultCode.SUCCESS.getCode();
        this.msg = ResultCode.SUCCESS.getMsg();
        this.data = data;
    }

    // 返回指定状态码,数据对象
    public ResultVo(StatusCode statusCode, Object data) {
        this.code = statusCode.getCode();
        this.msg = statusCode.getMsg();
        this.data = data;
    }

    // 只返回状态码
    public ResultVo(StatusCode statusCode) {
        this.code = statusCode.getCode();
        this.msg = statusCode.getMsg();
        this.data = null;
    }
}

4、使用,现在的返回肯定就不是 return data;这么简单了,而是需要 new ResultVo(data);

@PostMapping("/findByVo")
public ResultVo findByVo(@Validated ProductInfoVo vo) {
    ProductInfo productInfo = new ProductInfo();
    BeanUtils.copyProperties(vo, productInfo);
    return new ResultVo(productInfoService.getOne(new QueryWrapper(productInfo)));
}

最后返回就会是上面带了状态码的数据了

三、统一校验

1. 原始做法

假设有一个添加 ProductInfo的接口,在没有统一校验时,我们需要这么做

@Data
public class ProductInfoVo {
    // 商品名称
    private String productName;

    // 商品价格
    private BigDecimal productPrice;

    // 上架状态
    private Integer productStatus;
}

@PostMapping("/findByVo")
public ProductInfo findByVo(ProductInfoVo vo) {
    if (StringUtils.isNotBlank(vo.getProductName())) {
        throw new APIException("商品名称不能为空");
    }
    if (null != vo.getProductPrice() && vo.getProductPrice().compareTo(new BigDecimal(0)) < 0) {
        throw new APIException("&#x5546;&#x54C1;&#x4EF7;&#x683C;&#x4E0D;&#x80FD;&#x4E3A;&#x8D1F;&#x6570;");
    }
    ...

    ProductInfo productInfo = new ProductInfo();
    BeanUtils.copyProperties(vo, productInfo);
    return new ResultVo(productInfoService.getOne(new QueryWrapper(productInfo)));
}

这if写的人都傻了,能忍吗?肯定不能忍啊

2. @Validated参数校验

好在有 @Validated,又是一个校验参数必备良药了。有了 @Validated我们只需要再 vo上面加一点小小的注解,便可以完成校验功能

@Data
public class ProductInfoVo {
    @NotNull(message = "&#x5546;&#x54C1;&#x540D;&#x79F0;&#x4E0D;&#x5141;&#x8BB8;&#x4E3A;&#x7A7A;")
    private String productName;

    @Min(value = 0, message = "&#x5546;&#x54C1;&#x4EF7;&#x683C;&#x4E0D;&#x5141;&#x8BB8;&#x4E3A;&#x8D1F;&#x6570;")
    private BigDecimal productPrice;

    private Integer productStatus;
}

@PostMapping("/findByVo")
public ProductInfo findByVo(@Validated ProductInfoVo vo) {
    ProductInfo productInfo = new ProductInfo();
    BeanUtils.copyProperties(vo, productInfo);
    return new ResultVo(productInfoService.getOne(new QueryWrapper(productInfo)));
}

运行看看,如果参数不对会发生什么?
我们故意传一个价格为 -1的参数过去

productName : &#x6CE1;&#x811A;
productPrice : -1
productStatus : 1
{
  "timestamp": "2020-04-19T03:06:37.268+0000",
  "status": 400,
  "error": "Bad Request",
  "errors": [
    {
      "codes": [
        "Min.productInfoVo.productPrice",
        "Min.productPrice",
        "Min.java.math.BigDecimal",
        "Min"
      ],
      "arguments": [
        {
          "codes": [
            "productInfoVo.productPrice",
            "productPrice"
          ],
          "defaultMessage": "productPrice",
          "code": "productPrice"
        },
        0
      ],
      "defaultMessage": "&#x5546;&#x54C1;&#x4EF7;&#x683C;&#x4E0D;&#x5141;&#x8BB8;&#x4E3A;&#x8D1F;&#x6570;",
      "objectName": "productInfoVo",
      "field": "productPrice",
      "rejectedValue": -1,
      "bindingFailure": false,
      "code": "Min"
    }
  ],
  "message": "Validation failed for object\u003d\u0027productInfoVo\u0027. Error count: 1",
  "trace": "org.springframework.validation.BindException: org.springframework.validation.BeanPropertyBindingResult: 1 errors\nField error in object \u0027productInfoVo\u0027 on field \u0027productPrice\u0027: rejected value [-1]; codes [Min.productInfoVo.productPrice,Min.productPrice,Min.java.math.BigDecimal,Min]; arguments [org.springframework.context.support.DefaultMessageSourceResolvable: codes [productInfoVo.productPrice,productPrice]; arguments []; default message [productPrice],0]; default message [&#x5546;&#x54C1;&#x4EF7;&#x683C;&#x4E0D;&#x5141;&#x8BB8;&#x4E3A;&#x8D1F;&#x6570;]\n\tat org.springframework.web.method.annotation.ModelAttributeMethodProcessor.resolveArgument(ModelAttributeMethodProcessor.java:164)\n\tat org.springframework.web.method.support.HandlerMethodArgumentResolverComposite.resolveArgument(HandlerMethodArgumentResolverComposite.java:121)\n\tat org.springframework.web.method.support.InvocableHandlerMethod.getMethodArgumentValues(InvocableHandlerMethod.java:167)\n\tat org.springframework.web.method.support.InvocableHandlerMethod.invokeForRequest(InvocableHandlerMethod.java:134)\n\tat org.springframework.web.servlet.mvc.method.annotation.ServletInvocableHandlerMethod.invokeAndHandle(ServletInvocableHandlerMethod.java:105)\n\tat org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.invokeHandlerMethod(RequestMappingHandlerAdapter.java:879)\n\tat org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.handleInternal(RequestMappingHandlerAdapter.java:793)\n\tat org.springframework.web.servlet.mvc.method.AbstractHandlerMethodAdapter.handle(AbstractHandlerMethodAdapter.java:87)\n\tat org.springframework.web.servlet.DispatcherServlet.doDispatch(DispatcherServlet.java:1040)\n\tat org.springframework.web.servlet.DispatcherServlet.doService(DispatcherServlet.java:943)\n\tat org.springframework.web.servlet.FrameworkServlet.processRequest(FrameworkServlet.java:1006)\n\tat org.springframework.web.servlet.FrameworkServlet.doPost(FrameworkServlet.java:909)\n\tat javax.servlet.http.HttpServlet.service(HttpServlet.java:660)\n\tat org.springframework.web.servlet.FrameworkServlet.service(FrameworkServlet.java:883)\n\tat javax.servlet.http.HttpServlet.service(HttpServlet.java:741)\n\tat org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:231)\n\tat org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:166)\n\tat org.apache.tomcat.websocket.server.WsFilter.doFilter(WsFilter.java:53)\n\tat org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:193)\n\tat org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:166)\n\tat com.alibaba.druid.support.http.WebStatFilter.doFilter(WebStatFilter.java:124)\n\tat org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:193)\n\tat org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:166)\n\tat org.springframework.web.filter.RequestContextFilter.doFilterInternal(RequestContextFilter.java:100)\n\tat org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:119)\n\tat org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:193)\n\tat org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:166)\n\tat org.springframework.web.filter.FormContentFilter.doFilterInternal(FormContentFilter.java:93)\n\tat org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:119)\n\tat org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:193)\n\tat org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:166)\n\tat org.springframework.web.filter.CharacterEncodingFilter.doFilterInternal(CharacterEncodingFilter.java:201)\n\tat org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:119)\n\tat org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:193)\n\tat org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:166)\n\tat org.apache.catalina.core.StandardWrapperValve.invoke(StandardWrapperValve.java:202)\n\tat org.apache.catalina.core.StandardContextValve.invoke(StandardContextValve.java:96)\n\tat org.apache.catalina.authenticator.AuthenticatorBase.invoke(AuthenticatorBase.java:541)\n\tat org.apache.catalina.core.StandardHostValve.invoke(StandardHostValve.java:139)\n\tat org.apache.catalina.valves.ErrorReportValve.invoke(ErrorReportValve.java:92)\n\tat org.apache.catalina.core.StandardEngineValve.invoke(StandardEngineValve.java:74)\n\tat org.apache.catalina.connector.CoyoteAdapter.service(CoyoteAdapter.java:343)\n\tat org.apache.coyote.http11.Http11Processor.service(Http11Processor.java:373)\n\tat org.apache.coyote.AbstractProcessorLight.process(AbstractProcessorLight.java:65)\n\tat org.apache.coyote.AbstractProtocol$ConnectionHandler.process(AbstractProtocol.java:868)\n\tat org.apache.tomcat.util.net.NioEndpoint$SocketProcessor.doRun(NioEndpoint.java:1594)\n\tat org.apache.tomcat.util.net.SocketProcessorBase.run(SocketProcessorBase.java:49)\n\tat java.base/java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1128)\n\tat java.base/java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:628)\n\tat org.apache.tomcat.util.threads.TaskThread$WrappingRunnable.run(TaskThread.java:61)\n\tat java.base/java.lang.Thread.run(Thread.java:830)\n",
  "path": "/leilema/product/product-info/findByVo"
}

大功告成了吗?虽然成功校验了参数,也返回了异常,并且带上 "&#x5546;&#x54C1;&#x4EF7;&#x683C;&#x4E0D;&#x5141;&#x8BB8;&#x4E3A;&#x8D1F;&#x6570;"的信息。但是你要是这样返回给前端,前端妹妹就提刀过来了,当年约定好的 &#x72B6;&#x6001;&#x7801;,你个 &#x8D1F;&#x5FC3;&#x4EBA;说忘就忘?用户 &#x4F53;&#x9A8C;&#x5C0F;&#x4E8E;&#x7B49;&#x4E8E;0啊!所以我们要进行优化一下,每次出现异常的时候,自动把 &#x72B6;&#x6001;&#x7801;写好,不负妹妹之约!

3. 优化异常处理

首先我们先看看校验参数抛出了什么异常

Resolved [org.springframework.validation.BindException: org.springframework.validation.BeanPropertyBindingResult: 1 errors

我们看到代码抛出了 org.springframework.validation.BindException的绑定异常,因此我们的思路就是 AOP拦截所有 controller,然后异常的时候统一拦截起来,进行封装!完美!

Spring Boot 统一参数校验、统一异常、统一响应,这才是优雅的处理方式!

玩你个头啊完美,这么呆瓜的操作 springboot不知道吗? spring mvc当然知道拉,所以给我们提供了一个 @RestControllerAdvice来增强所有 @RestController,然后使用 @ExceptionHandler注解,就可以拦截到对应的异常。

这里我们就拦截 BindException.class就好了。最后在返回之前,我们对异常信息进行包装一下,包装成 ResultVo,当然要跟上 ResultCode.VALIDATE_ERROR的异常状态码。这样前端妹妹看到 VALIDATE_ERROR的状态码,就会调用数据校验异常的弹窗提示用户哪里没填好

@RestControllerAdvice
public class ControllerExceptionAdvice {

    @ExceptionHandler({BindException.class})
    public ResultVo MethodArgumentNotValidExceptionHandler(BindException e) {
        // &#x4ECE;&#x5F02;&#x5E38;&#x5BF9;&#x8C61;&#x4E2D;&#x62FF;&#x5230;ObjectError&#x5BF9;&#x8C61;
        ObjectError objectError = e.getBindingResult().getAllErrors().get(0);
        return new ResultVo(ResultCode.VALIDATE_ERROR, objectError.getDefaultMessage());
    }
}

来康康效果,完美。 1002与前端妹妹约定好的状态码

{
  "code": 1002,
  "msg": "&#x53C2;&#x6570;&#x6821;&#x9A8C;&#x5931;&#x8D25;",
  "data": "&#x5546;&#x54C1;&#x4EF7;&#x683C;&#x4E0D;&#x5141;&#x8BB8;&#x4E3A;&#x8D1F;&#x6570;"
}

四、统一响应

1. 统一包装响应

再回头看一下 controller层的返回

return new ResultVo(productInfoService.getOne(new QueryWrapper(productInfo)));

开发小哥肯定不乐意了,谁有空天天写 new ResultVo(data)啊,我就想返回一个实体!怎么实现我不管!好把,那就是 AOP拦截所有 Controller,再 @After的时候统一帮你封装一下咯

Spring Boot 统一参数校验、统一异常、统一响应,这才是优雅的处理方式!

怕是上一次脸打的不够疼,springboot能不知道这么个操作吗?

@RestControllerAdvice(basePackages = {"com.bugpool.leilema"})
public class ControllerResponseAdvice implements ResponseBodyAdvice<object> {
    @Override
    public boolean supports(MethodParameter methodParameter, Class<? extends HttpMessageConverter<?>> aClass) {
        // response&#x662F;ResultVo&#x7C7B;&#x578B;&#xFF0C;&#x6216;&#x8005;&#x6CE8;&#x91CA;&#x4E86;NotControllerResponseAdvice&#x90FD;&#x4E0D;&#x8FDB;&#x884C;&#x5305;&#x88C5;
        return !methodParameter.getParameterType().isAssignableFrom(ResultVo.class);
    }

    @Override
    public Object beforeBodyWrite(Object data, MethodParameter returnType, MediaType mediaType, Class<? extends HttpMessageConverter<?>> aClass, ServerHttpRequest request, ServerHttpResponse response) {
        // String&#x7C7B;&#x578B;&#x4E0D;&#x80FD;&#x76F4;&#x63A5;&#x5305;&#x88C5;
        if (returnType.getGenericParameterType().equals(String.class)) {
            ObjectMapper objectMapper = new ObjectMapper();
            try {
                // &#x5C06;&#x6570;&#x636E;&#x5305;&#x88C5;&#x5728;ResultVo&#x91CC;&#x540E;&#x8F6C;&#x6362;&#x4E3A;json&#x4E32;&#x8FDB;&#x884C;&#x8FD4;&#x56DE;
                return objectMapper.writeValueAsString(new ResultVo(data));
            } catch (JsonProcessingException e) {
                throw new APIException(ResultCode.RESPONSE_PACK_ERROR, e.getMessage());
            }
        }
        // &#x5426;&#x5219;&#x76F4;&#x63A5;&#x5305;&#x88C5;&#x6210;ResultVo&#x8FD4;&#x56DE;
        return new ResultVo(data);
    }
}
</object>
  1. @RestControllerAdvice(basePackages = {"com.bugpool.leilema"})自动扫描了所有指定包下的 controller,在 Response时进行统一处理
  2. 重写 supports方法,也就是说,当返回类型已经是 ResultVo了,那就不需要封装了,当不等与 ResultVo时才进行调用 beforeBodyWrite方法,跟过滤器的效果是一样的
  3. 最后重写我们的封装方法 beforeBodyWrite,注意除了 String的返回值有点特殊,无法直接封装成json,我们需要进行特殊处理,其他的直接 new ResultVo(data);就ok了

打完收工,康康效果

@PostMapping("/findByVo")
public ProductInfo findByVo(@Validated ProductInfoVo vo) {
    ProductInfo productInfo = new ProductInfo();
    BeanUtils.copyProperties(vo, productInfo);
    return productInfoService.getOne(new QueryWrapper(productInfo));
}

此时就算我们返回的是 po,接收到的返回就是标准格式了,开发小哥露出了欣慰的笑容

{
  "code": 1000,
  "msg": "&#x8BF7;&#x6C42;&#x6210;&#x529F;",
  "data": {
    "productId": 1,
    "productName": "&#x6CE1;&#x811A;",
    "productPrice": 100.00,
    "productDescription": "&#x4E2D;&#x836F;&#x6CE1;&#x811A;&#x52A0;&#x6309;&#x6469;",
    "productStatus": 0,
    ...

  }
}

2. NOT统一响应

不开启统一响应原因

开发小哥是开心了,可是其他系统就不开心了。举个例子:我们项目中集成了一个 &#x5065;&#x5EB7;&#x68C0;&#x6D4B;的功能,也就是这货

@RestController
public class HealthController {
    @GetMapping("/health")
    public String health() {
        return "success";
    }
}

公司部署了一套校验所有系统存活状态的工具,这工具就定时发送 get请求给我们系统

“兄弟,你死了吗?”
“我没死,滚”
“兄弟,你死了吗?”
“我没死,滚”

是的,web项目的本质就是复读机。一旦发送的请求 &#x6CA1;&#x54CD;&#x5E94;,就会给负责人发信息(企业微信或者短信之类的),你的 &#x7CFB;&#x7EDF;&#x6B7B;&#x5566;!赶紧回来 &#x6392;&#x67E5;bug吧!让大家感受一下。每次看到我都 &#x5C04;&#x5C04;&#x53D1;&#x6296;,早上6点!我tm!!!!!

Spring Boot 统一参数校验、统一异常、统一响应,这才是优雅的处理方式!

好吧,没办法,人家是老大,人家要的返回不是

{
  "code": 1000,
  "msg": "&#x8BF7;&#x6C42;&#x6210;&#x529F;",
  "data": "success"
}

人家要的返回只要一个 success,人家定的标准不可能因为你一个系统改。俗话说的好,如果你改变不了环境,那你就只能我****

新增不进行封装注解

因为百分之99的请求还是需要包装的,只有个别不需要,写在包装的过滤器吧?又不是很好维护,那就加个注解好了。所有不需要包装的就加上这个注解。

@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface NotControllerResponseAdvice {
}

然后在我们的增强过滤方法上过滤包含这个注解的方法

@RestControllerAdvice(basePackages = {"com.bugpool.leilema"})
public class ControllerResponseAdvice implements ResponseBodyAdvice<object> {
    @Override
    public boolean supports(MethodParameter methodParameter, Class<? extends HttpMessageConverter<?>> aClass) {
        // response&#x662F;ResultVo&#x7C7B;&#x578B;&#xFF0C;&#x6216;&#x8005;&#x6CE8;&#x91CA;&#x4E86;NotControllerResponseAdvice&#x90FD;&#x4E0D;&#x8FDB;&#x884C;&#x5305;&#x88C5;
        return !(methodParameter.getParameterType().isAssignableFrom(ResultVo.class)
                || methodParameter.hasMethodAnnotation(NotControllerResponseAdvice.class));
    }
    ...

</object>

最后就在不需要包装的方法上加上注解

@RestController
public class HealthController {

    @GetMapping("/health")
    @NotControllerResponseAdvice
    public String health() {
        return "success";
    }
}

这时候就不会自动封装了,而其他没加注解的则依旧自动包装

Spring Boot 统一参数校验、统一异常、统一响应,这才是优雅的处理方式!

五、统一异常

每个系统都会有自己的 &#x4E1A;&#x52A1;&#x5F02;&#x5E38;,比如 &#x5E93;&#x5B58;&#x4E0D;&#x80FD;&#x5C0F;&#x4E8E;0子类的,这种异常并非程序异常,而是业务操作引发的异常,我们也需要进行规范的编排业务 &#x5F02;&#x5E38;&#x72B6;&#x6001;&#x7801;,并且写一个专门处理的 &#x5F02;&#x5E38;&#x7C7B;,最后通过刚刚学习过的 &#x5F02;&#x5E38;&#x62E6;&#x622A;统一进行处理,以及打 &#x65E5;&#x5FD7;

1、异常状态码枚举,既然是状态码,那就肯定要实现我们的标准接口 StatusCode

@Getter
public enum  AppCode implements StatusCode {

    APP_ERROR(2000, "&#x4E1A;&#x52A1;&#x5F02;&#x5E38;"),
    PRICE_ERROR(2001, "&#x4EF7;&#x683C;&#x5F02;&#x5E38;");

    private int code;
    private String msg;

    AppCode(int code, String msg) {
        this.code = code;
        this.msg = msg;
    }
}

2、异常类,这里需要强调一下, code代表 AppCode的异常状态码,也就是2000; msg代表 &#x4E1A;&#x52A1;&#x5F02;&#x5E38;,这只是一个大类,一般前端会放到弹窗 title上;最后 super(message);这才是抛出的详细信息,在前端显示在 &#x5F39;&#x7A97;&#x4F53;中,在 ResultVo则保存在 data中。

@Getter
public class APIException extends RuntimeException {
    private int code;
    private String msg;

    // &#x624B;&#x52A8;&#x8BBE;&#x7F6E;&#x5F02;&#x5E38;
    public APIException(StatusCode statusCode, String message) {
        // message&#x7528;&#x4E8E;&#x7528;&#x6237;&#x8BBE;&#x7F6E;&#x629B;&#x51FA;&#x9519;&#x8BEF;&#x8BE6;&#x60C5;&#xFF0C;&#x4F8B;&#x5982;&#xFF1A;&#x5F53;&#x524D;&#x4EF7;&#x683C;-5&#xFF0C;&#x5C0F;&#x4E8E;0
        super(message);
        // &#x72B6;&#x6001;&#x7801;
        this.code = statusCode.getCode();
        // &#x72B6;&#x6001;&#x7801;&#x914D;&#x5957;&#x7684;msg
        this.msg = statusCode.getMsg();
    }

    // &#x9ED8;&#x8BA4;&#x5F02;&#x5E38;&#x4F7F;&#x7528;APP_ERROR&#x72B6;&#x6001;&#x7801;
    public APIException(String message) {
        super(message);
        this.code = AppCode.APP_ERROR.getCode();
        this.msg = AppCode.APP_ERROR.getMsg();
    }

}

3、最后进行统一异常的拦截,这样无论在 service层还是 controller层,开发人员只管抛出 API&#x5F02;&#x5E38;,不需要关系怎么返回给前端,更不需要关心 &#x65E5;&#x5FD7;的打印

@RestControllerAdvice
public class ControllerExceptionAdvice {

    @ExceptionHandler({BindException.class})
    public ResultVo MethodArgumentNotValidExceptionHandler(BindException e) {
        // &#x4ECE;&#x5F02;&#x5E38;&#x5BF9;&#x8C61;&#x4E2D;&#x62FF;&#x5230;ObjectError&#x5BF9;&#x8C61;
        ObjectError objectError = e.getBindingResult().getAllErrors().get(0);
        return new ResultVo(ResultCode.VALIDATE_ERROR, objectError.getDefaultMessage());
    }

    @ExceptionHandler(APIException.class)
    public ResultVo APIExceptionHandler(APIException e) {
     // log.error(e.getMessage(), e); &#x7531;&#x4E8E;&#x8FD8;&#x6CA1;&#x96C6;&#x6210;&#x65E5;&#x5FD7;&#x6846;&#x67B6;&#xFF0C;&#x6682;&#x4E14;&#x653E;&#x7740;&#xFF0C;&#x5199;&#x4E0A;TODO
        return new ResultVo(e.getCode(), e.getMsg(), e.getMessage());
    }
}

4、最后使用,我们的代码只需要这么写

if (null == orderMaster) {
    throw new APIException(AppCode.ORDER_NOT_EXIST, "&#x8BA2;&#x5355;&#x53F7;&#x4E0D;&#x5B58;&#x5728;&#xFF1A;" + orderId);
}

{
  "code": 2003,
  "msg": "&#x8BA2;&#x5355;&#x4E0D;&#x5B58;&#x5728;",
  "data": "&#x8BA2;&#x5355;&#x53F7;&#x4E0D;&#x5B58;&#x5728;&#xFF1A;1998"
}

就会自动抛出 AppCode.ORDER_NOT_EXIST状态码的响应,并且带上异常详细信息 &#x8BA2;&#x5355;&#x53F7;&#x4E0D;&#x5B58;&#x5728;&#xFF1A;xxxx。后端小哥开发有效率,前端妹妹获取到 2003状态码,调用对应警告弹窗, title写上 &#x8BA2;&#x5355;&#x4E0D;&#x5B58;&#x5728;body详细信息记载 "&#x8BA2;&#x5355;&#x53F7;&#x4E0D;&#x5B58;&#x5728;&#xFF1A;1998"。同时 &#x65E5;&#x5FD7;还自动打上去了!666!老哥们三连点个赞!

版权声明:本文为CSDN博主「bugpool」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。原文链接:https://blog.csdn.net/chaitoudaren/article/details/105610962

近期热文推荐:

  1. 1,000+ 道 Java面试题及答案整理(2022最新版)

  2. 劲爆!Java 协程要来了。。。

  3. Spring Boot 2.x 教程,太全了!

  4. 别再写满屏的爆爆爆炸类了,试试装饰器模式,这才是优雅的方式!!

  5. 《Java开发手册(嵩山版)》最新发布,速速下载!

觉得不错,别忘了随手点赞+转发哦!

Original: https://www.cnblogs.com/javastack/p/16496800.html
Author: Java技术栈
Title: Spring Boot 统一参数校验、统一异常、统一响应,这才是优雅的处理方式!

原创文章受到原创版权保护。转载请注明出处:https://www.johngo689.com/538965/

转载文章受原作者版权保护。转载请注明原作者出处!

(0)

大家都在看

  • Linux常用命令整理:文件目录管理

    据说,你要对Linux文件做的事情,98%都记录在这篇文章里了。 1.ls命令 最常见的命令,相信刚进入linux命令行界面的时候,都要用这个命令看看当前目录下都有哪些文件吧。 名…

    Java 2023年6月5日
    085
  • X86寄存器

    404. 抱歉,您访问的资源不存在。 可能是网址有误,或者对应的内容被删除,或者处于私有状态。 代码改变世界,联系邮箱 contact@cnblogs.com 园子的商业化努力-困…

    Java 2023年6月9日
    080
  • 面向对象3(Java)

    即同一方法可以根据发送对象的不同而采用多种不同的行为方式 一个对象的实际类型是确定的,但是可以指向对象的引用类型可以很多 多态存在的条件:a.有继承关系;b.子类重写父类方法;c….

    Java 2023年6月9日
    066
  • iOS macOS 回到主线程的三种方式

    简单说将代码同步到主线程执行的三种方法如下:// 1.NSThread [self performSelectorOnMainThread:@selector(updateUI) …

    Java 2023年5月29日
    090
  • SpringBoot整合Redis–RedisTemplate

    1、导入依赖 org.springframework.boot spring-boot-starter-data-redis 2、编写配置文件 spring: redis: hos…

    Java 2023年6月8日
    077
  • IO流

    404. 抱歉,您访问的资源不存在。 可能是网址有误,或者对应的内容被删除,或者处于私有状态。 代码改变世界,联系邮箱 contact@cnblogs.com 园子的商业化努力-困…

    Java 2023年6月7日
    079
  • 每个开发人员都应该关注的7个优秀的GitHub仓库

    1. FreeCodeCamp 2. Developer Roadmap 3. Awesome 4. Build Your Own X 5. Git Ignore 6. Syste…

    Java 2023年6月15日
    090
  • 基于监控服务打造微服务治理生态体系

    基于上一篇《微服务海量日志监控平台》介绍的平台架构,继续架构的优化和能功的扩展,实现服务治理能力。 问题现状态 日志监控平台为我们带来了很多排查解决线上问题的便利。但是从某种程度上…

    Java 2023年6月6日
    091
  • 支付宝沙箱服务 (结合springboot实现,这里对接的是easy版本,工具用的是IDEA,WebStrom)

    一:打开支付宝开发平台,登录,然后点击控制台 https://open.alipay.com/ 二:滚动到底部,选着沙箱服务 三:获取到对接要用的appId和公钥私钥 四:打开ID…

    Java 2023年6月15日
    085
  • This application has no explicit mapping for /error, so you are seeing this as a fallback.

    描述:项目可以通过启动类正常启动,但是访问路径时报错。 原因:启动类放的位置不对,启动类要放在所有包的最前面。 Original: https://www.cnblogs.com/…

    Java 2023年6月5日
    0107
  • Java学习 (22) 对象篇(02)类与对象

    类与对象的关系 类是一种抽象的数据类型,它是对某一类事物整体描述/定义,但是并不能代表某一个具体的事物. 动物(猫、狗)、植物(花、草)、手机(安卓、苹果)、电脑(联想、华硕)、&…

    Java 2023年6月8日
    096
  • 译文《Java并发编程之volatile》

    作者: 雅各布·詹科夫原文: http://tutorials.jenkov.com/java-concurrency/volatile.html翻译: 潘深练个人网站 如您有更好…

    Java 2023年6月13日
    090
  • Hyperledger Fabric 核心概念

    一、说明 区块链是一个透明的,基于不可变模式的去中心化系统,核心就是一个分布式账本,记录网络上发生的所有交易。 区块链网络主要有三种类型:公共区块链、联盟区块链,以及私有区块链;我…

    Java 2023年6月6日
    0102
  • Atlassian Confluence 6.15.5 添加甘特图

    Atlassian Confluence 6.15.5 添加甘特图 Atlassian Confluence 编辑模式 工具栏 “+”→其它宏→视觉&amp…

    Java 2023年6月15日
    060
  • 阿里的秒杀系统是怎么设计的?

    我之前写过一个秒杀系统的文章不过有些许瑕疵,所以我准备在之前的基础上进行二次创作,不过让我决心二创秒杀系统的原因是我最近面试了很多读者,动不动就是秒杀系统把我整蒙蔽了,我懵的主要是…

    Java 2023年6月9日
    0104
  • Ubuntu 20.04 查看内存信息

    输入命令 dmidecode -t memory 输出如下: dmidecode 3.2 Getting SMBIOS data from sysfs. SMBIOS 2.8 pr…

    Java 2023年6月7日
    090
亲爱的 Coder【最近整理,可免费获取】👉 最新必读书单  | 👏 面试题下载  | 🌎 免费的AI知识星球