Springdoc——根据JavaDoc生成接口文档

本文最后更新于:4 个月前

在现代应用程序开发中,API文档是非常重要的一部分,它不仅帮助开发者更好地理解接口的使用,也有助于跨团队协作。本文将介绍如何使用Springdoc自动地、无侵入地生成基于JavadocAPI文档,帮助你在开发中更简单地维护API文档。

项目配置

为了实现根据Javadoc自动生成API文档,需要在项目中引用Springdoc提供的springdoc-openapi-webmvc-corespringdoc-openapi-javadoc依赖,在POM文件中添加以下配置:

1
2
3
4
5
6
7
8
9
10
<dependency>
<groupId>org.springdoc</groupId>
<artifactId>springdoc-openapi-webmvc-core</artifactId>
<version>${springdoc.version}</version>
</dependency>
<dependency>
<groupId>org.springdoc</groupId>
<artifactId>springdoc-openapi-javadoc</artifactId>
<version>${springdoc.version}</version>
</dependency>

为了保留运行时的注释信息,使用了Maven Compiler Plugin并进行了如下配置:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.9.0</version>
<configuration>
<source>${java.version}</source>
<target>${java.version}</target>
<encoding>${project.build.sourceEncoding}</encoding>
<annotationProcessorPaths>
<path>
<groupId>com.github.therapi</groupId>
<artifactId>therapi-runtime-javadoc-scribe</artifactId>
<version>0.15.0</version>
</path>
</annotationProcessorPaths>
</configuration>
</plugin>

该依赖用于在编译时处理Javadoc注释并将其保留在运行时,方便Springdoc生成基于Javadoc的 API 文档。这使得文档信息可以在应用程序运行时动态提取。

Springdoc配置

为了更好地控制API文档的生成,我们可以创建一个配置类SpringDocProperties来定义文档的属性

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
@Data
@Component
@ConfigurationProperties(prefix = "springdoc")
public class SpringDocProperties {

/**
* 文档基本信息
*/
@NestedConfigurationProperty
private InfoProperties info = new InfoProperties();

/**
* 扩展文档地址
*/
@NestedConfigurationProperty
private ExternalDocumentation externalDocs;

/**
* 标签
*/
private List<Tag> tags = null;

/**
* 路径
*/
@NestedConfigurationProperty
private Paths paths = null;

/**
* 组件
*/
@NestedConfigurationProperty
private Components components = null;

@Data
public static class InfoProperties {

/**
* 标题
*/
private String title = null;

/**
* 描述
*/
private String description = null;

/**
* 联系人信息
*/
@NestedConfigurationProperty
private Contact contact = null;

/**
* 许可证
*/
@NestedConfigurationProperty
private License license = null;

/**
* 版本
*/
private String version = null;
}
}

配置Springdoc

