基於Spring的Restful介面生成工具

weixin_34148340發表於2016-09-28

場景

有時候需要為前端開發者提供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部署說明

  1. 在pom引入下面依賴:
        <dependency>
            <groupId>io.springfox</groupId>
            <artifactId>springfox-swagger2</artifactId>
            <version>2.4.0</version>
            <scope>test</scope>
        </dependency>
  1. 配置一個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();
    }
}
  1. 執行Spring專案
    執行專案後會釋出Spring的路由器多了若干個對外的介面,其中一個是/v2/api-docs/
    通過這個介面可以獲取到由Swagger生成的所有API介面的後設資料。
  2. Api介面部署
    呈現由Swagger生成的API大概有兩種方法(目前只找到兩種)
  • Swagger自帶有Swagger-UI可以直觀顯示API介面說明並可以線上除錯
  • 使用Swagger2Markerup外掛和AsciiDoc外掛可以將Swagger生成的JSON後設資料轉換成HTML、PDF

注意:springfox-swagger2是依賴與Spring MVC框架的!!

Swagger-UI部署

  1. 引入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>
  1. 執行Spring專案,並訪問htttp://[host]:[ip]/swagger-ui.html
Paste_Image.png

注意:必須首先部署Swagger

Swagger2Markerup與AsciiDoc外掛部署

  1. 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>
  1. 編寫文件生成指令碼
@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的生命週期中遠行單元測試生成文件,可以直接程式碼塊生成文件

  1. 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>
  1. 編寫文件生成指令碼
@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文件新增額外內容。

外掛使用

  1. 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中。

  1. 建立自定義的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的介面前面增加了一段描述


55235-02df21934a91ee61.png
截圖.png

其它的插入位置同理

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自定義掃描的檔名。

外掛使用

  1. maven外掛部署
    這裡我們使用與上一個述件不同的部署方式,直接到通過程式碼配置外掛
        <dependency>
            <groupId>io.github.swagger2markup</groupId>
            <artifactId>swagger2markup-spring-restdocs-ext</artifactId>
            <version>1.0.0</version>
            <scope>compile</scope>
        </dependency>
  1. 配置
        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

相關文章