概述
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.basegroupId>
<artifactId>aliyun-gts-base-starterartifactId>
<version>最新版本version>
dependency>
即可集成统一结果返回、异常处理、API接口文档生成等基础功能。
代码仓库地址
[email protected]: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向外暴露的方法都要创建一个返回的对象来封装这种处理,并在出现异常时捕获异常进行处理。
因此最好是能够统一处理这种转换,这样的话服务提供者就只需关注他原本就需要处理的事情:
为达到统一处理的目的,需要针对两个场景做单独的处理:
默认统一结果
默认使用@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;
}
}
返回结果如下:
{
"requestId": "76f9d04c-818c-4ad8-9d7c-e23d86be82bd",
"code": "200",
"message": "success",
"success": true,
"meta": null,
"traceId": null,
"data": {
"userName": "张三",
"userId": "C0001"
}
}
默认当项目工程中引入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
}
}
在微服务情境下,有如下接口:
@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支持自定义统一返回结果。
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);
}
}
@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
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、访问时间+请求体内容+返回结果