如何写好controller层

news2024/11/13 9:45:22
  • 前言

  • 一、Controller层参数接收

  • 二、统一状态码

  • 三、统一校验

  • 四、统一响应

  • 五、统一异常


前言

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

1. 接口地址(也就是URL地址)、

2. 请求方式(一般就是get、set,当然还有put、delete)、

3. 请求数据(request,有head跟body)、

4. 响应数据(response)

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

  • 当接收到请求时,如何优雅的校验参数

  • 返回响应数据该如何统一的进行处理

  • 接收到请求,处理业务逻辑时抛出了异常又该如何处理

一、Controller层参数接收

常见的请求就分为get跟post2种
@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) {
     ...
    }
}
  • @RestController: 之前解释过,@RestController = @Controller + ResponseBody。加上这个注解,springboot就会吧这个类当成controller进行处理,然后把所有返回的参数放到ResponseBody

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

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

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

  • 参数: 至于参数部分,只需要写上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?要这么写就真的书白读的了,写状态码当然是用枚举拉

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

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

然后去找前端妹妹,跟他约定好状态码(这可能是你们唯一的约定了)枚举类嘛,当然不能有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;
    }
}

写好枚举类开始写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;
    }
}

使用,现在的返回肯定就不是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("商品价格不能为负数");
    }
    ...
    
    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 = "商品名称不允许为空")
    private String productName;

    @Min(value = 0, message = "商品价格不允许为负数")
    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 : 泡脚
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": "商品价格不允许为负数",
      "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 [商品价格不允许为负数]\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"
}

大功告成了吗?虽然成功校验了参数,也返回了异常,并且带上"商品价格不允许为负数"的信息。但是你要是这样返回给前端,前端妹妹就提刀过来了,当年约定好的状态码,你个负心人说忘就忘?用户体验小于等于0啊!所以我们要进行优化一下,每次出现异常的时候,自动把状态码写好,不负妹妹之约!

拓展:关于参数的验证:

具体参考 Springboot整合JSR303参数校验

3. 优化异常处理

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

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

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

玩你个头啊完美,这么呆瓜的操作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) {
        // 从异常对象中拿到ObjectError对象
        ObjectError objectError = e.getBindingResult().getAllErrors().get(0);
        return new ResultVo(ResultCode.VALIDATE_ERROR, objectError.getDefaultMessage());
    }
}

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

{
  "code": 1002,
  "msg": "参数校验失败",
  "data": "商品价格不允许为负数"
}

这里可以以上的枚举可以规定好不同的业务模块对应的编号,大型系统一般有指定生成的;不同的业务异常分不同的枚举类来规定不同的业务异常信息;规范很重要,尤其是对外的api,良好的规范利于系统后期维护和迭代,有利于bug的定位和查找。

当然这是全局异常的处理方式:从全局的角度来

在局部上,指定捕获业务异常的方式,也用户提示;

以下为业务异常的类BusinessException 通过抛业对应的业务异常来给用户提示。将错误信息置于message中。

package com.bzcst.bop.common.exception;

public class BusinessException extends RuntimeException {
    private int code;
    private String msg;

    public BusinessException(String msg) {
        super(msg);
        this.code = 1;
        this.msg = msg;
    }

    public BusinessException(int code, String msg) {
        super(msg);
        this.code = code;
        this.msg = msg;
    }

    public BusinessException(String msg, Throwable cause) {
        super(msg, cause);
        this.code = 1;
        this.msg = msg;
    }

    public BusinessException(int code, String msg, Throwable cause) {
        super(msg, cause);
        this.code = code;
        this.msg = msg;
    }

    public BusinessException(ErrorEnum errorEnum) {
        super(errorEnum.message);
        this.code = errorEnum.errorCode;
        this.msg = errorEnum.message;
    }

    public BusinessException(ErrorEnum errorEnum, Throwable cause) {
        super(errorEnum.message, cause);
        this.code = errorEnum.errorCode;
        this.msg = errorEnum.message;
    }

    public int getCode() {
        return this.code;
    }

    public String getMsg() {
        return this.msg;
    }

    public void setCode(final int code) {
        this.code = code;
    }

    public void setMsg(final String msg) {
        this.msg = msg;
    }

    public boolean equals(final Object o) {
        if (o == this) {
            return true;
        } else if (!(o instanceof BusinessException)) {
            return false;
        } else {
            BusinessException other = (BusinessException)o;
            if (!other.canEqual(this)) {
                return false;
            } else if (this.getCode() != other.getCode()) {
                return false;
            } else {
                Object this$msg = this.getMsg();
                Object other$msg = other.getMsg();
                if (this$msg == null) {
                    if (other$msg != null) {
                        return false;
                    }
                } else if (!this$msg.equals(other$msg)) {
                    return false;
                }

                return true;
            }
        }
    }

