统一异常、返回、接口文档试用文档

概述
SpringBoot基于约定优于配置的思想,可以让开发人员不必在配置与逻辑业务之间进行思维的切换,全身心的投入到逻辑业务的代码编写中,从而大大提高了开发的效率,一定程度上缩短了项目周期。但是使用Springboot进行企业项目开发,我们依然需要进行最基础的重复开发。比如:对于统一结果的封装、统一异常的处理、API文档接口生成。
aliyun-gts-base-starter在springboot的基础上,提供了项目开发中项目所需要的基础支撑功能。

特性
统一结果返回
统一异常处理
API接口文档生成
其他

Maven 配置文件
settings.xml
(4 KB)
https://iwhale-citybrain.yuque.com/docs/share/c916333e-98e2-4536-992b-71887b1986ec?#

假设使用的是组件的快照版本,在引入maven依赖的时候,在idea中,请勾选如下配置。
在这里插入图片描述
线上打包部署的时候,请加上–update-snapshot:mvn clean package --update-snapshots

快速开始

引入maven坐标:

<dependency>
     <groupId>com.aliyun.gts.bpaas.base</groupId>
     <artifactId>aliyun-gts-base-starter</artifactId>
     <version>最新版本</version>
 </dependency>

即可集成统一结果返回、异常处理、API接口文档生成等基础功能。

代码仓库地址
git@code.dayu.work:aliyun-gts-backend-lib/aliyun-gts-base.git

统一结果返回

项目开发中,一般情况下对数据返回的格式可能会有一个统一的要求,一般会包括状态码、信息及数据三部分。举个例子,假设规范要求数据返回的结构如下所示:

{
	"requestId": "176de8c526e8435dac07cd804c85c26d",   //请求id
	"code": "",  //业务错误码,如101,-95
	"message": "success",  //额外消息
	"success": true,  //是否成功
	"meta": null,   //一些meta信息,例如需要crsf的token
	"data": {    //实际数据,一般来说是DTO
        "userName": "张三",
        "userId": "C0001"
    }
}

其中,data字段存储实际的返回数据;message存储当出现异常时的异常信息;code存储处理码,当无异常时,code为200;而出现异常时可以存储具体的异常码。
要返回这样的数据,最直接的做法当然是在每一个Controller中去处理,返回的数据本身就封装有处理码、数据、出现异常时的异常信息等字段。这样做导致的问题,就是每一个Controller向外暴露的方法都要创建一个返回的对象来封装这种处理,并在出现异常时捕获异常进行处理。
因此最好是能够统一处理这种转换,这样的话服务提供者就只需关注他原本就需要处理的事情:

  1. 在无异常时返回数据本身;
  2. 在出现异常时抛出合适的异常。

为达到统一处理的目的,需要针对两个场景做单独的处理:

  1. 当无异常时,在原返回的数据基础上封装一层,将状态码等信息包含进来;
  2. 二是当出现异常时,将异常信息进行封装然后返回给调用方。
    进行统一结果的封装以及异常处理,这样可以不仅能使我们的接口看起来更漂亮,而且还可以使前端统一处理很多东西,避免很多问题的产生。

默认统一结果

默认使用@GtsRestController注解标识当前类所在接口需要统一结果返回,默认结果包装类使用ResultResponse。

