Gradle環境下匯出Swagger為PDF

鵬徙南暝發表於2019-06-25

更多精彩博文,歡迎訪問我的個人部落格


說明

我個人是一直使用Swagger作為介面文件的說明的。但是由於在一些情況下,介面文件說明需要以檔案的形式交付出去,如果再重新寫一份文件難免有些麻煩。於是在網上看到了Swagger2Markup + asciidoctor匯出PDF的方法,百度一番後感覺網上的文章還是有很多沒有描述清楚的地方,遂還是硬著頭皮把官方的英文文件大致瀏覽了一下,按照自己的思路整理出具體的步驟。

本文用到的工具:

  • Gradle - 4.10.3
  • SpringBoot - 2.1.6.RELEASE
  • Swagger - 2.9.2
  • Swagger2Markup - 1.3.3
  • asciidoctor
  • spring-restdocs-mockmvc

準備Swagger資料

SpringBoot中使用Swagger的過程就不再贅述了,下面是本文使用的範例:

@Configuration
@EnableSwagger2
class SwaggerConfig {
    @Bean
    public Docket createRestApi() {
        return new Docket(DocumentationType.SWAGGER_2)
            .apiInfo(apiInfo())
            .select()
            .apis(RequestHandlerSelectors.basePackage("com.jptangchina.gradle.controller"))
            .paths(PathSelectors.any())
            .build();
    }

    private ApiInfo apiInfo() {
        return new ApiInfoBuilder()
            .title("Swagger2Markup Test Api")
            .version("1.0")
            .build();
    }
}
複製程式碼
@RestController
@RequestMapping("/user")
@Api(tags = "使用者介面")
public class UserController {

    @ApiOperation("使用者登入")
    @ResponseBody
    @PostMapping("/login")
    public Result<Void> login(
        @ApiParam(value = "使用者名稱", example = "jptangchina", required = true) @RequestParam String username,
        @ApiParam(value = "密碼", example = "jptangchina", required = true) @RequestParam String password) {
        return Result.ok();
    }
}
複製程式碼

使用org.asciidoctor.convert生成PDF(個人不推薦使用)

官方教程地址:github.com/Swagger2Mar…

僅為了簡單的匯出PDF而言,本文針對官方案例均有所改動,去掉了部分沒有用到的配置。

1. 獲取Swagger json檔案

Swagger頁面本質上也就是對json檔案進行解析。這裡需要先編寫單元測試訪問/v2/api-docs介面並將json檔案儲存到本地。

@RunWith(SpringRunner.class)
@SpringBootTest
@AutoConfigureMockMvc
class SwaggerTest {
    @Autowired
    private MockMvc mockMvc;
    @Test
    public void generateAsciiDocsToFile() throws Exception {
        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);
        }
    }

}
複製程式碼

System.getProperty("io.springfox.staticdocs.outputDir");來自於build.gradle中的配置

2. 將json檔案轉換為adoc檔案

轉換json檔案需要使用到io.github.swagger2markup外掛的convertSwagger2markup方法。

引入相關依賴:

buildscript {
    ...
    dependencies {
        ...
        classpath 'io.github.swagger2markup:swagger2markup-gradle-plugin:1.3.3'
    }
}
  
apply plugin: 'io.github.swagger2markup'
複製程式碼

配置convertSwagger2markup:

ext {
    asciiDocOutputDir = file("${buildDir}/asciidoc")
    swaggerOutputDir = file("${buildDir}/swagger")
}

test {
    systemProperty 'io.springfox.staticdocs.outputDir', swaggerOutputDir
}

convertSwagger2markup {
    dependsOn test
    swaggerInput "${swaggerOutputDir}/swagger.json"
    outputDir asciiDocOutputDir
    config = [
            'swagger2markup.pathsGroupedBy' : 'TAGS',
    ]
}
複製程式碼

更多config配置可以參考:swagger2markup.github.io/swagger2mar…