    protected boolean canEqual(final Object other) {
        return other instanceof BusinessException;
    }

    public int hashCode() {
        int PRIME = true;
        int result = 1;
        int result = result * 59 + this.getCode();
        Object $msg = this.getMsg();
        result = result * 59 + ($msg == null ? 43 : $msg.hashCode());
        return result;
    }

    public String toString() {
        return "BusinessException(code=" + this.getCode() + ", msg=" + this.getMsg() + ")";
    }
}

四、统一响应

1. 统一包装响应

再回头看一下controller层的返回

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

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

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

@RestControllerAdvice(basePackages = {"com.bugpool.leilema"})
public class ControllerResponseAdvice implements ResponseBodyAdvice<Object> {
    @Override
    public boolean supports(MethodParameter methodParameter, Class<? extends HttpMessageConverter<?>> aClass) {
        // response是ResultVo类型,或者注释了NotControllerResponseAdvice都不进行包装
        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类型不能直接包装
        if (returnType.getGenericParameterType().equals(String.class)) {
            ObjectMapper objectMapper = new ObjectMapper();
            try {
                // 将数据包装在ResultVo里后转换为json串进行返回
                return objectMapper.writeValueAsString(new ResultVo(data));
            } catch (JsonProcessingException e) {
                throw new APIException(ResultCode.RESPONSE_PACK_ERROR, e.getMessage());
            }
        }
        // 否则直接包装成ResultVo返回
        return new ResultVo(data);
    }
}
  • @RestControllerAdvice(basePackages = {"com.bugpool.leilema"})自动扫描了所有指定包下的controller,在Response时进行统一处理

  • 重写supports方法,也就是说,当返回类型已经是ResultVo了,那就不需要封装了,当不等与ResultVo时才进行调用beforeBodyWrite方法,跟过滤器的效果是一样的

  • 最后重写我们的封装方法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": "请求成功",
  "data": {
    "productId": 1,
    "productName": "泡脚",
    "productPrice": 100.00,
    "productDescription": "中药泡脚加按摩",
    "productStatus": 0,
    ...
  }
}

目前很多公司的项目一般都是要去包装,给人感觉很机械,应该去灵活处理

2. NOT统一响应

  • 不开启统一响应原因

开发小哥是开心了,可是其他系统就不开心了。举个例子:我们项目中集成了一个健康检测的功能,也就是这货

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

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

{
  "code": 1000,
  "msg": "请求成功",
  "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是ResultVo类型,或者注释了NotControllerResponseAdvice都不进行包装
        return !(methodParameter.getParameterType().isAssignableFrom(ResultVo.class)
                || methodParameter.hasMethodAnnotation(NotControllerResponseAdvice.class));
    }
    ...

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

@RestController
public class HealthController {

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

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

它通过切面编程+注释的方式来同一处理一些个别需求,是一个处理问题的逻辑!

五、统一异常

每个系统都会有自己的业务异常,比如库存不能小于0子类的,这种异常并非程序异常,而是业务操作引发的异常,我们也需要进行规范的编排业务异常状态码,并且写一个专门处理的异常类,最后通过刚刚学习过的异常拦截统一进行处理,以及打日志

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

@Getter
public enum  AppCode implements StatusCode {

    APP_ERROR(2000, "业务异常"),
    PRICE_ERROR(2001, "价格异常");

    private int code;
    private String msg;

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

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

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

    // 手动设置异常
    public APIException(StatusCode statusCode, String message) {
        // message用于用户设置抛出错误详情,例如:当前价格-5,小于0
        super(message);
        // 状态码
        this.code = statusCode.getCode();
        // 状态码配套的msg
        this.msg = statusCode.getMsg();
    }

    // 默认异常使用APP_ERROR状态码
    public APIException(String message) {
        super(message);
        this.code = AppCode.APP_ERROR.getCode();
        this.msg = AppCode.APP_ERROR.getMsg();
    }

}

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

@RestControllerAdvice
public class ControllerExceptionAdvice {

    @ExceptionHandler({BindException.class})
    public ResultVo MethodArgumentNotValidExceptionHandler(BindException e) {
        // 从异常对象中拿到ObjectError对象
        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); 由于还没集成日志框架,暂且放着,写上TODO
        return new ResultVo(e.getCode(), e.getMsg(), e.getMessage());
    }
}

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