如果需要对所有@RestController标识的类接口做统一结果包装,我们可以设置gts.common.custom-result-controller=false(默认`gts.common.custom-result-controller为true)

例如,有如下Controller。

@GtsRestController
@RequestMapping("/api/v1/test")
public class TestController {
 @GetMapping("/test-object")
    public UserVO testObject() {
        return new UserVO("张三", "C0001");
    }
    
    @GetMapping("/test-page")
    public IPage<UserVO> testPage() {
        IPage<UserVO> result = new Page<UserVO>();
        result.setTotal(1);
        result.setSize(10);
        result.setCurrent(1);
        List<UserVO> records = new ArrayList<UserVO>();
        records.add(new UserVO("张三", "C0001"));
        result.setRecords(records);
        return result;
    }
}
  1. 普通对象

返回结果如下:

{
    "requestId": "76f9d04c-818c-4ad8-9d7c-e23d86be82bd",
    "code": "200",
    "message": "success",
    "success": true,
    "meta": null,
    "traceId": null,
    "data": {
        "userName": "张三",
        "userId": "C0001"
    }
}
  1. 分页对象

默认当项目工程中引入mybatis plus依赖,将对分页结果进行统一包装,返回结果如下:

{
    "requestId": "31b05b98-832a-43f0-85b0-ac9311086d2b",
    "code": "200",
    "message": "success",
    "success": true,
    "meta": null,
    "data": {
        "totalCount": 1,
        "list": [
            {
                "userName": "张三",
                "userId": "C0001"
            }
        ],
        "pageNum": 1,
        "pageSize": 10
    }
}
  1. Feign调用处理结果

在微服务情境下,有如下接口:

    @Override
    @GetMapping("/api/v1/products")
    public List<ProductVO> getProductList() {
        ProductVO productVO = new ProductVO();
        productVO.setName("小明");
        productVO.setAge(20);
        return Lists.newArrayList(productVO);
    }

进行统一结果返回包装之后,接口返回的数据接口如下:

{
    "data": [
        {
            "name": "小明",
            "age": 20
        }
    ],
    "code": "sys.success",
    "message": null,
    "traceId": "7e942dfafa6d485cb8ac46bb40b6af6a"
}

一般情况下controller接口与feign接口方法定义的入参、出参一致:

@GetMapping("/api/v1/products")
List<ProductVO> getProductList();

如果不做处理,在这种情况下,会出现如下异常。

2020-11-26 11:05:04 ERROR 13616 [023411adecd144aea2db53007727817f] --- [nio-9002-exec-2] .b.w.m.p.e.AbstractMvcExceptionProcessor : Error while extracting response for type [java.util.List<com.kim.cloud.boot.server.api.ProductVO>] and content type [application/json;charset=UTF-8]; nested exception is org.springframework.http.converter.HttpMessageNotReadableException: JSON parse error: Cannot deserialize instance of `java.util.ArrayList` out of START_OBJECT token; nested exception is com.fasterxml.jackson.databind.exc.MismatchedInputException: Cannot deserialize instance of `java.util.ArrayList` out of START_OBJECT token
 at [Source: (ByteArrayInputStream); line: 1, column: 1]
feign.codec.DecodeException: Error while extracting response for type [java.util.List<com.kim.cloud.boot.server.api.ProductVO>] and content type [application/json;charset=UTF-8]; nested exception is org.springframework.http.converter.HttpMessageNotReadableException: JSON parse error: Cannot deserialize instance of `java.util.ArrayList` out of START_OBJECT token; nested exception is com.fasterxml.jackson.databind.exc.MismatchedInputException: Cannot deserialize instance of `java.util.ArrayList` out of START_OBJECT token
 at [Source: (ByteArrayInputStream); line: 1, column: 1]
    at feign.SynchronousMethodHandler.decode(SynchronousMethodHandler.java:182)
    at feign.SynchronousMethodHandler.executeAndDecode(SynchronousMethodHandler.java:142)

当然我们可以如下定义feign接口,会正确获取返回接口。

@GetMapping("/api/v1/products")
ResultData<List<ProductVO>> getProductList();

开启feign统一结果返回

第一种方式:配置参数gts.common.feign.enable =true

第二种方式:在feign接口设置configuration

@FeignClient(name = "XXXXX",configuration = CommonResultConfiguration.class)

默认使用ResultResponse类进行统一结果包装。当客户端使用服务端接口的时候,会根据接口返回声明,去除包装类,直接返回返回内容。

@GetMapping("/api/v1/products")
List<ProductVO> getProductList();

自定义统一结果

由于工程规范标准不同,工程改造代价过大等原理,使用默认的ResultResponse有时候并不能满足项目的统一结果返回。aliyun-gts-base-starter支持自定义统一返回结果。

  1. 实现AbstractResultConverter类,自定义统一结果返回逻辑。比如:
public class CustomResultConverter extends AbstractResultConverter<Object> {
    @Override
    protected Object doConvert(String traceId, Object source) throws Exception {
        if (source instanceof AjaxResult) {
            ((AjaxResult<?>) source).setTraceId(traceId);
            return source;
        }
        if (source instanceof IPage) {
            IPage page = (IPage) source;
            Page pageInfo = new Page();
            pageInfo.setPageNum(page.getCurrent());
            pageInfo.setPageSize(page.getSize());
            pageInfo.setTotalCount(page.getTotal());
            pageInfo.setList(page.getRecords());
            return new AjaxResult<>(traceId, pageInfo);
        }
        return new AjaxResult(traceId, source);
    }
}
  1. 注册统一结果转换器到spring容器。
@Configuration
public class SpringConfiguration {
 /**
     * 自定义统一结果.
     *
     * @return the abstract result converter
     */
    @Bean
    public AbstractResultConverter createResultResolvableConverter() {
        return new CustomResultConverter();
    }
}

代码示例可参考如下工程:https://code.dayu.work/aliyun-gts-backend-lib/aliyun-gts-base/-/tree/1.1-SNAPSHOT/aliyun-gts-base-starter-test

自定义feign返回结果处理类

由于服务端可能使用不同的包装类进行请求结果包装,客户端需要根据不同的包装类进行相应处理。默认使用ResultResponse类进行统一结果包装,示例使用自定义ResultData进行结果包装。

/**
 * 自定义feign返回结果处理类.
 *
 * @author duanledexianxianxian
 */
@Configuration
public class CustomCommonResultConfiguration {
    @Autowired
    private ObjectFactory<HttpMessageConverters> messageConverters;

    /**
     * 自定义decoder.
     * 自定义转换逻辑
     * @return the decoder
     */
    @Bean
    @ConditionalOnMissingBean
    public Decoder createCommonResultDecoder() {
        return new OptionalDecoder(
                new ResponseEntityDecoder(new DefaultCommonResultDecoder(this.messageConverters, ResultData.class, (result) -> ((ResultData) result).getData())));
    }
}

DefaultCommonResultDecoder(ObjectFactory messageConverters,Class resultClass,Function<Object, Object> convert)

messageConverters:为message转换器,可以直接从Spring上下文获取,然后注入;
resultClass:为包装类Class对象,默认为ResultResponse。
convert:转换函数,用于自定义转换逻辑,入参为请求返回结果对象,出参为转换后接口返回的对象。在此处我们可以从请求返回结果对象中,获取任意我们需要的值,作为接口的返回对象。比如:获取分页信息、获取真实业务数据内容等。

如果要为当前Spring容器管理的所有Feign都指定这个解码器,就把CustomCommonResultConfiguration类挪到Feign接口外面,再加@Configuration;
如果只是为一个Feign Client指定自定义的解码器,CustomCommonResultConfiguration就不要加Spring注解(不要被Spring管理)了,否则就成了全局的了。

忽略统一结果返回

在某些场景下,我们可能不需要对接口进行统一结果包装,可以通过如下方式:

当gts.common.custom-result-controller=true,无@GtsRestController标识的类。类下所有接口将不会进行结果包装。
当gts.common.custom-result-controller=false,无@RestController标识的类。类下所有接口将不会进行结果包装。
使用@SkipAutoWrap标识的接口方法,将不会进行结果包装。

统一异常处理

Controller层参数校验

 @PostMapping("/test-bean-valid")
    public UserVO testBeanValid(@RequestBody @Valid QueryRequest queryRequest) {
        return new UserVO("张三", "C0001");
    }

@Data
public class QueryRequest {
    public static final String NAME_ERROR_MESSAGE = "姓名不能是空";
    @NotBlank(message = NAME_ERROR_MESSAGE)
    private String name;
}

返回结果如下:

{
    "requestId": "7cce44f7-cae7-444c-af31-82d376cce459",
    "code": "500",
    "message": "姓名不能是空",
    "success": false,
    "meta": null,
    "data": null
}

Service层参数校验
在需要进行参数验证的接口类或者方法上,加上@ValidateRequest注解,service代码如下:

@Service
@Slf4j
@ValidateRequest
public class SomeBizService {

    public ResultResponse bizQuery(QueryRequest queryRequest) {
        log.info("query: {}", queryRequest);
        return ResultResponse.succResult();
    }
}

QueryRequest代码如下:

@Data
public class QueryRequest {
    public static final String NAME_ERROR_MESSAGE = "姓名不能是空";
    @NotBlank(message = NAME_ERROR_MESSAGE)
    private String name;
}

返回结果将与Controller层参数校验返回结果一致。

业务异常

通过实现IErrorMessage或者IHttpStatusErrorMessage接口,定义业务异常常量类或者枚举类。比如:

public enum UserErrorEnum implements IErrorMessage {

    /**
     * User name exist user error enum.
     */
    USER_NAME_EXIST("USER_NAME_EXIST", "用户名称已经存在");

    private final String code;
    private final String description;
    .....
    
    }

通过throw new ErrorCodeException(错误编码)或者throw new ErrorCodeException(异常枚举)抛出自定义的义务异常。比如在Controller或者service层代码中:

 @GetMapping("/test-inner-e2")
    public String exception2() {
        throw new ErrorCodeException(USER_NAME_EXIST);
}

返回请求结果code为错误编码或者异常枚举编码。实现接口IHttpStatusErrorMessage接口,我们甚至可以控制结果返回的http状态码。更多详情请查看ErrorCodeException类。

对于系统中没有捕获的异常,将会统一由全局异常处理器处理,转换成系统异常,默认http请求状态码以及返回请求结果code都为500(返回请求状态码为500,方便做异常追踪)。

httpStatus:500
{
    "requestId": "e15bc8c8-a407-4f7c-b4a3-4f49fb9c4af6",
    "code": "500",
    "message": "异常测试1",
    "success": false,
    "meta": null,
    "traceId": null,
    "data": null
}

通过配置gts.common.error.system-http-status全局设置未捕获异常http状态码。

自定义异常结果返回
1.实现AbstractExceptionResolver类,自定义统一结果返回逻辑。比如:

public class CustomExceptionResolver extends AbstractExceptionResolver<Object> {


    /**
     * 构造函数.
     *
     * @param defaultErrorCode the default error code
     */
    public CustomExceptionResolver(String defaultErrorCode, HttpStatus httpStatus) {
        super(defaultErrorCode, httpStatus);
    }

    @Override
    protected Object resolve(String traceId, String errorCode, String errorMessage, Object errorData) {
        return new AjaxResult<>(traceId, errorData, errorCode, errorMessage);
    }
}

2.注册统一结果转换器到spring容器。

@Configuration
@EnableConfigurationProperties(value = GtsCommonProperties.class)
public class SpringConfiguration {
   private final GtsCommonProperties gtsCommonProperties;

    /**
     * Instantiates a new Spring configuration.
     *
     * @param gtsCommonProperties the gts common properties
     */
    public SpringConfiguration(GtsCommonProperties gtsCommonProperties) {
        this.gtsCommonProperties = gtsCommonProperties;
        System.out.println("SpringConfiguration");
    }
     /**
     * 自定义统一异常
     *
     * @return abstract exception resolver
     */
    @Bean
    public AbstractExceptionResolver createCustomExceptionResolver(ObjectProvider<List<ErrorResolvableConverter>> provider) {
        CustomExceptionResolver exceptionResolver = new CustomExceptionResolver(gtsCommonProperties.getErrorCode(), gtsCommonProperties.getError().getBusinessHttpStatus());
        List<ErrorResolvableConverter> converters = provider.getIfAvailable();
        if (CollectionUtils.isNotEmpty(converters)) {
            exceptionResolver.setErrorResolvableConverter(new ErrorResolvableConverterComposite(converters));
        }
        return exceptionResolver;
    }
}

代码示例可参考如下工程:https://code.dayu.work/aliyun-gts-backend-lib/aliyun-gts-base/-/tree/1.1-SNAPSHOT/aliyun-gts-base-starter-test

错误转换器
前面已经提到对于系统中没有捕获的异常,将会统一由全局异常处理器处理,转换成系统异常,默认http请求状态码以及返回请求结果code都为500。但是有的时候我们需要对系统某一类异常进行统一处理。比如MethodArgumentNotValidException这一类异常都想处理成返回错误编码500,错误信息:参数异常。此时我们自定义异常转换器:

public class MethodArgumentNotValidExceptionErrorConverter implements ErrorResolvableConverter {

    @Override
    public boolean support(Throwable cause) {
        // 需要转换的异常
        return cause instanceof MethodArgumentNotValidException;
    }


    @Override
    public ErrorResolvable convert(Throwable cause) {
        MethodArgumentNotValidException e = (MethodArgumentNotValidException) cause;
        ...
        // 返回异常结果
        return new ErrorWebStatusResolver("500", “参数错误”);
    }
}

默认支持MethodArgumentNotValidException与ConstraintViolationException类异常转换。详情请参考:ConstraintViolationExceptionErrorConverter与MethodArgumentNotValidExceptionErrorConverter异常转换类。

API接口文档生成

前后端分离,后台负责写接口。随着接口越来越多,接口清单越来越重要,传统是需要自己去维护一个doc的文档或者公司统一放在一个接口清单的web服务上。每次开发者需要单独添加上去。修改后还需要维护。现接入swagger,后端开发人员只需要根据 OpenAPI 官方定义的注解就可以把接口文档非常丰富的呈现给前端接口对接人员。并且接口文档是随着代码的变动实时更新,同时提供了在线 HTML 文档辅助开发人员可以进行接口联调测试,这大大省去了技术人员写文档的烦恼,也提升了企业开发的效率,减少沟通成本。

Knife4j 是一个 SwaggerUI 的增强工具,同时也提供了一些增强功能,使用 Java+Vue 进行开发,帮助开发者能在编写接口注释时更加完善,基于 OpenAPI 的规范完全重写 UI 界面,左右布局的方式也更加适合国人的习惯。

本starter集成Knife4j 来生成API接口文档。

引入aliyun-gts-base-starter依赖,默认不开启swagger ;当gts.common.swagger.enable等于true,则开启API接口文档生成。
为了保护系统的安全,在生成环境(profile标识:prd、prod、production)下不开启API接口文档生成,用户访问接口文档链接无效。
gts.common.swagger.enable=falseAPI接口文档生成。

浏览器输入访问http://127.0.0.1:8080/doc.html ,界面效果如下:
在这里插入图片描述
接口MOCK:
在这里插入图片描述
离线Markdown文档生成:
在这里插入图片描述
具体页面如何操作以及详情,请参考knife4j官网文档。

其他

请求打印输出

默认开启请求打印输出,打印请求相关的信息。不同的追踪级别,输出的日志内容详情程度不同。默认追踪级别为LOW。

LOW
输出日志打印请求url、请求入参、访问api、访问时间。
MEDIUM
输出日志打印请求url、请求入参、访问api、访问时间+请求体内容
HIGH
输出日志打印请求url、请求入参、访问api、访问时间+请求体内容+返回结果

参数配置
在这里插入图片描述
在这里插入图片描述

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

weixin_46007090

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

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

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

打赏作者

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

抵扣说明:

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

余额充值