iT邦幫忙

2018 iT 邦幫忙鐵人賽
DAY 6
0

Swagger 線上可以測試真的很方便, 但畢竟還是要啟動服務, 有時候對外或是對一些非技術人員比較不方便點
還是有個檔案給他讓他方便查看或是寄給對方

不過我們都寫好在註解上了啊, 我們要聰明點, 不要老是在做重複的工作
那今天就教你怎麼把 Swagger 轉成 PDF or Html 文件

注意!這篇因為新版套件造成衝突問題還要等更新喔

官方文件在這裡 Swagger2Markup Documentation

首先我們要增加這個插件 org.asciidoctor.convert

修改 build.gradle

buildscript {
    ext {
        springBootVersion = '1.5.9.RELEASE'
    }
    repositories {
        mavenCentral()
        maven {
            url "https://plugins.gradle.org/m2/"
        }
    }
    dependencies {
        classpath("org.springframework.boot:spring-boot-gradle-plugin:${springBootVersion}")
        classpath "org.asciidoctor:asciidoctor-gradle-plugin:1.5.7"
    }
}

apply plugin: 'org.asciidoctor.convert'

jar {
    baseName = 'bookservice'
    version = '0.0.1-SNAPSHOT'
    dependsOn asciidoctor
}

asciidoctor {
    dependsOn test
    sourceDir 'src/docs/asciidoc'
    attributes 'snippets': file("build/asciidoc"),
            'doctype': 'book',
            'icons': 'font',
            'source-highlighter': 'highlightjs',
            'toc': 'left',
            'toclevels': 2,
            'sectlinks': true
    outputDir = file('build/asciidoc/generated')
//    backends 'html5', 'pdf', 'epub3', 'docbook' , 'revealjs'
//    sourceDir = file('src/docs/asciidoc')
//    outputDir = file('build/asciidoc/generated')
}

dependencies {
    compile('org.springframework.boot:spring-boot-starter-data-jpa')
    compile('org.springframework.boot:spring-boot-starter-data-rest')
    compile('org.springframework.boot:spring-boot-starter-web')
    runtime('com.h2database:h2')
    compileOnly('org.projectlombok:lombok')
    testCompile('org.springframework.boot:spring-boot-starter-test')
    compile 'io.springfox:springfox-swagger2:2.7.0'
    compile 'io.springfox:springfox-swagger-ui:2.7.0'
    compile 'io.springfox:springfox-data-rest:2.7.0'
    // 新增的
    testCompile 'io.springfox:springfox-staticdocs:2.6.1'
    testCompile 'org.springframework.restdocs:spring-restdocs-mockmvc:2.0.0.RELEASE'
}

準備我們的測試 Swagger2MarkupTest.java, 因為是透過測試的功能在打包之前把文件產出

package com.example.book;

import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.ObjectNode;
import io.github.robwin.markup.builder.MarkupLanguage;
import io.github.robwin.swagger2markup.GroupBy;
import io.github.robwin.swagger2markup.Swagger2MarkupConverter;
import org.apache.commons.lang3.Validate;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.http.MediaType;
import org.springframework.mock.web.MockHttpServletResponse;
import org.springframework.test.context.junit4.SpringRunner;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.MvcResult;
import org.springframework.test.web.servlet.ResultHandler;
import springfox.documentation.staticdocs.Swagger2MarkupResultHandler;

import java.util.Arrays;
import java.util.Iterator;
import java.util.List;

import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;


@AutoConfigureMockMvc
@RunWith(SpringRunner.class)
@SpringBootTest(classes = BookApplication.class)
public class Swagger2MarkupTest {

    @Autowired
    private MockMvc mockMvc;

    @Before
    public void setUp() {

    }