if (null == orderMaster) {
  throw new APIException(AppCode.ORDER_NOT_EXIST, "订单号不存在:" + orderId);
}
{
  "code": 2003,
  "msg": "订单不存在",
  "data": "订单号不存在:1998"
}

就会自动抛出AppCode.ORDER_NOT_EXIST状态码的响应,并且带上异常详细信息订单号不存在:xxxx。后端小哥开发有效率,前端妹妹获取到2003状态码,调用对应警告弹窗,title写上订单不存在,body详细信息记载"订单号不存在:1998"。同时日志还自动打上去了!当然为了避免data 的二意性,可以只让前端展示msg即可;data可以在排查问题时查看。

ps:一般对于controller 的处理一般公司都会封装成脚手架,拿来直接用即可,但是构建原理都大同小异!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/360065.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

运行redis报错 由于目标计算机积极拒绝,无法连接

第一打开redis服务器时出现闪退第二再点击redis-cli.exe时&#xff0c;出现了由于目标计算机积极拒绝&#xff0c;无法连接尝试用这个命令解决此问题&#xff1a;打开命令运行窗口&#xff0c;进入cmd&#xff0c;然后进入redis安装的目录&#xff0c;如下图&#xff1a;回车之…

“与众不同”的TOP250详细数据采集,pyecharts世界地图多维可视化展示

“与众不同”的TOP250详细数据采集&#xff0c;pyecharts世界地图多维可视化展示 前言&#xff1a; 本文描述爬取逗瓣250的电影详细信息&#xff0c;包括对电影名、评分、评论人数、电影名言、导演演员信息、电影年份、电影国家、电影类型等详细爬取&#xff1b; 并且针对爬…

EasyExcel 低内存导出大数据量的Excel方案探索 50万行 50列 (附:实现代码)

文章目录1.前言2.准备工作3.导出测试3.1.单次查询、全量导出3.2. 多次查询&#xff0c;多个文件&#xff0c;单次写入3.3.多次查询&#xff0c;多个文件&#xff0c;多次写入3.4.多线程导出探索3.5.文件打包成ZIP3.6.响应给客户4.实现代码5.结语1.前言 最近接到一个需求&#…

唤醒手腕 Java 后端 Springboot 结合 Redis 数据库学习笔记(更新中)

Redis 基本介绍 Redis Introduction The open source, in-memory data store used by millions of developers as a database, cache, streaming engine, and message broker. 基本概念&#xff1a;redis 是一个开源的、使用 C 语言编写的、支持网络交互的、可基于内存也可持…

没有资源没有人脉,23年跨境电商仍值得入局!

随着经济全球化的不断深入&#xff0c;越来越多人关注到跨境电商行业。作为新兴的贸易业态&#xff0c;跨境电商拥有多边化、交易链条短等传统电商无法比拟的优势&#xff0c;能够有效地推广更多中国优质产品到全球市场上&#xff0c;在促进国家经济发展过程中发挥着举足轻重的…

谷歌推出新优化器Lion:优化算法的符号发现

文章目录谷歌推出新优化器Lion&#xff1a;优化算法的符号发现Lion VS AdamW论文实验1.图像分类2.视觉语言对比学习3.扩散模型4.语言建模和微调5.与其他流行优化器的比较超参数设置小结谷歌推出新优化器Lion&#xff1a;优化算法的符号发现 优化器即优化算法&#xff0c;优化器…

加入蓝精灵协会的快速指南

了解使用蓝精灵协会应用程序所需的所有要点。 什么是蓝精灵协会&#xff1f; 蓝精灵协会是唯一一个由蓝精灵官方品牌支持的 PFP 项目。 我们目前正处于这个交互式项目的第二阶段&#xff0c;重点是通过游戏化的 Web3 体验建立一个大型社区。下面是参与游戏的基本步骤&#xff0…

C++进阶:二叉搜索树

文章目录1 二叉搜索树概念2 二叉搜索树的实现2.1 结点的定义2.2 二叉搜索树的插入2.2 二叉搜索树的查找2.3 二叉搜索树的删除2.4 二叉搜索树的默认成员函数2.4.1 拷贝构造2.4.2 析构函数2.4.3 赋值重载3 二叉搜索树的应用3.1 k模型3.2 kv模型4 二叉搜索树的性能分析1 二叉搜索树…

【字典转模型 Objective-C语言】

一、点按钮,弹出的这个效果,这实际上是个Label, 这实际上是一个Label,点按钮弹出的这个效果, 设置一个Label的背景色、前景色、透明度、等等, 让它加进来,然后通过动画让它隐藏掉, 这就是,这个效果的实现思路, 咱们这个效果,先稍微往后放一放, 这个并不是重点…

