场景
有时候需要为前端开发者提供Restful Api说明文档,通过word文档创建和修改非常耗时,希望有一种比较便捷的第三方库可以减少生成Api说明文档的工作量
基于Spring的Restful Api生成工具
术语解析
- Springfox-swagger2
Swagger是一个可以生成基于多种语言编写的Restful Api的文档生成工具,详见这里。
查看Springfox-swagger2注解文档,请点击这里 - Swagger2markup
Swagger2markup是一个使用java编写的将Swagger语义转换成markdown、asciidoc文本格式的开源项目
Swagger2Markerup的详细说明请见这里 - Asciidoc
AsciiDoc是一种MarkDown的扩展文本格式,AsciiDoc相比MarkDown更适合编写类似API文档,学术文档这样的文档。
详见这里 - Asciidoctor
asciidoctor是一个由ruby编写的可以将 asciidoc转换成html、pdf的开源项目,这个项目有java版本和maven插件,详见这里
接口生成原理
- generate an up-to-date Swagger JSON file during an unit or integration test
使用Springfox-swagger2生成swagger json文件 - convert the Swagger JSON file into AsciiDoc
使用Swagger2markup将swagger json文件转换成asciidoc文档片段 - add hand-written AsciiDoc documentation
编写asciidoc的文档(主要是组装步骤2中生成的asciidoc文档片段) - convert AsciiDoc into HTML and PDF
使用Asciidoctor将asciidoc转换成HTML 或pdf
Swagger部署说明
- 在pom引入下面依赖:
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger2</artifactId>
<version>2.4.0</version>
<scope>test</scope>
</dependency>
- 配置一个SwaggerConfig
@EnableSwagger2
@Configuration
@Import(BeanValidatorPluginsConfiguration.class)
public class SwaggerConfig {
@Bean
public Docket restApi() {
return new Docket(DocumentationType.SWAGGER_2)
.apiInfo(apiInfo())
.securitySchemes(asList(
new OAuth(
"petstore_auth",
asList(new AuthorizationScope("write_pets", "modify pets in your account"),
new AuthorizationScope("read_pets", "read your pets")),
Arrays.<GrantType>asList(new ImplicitGrant(new LoginEndpoint("http://petstore.swagger.io/api/oauth/dialog"), "tokenName"))
),
new ApiKey("api_key", "api_key", "header")
))
.select()
.paths(Predicates.and(ant("/**"), Predicates.not(ant("/error")), Predicates.not(ant("/management/**")), Predicates.not(ant("/management*"))))
.build();
}
private ApiInfo apiInfo() {
return new ApiInfoBuilder()
.title("Swagger Petstore")
.description("Petstore API Description")
.contact(new Contact("TestName", "http:/test-url.com", "test@test.de"))
.license("Apache 2.0")
.licenseUrl("http://www.apache.org/licenses/LICENSE-2.0.html")
.version("1.0.0")
.build();
}
}
- 运行Spring项目
运行项目后会发布Spring的路由器多了若干个对外的接口,其中一个是/v2/api-docs/
。
通过这个接口可以获取到由Swagger生成的所有API接口的元数据。 - Api界面部署
呈现由Swagger生成的API大概有两种方法(目前只找到两种)
- Swagger自带有Swagger-UI可以直观显示API接口说明并可以在线调试
- 使用Swagger2Markerup插件和AsciiDoc插件可以将Swagger生成的JSON元数据转换成HTML、PDF
注意:springfox-swagger2是依赖与Spring MVC框架的!!
Swagger-UI部署
- 引入Swagger-UI依赖
<!-- https://mvnrepository.com/artifact/io.springfox/springfox-swagger-ui -->
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger-ui</artifactId>
<version>2.5.0</version>
</dependency>
- 运行Spring项目,并访问htttp://[host]:[ip]/swagger-ui.html
注意:必须首先部署Swagger
Swagger2Markerup与AsciiDoc插件部署
- maven插件部署
<!-- First, use the swagger2markup plugin to generate asciidoc -->
<plugin>
<groupId>io.github.swagger2markup</groupId>
<artifactId>swagger2markup-maven-plugin</artifactId>
<version>${swagger2markup.version}</version>
<dependencies>
<dependency>
<groupId>io.github.swagger2markup</groupId>
<artifactId>swagger2markup-import-files-ext</artifactId>
<version>${swagger2markup.version}</version>
</dependency>
<dependency>
<groupId>io.github.swagger2markup</groupId>
<artifactId>swagger2markup-spring-restdocs-ext</artifactId>
<version>${swagger2markup.version}</version>
</dependency>
</dependencies>
<configuration>
<swaggerInput>${swagger.input}</swaggerInput>
<outputDir>${generated.asciidoc.directory}</outputDir>
<config>
<swagger2markup.markupLanguage>ASCIIDOC</swagger2markup.markupLanguage>
<swagger2markup.pathsGroupedBy>TAGS</swagger2markup.pathsGroupedBy> <swagger2markup.extensions.dynamicOverview.contentPath>${project.basedir}/src/docs/asciidoc/extensions/overview</swagger2markup.extensions.dynamicOverview.contentPath>
<swagger2markup.extensions.dynamicDefinitions.contentPath>${project.basedir}/src/docs/asciidoc/extensions/definitions</swagger2markup.extensions.dynamicDefinitions.contentPath>
<swagger2markup.extensions.dynamicPaths.contentPath>${project.basedir}/src/docs/asciidoc/extensions/paths</swagger2markup.extensions.dynamicPaths.contentPath>
<swagger2markup.extensions.dynamicSecurity.contentPath>${project.basedir}src/docs/asciidoc/extensions/security/</swagger2markup.extensions.dynamicSecurity.contentPath>
<swagger2markup.extensions.springRestDocs.snippetBaseUri>${swagger.snippetOutput.dir}</swagger2markup.extensions.springRestDocs.snippetBaseUri>
<swagger2markup.extensions.springRestDocs.defaultSnippets>false</swagger2markup.extensions.springRestDocs.defaultSnippets>
</config>
</configuration>
<executions>
<execution>
<phase>test</phase>
<goals>
<goal>convertSwagger2markup</goal>
</goals>
</execution>
</executions>
</plugin>
<!-- Run the generated asciidoc through Asciidoctor to generate
other documentation types, such as PDFs or HTML5 -->
<plugin>
<groupId>org.asciidoctor</groupId>
<artifactId>asciidoctor-maven-plugin</artifactId>
<version>1.5.3</version>
<!-- Include Asciidoctor PDF for pdf generation -->
<dependencies>
<dependency>
<groupId>org.asciidoctor</groupId>
<artifactId>asciidoctorj-pdf</artifactId>
<version>1.5.0-alpha.10.1</version>
</dependency>
</dependencies>
<!-- Configure generic document generation settings -->
<configuration>
<sourceDirectory>${asciidoctor.input.directory}</sourceDirectory>
<sourceDocumentName>index.adoc</sourceDocumentName>
<attributes>
<doctype>book</doctype>
<toc>left</toc>
<toclevels>3</toclevels>
<numbered></numbered>
<hardbreaks></hardbreaks>
<sectlinks></sectlinks>
<sectanchors></sectanchors>
<generated>${generated.asciidoc.directory}</generated>
</attributes>
</configuration>
<!-- Since each execution can only handle one backend, run
separate executions for each desired output type -->
<executions>
<execution>
<id>output-html</id>
<phase>test</phase>
<goals>
<goal>process-asciidoc</goal>
</goals>
<configuration>
<backend>html5</backend>
<outputDirectory>${asciidoctor.html.output.directory}</outputDirectory>
</configuration>
</execution>
<!--
<execution>
<id>output-pdf</id>
<phase>test</phase>
<goals>
<goal>process-asciidoc</goal>
</goals>
<configuration>
<backend>pdf</backend>
<outputDirectory>${asciidoctor.pdf.output.directory}</outputDirectory>
</configuration>
</execution>
-->
</executions>
</plugin>
- 编写文档生成脚本
@WebAppConfiguration
@RunWith(SpringJUnit4ClassRunner.class)
@AutoConfigureRestDocs(outputDir = "build/asciidoc/snippets")
@SpringBootTest(classes = {Application.class, SwaggerConfig.class})
@AutoConfigureMockMvc
public class Swagger2MarkupTest {
private static final Logger LOG = LoggerFactory.getLogger(Swagger2MarkupTest.class);
@Autowired
private MockMvc mockMvc;
@Test
public void createSpringfoxSwaggerJson() throws Exception {
//String designFirstSwaggerLocation = Swagger2MarkupTest.class.getResource("/swagger.yaml").getPath();
String outputDir = System.getProperty("io.springfox.staticdocs.outputDir");
MvcResult mvcResult = this.mockMvc.perform(get("/v2/api-docs")
.accept(MediaType.APPLICATION_JSON))
.andExpect(status().isOk())
.andReturn();
MockHttpServletResponse response = mvcResult.getResponse();
String swaggerJson = response.getContentAsString();
Files.createDirectories(Paths.get(outputDir));
try (BufferedWriter writer = Files.newBufferedWriter(Paths.get(outputDir, "swagger.json"), StandardCharsets.UTF_8)){
writer.write(swaggerJson);
}catch(Exception e){
e.printStackTrace();
}
}
}
注意上述的代码来源于swagger2markup官方说明一个demo,代码基于Spring Boot 1.4.0.release
Swagger2Markerup与AsciiDoc无插件调用(推荐)
有时候没有必须在maven的生命周期中远行单元测试生成文档,可以直接代码块生成文档
- maven依赖
<!-- swagger -->
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger2</artifactId>
<version>2.5.0</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>io.github.swagger2markup</groupId>
<artifactId>swagger2markup-maven-plugin</artifactId>
<version>1.0.1</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.asciidoctor</groupId>
<artifactId>asciidoctorj</artifactId>
<version>1.5.4.1</version>
<scope>test</scope>
</dependency>
- 编写文档生成脚本
@WebAppConfiguration
@RunWith(SpringJUnit4ClassRunner.class)
@SpringApplicationConfiguration(classes = {BIMobileMasterApplication.class, SwaggerConfig.class})
public class Swagger2MarkupTest {
@Autowired
private WebApplicationContext context;
private MockMvc mockMvc;
@Before
public void setUp() {
this.mockMvc = MockMvcBuilders.webAppContextSetup(this.context).build();
}
@Test
public void convertSwaggerToAsciiDoc() throws Exception {
MvcResult mvcResult = this.mockMvc.perform(get("/v2/api-docs")
.accept("application/json;charset=utf-8"))
.andExpect(status().isOk())
.andReturn();
//文档输出目录
String outputDirectory = "docs/restful/generated";
Path outputDirectoryPath = Paths.get(outputDirectory);
MockHttpServletResponse response = mvcResult.getResponse();
String swaggerJson = response.getContentAsString();
swaggerJson = swaggerJson.replace("{\"status\":200,\"message\":\"\",\"data\":", "");
swaggerJson = swaggerJson.substring(0,swaggerJson.length()-1);
Swagger2MarkupConverter.from(swaggerJson)
.build()
.toFolder(outputDirectoryPath);
Asciidoctor asciidoctor = Asciidoctor.Factory.create();
Attributes attributes = new Attributes();
attributes.setCopyCss(true);
attributes.setLinkCss(false);
attributes.setSectNumLevels(3);
attributes.setAnchors(true);
attributes.setSectionNumbers(true);
attributes.setHardbreaks(true);
attributes.setTableOfContents(Placement.LEFT);
attributes.setAttribute("generated", "generated");
OptionsBuilder optionsBuilder = OptionsBuilder.options()
.backend("html5")
.docType("book")
.eruby("")
.inPlace(true)
.safe(SafeMode.UNSAFE)
.attributes(attributes);
String asciiInputFile = "docs/restful/index.adoc";
asciidoctor.convertFile(
new File(asciiInputFile),
optionsBuilder.get());
}
Swagger2Markup插件的说明及使用
swagger2markup-import-files-ext
插件说明
有时在写接口注释时,可能需要对接口附加一些特别说明,这种情况下Swagger2的Java注释感觉有点鸡肋。 这时可以使用swagger2markup-import-files-ext动态向adoc文档添加额外内容。
插件使用
- maven插件部署
swagger2markup-import-files-ext是可以与swagger2markup-maven-plugin插件一起使用的
<plugin>
<groupId>io.github.swagger2markup</groupId>
<artifactId>swagger2markup-maven-plugin</artifactId>
<version>${swagger2markup.version}</version>
<dependencies>
<dependency>
<groupId>io.github.swagger2markup</groupId>
<artifactId>swagger2markup-import-files-ext</artifactId>
<version>${swagger2markup.version}</version>
</dependency>
<dependency>
<groupId>io.github.swagger2markup</groupId>
<artifactId>swagger2markup-spring-restdocs-ext</artifactId>
<version>${swagger2markup.version}</version>
</dependency>
</dependencies>
<configuration>
<swaggerInput>${swagger.input}</swaggerInput>
<outputDir>${generated.asciidoc.directory}</outputDir>
<config>
<swagger2markup.markupLanguage>ASCIIDOC</swagger2markup.markupLanguage>
<swagger2markup.pathsGroupedBy>TAGS</swagger2markup.pathsGroupedBy>
<swagger2markup.extensions.dynamicOverview.contentPath>${project.basedir}/src/docs/asciidoc/extensions/overview</swagger2markup.extensions.dynamicOverview.contentPath>
<swagger2markup.extensions.dynamicDefinitions.contentPath>${project.basedir}/src/docs/asciidoc/extensions/definitions</swagger2markup.extensions.dynamicDefinitions.contentPath>
<swagger2markup.extensions.dynamicPaths.contentPath>${project.basedir}/src/docs/asciidoc/extensions/paths</swagger2markup.extensions.dynamicPaths.contentPath>
<swagger2markup.extensions.dynamicSecurity.contentPath>${project.basedir}src/docs/asciidoc/extensions/security/</swagger2markup.extensions.dynamicSecurity.contentPath>
<swagger2markup.extensions.springRestDocs.snippetBaseUri>${swagger.snippetOutput.dir}</swagger2markup.extensions.springRestDocs.snippetBaseUri>
<swagger2markup.extensions.springRestDocs.defaultSnippets>false</swagger2markup.extensions.springRestDocs.defaultSnippets>
</config>
</configuration>
<executions>
<execution>
<phase>test</phase>
<goals>
<goal>convertSwagger2markup</goal>
</goals>
</execution>
</executions>
</plugin>
swagger2markup.extensions.dynamicOverview.contentPath
swagger2markup.extensions.dynamicDefinitions.contentPath
swagger2markup.extensions.dynamicPaths.contentPath
swagger2markup.extensions.dynamicSecurity.contentPath
四个参数是必须要指定的,意思是swagger2markup-maven-plugin启动时,会在上述四个目录中分别查找adoc,并把adoc内容分别插入到原adoc中。
- 创建自定义的adc
完成步骤1部署后,我们需要定义每一个adoc的内容及其插入的位置。Swagger2Markup文档中定义下面规则用于将自定义的adoc插入到接口文档指定位置中:
All extensions, relatively to each extension contentPath :
DOCUMENT_BEFORE : document-before-.<markup.ext>
DOCUMENT_BEGIN : document-begin-.<markup.ext>
DOCUMENT_END : document-end-.<markup.ext>
DOCUMENT_AFTER : document-after-.<markup.ext>
Paths extensions, relatively to each extension contentPath :
OPERATION_BEFORE : <operationId>/operation-before-.<markup.ext>
OPERATION_BEGIN : <operationId>/operation-begin-.<markup.ext>
OPERATION_END : <operationId>/operation-end-.<markup.ext>
OPERATION_AFTER : <operationId>/operation-after-.<markup.ext>
OPERATION_DESCRIPTION_BEFORE: <operationId>/operation-description-before-.<markup.ext>
OPERATION_DESCRIPTION_BEGIN: <operationId>/operation-description-begin-.<markup.ext>
OPERATION_DESCRIPTION_END: <operationId>/operation-description-end-.<markup.ext>
OPERATION_DESCRIPTION_AFTER: <operationId>/operation-description-after-.<markup.ext>
OPERATION_PARAMETERS_BEFORE: <operationId>/operation-parameters-before-.<markup.ext>
OPERATION_PARAMETERS_BEGIN: <operationId>/operation-parameters-begin-.<markup.ext>
OPERATION_PARAMETERS_END: <operationId>/operation-parameters-end-.<markup.ext>
OPERATION_PARAMETERS_AFTER: <operationId>/operation-parameters-after-.<markup.ext>
OPERATION_RESPONSES_BEFORE: <operationId>/operation-responses-before-.<markup.ext>
OPERATION_RESPONSES_BEGIN: <operationId>/operation-responses-begin-.<markup.ext>
OPERATION_RESPONSES_END: <operationId>/operation-responses-end-.<markup.ext>
OPERATION_RESPONSES_AFTER: <operationId>/operation-responses-after-.<markup.ext>
OPERATION_SECURITY_BEFORE: <operationId>/operation-security-before-.<markup.ext>
OPERATION_SECURITY_BEGIN: <operationId>/operation-security-begin.<markup.ext>
OPERATION_SECURITY_END: <operationId>/operation-security-end-.<markup.ext>
OPERATION_SECURITY_AFTER: <operationId>/operation-security-after-.<markup.ext>
Definitions extensions, relatively to each extension contentPath :
DEFINITION_BEFORE : <definitionName>/definition-before-.<markup.ext>
DEFINITION_BEGIN : <definitionName>/definition-begin-.<markup.ext>
DEFINITION_END : <definitionName>/definition-end-.<markup.ext>
DEFINITION_AFTER : <definitionName>/definition-after-.<markup.ext>
Security extensions, relatively to each extension contentPath :
SECURITY_SCHEME_BEFORE : <securitySchemeName>/security-scheme-before-.<markup.ext>
SECURITY_SCHEME_BEGIN : <securitySchemeName>/security-scheme-begin-.<markup.ext>
SECURITY_SCHEME_END : <securitySchemeName>/security-scheme-end-.<markup.ext>
SECURITY_SCHEME_AFTER : <securitySchemeName>/security-scheme-after-.<markup.ext>
注意:operationId相当于@ApiOperation注释中的nickname属性
假设现在要将一段接口说明插入到operationId为findPet的接口中,我们需要做如下步骤:
- 创建一个新目录src/docs/asciidoc/extensions/paths/
- 在上述目录下创建一个
findPet
的子目录 - 创建一个operation-before-test.adoc文件,内容为:
这是一个插件入adoc
可以看到生成文章后在在findPet的接口前面增加了一段描述
其它的插入位置同理
swagger2markup-spring-restdocs-ext
插件说明
此插件可以自动将Spring rest doc生成的adoc片段添加到Paths说明部分的最后。
Spring Rest doc生成的adoc版片段:
- curl-request.adoc
- http-request.adoc
- http-response.adoc
- httpie-request.adoc
插件默认扫描前3个adoc,当然也可以通过withExplicitSnippets自定义扫描的文件名。
插件使用
- maven插件部署
这里我们使用与上一个述件不同的部署方式,直接到通过代码配置插件
<dependency>
<groupId>io.github.swagger2markup</groupId>
<artifactId>swagger2markup-spring-restdocs-ext</artifactId>
<version>1.0.0</version>
<scope>compile</scope>
</dependency>
- 配置
Map<String, String> configMap = new HashMap<>();
configMap.put("swagger2markup.extensions.springRestDocs.snippetBaseUri", "docs/restful/snippets");
configMap.put("swagger2markup.extensions.springRestDocs.defaultSnippets", "true");
configMap.put(Swagger2MarkupProperties.PATHS_GROUPED_BY, GroupBy.TAGS.name());
Swagger2MarkupConfig config = new Swagger2MarkupConfigBuilder(configMap)
其中snippetBaseUri是Spring Rest Doc生成文档的位置;defaultSnippets指明是否使用默认的文件名扫描,如果设置为false,需要手工创建插件并通过withExplicitSnippets自行设置扫描的文件名。
其他参考资料
Spring Boot Test
http://docs.spring.io/spring-boot/docs/current/reference/html/boot-features-testing.html