    // 輸出成 adoc 格式
    @Test
    public void convertSwaggerToAsciiDoc() throws Exception {
        this.mockMvc.perform(get("/v2/api-docs")
                .accept(MediaType.APPLICATION_JSON))
                // 原本的寫法
                .andDo(Swagger2MarkupResultHandler.outputDirectory("src/docs/asciidoc/generated").build())
                // 稍微客製化只留下api開頭的path
                //.andDo(new PsSwagger2MarkupHandler("src/docs/asciidoc/generated"))
                .andExpect(status().isOk());
    }

    // 輸出成 Markdown 格式
//    @Test
//    public void convertSwaggerToMarkdown() throws Exception {
//        this.mockMvc.perform(get("/v2/api-docs")
//                .accept(MediaType.APPLICATION_JSON))
//                .andDo(Swagger2MarkupResultHandler.outputDirectory("docs/markdown/generated")
//                        .withMarkupLanguage(MarkupLanguage.MARKDOWN).build())
//                .andExpect(status().isOk());
//    }

    // 一般寫測試兼輸出的方式
//    @Test
//    public void testIndex() throws Exception {
//        this.mockMvc.perform(get("/api/test").accept(MediaType.APPLICATION_JSON))
//                .andExpect(status().isOk())
//                .andDo(document("test", preprocessResponse(prettyPrint())));
//    }

    class PsSwagger2MarkupHandler implements ResultHandler {
        private String outputDir;
        private final MarkupLanguage markupLanguage = MarkupLanguage.ASCIIDOC;
        private String examplesFolderPath;
        private final GroupBy pathsGroupedBy = GroupBy.AS_IS;
        private final String encoding = "UTF-8";
        private String passStr = "listRepositories,getCollectionResource,getItemResource,getCollectionResourceCompact,listSearches,errorHtml,listAllFormsOfMetadata,schema";


        public PsSwagger2MarkupHandler(String outputDir) {
            Validate.notEmpty(outputDir, "outputDir must not be empty!");
            this.outputDir = outputDir;
        }

        @Override
        public void handle(MvcResult result) throws Exception {
            MockHttpServletResponse response = result.getResponse();
            response.setCharacterEncoding(encoding);
            String swaggerJson = response.getContentAsString();
            // 去除掉 Spring Data Rest 提供的基礎路徑
            List<String> passSummary = Arrays.asList(passStr.split(","));
            ObjectMapper mapper = new ObjectMapper();
            ObjectNode newPaths = mapper.createObjectNode();
            JsonNode jsonRoot = mapper.readTree(swaggerJson);
            JsonNode jsonPaths = jsonRoot.get("paths");
            Iterator<String> keyit = jsonPaths.fieldNames();
            while (keyit.hasNext()) {
                String key = keyit.next();
                JsonNode jsonPath = jsonPaths.get(key);
                String summary = jsonPath.findPath("summary").asText();
                if (!passSummary.contains(summary)) {
                    newPaths.set(key, jsonPath);
                }
            }
            ((ObjectNode) jsonRoot).set("paths", newPaths);
            swaggerJson = jsonRoot.toString();
            // 這邊輸出成 adoc
            Swagger2MarkupConverter.fromString(swaggerJson).withMarkupLanguage(markupLanguage)
                    .withPathsGroupedBy(this.pathsGroupedBy)
                    .withExamples(examplesFolderPath).build().intoFolder(outputDir);
        }
    }
}

測試用的 H2 資料庫也需要改一下配置
test/resources/application.yml

spring:
  datasource:
    driver-class-name: org.h2.Driver
    url: jdbc:h2:mem:test;MODE=MySQL;DB_CLOSE_DELAY=-1
    initialize: false
    sql-script-encoding: UTF-8
  jpa:
    hibernate:
      ddl-auto: create-drop
    database-platform: H2
    show-sql: true
    generate-ddl: false

我們把 initialize 關掉 改用 jpa.hibernate.ddl-auto
因為測試的時候...springboottest 似乎不會自己引入 schema.sql import.sql 來執行
所以會造成啟動失敗 (沒有表格)
我們簡單一點讓 hibernete 根據 Entity 自訂產生對應的表格就可以了
因為在這邊我們沒有要真的驗證資料對不對, 只是需要讓他產生出 swagger 的 json 資料

