编写优雅的Controller

前言

这篇笔记主要是对Controller层的处理,包括状态码、响应、参数接收、校验以及异常处理等,编写出优雅的Controller层代码,只是大概聊聊思路哈,就不做详细的描述了哈,笔记是基于Spring系列框架的,没接触过的话可以跳过啦。

开发环境

操作系统

JDK

IDEA

Apipost

Maven

Spring Boot

统一状态码

在前后端交互的过程中,后端的响应数据有必要加上状态码和状态描述用于标记和描述单次请求的请求状态、操作结果、异常状态等,便于前端根据状态码来区分不同的响应,从而编写不同的处理逻辑。

关于怎么实现的话,有的人喜欢在一个常量类里来定义这些状态码,不过我比较喜欢用枚举来定义不同类别的状态码,比如业务处理状态、参数校验状态、权限状态等。

首先就是定义一个状态码的接口,写几个方法,当做一个规范去被实现。

public interface StatusCode {
    /**
     * 获取状态码
     * @return 状态码
     */
    Integer getCode();

    /**
     * 获取状态描述
     * @return 状态描述
     */
    String getMsg();

}

然后定义一个枚举来实现这个接口。

import lombok.AllArgsConstructor;
import lombok.Getter;

/**
 * 响应状态码枚举类
 */
@Getter
@AllArgsConstructor
public enum ResultCode implements StatusCode {
    SUCCESS(1000, "操作成功"),
    FAILED(1001, "操作失败"),
    VALIDATE_ERROR(1002, "参数校验失败"),
    RESPONSE_PACK_ERROR(1003, "response返回包装失败");
    private Integer code;
    private String msg;
}

这样的话就可以根据不同的处理结果返回不同的状态码及状态描述。

{
    "code": 1000,
    "msg": "操作成功"
}

统一响应

交互的过程中后端肯定是要返回数据的,但是每个数据的格式都不同,如果原样的返回给前端的话,可能容易挨打,所以就需要统一的响应数据封装。

我们可以定义一个类,里边定义好属性,就是响应数据的格式,每次返回响应数据的时候,new这个类的对象,把数据按照定义的格式进行填充后返回就可以啦。

其实响应的结构大同小异,主要包括状态码、状态描述、数据这三个部分。

import com.example.elegant_controller.common.enmu.ResultCode;
import lombok.AllArgsConstructor;
import lombok.Data;

/**
 * 统一响应类定义
 */
@Data
@AllArgsConstructor
public class ResultVo {
    /**
     * 状态码
     */
    private Integer code;
    /**
     * 状态描述
     */
    private String msg;
    /**
     * 响应的数据
     */
    private Object data;

    public ResultVo(Object data) {
        this.code = ResultCode.SUCCESS.getCode();
        this.msg = ResultCode.SUCCESS.getMsg();
        this.data = data;
    }

    public ResultVo(Integer code, String msg) {
        this.code = code;
        this.msg = msg;
        this.data = null;
    }

    public ResultVo(ResultCode resultCode) {
        this.code = resultCode.getCode();
        this.msg = resultCode.getMsg();
        this.data = null;
    }

    public ResultVo(ResultCode resultCode, Object data) {
        this.code = resultCode.getCode();
        this.msg = resultCode.getMsg();
        this.data = data;
    }
}

这样的一个类定义好之后,我们把每次需要响应的数据,例如状态码、状态码描述、数据等,按照定义封装到new的响应对象中返回给前端就可以啦。

    @RequestMapping("/info")
    public ResultVo getInfo(@RequestBody @Validated ProductInfoReqVo params) {
        ProductEntity product = new ProductEntity();
        BeanUtils.copyProperties(params, product);
        return new ResultVo(ResultCode.SUCCESS, product);
    }
{
    "code": 1000,
    "msg": "操作成功",
    "data": {
        "productId": 3,
        "productName": "搓澡",
        "productPrice": 198,
        "productDescription": “搓澡澡”,
        "productStatus": 1
    }
}

但是这样的话,我们每次返回响应数据都要new一个这样的对象,进行手动填充数据封装后返回,确实有些繁琐哈。我们可以在此基础上进行优化下,达到自动封装的效果。

实现这个需求,可以使用@RestControllerAdvice注解和ResponseBodyAdvice接口,来实现拦截响应然后进行统一的响应数据封装。

不过自动封装也有可能出现异常,这个时候我们需要自定义异常,在我们的业务逻辑出现异常的时候可以分类且友好的处理、响应及提示,在此之前我们首先需要的是一个枚举来定义不状态码,用于区分不同的异常。

