搭建统一状态码、统一参数校验、统一响应、统一异常的spring基础框架

本文介绍了如何在SpringBoot应用中建立标准的开发框架,包括Controller层的参数验证、统一响应格式、封装ResultVo、异常处理(如@Validated注解和自定义异常类)和使用AOP进行异常拦截。
摘要由CSDN通过智能技术生成

目标

  • 搭建比较标准的开发框架
    • 当接收到请求时,优雅的校验参数
    • 返回响应数据统一进行处理
    • 业务异常的统一处理

Controller 层参数接收

常见的get和post请求

@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 : 泡脚

统一状态码

返回格式

不封装的情况:

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

封装后的情况:

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

封装ResultVo

  • 首先先定义一个状态码的接口,所有状态码都需要实现它,有了标准才好做事:
public interface StatusCode {
    public int getCode();
    public String getMsg();
}
  • 然后和前端约定好状态码
@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;
    }
}

使用:

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

统一校验

@Validated 参数校验

引用的包是:import javax.validation.constraints.*
@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)));
}

如此这样,前端收到的数据就不再是优雅的ResultVo,所以我们还要再进行优化处理

优化异常处理

首先观察控制台打印的异常类型:

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 的异常状态码。

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

统一响应

统一包装响应

反思这行代码:

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

作为公司的好牛马真没空天天写new ResultVo吧,所以就直接返回实体!
根据国际惯例,上边的优化异常spring知道,那这里的统一包装响应sping也应该知道。

@RestControllerAdvice(basePackages = {"com.beiwo.adventure"})
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.beiwo.adventure”}) 自动扫描了所有指定包下的 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,
    ...
  }
}

统一响应中的不统一

众所周知,伯乐不常有,但害群之马常有。所以有的人就是不想要封装好的标准格式,他就要一个success。
你给他一个

{
  "code": 1000,
  "msg": "请求成功",
  "data": "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 子类的,这种异常并非程序异常,而是业务操作引发的异常,我们也需要进行规范的编排业务异常状态码,并且写一个专门处理的异常类,最后通过刚刚学习过的异常拦截统一进行处理,以及打日志。

  • 异常状态码枚举,既然是状态码,那就肯定要实现我们的标准接口 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;
    }
}
  • 异常类,这里需要强调一下,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();
    }

}
  • 最后进行统一异常的拦截,这样无论在 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());
    }
}
  • 最后使用,我们的代码只需要这么写。
 if (null == orderMaster) {
	throw new APIException(AppCode.ORDER_NOT_EXIST, "订单号不存在:" + orderId);
}
{
  "code": 2003,
  "msg": "订单不存在",
  "data": "订单号不存在:1998"
}
  • 22
    点赞
  • 19
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值