本文最后更新于:4 个月前
在现代应用程序开发中,API文档是非常重要的一部分,它不仅帮助开发者更好地理解接口的使用,也有助于跨团队协作。本文将介绍如何使用Springdoc
自动地、无侵入地生成基于Javadoc
的API
文档,帮助你在开发中更简单地维护API文档。
项目配置 为了实现根据Javadoc
自动生成API文档,需要在项目中引用Springdoc
提供的springdoc-openapi-webmvc-core
和springdoc-openapi-javadoc
依赖,在POM文件中添加以下配置:
<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) @ConditionalOnProperty(name = "springdoc.api-docs.enabled", havingValue = "true", matchIfMissing = true) public class SpringDocConfig { private final SpringDocProperties springDocProperties; private final ServerProperties serverProperties; @Bean @ConditionalOnMissingBean(OpenAPI.class) 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; } 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; } @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); } @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); }; } 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<>(); private final Map<HandlerMethod, io.swagger.v3.oas.models.tags.Tag> springdocTags = new HashMap<>(); private final Optional<List<OpenApiBuilderCustomizer>> openApiBuilderCustomisers; private final Optional<List<ServerBaseUrlCustomizer>> serverBaseUrlCustomizers; private final SpringDocConfigProperties springDocConfigProperties; private OpenAPI openAPI; private final Map<String, OpenAPI> cachedOpenAPI = new HashMap<>(); private boolean isServersPresent; private String serverBaseUrl; private final PropertyResolverUtils propertyResolverUtils; private final Optional<JavadocProvider> javadocProvider; private static Class<?> basicErrorController; static { try { basicErrorController = Class.forName("org.springframework.boot.autoconfigure.web.servlet.error.BasicErrorController" ); } catch (ClassNotFoundException e) { try { 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 ); } @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()); 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()) { 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)) { 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) { 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
的配置
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 @RestController @RequestMapping("/test") public class TestController { @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 @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-docs
,api-docs
的路径为服务配置的/v3/api-docs
或者使用ApiFox
等对API文档支持更好的软件,能看到接口的详细信息
如图,接口、实体类的JavaDoc
信息已被提取到API文档中。
总结 通过结合Springdoc
和Javadoc
,你可以轻松地生成完整的API文档,不仅节省了手动编写文档的时间,也减少了维护文档的工作量。结合本文中的配置方法,你可以自定义文档的各个部分,使项目协作更加高效便捷。