import lombok.AllArgsConstructor;
import lombok.Getter;

@Getter
@AllArgsConstructor
public enum AppCode implements StatusCode {
    APP_ERROR(2000, "业务异常"),
    PRICE_ERROR(2001, "价格异常"),
    PRODUCT_NOT_EXIST(2002, "商品不存在");

    private Integer code;
    private String msg;
}

自定义异常

import com.example.elegant_controller.common.enmu.AppCode;
import com.example.elegant_controller.common.enmu.StatusCode;
import lombok.Getter;

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

    public APIException(StatusCode statusCode, String message) {
        super(message);
        this.code = statusCode.getCode();
        this.msg = statusCode.getMsg();
    }

    public APIException(String message) {
        super(message);
        this.code = AppCode.APP_ERROR.getCode();
        this.msg = AppCode.APP_ERROR.getMsg();
    }

}

拦截响应然后进行统一封装。

import com.example.elegant_controller.common.annotation.NotControllerResponseAdvice;
import com.example.elegant_controller.common.enmu.ResultCode;
import com.example.elegant_controller.common.exception.APIException;
import com.example.elegant_controller.vo.response.ResultVo;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.core.MethodParameter;
import org.springframework.http.MediaType;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.http.server.ServerHttpRequest;
import org.springframework.http.server.ServerHttpResponse;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import org.springframework.web.servlet.mvc.method.annotation.ResponseBodyAdvice;

@RestControllerAdvice(basePackages = {"com.example.elegant_controller"})
public class ControllerResponseAdvice implements ResponseBodyAdvice<Object> {
    @Override
    public boolean supports(MethodParameter methodParameter, Class<? extends HttpMessageConverter<?>> aClass) {
        return !methodParameter.getParameterType().isAssignableFrom(ResultVo.class);
    }

    @Override
    public Object beforeBodyWrite(Object data, MethodParameter returnType, MediaType mediaType, Class<? extends HttpMessageConverter<?>> aClass, ServerHttpRequest request, ServerHttpResponse response) {
        if (returnType.getGenericParameterType().equals(String.class)) {
            ObjectMapper objectMapper = new ObjectMapper();
            try {
                return objectMapper.writeValueAsString(new ResultVo(data));
            } catch (JsonProcessingException e) {
                throw new APIException(ResultCode.RESPONSE_PACK_ERROR, e.getMessage());
            }
        }
        return new ResultVo(data);
    }
}

非统一响应处理

我们偶尔会遇到不需要统一响应的需求,比如说在和第三方系统进行集成联调的时候,他们所需要的响应格式未必和我们的一样,但是我的系统之前又已经做了统一的响应封装,那怎么办呢?

也不难,我们只要对不需要统一响应处理的接口进行标记,然后再统一封装的时候判断一下,将标记的接口进行忽略就好了。那么怎么标记呢?其实挺简单的,我们只要定义一个注解就好了,然后在统一响应封装的时候判断一下,如果这个接口上有我们定义的注解,把它忽略掉就可以了。

首先呢,我们需要定义一个注解用于标记那些不需要统一响应的接口。

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

/**
 * 用于标记不统一响应接口的注解
 */
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface NotControllerResponseAdvice {

}

然后在我们之前做统一响应时实现ResponseBodyAdvice接口的RestControllerAdvice类中增加一个判断就好,具体就是判断下处理的接口是否有@NotControllerResponseAdvice这个注解就好了。具体实现就是在实现supports方法时增加一个条件,判断接口是否包含@NotControllerResponseAdvice注解即可。

import com.example.elegant_controller.common.annotation.NotControllerResponseAdvice;
import com.example.elegant_controller.common.enmu.ResultCode;
import com.example.elegant_controller.common.exception.APIException;
import com.example.elegant_controller.vo.response.ResultVo;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.core.MethodParameter;
import org.springframework.http.MediaType;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.http.server.ServerHttpRequest;
import org.springframework.http.server.ServerHttpResponse;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import org.springframework.web.servlet.mvc.method.annotation.ResponseBodyAdvice;

@RestControllerAdvice(basePackages = {"com.example.elegant_controller"})
public class ControllerResponseAdvice implements ResponseBodyAdvice<Object> {
    @Override
    public boolean supports(MethodParameter methodParameter, Class<? extends HttpMessageConverter<?>> aClass) {
        return !(methodParameter.getParameterType().isAssignableFrom(ResultVo.class) || methodParameter.hasMethodAnnotation(NotControllerResponseAdvice.class));
    }