如果你需要事先寫入資料的話 可以參考這做法 TestingDataSourceConfig.java

package com.example.book;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseBuilder;

import javax.sql.DataSource;

import static org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseType.H2;

@Configuration
public class TestingDataSourceConfig {

    @Bean
    @Primary
    public DataSource dataSource() {
        return new EmbeddedDatabaseBuilder()
                .generateUniqueName(true)
                .setType(H2)
                .setScriptEncoding("UTF-8")
                .ignoreFailedDrops(true)
                .addScript("schema.sql")
                .addScripts("import.sql")
                .build();
    }
}

原則上來說, 這時候如果我們執行 gradle test, 就會在我們專案下產出類似這樣的資料

但是目前測試是錯誤的, 看起來像是套件間的問題

io.swagger.models.parameters.AbstractSerializableParameter.getDefaultValue()Ljava/lang/String;
java.lang.NoSuchMethodError: io.swagger.models.parameters.AbstractSerializableParameter.getDefaultValue()Ljava/lang/String;
	at io.github.robwin.swagger2markup.utils.ParameterUtils.getDefaultValue(ParameterUtils.java:108)
	at io.github.robwin.swagger2markup.builder.document.PathsDocument.parametersSection(PathsDocument.java:280)
	at io.github.robwin.swagger2markup.builder.document.PathsDocument.path(PathsDocument.java:184)
	at io.github.robwin.swagger2markup.builder.document.PathsDocument.createPathSections(PathsDocument.java:170)
	at io.github.robwin.swagger2markup.builder.document.PathsDocument.paths(PathsDocument.java:142)
	at io.github.robwin.swagger2markup.builder.document.PathsDocument.build(PathsDocument.java:127)
	at io.github.robwin.swagger2markup.Swagger2MarkupConverter.buildDocuments(Swagger2MarkupConverter.java:119)
	at io.github.robwin.swagger2markup.Swagger2MarkupConverter.intoFolder(Swagger2MarkupConverter.java:98)
	at springfox.documentation.staticdocs.Swagger2MarkupResultHandler.handle(Swagger2MarkupResultHandler.java:74)
	at org.springframework.test.web.servlet.MockMvc$1.andDo(MockMvc.java:177)
	at com.example.book.Swagger2MarkupTest.convertSwaggerToAsciiDoc(Swagger2MarkupTest.java:58)
	at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
	at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
	at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
	at java.lang.reflect.Method.invoke(Method.java:498)
	at org.junit.runners.model.FrameworkMethod$1.runReflectiveCall(FrameworkMethod.java:50)
	at org.junit.internal.runners.model.ReflectiveCallable.run(ReflectiveCallable.java:12)
	at org.junit.runners.model.FrameworkMethod.invokeExplosively(FrameworkMethod.java:47)
	at org.junit.internal.runners.statements.InvokeMethod.evaluate(InvokeMethod.java:17)
	at org.junit.internal.runners.statements.RunBefores.evaluate(RunBefores.java:26)
	at org.springframework.test.context.junit4.statements.RunBeforeTestMethodCallbacks.evaluate(RunBeforeTestMethodCallbacks.java:75)
	at org.springframework.test.context.junit4.statements.RunAfterTestMethodCallbacks.evaluate(RunAfterTestMethodCallbacks.java:86)
	at org.springframework.test.context.junit4.statements.SpringRepeat.evaluate(SpringRepeat.java:84)
	at org.junit.runners.ParentRunner.runLeaf(ParentRunner.java:325)
	at org.springframework.test.context.junit4.SpringJUnit4ClassRunner.runChild(SpringJUnit4ClassRunner.java:252)
	at org.springframework.test.context.junit4.SpringJUnit4ClassRunner.runChild(SpringJUnit4ClassRunner.java:94)
	at org.junit.runners.ParentRunner$3.run(ParentRunner.java:290)
	at org.junit.runners.ParentRunner$1.schedule(ParentRunner.java:71)
	at org.junit.runners.ParentRunner.runChildren(ParentRunner.java:288)
	at org.junit.runners.ParentRunner.access$000(ParentRunner.java:58)
	at org.junit.runners.ParentRunner$2.evaluate(ParentRunner.java:268)
	at org.springframework.test.context.junit4.statements.RunBeforeTestClassCallbacks.evaluate(RunBeforeTestClassCallbacks.java:61)
	at org.springframework.test.context.junit4.statements.RunAfterTestClassCallbacks.evaluate(RunAfterTestClassCallbacks.java:70)
	at org.junit.runners.ParentRunner.run(ParentRunner.java:363)
	at org.springframework.test.context.junit4.SpringJUnit4ClassRunner.run(SpringJUnit4ClassRunner.java:191)
	at org.gradle.api.internal.tasks.testing.junit.JUnitTestClassExecuter.runTestClass(JUnitTestClassExecuter.java:114)
	at org.gradle.api.internal.tasks.testing.junit.JUnitTestClassExecuter.execute(JUnitTestClassExecuter.java:57)
	at org.gradle.api.internal.tasks.testing.junit.JUnitTestClassProcessor.processTestClass(JUnitTestClassProcessor.java:66)
	at org.gradle.api.internal.tasks.testing.SuiteTestClassProcessor.processTestClass(SuiteTestClassProcessor.java:51)
	at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
	at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
	at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
	at java.lang.reflect.Method.invoke(Method.java:498)
	at org.gradle.internal.dispatch.ReflectionDispatch.dispatch(ReflectionDispatch.java:35)
	at org.gradle.internal.dispatch.ReflectionDispatch.dispatch(ReflectionDispatch.java:24)
	at org.gradle.internal.dispatch.ContextClassLoaderDispatch.dispatch(ContextClassLoaderDispatch.java:32)
	at org.gradle.internal.dispatch.ProxyDispatchAdapter$DispatchingInvocationHandler.invoke(ProxyDispatchAdapter.java:93)
	at com.sun.proxy.$Proxy2.processTestClass(Unknown Source)
	at org.gradle.api.internal.tasks.testing.worker.TestWorker.processTestClass(TestWorker.java:109)
	at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
	at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
	at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
	at java.lang.reflect.Method.invoke(Method.java:498)
	at org.gradle.internal.dispatch.ReflectionDispatch.dispatch(ReflectionDispatch.java:35)
	at org.gradle.internal.dispatch.ReflectionDispatch.dispatch(ReflectionDispatch.java:24)
	at org.gradle.internal.remote.internal.hub.MessageHubBackedObjectConnection$DispatchWrapper.dispatch(MessageHubBackedObjectConnection.java:147)
	at org.gradle.internal.remote.internal.hub.MessageHubBackedObjectConnection$DispatchWrapper.dispatch(MessageHubBackedObjectConnection.java:129)
	at org.gradle.internal.remote.internal.hub.MessageHub$Handler.run(MessageHub.java:404)
	at org.gradle.internal.concurrent.ExecutorPolicy$CatchAndRecordFailures.onExecute(ExecutorPolicy.java:63)
	at org.gradle.internal.concurrent.StoppableExecutorImpl$1.run(StoppableExecutorImpl.java:46)
	at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1149)
	at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624)
	at java.lang.Thread.run(Thread.java:748)

1 test completed, 1 failed

我再找找如何修正吧...

比較詳細的說明可以先看我之前的版本
將 Swagger 輸出成 HTML 文檔

但是我舊的專案以前可以跑....打開來更新後就一樣套件衝突了....QQ


上一篇
Day 05 - 增加 Swagger 來提供線上版 API 規格說明
下一篇
Day 07 - 寫 SpringBoot 的 Unit test
系列文
30天從零開始 使用 Spring Boot 跟 Spring Cloud 建構完整微服務架構35
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言