3. 將adoc檔案轉換為PDF檔案

轉換PDF檔案需要用到org.asciidoctor.convert外掛的asciidoctor方法。 引入相關依賴:

buildscript {
    ...
    dependencies {
        ...
        classpath 'org.asciidoctor:asciidoctor-gradle-plugin:1.5.3'
    }
}
apply plugin: 'org.asciidoctor.convert'
複製程式碼

手動編寫index.adoc檔案,放置到${asciiDocOutputDir.absolutePath}中:

include::{generated}/overview.adoc[]
include::{generated}/paths.adoc[]
include::{generated}/definitions.adoc[]
include::{generated}/security.adoc[]
複製程式碼

{generated}預設值為${build}/asciidoc,參見:github.com/Swagger2Mar…

配置asciidoctor:

asciidoctor {
    dependsOn convertSwagger2markup
    // sourceDir中需要包含有之前手動編寫的index.adoc檔案
    sourceDir(asciiDocOutputDir.absolutePath)
    sources {
        include "index.adoc"
    }
    backends = ['pdf']
    attributes = [
            doctype: 'book',
            toc: 'left',
            toclevels: '3',
            numbered: '',
            sectlinks: '',
            sectanchors: '',
            hardbreaks: '',
            generated: asciiDocOutputDir
    ]
}
複製程式碼

4. 編寫一個自定義task用來執行上述流程:

task genPdf(type: Test, dependsOn: test) {
    include '**/*SwaggerTest.class'
    exclude '**/*'
    dependsOn(asciidoctor)
}
複製程式碼

執行genPdf,就可以生成Swagger對應的PDF檔案。

5. 小結

使用此方法步驟還是比較繁瑣的,總體來講就是json -> adoc -> pdf,並且使用此種方法目前有幾個比較大的問題我仍然沒有找到解決方案:

  • 從官方文件中可以看到支援的語言預設有EN, DE, FR, RU。沒錯,不支援CN,從匯出的文件也可以看到,部分中文無法顯示,目前我也尚未找到是否有配置可以實現這個功能。網上的文章部分是通過替換原始碼包裡面的字型檔案來實現,但是由於後面有更好的解決方案,這裡就不再討論。
  • 從asciidoctorj-pdf的1.5.0-alpha.16版本以後(目前最新是1.5.0-alpha.18),這種方式生成檔案會丟擲異常,我個人並沒有深究這個異常,有興趣的讀者可以通過修改版本號試一試。
  • 生成的adoc檔案一般包含overview.adoc、paths.adoc、definitions.adoc、security.adoc一共4個檔案,這也是為什麼要手動編寫index.adoc進行整合的原因,感覺不太方便。 綜上,我個人並不推薦採用此方式生成PDF。

build.gradle完整檔案參考:

buildscript {
    ext {
        springbootVersion = '2.1.6.RELEASE'
    }
    repositories {
        maven {
            url 'http://maven.aliyun.com/nexus/content/groups/public'
        }
    }
    dependencies {
        classpath "org.springframework.boot:spring-boot-gradle-plugin:${springbootVersion}"
        classpath 'org.asciidoctor:asciidoctor-gradle-plugin:1.5.3'
        classpath 'io.github.swagger2markup:swagger2markup-gradle-plugin:1.3.3'
    }
}

repositories {
    maven {
        url 'http://maven.aliyun.com/nexus/content/groups/public'
    }
}

apply plugin: 'java'
apply plugin: 'maven'
apply plugin: 'org.springframework.boot'
apply plugin: 'io.spring.dependency-management'
apply plugin: 'io.github.swagger2markup'
apply plugin: 'org.asciidoctor.convert'

group 'com.jptangchina'
version '1.0-SNAPSHOT'

sourceCompatibility = 1.8
targetCompatibility = 1.8

ext {
    asciiDocOutputDir = file("${buildDir}/asciidoc")
    swaggerOutputDir = file("${buildDir}/swagger")
    swaggerVersion = '2.9.2'
}