接下来,创建SpringDocConfig配置类,这个类将根据前面的SpringDocProperties来配置生成的文档信息

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
@RequiredArgsConstructor
@Configuration
@AutoConfigureBefore(SpringDocConfiguration.class) // 在 SpringDocConfiguration 配置类之前自动配置
@ConditionalOnProperty(name = "springdoc.api-docs.enabled", havingValue = "true", matchIfMissing = true) // 只有在属性 springdoc.api-docs.enabled 设置为true或缺省时,才启用此配置类
public class SpringDocConfig {

// 注入SpringDocProperties配置类,用于获取文档的基本属性配置
private final SpringDocProperties springDocProperties;

// 注入ServerProperties用于获取服务器的上下文路径等信息
private final ServerProperties serverProperties;

@Bean
@ConditionalOnMissingBean(OpenAPI.class) // 如果没有OpenAPI的Bean实例,则创建一个
public OpenAPI openApi() {
OpenAPI openApi = new OpenAPI();
// 设置文档基本信息
SpringDocProperties.InfoProperties infoProperties = springDocProperties.getInfo();
Info info = convertInfo(infoProperties);
openApi.info(info);
// 设置扩展文档信息
openApi.externalDocs(springDocProperties.getExternalDocs());
// 设置文档标签
openApi.tags(springDocProperties.getTags());
// 设置路径信息
openApi.paths(springDocProperties.getPaths());
// 设置组件信息
openApi.components(springDocProperties.getComponents());

// 配置安全需求
Set<String> keySet = springDocProperties.getComponents().getSecuritySchemes().keySet();
List<SecurityRequirement> list = new ArrayList<>();
SecurityRequirement securityRequirement = new SecurityRequirement();
// 将每个安全方案添加到安全需求列表中
keySet.forEach(securityRequirement::addList);
list.add(securityRequirement);
openApi.security(list);

return openApi;
}

// 将SpringDocProperties的InfoProperties转换为OpenAPI 的Info对象
private Info convertInfo(SpringDocProperties.InfoProperties infoProperties) {
Info info = new Info();
info.setTitle(infoProperties.getTitle()); // 设置标题
info.setDescription(infoProperties.getDescription()); // 设置描述
info.setContact(infoProperties.getContact()); // 设置联系人信息
info.setLicense(infoProperties.getLicense()); // 设置许可证信息
info.setVersion(infoProperties.getVersion()); // 设置版本信息
return info;
}

/**
* 自定义openapi处理器
*/
@Bean
public OpenAPIService openApiBuilder(Optional<OpenAPI> openAPI,
SecurityService securityParser,
SpringDocConfigProperties springDocConfigProperties, PropertyResolverUtils propertyResolverUtils,
Optional<List<OpenApiBuilderCustomizer>> openApiBuilderCustomisers,
Optional<List<ServerBaseUrlCustomizer>> serverBaseUrlCustomisers, Optional<JavadocProvider> javadocProvider) {
return new OpenApiHandler(openAPI, securityParser, springDocConfigProperties, propertyResolverUtils, openApiBuilderCustomisers, serverBaseUrlCustomisers, javadocProvider);
}

/**
* 对已经生成好的OpenApi进行自定义操作
*/
@Bean
public OpenApiCustomiser openApiCustomiser() {
String contextPath = serverProperties.getServlet().getContextPath();
String finalContextPath;
// 如果上下文路径为空或为"/",则设置为空字符串,否则使用原始上下文路径
if (StringUtils.isBlank(contextPath) || "/".equals(contextPath)) {
finalContextPath = "";
} else {
finalContextPath = contextPath;
}
// 对所有路径增加前置上下文路径
return openApi -> {
Paths oldPaths = openApi.getPaths();
if (oldPaths instanceof PlusPaths) {
return;
}
PlusPaths newPaths = new PlusPaths();
// 将旧路径增加上下文路径后存入新路径对象中
oldPaths.forEach((k,v) -> newPaths.addPathItem(finalContextPath + k, v));
openApi.setPaths(newPaths);
};
}

/**
* 单独使用一个类便于判断,解决springdoc路径拼接重复问题
*/
static class PlusPaths extends Paths {
public PlusPaths() {
super();
}
}
}

自定义API处理器