匈牙利算法学习笔记

匈牙利算法学习笔记1. 前言1.1 二分图1.2 二分图匹配2. 匈牙利算法(Hungarian Algorithm)2.1 基础概念2.2 实现步骤参考链接&#xff1a;1. 14-4&#xff1a;匈牙利算法 Hungarian Algorithm1. 前言 1.1 二分图 二分图通常针对无向图问题。假设G(V,E)G(V,E)G(V,E)是一个无向图…

Linux搭建gitlab服务器

第一步&#xff1a;切换到root用户 sudo root或者 sudo -i第二步&#xff1a;执行以下命令 yum install curl openssh-server openssh-clients postfix cronie -y​systemctl start postfix.servicechkconfig postfix onlokkit -s http -s ssh第三步&#xff1a;添加Gitlab&…

如何理解 Python 的赋值逻辑

摘要&#xff1a; 如果你学过 C 语言&#xff0c;那么当你初见 Python 时可能会觉得 Python 的赋值方式略有诡异&#xff1a;好像差不多&#xff0c;但又好像哪里有点不太对劲。 本文比较并解释了这种赋值逻辑上的差异。回答了为什么需要这种赋值逻辑以及如何使用这种赋值逻辑…

Acer新蜂鸟Swift3电脑开机总是蓝屏错误怎么办?

Acer新蜂鸟Swift3电脑开机总是蓝屏错误怎么办&#xff1f;有用户使用的Acer新蜂鸟Swift3电脑一开机的时候&#xff0c;没过几秒电脑桌面就变成了蓝屏的了&#xff0c;通过强制重启之后依然会重复这个问题&#xff0c;那么这个问题要怎么去进行解决了&#xff0c;今天将你怎么重…

Android 反序列化漏洞攻防史话

Java 在历史上出现过许多反序列化的漏洞&#xff0c;但大部分出自 J2EE 的组件。即便是 FastJSON 这种漏洞&#xff0c;似乎也很少看到在 Android 中被实际的触发和利用。本文即为对历史上曾出现过的 Android Java 反序列化漏洞的分析和研究记录。 序列化和反序列化是指将内存数…

k8s-kubectl命令

文章目录一、kubectl 基本命令1、陈述式资源管理方法:2、声明式资源管理办法二、基本信息查看三、项目的生命周期创建kubectl run命令四、金丝雀发布(Canary Release)——陈述式管理方法五、声明式管理方法kubectl create 和 kubectl apply区别一、kubectl 基本命令 1、陈述式…

交叉验证 | 机器学习

1、交叉验证 1.1概念 交叉验证的核心思想&#xff1a;对数据集进行多次划分&#xff0c;对多次评估的结果取平均&#xff0c;从而消除单次划分时数据划分得不平衡而造成的不良影响。因为这种不良影响在小规模数据集上更容易出现&#xff0c;所以交叉验证方法在小规模数据集上更…

Profinet转ModbusTCP网关连接昆仑通态触摸屏配置案例

本案例是模拟将Modbus TCP 设备数据接入到西门子PROFINET 网络中。 使用设备为西门子 S7-1500 型 PLC, Profinet转ModbusTCP网关。MODBUS 从站昆仑通态触摸屏。 配置方法&#xff1a; 打开博图&#xff0c;新建项目并添加站点。 添加1513PLC。 设置好IP并处于联网状态 导入Pr…

【前端】小程序开发入门:安装开发工具、目录结构与项目配置

文章目录前期准备目录结构app.jsonpageswindow其他前期准备 开发小程序要先申请一个对应的AppID&#xff1a;微信小程序 (qq.com) 微信官方小程序开发文档&#xff1a;微信开放文档 (qq.com) 然后安装一个小程序开发工具&#xff1a; 选择稳定版&#xff1a; 安装后打开&…

组合由于继承

目录 前言&#xff1a; 1.什么是继承&#xff1f; 2.继承的劣势、问题&#xff1f; 3.组合相比继承有哪些优势&#xff1f; 4、如何判断该用组合还是继承&#xff1f; 参考资料 前言&#xff1a; 我们在平时日常开发设计的过程中&#xff0c;经常会有人提到一条经典的设…

音视频基础之封装格式与音视频同步

封装格式的概念 封装格式(也叫容器&#xff09;就是将已经编码压缩好的视频流、音频流及字幕按照一定的方案放到一个文件中&#xff0c;便于播放软件播放。 一般来说&#xff0c;视频文件的后缀名就是它的封装格式。 封装的格式不一样&#xff0c;后缀名也就不一样。 比如&a…