dependencies {
    compile 'org.springframework.boot:spring-boot-starter-web'
    compile "io.springfox:springfox-swagger2:${swaggerVersion}"
    compile "io.springfox:springfox-swagger-ui:${swaggerVersion}"
    compile 'io.github.swagger2markup:swagger2markup:1.3.3'
    asciidoctor 'org.asciidoctor:asciidoctorj-pdf:1.5.0-alpha.16'
    testCompile 'org.springframework.boot:spring-boot-starter-test'
    testCompile 'org.springframework.restdocs:spring-restdocs-mockmvc'
}

test {
    systemProperty 'io.springfox.staticdocs.outputDir', swaggerOutputDir
}

convertSwagger2markup {
    dependsOn test
    swaggerInput "${swaggerOutputDir}/swagger.json"
    outputDir asciiDocOutputDir
    config = [
            'swagger2markup.pathsGroupedBy' : 'TAGS',
    ]
}

asciidoctor {
    dependsOn convertSwagger2markup
    sourceDir(asciiDocOutputDir.absolutePath)
    sources {
        include "index.adoc"
    }
    backends = ['pdf']
    attributes = [
            doctype: 'book',
            toc: 'left',
            toclevels: '3',
            numbered: '',
            sectlinks: '',
            sectanchors: '',
            hardbreaks: '',
            generated: asciiDocOutputDir
    ]
}

task genPdf(type: Test, dependsOn: test) {
    include '**/*SwaggerTest.class'
    exclude '**/*'
    dependsOn(asciidoctor)
}

複製程式碼

使用asciidoctor-gradle-plugin生成PDF(推薦)

asciidoctor-gradle-plugin也是官方推薦的使用方式。相對前面的方式,使用起來更加簡單,也可以修改配置輸出中文。

1. 引入外掛

plugins {
    id 'org.asciidoctor.jvm.pdf' version '2.2.0'
}
複製程式碼

2. 編寫測試類生成adoc

與第一中方法不同的是,不需要再將json檔案儲存到本地了。

@RunWith(SpringRunner.class)
@SpringBootTest
@AutoConfigureMockMvc
public class SwaggerTest {
    @Autowired
    private MockMvc mockMvc;
    @Test
    public void generateAsciiDocsToFile() throws Exception {
        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();

        Swagger2MarkupConfig config = new Swagger2MarkupConfigBuilder()
            .withMarkupLanguage(MarkupLanguage.ASCIIDOC)
            .withOutputLanguage(Language.ZH)
            .withPathsGroupedBy(GroupBy.TAGS)
            .withGeneratedExamples()
            .withoutInlineSchema()
            .build();

        MockHttpServletResponse response = mvcResult.getResponse();
        String swaggerJson = response.getContentAsString();
        Swagger2MarkupConverter.from(swaggerJson)
            .withConfig(config)
            .build()
            .toFile(Paths.get(outputDir + "/swagger"));
    }
}
複製程式碼

有興趣的讀者可以閱讀下toFile方法的原始碼,裡面對第一種方法生成的4個檔案進行了整合,這也是不再需要手動編寫index.adoc檔案的原因。

3. 配置asciidoctorPdf


ext {
    asciiDocOutputDir = file("${buildDir}/asciidoc")
    // 建立字型與主題的資料夾
    pdfFontsDir = file("${buildDir}/fonts")
    pdfThemesDir = file("${buildDir}/themes")
    swaggerVersion = '2.9.2'
}

pdfThemes {
    local 'basic', {
        styleDir = pdfThemesDir
        // styleName會被程式用於匹配${styleName}-theme.yml,如default-styleName-theme.yml
        styleName = 'default'
    }
}
asciidoctorPdf{
    sourceDir(asciiDocOutputDir.absolutePath)
    sources {
        include "swagger.adoc"
    }
    fontsDir(pdfFontsDir.absolutePath)
    theme("basic")
}
複製程式碼