为了更好地生成文档信息,我们可以实现一个自定义的OpenAPI处理器:OpenApiHandler。这个类可以从Javadoc中提取注释信息,并将其添加到OpenAPI文档中。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
public class OpenApiHandler extends OpenAPIService {
/**
* 日志记录器,用于记录处理过程中的信息
*/
private static final Logger LOGGER = LoggerFactory.getLogger(OpenAPIService.class);

/**
* 应用程序上下文
*/
private ApplicationContext context;

/**
* 安全解析器,用于处理安全相关的配置
*/
private final SecurityService securityParser;

/**
* 存储映射信息的映射表
*/
private final Map<String, Object> mappingsMap = new HashMap<>();

/**
* 存储Springdoc标签信息的映射表
*/
private final Map<HandlerMethod, io.swagger.v3.oas.models.tags.Tag> springdocTags = new HashMap<>();

/**
* OpenAPI构建自定义器列表
*/
private final Optional<List<OpenApiBuilderCustomizer>> openApiBuilderCustomisers;

/**
* 服务器基础URL自定义器列表
*/
private final Optional<List<ServerBaseUrlCustomizer>> serverBaseUrlCustomizers;

/**
* SpringDoc配置属性
*/
private final SpringDocConfigProperties springDocConfigProperties;

/**
* OpenAPI对象
*/
private OpenAPI openAPI;

/**
* 缓存的OpenAPI映射表
*/
private final Map<String, OpenAPI> cachedOpenAPI = new HashMap<>();

/**
* 标识是否存在服务器配置
*/
private boolean isServersPresent;

/**
* 服务器基础URL
*/
private String serverBaseUrl;

/**
* 属性解析工具类
*/
private final PropertyResolverUtils propertyResolverUtils;

/**
* Javadoc提供者,用于从Javadoc中提取注释信息
*/
private final Optional<JavadocProvider> javadocProvider;

/**
* 基础错误控制器类
*/
private static Class<?> basicErrorController;

static {
try {
// 尝试加载Spring Boot 2的基础错误控制器类
basicErrorController = Class.forName("org.springframework.boot.autoconfigure.web.servlet.error.BasicErrorController");
} catch (ClassNotFoundException e) {
try {
// 如果找不到,尝试加载Spring Boot 1的基础错误控制器类
basicErrorController = Class.forName("org.springframework.boot.autoconfigure.web.BasicErrorController");
} catch (ClassNotFoundException classNotFoundException) {
// 如果仍然找不到,记录错误信息
LOGGER.trace(classNotFoundException.getMessage());
}
}
}

/**
* 构造函数,初始化各个属性
*/
public OpenApiHandler(Optional<OpenAPI> openAPI, SecurityService securityParser,
SpringDocConfigProperties springDocConfigProperties, PropertyResolverUtils propertyResolverUtils,
Optional<List<OpenApiBuilderCustomizer>> openApiBuilderCustomizers,
Optional<List<ServerBaseUrlCustomizer>> serverBaseUrlCustomizers,
Optional<JavadocProvider> javadocProvider) {
super(openAPI, securityParser, springDocConfigProperties, propertyResolverUtils, openApiBuilderCustomizers, serverBaseUrlCustomizers, javadocProvider);
if (openAPI.isPresent()) {
this.openAPI = openAPI.get();
if (this.openAPI.getComponents() == null)
this.openAPI.setComponents(new Components());
if (this.openAPI.getPaths() == null)
this.openAPI.setPaths(new Paths());
if (!CollectionUtils.isEmpty(this.openAPI.getServers()))
this.isServersPresent = true;
}
this.propertyResolverUtils = propertyResolverUtils;
this.securityParser = securityParser;
this.springDocConfigProperties = springDocConfigProperties;
this.openApiBuilderCustomisers = openApiBuilderCustomizers;
this.serverBaseUrlCustomizers = serverBaseUrlCustomizers;
this.javadocProvider = javadocProvider;
if (springDocConfigProperties.isUseFqn())
TypeNameResolver.std.setUseFqn(true);
}

/**
* 构建操作标签
*
* @param handlerMethod 处理方法
* @param operation 操作对象
* @param openAPI OpenAPI 对象
* @param locale 语言环境
* @return 构建后的操作对象
*/
@Override
public Operation buildTags(HandlerMethod handlerMethod, Operation operation, OpenAPI openAPI, Locale locale) {
Set<Tag> tags = new HashSet<>();
Set<String> tagsStr = new HashSet<>();

// 从方法和类中构建标签
buildTagsFromMethod(handlerMethod.getMethod(), tags, tagsStr, locale);
buildTagsFromClass(handlerMethod.getBeanType(), tags, tagsStr, locale);

if (!CollectionUtils.isEmpty(tagsStr))
tagsStr = tagsStr.stream()
.map(str -> propertyResolverUtils.resolve(str, locale))
.collect(Collectors.toSet());

// 处理自定义的Springdoc标签
if (springdocTags.containsKey(handlerMethod)) {
io.swagger.v3.oas.models.tags.Tag tag = springdocTags.get(handlerMethod);
tagsStr.add(tag.getName());
if (openAPI.getTags() == null || !openAPI.getTags().contains(tag)) {
openAPI.addTagsItem(tag);
}
}

// 设置操作的标签
if (!CollectionUtils.isEmpty(tagsStr)) {
if (CollectionUtils.isEmpty(operation.getTags()))
operation.setTags(new ArrayList<>(tagsStr));
else {
Set<String> operationTagsSet = new HashSet<>(operation.getTags());
operationTagsSet.addAll(tagsStr);
operation.getTags().clear();
operation.getTags().addAll(operationTagsSet);
}
}

// 自动从类名生成标签
if (isAutoTagClasses(operation)) {
if (javadocProvider.isPresent()) {
// 使用Javadoc提供者从类中提取注释作为标签
String description = javadocProvider.get().getClassJavadoc(handlerMethod.getBeanType());
if (StringUtils.isNotBlank(description)) {
io.swagger.v3.oas.models.tags.Tag tag = new io.swagger.v3.oas.models.tags.Tag();
List<String> list = IoUtil.readLines(new StringReader(description), new ArrayList<>());
tag.setName(list.get(0));
operation.addTagsItem(list.get(0));
tag.setDescription(description);
if (openAPI.getTags() == null || !openAPI.getTags().contains(tag)) {
openAPI.addTagsItem(tag);
}
}
} else {
String tagAutoName = splitCamelCase(handlerMethod.getBeanType().getSimpleName());
operation.addTagsItem(tagAutoName);
}
}

if (!CollectionUtils.isEmpty(tags)) {
// Existing tags
List<io.swagger.v3.oas.models.tags.Tag> openApiTags = openAPI.getTags();
if (!CollectionUtils.isEmpty(openApiTags))
tags.addAll(openApiTags);
openAPI.setTags(new ArrayList<>(tags));
}

// 添加安全需求到操作级别
io.swagger.v3.oas.annotations.security.SecurityRequirement[] securityRequirements = securityParser
.getSecurityRequirements(handlerMethod);
if (securityRequirements != null) {
if (securityRequirements.length == 0)
operation.setSecurity(Collections.emptyList());
else
securityParser.buildSecurityRequirement(securityRequirements, operation);
}

return operation;
}

private void buildTagsFromMethod(Method method, Set<io.swagger.v3.oas.models.tags.Tag> tags, Set<String> tagsStr, Locale locale) {
// method tags
Set<Tags> tagsSet = AnnotatedElementUtils
.findAllMergedAnnotations(method, Tags.class);
Set<io.swagger.v3.oas.annotations.tags.Tag> methodTags = tagsSet.stream()
.flatMap(x -> Stream.of(x.value())).collect(Collectors.toSet());
methodTags.addAll(AnnotatedElementUtils.findAllMergedAnnotations(method, io.swagger.v3.oas.annotations.tags.Tag.class));
if (!CollectionUtils.isEmpty(methodTags)) {
tagsStr.addAll(methodTags.stream().map(tag -> propertyResolverUtils.resolve(tag.name(), locale)).collect(Collectors.toSet()));
List<io.swagger.v3.oas.annotations.tags.Tag> allTags = new ArrayList<>(methodTags);
addTags(allTags, tags, locale);
}
}

private void addTags(List<io.swagger.v3.oas.annotations.tags.Tag> sourceTags, Set<io.swagger.v3.oas.models.tags.Tag> tags, Locale locale) {
Optional<Set<io.swagger.v3.oas.models.tags.Tag>> optionalTagSet = AnnotationsUtils
.getTags(sourceTags.toArray(new io.swagger.v3.oas.annotations.tags.Tag[0]), true);
optionalTagSet.ifPresent(tagsSet -> {
tagsSet.forEach(tag -> {
tag.name(propertyResolverUtils.resolve(tag.getName(), locale));
tag.description(propertyResolverUtils.resolve(tag.getDescription(), locale));
if (tags.stream().noneMatch(t -> t.getName().equals(tag.getName())))
tags.add(tag);
});
});
}

}