    @Override
    public Object beforeBodyWrite(Object data, MethodParameter returnType, MediaType mediaType, Class<? extends HttpMessageConverter<?>> aClass, ServerHttpRequest request, ServerHttpResponse response) {
        if (returnType.getGenericParameterType().equals(String.class)) {
            ObjectMapper objectMapper = new ObjectMapper();
            try {
                return objectMapper.writeValueAsString(new ResultVo(data));
            } catch (JsonProcessingException e) {
                throw new APIException(ResultCode.RESPONSE_PACK_ERROR, e.getMessage());
            }
        }
        return new ResultVo(data);
    }
}

然后在我们不需要做统一响应的接口上加上这个注解就好了。

@NotControllerResponseAdvice
@RequestMapping("/notControllerResponseAdvice")
public String notControllerResponseAdvice() {
    return "success";
}

这样就可以看到没有统一包装的响应啦。

success

统一参数校验

前端请求后端服务的是难免会带一些请求参数,我们根据不同的参数进行不同的逻辑处理,既然有参数进来,必然少不了参数的校验,如果只想着依靠前端进行格式校验的话,那估计就离挨打不远啦。

关于参数校验,有的人对于入参进行“整齐的”if-else进行校验,效果可以实现,但是还是谈不上优雅一说。我们可以使用@Validated注解来进行统一 的参数校验。

首先,定义一个接收请求参数的类来封装参数,然后使用注解对属性进行格式的定义,也就相当于对入参的格式进行了规定。

import jakarta.validation.constraints.Min;
import jakarta.validation.constraints.NotBlank;
import lombok.Data;

import java.math.BigDecimal;

/**
 * 商品详情请求VO
 */
@Data
public class ProductInfoReqVo {
    /**
     * 商品名称
     */
    @NotBlank(message = "商品名称不可为空")
    private String productName;
    /**
     * 商品价格
     */
    @Min(value = 0, message = "商品价格不可为负数")
    private BigDecimal productPrice;
    /**
     * 商品状态
     */
    private Integer productStatus;
}

常见的格式校验注解有@NotBlank、@Min、@Max、@Email、@Pattern等,具体用法和其他注解自己去查哈。

定义完封装类后,在Controller中方法的参数列表使用@Validated注解即可开启参数校验。

    @RequestMapping("/info")
    public ResultVo getInfo(@RequestBody @Validated ProductInfoReqVo params) {
        ProductEntity product = new ProductEntity();
        BeanUtils.copyProperties(params, product);
        return new ResultVo(ResultCode.SUCCESS, product);
    }

当我们传合法的参数时,是可以正常响应的。但是当我们出入非法的参数时,就会抛出400的异常。

{
    "productName": "搓澡",
    "productPrice": -198,
    "productStatus": 1
}
{
  "timestamp": "2023-01-10T03: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"
    }
  ]
}

统一异常处理

我们的系统中可能会有很多的异常,有的需要抛出来,有的需要捕获处理,所以说弄一个统一的异常处理逻辑是非常必要的,要不然就会陷入无穷无尽、五花八门的try-catch中,后期的维护及问题排查也会非常的不友好。

就像上面我们做的参数校验,如果校验不通过的话就会抛出一个400异常,异常类型是BindException,然后如果我们把这么一串异常信息返回给前端的话,估计得挨批。那么就需要一个异常的响应的统一处理逻辑。

具体的话就是,我们在Controller层把异常的响应拦截下来,然后做一下封装,友好的返回给调用方即可。重点是怎么拦截呢?我们使用@RestControllerAdvice这个注解,配合@ExceptionHandler注解,就可以对Controller层响应的异常进行全局的拦截,然后进行优化响应即可。

import com.example.elegant_controller.common.enmu.ResultCode;
import com.example.elegant_controller.common.exception.APIException;
import com.example.elegant_controller.vo.response.ResultVo;
import org.springframework.validation.BindException;
import org.springframework.validation.ObjectError;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;

/**
 * 异常统一处理处理
 */
@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());
    }
}

再次传入非法参数后,即使出现异常,我们也会得到我们之前统一格式的响应结果,并且有友好的提示。

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

至此,我们有了统一的响应处理、不统一的响应处理、统一的参数检验、统一的状态码、统一的异常处理,Controller层编码相对来说更加的简洁优雅。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

RickChandler

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值