本文字型與主題檔案均來自於asciidoctorj-pdf-1.5.0-alpha.18.jar原始碼包,其路徑位於:gems/asciidoctorj-pdf-1.5.0-alpha.18/data

4. 複製並修改default-theme.yml檔案配置

為了解決中文無法顯示的問題,首先需要自行下載一個支援中文的字型檔案。

修改主題檔案,將mplus1p-regular-fallback.ttf替換為自己下載的字型檔案的名稱。

M+ 1p Fallback:
  normal: your-font.ttf
  bold: your-font.ttf
  italic: your-font.ttf
  bold_italic: your-font.ttf
複製程式碼

由於手動指定了字型檔案的路徑,所以除了中文以外,還需要將原始碼中的其他字型檔案一併複製到${pdfFontsDir}資料夾。如果不願意使用官方的字型,也可以考慮將default-theme.yml中其他的字型檔案都修改為自己想要的檔案。

保持task genPdf不變,再次執行即可生成PDF檔案,生成的檔案預設路徑為${build}/docs/asciidocPdf

小結

asciidoctor-gradle-plugin的方式可以支援配置字型與主題,通過配置不僅規避了中文無法顯示的問題,同時使用起來也更加簡單。需要注意的是,採用此種方案生成出的文件會在封面寫有專案的版本號,此版本號為build.gradle中的version,而非SwaggerConfig類中的version。

官方提供了很多配置,可以自行參考官方文件檢視。

build.gradle完整檔案參考:

buildscript {
    ext {
        springbootVersion = '2.1.6.RELEASE'
    }
    repositories {
        maven {
            url 'http://maven.aliyun.com/nexus/content/groups/public'
        }
    }
    dependencies {
        classpath "org.springframework.boot:spring-boot-gradle-plugin:${springbootVersion}"
    }
}

plugins {
    id 'org.asciidoctor.jvm.pdf' version '2.2.0'
}

repositories {
    maven {
        url 'http://maven.aliyun.com/nexus/content/groups/public'
    }
}

apply plugin: 'java'
apply plugin: 'maven'
apply plugin: 'org.springframework.boot'
apply plugin: 'io.spring.dependency-management'

group 'com.jptangchina'
version '1.0-SNAPSHOT'

sourceCompatibility = 1.8
targetCompatibility = 1.8

ext {
    asciiDocOutputDir = file("${buildDir}/asciidoc")
    pdfFontsDir = file("${buildDir}/fonts")
    pdfThemesDir = file("${buildDir}/themes")
    swaggerVersion = '2.9.2'
}

dependencies {
    compile 'org.springframework.boot:spring-boot-starter-web'
    compile "io.springfox:springfox-swagger2:${swaggerVersion}"
    compile "io.springfox:springfox-swagger-ui:${swaggerVersion}"
    compile 'io.github.swagger2markup:swagger2markup:1.3.3'
    testCompile 'org.springframework.boot:spring-boot-starter-test'
    testCompile 'org.springframework.restdocs:spring-restdocs-mockmvc'
}

test {
    systemProperty 'io.springfox.staticdocs.outputDir', asciiDocOutputDir
}

pdfThemes {
    local 'basic', {
        styleDir = pdfThemesDir
        styleName = 'default'
    }
}
asciidoctorPdf{
    sourceDir(asciiDocOutputDir.absolutePath)
    sources {
        include "swagger.adoc"
    }
    fontsDir(pdfFontsDir.absolutePath)
    theme("basic")
}

task genPdf(type: Test, dependsOn: test) {
    include '**/*SwaggerTest.class'
    exclude '**/*'
    dependsOn(asciidoctorPdf)
}
複製程式碼

參考

github.com/Swagger2Mar… github.com/Swagger2Mar… swagger2markup.github.io/swagger2mar…


更多精彩博文,歡迎訪問我的個人部落格

相關文章