在我们开发新的业务模块前,我们再搞完最后一块内容——Rest API在线文档。为啥要在线文档?这个主要不是给我们后端开发人员用的,而是给前端开发的mm来对接用的。
后台开发的gg在开发一个完整的业务模块前,可以先定好输入输出,并返回构造的数据,然后就把在线文档服务提供出来,给前端开发mm先熟悉和调用在线文档,这样后台开发的gg再放心的做业务层的逻辑开发,也不耽误前端开发嘛。废话不多说,开整!
集成swagger
集成swagger很简单,只需要下面几步:
首先我们引入相关依赖:
dependencies {
...
implementation 'org.springdoc:springdoc-openapi-ui:1.5.13'
}
在application.yml中加入swagger的配置和自定义的文档说明配置:
...
springdoc:
packagesToScan: com.xiaojuan.boot.web.controller
pathsToMatch: /**
swagger-ui:
disable-swagger-default-url: true
path: /swagger-ui.html
api-docs:
# 页面访问地址 http://localhost:8080/swagger-ui/index.html?configUrl=/v3/api-docs/swagger-config
path: /v3/api-docs
# 自定义open API公共描述信息
api:
common:
version: 3.0.0
title: 小卷生鲜在线API
description: 这是小卷生鲜电商项目的在线API
termsOfService:
license: 未经许可不得转载
licenseUrl: xxx.license.com
externalDocDesc:
externalDocUrl:
contact:
name: xiaojuan
url: edu.xiaojuan.com
email: xiaojuan@xxx.com
配置类
新加一个SwaggerConfig
配置类来完成swagger配置:
package com.xiaojuan.boot.web;
import io.swagger.v3.oas.models.ExternalDocumentation;
import io.swagger.v3.oas.models.OpenAPI;
import io.swagger.v3.oas.models.info.Contact;
import io.swagger.v3.oas.models.info.Info;
import io.swagger.v3.oas.models.info.License;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class SwaggerConfig {
@Value("${api.common.version}") String apiVersion;
@Value("${api.common.title}") String apiTitle;
@Value("${api.common.description}") String apiDescription;
@Value("${api.common.termsOfService}") String apiTermsOfService;
@Value("${api.common.license}") String apiLicense;
@Value("${api.common.licenseUrl}") String apiLicenseUrl;
@Value("${api.common.externalDocDesc}") String apiExternalDocDesc;
@Value("${api.common.externalDocUrl}") String apiExternalDocUrl;
@Value("${api.common.contact.name}") String apiContactName;
@Value("${api.common.contact.url}") String apiContactUrl;
@Value("${api.common.contact.email}") String apiContactEmail;
@Bean
public OpenAPI getOpenApiDocumentation() {
return new OpenAPI()
.info(new Info().title(apiTitle)
.description(apiDescription)
.version(apiVersion)
.contact(new Contact()
.name(apiContactName)
.url(apiContactUrl)
.email(apiContactEmail))
.termsOfService(apiTermsOfService)
.license(new License()
.name(apiLicense)
.url(apiLicenseUrl)))
.externalDocs(new ExternalDocumentation()
.description(apiExternalDocDesc)
.url(apiExternalDocUrl));
}
}
在全局响应处理类RestBodyAdvice
中排除掉swagger的请求地址:
@Override
public boolean supports(MethodParameter returnType, Class> converterType) {
// 排除swagger的rest api请求
String clzName = returnType.getDeclaringClass().getName();
if (clzName.startsWith("org.springdoc")) return false;
return true;
}
前面我们实现了通过filter
组件来记录请求日志,现在我们要排除swagger的请求了,看下相关类内部的调整:
package com.xiaojuan.boot.web.filter;
import ...
@Slf4j
public class RequestLogFilter extends OncePerRequestFilter {
...
private AntPathMatcher pathMatcher = new AntPathMatcher();
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
if (excludeUrl(request)) {
filterChain.doFilter(request, response);
return;
}
...
}
...
private boolean excludeUrl(HttpServletRequest request) {
List excludeUrlPatterns = Arrays.asList("/**/swagger-ui/**", "/**/v3/api-docs/**");
String uri = request.getRequestURI();
for (String pattern : excludeUrlPatterns) {
if (pathMatcher.match(pattern, uri)) {
return true;
}
}
return false;
}
}
完成上面几个步骤后,我们的swagger就集成进来了。
启动web后,在线api访问地址:http://localhost:8080/swagger-ui/index.html?configUrl=/v3/api-docs/swagger-config
看到的界面:
统一Response格式
为了进一步规范我们统一响应格式的输出,我们制定出更明确的响应规范,这些规范也将会在我们的swagger在线文档的响应模块中体现出来。我们定的规范如下:
-
正常响应
响应成功的情况下,我们只需要返回
status
为0
即可,不需要把诸如success
的成功信息返回,因为只要不是异常状态,就是执行成功,就像linux上执行命令的结果一样。-
api接口返回值为
void
这种情况下,后台不需要返回数据,只需要响应一个状态即可:
{ "status": 0 }
-
返回非
void
说明不管取到结果是否为空,都要返回,响应格式:
{ "status": 0, "data": ... }
这里的
data
类型不固定,在swagger中会由具体的数据类型的schema
来决定。
-
-
异常响应
异常响应的情况下,我们将所有的信息都返回,格式如下:
{ "status": 1, "msg": "错误消息", "data": ..., "errCode": "10001" }
其中,
data
和errCode
可以为空。
按照如上的约定,现在我们调整下RestBodyAdvice
的正常响应包装逻辑:
package com.xiaojuan.boot.common.web.support;
import ...
@Slf4j
@RestControllerAdvice
public class RestBodyAdvice implements ResponseBodyAdvice {
...
@SneakyThrows
@Override
public Object beforeBodyWrite(...) {
...
Map result = new HashMap();
result.put("status", 0);
Type type = returnType.getGenericParameterType();
if (type == String.class) {
// 字符串需要手动序列化为json
response.getHeaders().set("Content-Type", MediaType.APPLICATION_JSON_UTF8_VALUE);
result.put("data", body);
logSuccess(result);
return objectMapper.writeValueAsString(result);
}
if (StringUtils.equals("void", type.getTypeName())) {
logSuccess(result);
return result;
}
result.put("data", body);
logSuccess(result);
return result;
}
...
@SneakyThrows
private void logSuccess(Map result) {
log.info("========== 响应结果: {}", objectMapper.writeValueAsString(result));
log.info("====================================================================================================");
}
}
因为我们之前实现了全局统一的响应格式,而对Rest API接口的返回值类型做了精简,现在要生成swagger响应的schema,默认无法展示出统一响应格式的全部内容,比如,看下用户信息查询的swagger schema默认展示:
swagger为我们提供了对API操作的定制形式,可以个性化展示各种信息,现在我们做如下定制:
package com.xiaojuan.boot.web;
import ...
@Slf4j
@Configuration
public class SwaggerConfig {
...
@Bean
public OperationCustomizer operationCustomizer() {
return (operation, method) -> {
ApiResponses responses = operation.getResponses();
for (String statusKey : responses.keySet()) {
ApiResponse resp = responses.get(statusKey);
Type type = method.getReturnType().getGenericParameterType();
ResolvedSchema resolvedSchema;
if (type instanceof ParameterizedTypeImpl) {
resolvedSchema = ModelConverters.getInstance().readAllAsResolvedSchema(new AnnotatedType(type).resolveAsRef(true));
} else {
resolvedSchema = ModelConverters.getInstance().resolveAsResolvedSchema(new AnnotatedType(type));
}
Schema respSchema = null;
if ("200".equals(statusKey)) {
respSchema = new ObjectSchema()
.type("object")
.addProperties("status", new IntegerSchema()._default(0))
.addProperties("data", StringUtils.equals("void", type.getTypeName()) ? null : resolvedSchema.schema);
} else {
respSchema = new ObjectSchema()
.type("object")
.addProperties("status", new IntegerSchema()._default(1))
.addProperties("msg", new StringSchema())
.addProperties("errCode", new IntegerSchema())
.addProperties("data", new ObjectSchema());
}
resp.setContent(new Content().addMediaType("application/json",new MediaType().schema(respSchema)));
}
return operation;
};
}
}
代码说明
这里我们获取
controller
每个方法的返回值类型后,通过swagger提供的工具API将其转成swagger的schema
对象,用于在swagger响应中描述响应对象的schema
。这里要注意返回值类型可能是一个泛型类型,则我们用其提供的readAllAsResolvedSchema
方法进行级联解析,并以$ref
属性来绑定schema
的引用关系,而普通类型这直接解析为相应的schema
即可。然后我们判断这条响应操作的http状态码,也就是我们在API接口上通过如下形式定义的响应状态值:
@ApiResponses({ @ApiResponse(responseCode = "200", description = "注册成功"), @ApiResponse(responseCode = "500 - 10001", description = "参数校验失败"), @ApiResponse(responseCode = "500 - 10002", description = "用户已注册") }) @PostMapping("register") void register(@Valid UserRegisterDTO userRegisterDTO);
我们判断响应操作是不是
200
的http状态码,再构造成功响应和失败响应的schema
结构,对响应操作进行设置。这样我们就避免了手动在@ApiResponse
中通过相关注解设置schema
,而该方式对于正常响应无法指定泛型类型。
这样我们看到的效果,
查看用户列表的swagger响应参考:
这里我们并没有在API接口上加@ApiResponses
注解来指定一条200
状态码的响应记录,swagger会默认给我们生成一条对应的响应文档。而前面我们对用户注册的响应描述做了定义后,会看到:
这里为了排版看的舒服,截图后横向排列了。
这里我们为响应码约定了一种格式:http状态码 - 业务错误码
。除了这种方式,我们也可以用一个500
的http状态码,通过swagger响应注解提供的扩展配置来实现其他信息的设置:
@ApiResponse(responseCode = "500", description = "注册失败",
extensions = @Extension(name = SysConst.ERROR_CODE,
properties = {
@ExtensionProperty(name = "10001", value="参数校验失败"),
@ExtensionProperty(name = "10002", value="用户已注册")
}))
这样的话,我们可以将额外的信息设置到schema
的errCode
字段描述中,调整下个性化的配置:
package com.xiaojuan.boot.web;
import ...
@Slf4j
@Configuration
public class SwaggerConfig {
private static final String EXTENSION_PREFIX = "x-";
...
@Bean
public OperationCustomizer operationCustomizer() {
return (operation, method) -> {
ApiResponses responses = operation.getResponses();
for (String statusKey : responses.keySet()) {
...
Schema respSchema;
if ("200".equals(statusKey)) {
...
} else {
respSchema = new ObjectSchema()...
appendErrCodeInfo(resp, (ObjectSchema) respSchema);
}
...
}
return operation;
};
}
private void appendErrCodeInfo(ApiResponse resp, ObjectSchema schema) {
Map extMap = resp.getExtensions();
if (ObjectUtils.isNotEmpty(extMap)) {
String key = EXTENSION_PREFIX + SysConst.ERROR_CODE;
if (extMap.containsKey(key)) {
Map errCodeMap = (Map) extMap.get(key);
StringBuilder builder = new StringBuilder("可能返回的错误码:");
int i = 0;
for (Map.Entry entry : errCodeMap.entrySet()) {
builder.append(entry.getKey()).append("-").append(entry.getValue());
if (i < errCodeMap.keySet().size() - 1) {
builder.append("、");
}
i++;
}
schema.getProperties().get("errCode").setDescription(builder.toString());
}
}
}
}
要注意,扩展的配置属性名会以x-
开头。这样我们可以看到schema
所展示的信息多了一个:
这里不得吐槽下,swagger官方的文档风格不够大气,后面我们将用其他的UI依赖来升级这种用户体验。
swagger常用注解
关于swagger常用注解的用法,这里我们将以实际的例子来介绍。
package com.xiaojuan.boot.web.api;
import ...
@Tag(name = "UserAPI", description = "用户API")
...
public interface UserAPI {
...
@Operation(summary = "用户注册接口", description = "注册一个普通用户")
...
void register(@Valid UserRegisterDTO userRegisterDTO);
...
}
这些说明性的注解,会给swagger文档带来如下装饰:
用于方法参数(@Parameter
)和DTO字段(@Schema
)修饰的swagger注解用法示例如下:
@PostMapping("login")
UserInfoDTO login(
@Parameter(name = "用户名", description = "请输入用户名")
...
String username,
@Parameter(name = "密码", description = "请输入密码", schema = @Schema(minLength = 3, maxLength = 10))
...
String password
);
public class UserRegisterDTO {
...
@Schema(description = "年龄", maximum = "60")
private Integer age;
...
@Schema(description = "手机号", pattern = PatternConst.MOBILE)
private String mobileNo;
...
}
上面我们用swagger注解为密码参数和手机号字段指定了约束,这些约束仅用于swagger在线文档上文本框输入的检查或DTO类型schema
的格式描述,以增加可读性,而不会应用到后台的校验上;另外,我们原来加的validation校验注解同样会被应用到swagger在线文档中,对字段约束和格式进行描述或控制表单参数输入文本框的检查。看下应用的效果:
只是这种展示风格不够大气罢了。
api分组
当我们开发的一个单体应用中业务模块太多的话,我们可以实现api的分组,按照分组来隔开各模块的API在线文档。
现在我们将开发的controller组件按照模块划分到不同的子包下:
在application.yml中进行分组配置:
springdoc:
# packagesToScan: com.xiaojuan.boot.web.controller
# pathsToMatch: /**
group-configs:
- group: '用户模块'
packagesToScan: com.xiaojuan.boot.web.controller.user
pathsToMatch: /**
- group: '管理模块'
packagesToScan: com.xiaojuan.boot.web.controller.admin
pathsToMatch: /**
- group: '测试模块'
packagesToScan: com.xiaojuan.boot.web.controller.test
pathsToMatch: /**
这样就可以选择一个分组来进入其api文档页面了:
本节我们实现了spring boot与swagger在线文档的整合,但我们也发现本身swagger官方的文档功能都展示出来了,就排版不是那么友好。且我们在开发Rest API时要手动声明swagger文档的注解,会做很多额外的工作。后面我们会介绍用swagger-codegen插件来基于我们定义的swagger遵循的open api3规范的yml配置来帮我们生成Rest API的接口代码,并包含生成好的swagger注解。这样我们只要维护配置文件即可。大家加油!