公司从大厂挖了一个资深工程师,他写的Controller代码同事都说漂亮! 上

简介: 公司从大厂挖了一个资深工程师,他写的Controller代码同事都说漂亮! 上


前言

本篇主要要介绍的就是controller层的处理,一个完整的后端请求由4部分组成:1. 接口地址(也就是URL地址)、2. 请求方式(一般就是get、set,当然还有put、delete)、3. 请求数据(request,有head跟body)、4. 响应数据(response)

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

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

基于 Spring Boot + MyBatis Plus + Vue & Element 实现的后台管理系统 + 用户小程序,支持 RBAC 动态权限、多租户、数据权限、工作流、三方登录、支付、短信、商城等功能

一、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 : 泡脚

基于 Spring Cloud Alibaba + Gateway + Nacos + RocketMQ + Vue & Element 实现的后台管理系统 + 用户小程序,支持 RBAC 动态权限、多租户、数据权限、工作流、三方登录、支付、短信、商城等功能

二、统一状态码

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啊!所以我们要进行优化一下,每次出现异常的时候,自动把状态码写好,不负妹妹之约!

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": "商品价格不允许为负数"
}

四、统一响应

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,
    ...
  }
}
相关文章
|
开发框架 Java .NET
工作这么多年了,我为什么还在考证
毕业以来参加工作后,一直以来认为考证没有啥用处。一方面,因为找工作太过顺利,那会儿找工作也没有看到别人拿什么证去找工作,也没有看到那家公司的招聘广告上写需要什么什么证书。另一方面,由于才从学校出来对于学习考试有种天然的排斥。想想这么多年以来一直都是在学习考试。好不容易毕业了那还不好好放松一下。现在回想起来真是too young,too simple!
232 1
|
12月前
|
运维 架构师 网络架构
公司架构师说不写代码,我直接怼他了!
公司架构师说不写代码,我直接怼他了!
63 2
|
人工智能 运维 中间件
推荐一位阿里朋友的巨作,助你成为顶尖服务端开发者——《服务端开发:技术、方法与实用解决方案》
推荐一位阿里朋友的巨作,助你成为顶尖服务端开发者——《服务端开发:技术、方法与实用解决方案》
566 0
|
前端开发 API
公司从大厂挖了一个资深工程师,他写的Controller代码同事都说漂亮! 下
公司从大厂挖了一个资深工程师,他写的Controller代码同事都说漂亮! 下
|
存储 程序员 项目管理
六年团队Leader实战秘诀|程序员最重要的八种软技能
笔者在带团队的六年中发现,程序员们在职场都有一个共同的困扰:“好像写代码都没什么问题了,日常工作基本上都是应付业务需求的开发,好像找不到其他的更大的附加价值了,我应该找一些什么样的发力点才能让我的价值更突出呢?” 。本文将和大家聊聊程序员的软技能。
六年团队Leader实战秘诀|程序员最重要的八种软技能
|
敏捷开发 架构师 程序员
数字化转型项目做了多年,主架构师都绝望了:当初就不应该用外包!
数字化转型已经成为了企业发展的主旋律,甚至成为各国的发展战略,疫情的发生进一步加速了全球数字技术的产业化应用步伐。“数字化”说起来容易做起来难,新兴技术变化如此之快,以至于大多数组织内部 IT 团队缺乏相应的技术能力或业务知识,如若设计或实施不当就有可能将“转型”变为一场灾难。
218 0
数字化转型项目做了多年,主架构师都绝望了:当初就不应该用外包!
|
Java 测试技术 项目管理
15 年经验资深测试经理的经验分享:测试人职场晋升“潜规则”
本文整理自霍格沃兹测试学院「测试开发在线沙龙」主题分享《测试人晋升指南:如何做好个人绩效管理》,略有删减。文末链接进阶学习。
15 年经验资深测试经理的经验分享:测试人职场晋升“潜规则”
|
中间件 程序员
历经外企、创业公司、大厂的程序员告诉你:第一份工作有多重要!
  笔者毕业5年,先后经历了创业公司,外企和国内一线互联网公司。本文用经历告诉你,第一份工作对于你的重要性和怎么选择第一份工作。   笔者第一份工作去了外企,也正是因为去了外企让我学会了很多,也塑造了自己的编程习惯和工作方式,时至现在也未曾改变。其实公司就是学校,第一家公司对你的工作习惯的塑造很重要。因为是外企,公司的文化相对扁平,没有尔虞我诈也没有勾心斗角。举几个具体的公司特质说一下对笔者的职业习惯的塑造。   新人培训:这里的新人培训不同其他公司,它会有6个月试用期,这6个月会有一个老师专门负责你的成长,定期给你 Review 表现。同时每一个月所有和你接触过的人会去一个小黑屋说你“坏
266 0
|
安全 架构师 网络协议
Google 测试总监聊如何经营成功的测试职业生涯
​重温经典:本文是前 Google 测试总监 James A. Whittaker 的经典分享,成文已经快 10 年了,但经典却总是常读常新,推荐给各位测试行业的从业人员。:)
下一篇
无影云桌面