测试

为服务添加Springdoc的配置

1
2
3
4
5
springdoc.api-docs.enabled=true
springdoc.api-docs.path=/v3/api-docs
springdoc.info.title=System API
springdoc.info.description=System API
springdoc.info.version=1.0.0

编写Controller接口如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
/**
* 测试Controller
*/
@RestController
@RequestMapping("/test")
public class TestController {

/**
* 查询
*
* @param queryDTO 查询条件
* @return {@link List }<{@link TestVO }> 查询结果
*/
@PostMapping("/query")
public List<TestVO> query(@RequestBody TestQueryDTO queryDTO) {
return null;
}

}

接口参数DTO和返回结果VO定义如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
/**
* 测试DTO
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
@Accessors(chain = true)
public class TestQueryDTO {
/**
* 名称
*/
private String name;
/**
* 年龄
*/
private Integer age;
/**
* 生日
*/
private LocalDate birthday;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
@Data
@NoArgsConstructor
@AllArgsConstructor
@Accessors(chain = true)
public class TestVO {
/**
* 名称
*/
private String name;
/**
* 年龄
*/
private Integer age;
/**
* 生日
*/
private LocalDate birthday;
/**
* 其他
*/
private String others;
}

启动项目,使用Postman导入接口api-docsapi-docs的路径为服务配置的/v3/api-docs

或者使用ApiFox等对API文档支持更好的软件,能看到接口的详细信息

如图,接口、实体类的JavaDoc信息已被提取到API文档中。

总结

通过结合SpringdocJavadoc,你可以轻松地生成完整的API文档,不仅节省了手动编写文档的时间,也减少了维护文档的工作量。结合本文中的配置方法,你可以自定义文档的各个部分,使项目协作更加高效便捷。