SpringBoot2.0.4+Swagger2.9.2+asciidoctor1.5.7.1生成靜態HTML的API文檔

說明

  • 目前的公司項目的API文檔是通過swaggerUI進行手動編輯生成的,每一次接口增加和變動都是工作量,因此就這次項目微服務化整合了swagger2。
  • swagger2本身需要項目啓動才能訪問,並且每一個服務都是分割開的,因此加入了asciidoctor生成了靜態文檔,並且把所有微服務的接口都融入到一個文檔中,並修改了展示內容,文檔直接顯示zuul網關之後的接口。

術語

  • Springfox-swagger2
    Swagger是一個可以生成基於多種語言編寫的Restful Api的文檔生成工具
  • Swagger2markup
    Swagger2markup是一個使用java編寫的將Swagger語義轉換成markdown、asciidoc文本格式的開源項目
  • Asciidoc
    AsciiDoc是一種MarkDown的擴展文本格式,AsciiDoc相比MarkDown更適合編寫類似API文檔,學術文檔這樣的文檔。
  • Asciidoctor
    asciidoctor是一個由ruby編寫的可以將 asciidoc轉換成html、pdf的開源項目,這個項目有java版本和maven插件

Springboot整合Swagger2

Swagger2的jar包引用

        <dependency>
            <groupId>io.springfox</groupId>
            <artifactId>springfox-swagger2</artifactId>
            <version>2.9.2</version>
        </dependency>
        <dependency>
            <groupId>io.springfox</groupId>
            <artifactId>springfox-swagger-ui</artifactId>
            <version>2.9.2</version>
        </dependency>

SpringBoot 的攔截器配置

  • 從springboot2.X開始,WebMvcConfigurerAdapter已經過時了,可以用通過繼承WebMvcConfigurationSupport類,或者實現 WebMvcConfigurer這個接口;在使用這個SpringBoot 2.X配置攔截器後,需要我們在過濾條件放開靜態資源路徑,我使用的是繼承 WebMvcConfigurationSupport這個類。下面是攔截器的配置:
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurationSupport;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

/**
 * 〈一句話功能簡述〉<br> 
 * 〈spring boot mvc config〉
 *
 * @create 2018/10/16
 * @since 1.0.0
 */
@Configuration
public class WebMvcConfig extends WebMvcConfigurationSupport{

    /**
     * 自定義攔截器
     *
     * @return HandlerInterceptor
     */
    @Bean
    public HandlerInterceptor getAccessInterceptor() {
        return new HandlerInterceptor() {
            @Override
            public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
                //TODO 增加自定義攔截
                return true;
            }

            @Override
            public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {

            }

            @Override
            public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {

            }
        };
    }

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        //addPathPatterns 用於添加攔截規則
        //excludePathPatterns 用於排除攔截
        registry.addInterceptor(getAccessInterceptor()).addPathPatterns("/**")
                /*放行swagger*/
                .excludePathPatterns("/swagger-resources/**", "/webjars/**", "/v2/**", "/swagger-ui.html/**");
        registry.addInterceptor(getAccessInterceptor());
    }

    @Override
    protected void addResourceHandlers(ResourceHandlerRegistry registry) {
        /*放行靜態資源*/
        registry.addResourceHandler("/static/**").addResourceLocations("classpath:/static/");
        registry.addResourceHandler("/templates/**").addResourceLocations("classpath:/templates/");
        /*放行swagger*/
        registry.addResourceHandler("swagger-ui.html")
                .addResourceLocations("classpath:/META-INF/resources/");
        registry.addResourceHandler("/webjars/**")
                .addResourceLocations("classpath:/META-INF/resources/webjars/");
    }
}

Swagger2自身展示內容配置


import com.fasterxml.classmate.TypeResolver;
import com.google.common.base.Function;
import com.google.common.base.Predicate;
import io.swagger.annotations.ApiOperation;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.stereotype.Component;
import springfox.documentation.RequestHandler;
import springfox.documentation.builders.ApiInfoBuilder;
import springfox.documentation.builders.PathSelectors;
import springfox.documentation.service.ApiInfo;
import springfox.documentation.spi.DocumentationType;
import springfox.documentation.spring.web.plugins.Docket;
import springfox.documentation.swagger2.annotations.EnableSwagger2;

import java.time.LocalDate;
import java.time.LocalDateTime;
import java.util.Objects;
import java.util.Optional;

/**
 * 〈一句話功能簡述〉<br> 
 * 〈Swagger Config〉
 *
 * @create 2018/10/16
 * @since 1.0.0
 */
@Configuration
@EnableSwagger2
@Component
public class SwaggerConfig {

    @Value(value = "${server.servlet.context-path}")
    private String servletContextPath;

    @Value(value = "${server.port}")
    private String serverPort;

    @Autowired
    private TypeResolver typeResolver;

    @Bean
    public Docket createRestApi() {

        Predicate<RequestHandler> predicate = input -> Objects.requireNonNull(input).isAnnotatedWith(ApiOperation.class);

        return new Docket(DocumentationType.SWAGGER_2)
                .apiInfo(apiInfo())
                .select()
         		//多路徑掃描配置           
     			//.apis(SwaggerConfig.basePackage("com.a.controller,com.b.controller"))
     			//單路徑掃描配置
                //.apis(RequestHandlerSelectors.basePackage("com.aaa"))
                //註解掃描
                .apis(predicate)
                .paths(PathSelectors.any())
                .build()
                //項目上下文根
                .pathMapping(servletContextPath)
                //時間類型全部處理爲String類型
                .directModelSubstitute(LocalDate.class, String.class)
                .directModelSubstitute(LocalDateTime.class, String.class)
                //通用模型,Result解析爲內部的引用類型
                .genericModelSubstitutes(DeferredResult.class)
                //自定義泛型處理,如果遇到ResuLt<List<Modele>>類,解析爲內部引用類型Modle
                .alternateTypeRules(
                        newRule(typeResolver.resolve(DeferredResult.class,
                                typeResolver.resolve(List.class, Object.class)),
                                typeResolver.resolve(Object.class)))
                 //通用返回消息
                .globalResponseMessage(RequestMethod.GET,responseMessages)
                ;
    }

    /**
     * 返回api對象
     *
     * @return ApiInfo
     */aaa
    private ApiInfo apiInfo() {
        return new ApiInfoBuilder()
                .title("XXX-APIs")
                .description("XXX項目api接口文檔")
                .version("1.0")
                .build();
    }

    /**
     * 自定義解析基礎包路徑,解決多個包路徑解析問題
     *
     * @param basePackage 多個包路徑
     * @return Predicate<RequestHandler>
     */
    private static Predicate<RequestHandler> basePackage(final String basePackage) {
        return input -> {
            assert input != null;
            return declaringClass(input).map(handlerPackage(basePackage)::apply).orElse(true);
        };
    }

    /**
     * 處理包路徑配置規則,支持多路徑掃描匹配以逗號隔開
     *
     * @param basePackage 掃描包路徑
     * @return Function
     */
    private static Function<Class<?>, Boolean> handlerPackage(final String basePackage) {
        return input -> {
            for (String strPackage : basePackage.split(",")) {
                boolean isMatch = input != null && input.getPackage().getName().startsWith(strPackage);
                if (isMatch) {
                    return true;
                }
            }
            return false;
        };
    }

    /**
     * 判斷是否爲空
     *
     * @param input RequestHandler 請求Handler
     * @return Optional
     */
    private static Optional<? extends Class<?>> declaringClass(RequestHandler input) {
        return Optional.ofNullable(input.declaringClass());
    }

}

通用返回消息的修改

  • swagger2本身的返回類型中定義了包含200,400,403,500等多種狀態返回,但我的項目本身只需要200狀態返回,其他的也沒有定義,因此我就做了一點點改動,刪除了其他的返回狀態。需要做的是實現OperationBuilderPlugin接口的apply方法,代碼如下:
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
import springfox.documentation.service.ResponseMessage;
import springfox.documentation.spi.DocumentationType;
import springfox.documentation.spi.service.OperationBuilderPlugin;
import springfox.documentation.spi.service.contexts.OperationContext;
import springfox.documentation.swagger.common.SwaggerPluginSupport;

import java.util.Iterator;
import java.util.Set;

/**
 * 〈一句話功能簡述〉<br> 
 * 〈swagger2 自定義 OperationBuilder 拓展〉
 *
 * @create 2018/10/17
 * @since 1.0.0
 */
@Component
@Order(SwaggerPluginSupport.SWAGGER_PLUGIN_ORDER + 1002)
public class CustomOperationBuilder implements OperationBuilderPlugin {

    @Override
    public void apply(OperationContext operationContext) {
        Set<ResponseMessage> def = operationContext.operationBuilder().build().getResponseMessages();
        Iterator iterator = def.iterator();
        while (iterator.hasNext()) {
            ResponseMessage rm  = (ResponseMessage)iterator.next();
            if (rm.getCode() != 200) {
                iterator.remove();
            }
        }
    }

    @Override
    public boolean supports(DocumentationType documentationType) {
        return true;
    }
}

至此,springboot2整合swagger2完成,只需要在controller層增加相應的註解,使用註解的方式不再這些展示了,網上很多介紹,增加註解後啓動項目即可訪問接口文檔。訪問路徑爲http://host:yport/contextPaht/swagger-ui.html

Swagger2轉換爲靜態文檔

Swagger2轉換爲靜態文檔的主要思路是,
	1.通過mock調用swagger2的接口**/v2/doc,使用Springfox-swagger2生成 json文件
	2.使用Swagger2markup將 json文件轉換成adoc文檔,並且合併多個項目的文檔在一起
	3.通過插件Asciidoctor把adoc文檔轉換爲html
我的項目大概有10個小項目,因爲都是繼承了統一的父類進行版本管理。
插件都是在父類中,可以一步對所有項目進行統一的處理,因此生成文檔的方式很快。
如果其他的項目沒有使用這種方式,可能還需要改進一下,否則一個一個生成太累了。
我的項目結構大概如下:

項目框架

jar包

        <dependency>
            <groupId>io.github.swagger2markup</groupId>
            <artifactId>swagger2markup</artifactId>
            <version>1.3.3</version>
        </dependency>
        <dependency>
            <groupId>io.github.swagger2markup</groupId>
            <artifactId>swagger2markup-spring-restdocs-ext</artifactId>
            <version>1.3.3</version>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>org.springframework.restdocs</groupId>
            <artifactId>spring-restdocs-mockmvc</artifactId>
        </dependency>
        <dependency>
            <groupId>io.springfox</groupId>
            <artifactId>springfox-staticdocs</artifactId>
            <version>2.6.1</version>
        </dependency>

插件

    <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
        <java.version>1.8</java.version>
        <spring-cloud.version>Finchley.RELEASE</spring-cloud.version>
        <spring-boot.version>2.0.4.RELEASE</spring-boot.version>
        <swagger.markup.version>1.3.7</swagger.markup.version>
        <asciidoctor.input.directory>${basedir}/../fdp-base/src/main/resources/asciidoctor</asciidoctor.input.directory>
        <swagger.output.dir>${project.basedir}/../fdp-docs/swagger</swagger.output.dir>
        <asciidoctor.html.output.directory>${project.basedir}/../fdp-docs/html</asciidoctor.html.output.directory>
        <spring.boot.war.output.directory>${project.basedir}/../fdp-docs/war</spring.boot.war.output.directory>
    </properties>

	......
	......

    <!-- 插件資源中心,主要是asciidoctor插件下載-->
    <pluginRepositories>
        <pluginRepository>
            <id>jcenter-snapshots</id>
            <name>jcenter</name>
            <url>http://oss.jfrog.org/artifactory/oss-snapshot-local/</url>
        </pluginRepository>
        <pluginRepository>
            <snapshots>
                <enabled>false</enabled>
            </snapshots>
            <id>jcenter-releases</id>
            <name>jcenter</name>
            <url>http://jcenter.bintray.com</url>
        </pluginRepository>
    </pluginRepositories>

    <repositories>
        <repository>
            <id>jcentral</id>
            <name>bintray</name>
            <url>http://jcenter.bintray.com</url>
            <snapshots>
                <enabled>false</enabled>
            </snapshots>
        </repository>
        <repository>
            <id>jcenter-snapshots</id>
            <name>jcenter</name>
            <url>http://oss.jfrog.org/artifactory/oss-snapshot-local/</url>
        </repository>
    </repositories>
    <build>
        <pluginManagement>
            <plugins>
                <plugin>
                    <groupId>org.apache.maven.plugins</groupId>
                    <artifactId>maven-compiler-plugin</artifactId>
                    <version>3.8.0</version>
                    <configuration>
                        <source>1.8</source>
                        <target>1.8</target>
                        <encoding>UTF-8</encoding>
                    </configuration>
                </plugin>
                <plugin>
                    <artifactId>maven-source-plugin</artifactId>
                    <version>3.0.1</version>
                    <executions>
                        <execution>
                            <id>${project.name}</id>
                            <goals>
                                <goal>jar-no-fork</goal>
                            </goals>
                        </execution>
                    </executions>
                </plugin>
                <plugin>
                    <artifactId>maven-war-plugin</artifactId>
                    <version>3.2.2</version>
                    <configuration>
                        <failOnMissingWebXml>false</failOnMissingWebXml>
                        <resourceEncoding>UTF-8</resourceEncoding>
                    </configuration>
                </plugin>
                <plugin>
                    <artifactId>maven-surefire-plugin</artifactId>
                    <version>2.20</version>
                    <configuration>
                        <forkMode>once</forkMode>
                        <argLine>-Dfile.encoding=UTF-8</argLine>
                        <systemPropertyVariables>
                       		 <!-- package時候通過test類生成adoc文檔,定義文檔的輸出路徑-->
                            <io.springfox.staticdocs.outputDir>${swagger.output.dir}</io.springfox.staticdocs.outputDir>
                        </systemPropertyVariables>
                    </configuration>
                </plugin>
                <!-- 通過插件將adoc文檔生成html-->
                <plugin>
                    <groupId>org.asciidoctor</groupId>
                    <artifactId>asciidoctor-maven-plugin</artifactId>
                    <version>1.5.7.1</version>
                    <dependencies>
                        <dependency>
                            <groupId>org.asciidoctor</groupId>
                            <artifactId>asciidoctorj-pdf</artifactId>
                            <version>1.5.0-alpha.16</version>
                        </dependency>
                        <dependency>
                            <groupId>org.jruby</groupId>
                            <artifactId>jruby-complete</artifactId>
                            <version>9.1.17.0</version>
                        </dependency>
                    </dependencies>
                    <configuration>
                    	<!-- 插件統一映射地址文件夾-->
                        <sourceDirectory>${asciidoctor.input.directory}</sourceDirectory>
                        <sourceDocumentName>index.adoc</sourceDocumentName>
                        <!-- 通過插件將adoc文檔生成html-->
                        <outputDirectory>${asciidoctor.html.output.directory}</outputDirectory>
                        <outputFile>fdp.html</outputFile>
                        <backend>html5</backend>
                        <!-- 屬性定義-->
                        <attributes>
                            <doctype>book</doctype>
                            <toc>left</toc>
                            <toclevels>3</toclevels>
                            <numbered></numbered>
                            <hardbreaks></hardbreaks>
                            <sectlinks></sectlinks>
                            <sectanchors></sectanchors>
                            <generated>${swagger.output.dir}</generated>
                        </attributes>
                    </configuration>
                </plugin>
            </plugins>
        </pluginManagement>
    </build>

代碼

代碼分爲幾部分說明
  • 生成adoc文檔
  • 讀取adoc文檔的配置文件
  • 轉換爲HTML

生成adoc文檔

本項目沒有使用插件方式生成adoc文檔,主要考慮到合併多個服務的接口文檔的問題。代碼方式更好操控。
使用mock讀取swagger2的接口信息,是直接使用的test方法來讀取的,沒有新開一個服務進行。
如果代碼存放地不一樣,但是需要文檔整合在一起,可以考慮單獨起一個服務定時讀取接口文檔。

測試類文件代碼

import com.cscn.fdp.base.swagger.Swagger2Markup;
import com.cscn.fdp.base.swagger.SwaggerConfig;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.setup.MockMvcBuilders;
import org.springframework.util.CollectionUtils;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.context.WebApplicationContext;

import java.util.Map;

@RunWith(SpringJUnit4ClassRunner.class)
@SpringBootTest(classes = {ServiceWeatherApplication.class, SwaggerConfig.class}, webEnvironment = SpringBootTest.WebEnvironment.DEFINED_PORT)
public class Swagger2MarkupTest {

    private static final Logger LOG = LoggerFactory.getLogger(Swagger2MarkupTest.class);

    @Value("${server.servlet.context-path}")
    private String contextPath;

    @Autowired
    private WebApplicationContext wac;

    private MockMvc mockMvc;

    @Before
    public void setup() {
        mockMvc = MockMvcBuilders.webAppContextSetup(wac).build();
    }


    @Test
    public void createSpringfoxSwaggerJson() throws Exception {
        Map<String, Object> map = wac.getBeansWithAnnotation(RestController.class);
        StringBuilder sb = new StringBuilder("");
        if (!CollectionUtils.isEmpty(map)) {
            map.keySet().forEach(key -> sb.append(Character.toUpperCase(key.charAt(0))).append(key.substring(1)).append("   "));
        }
        Swagger2Markup.createSpringfoxSwaggerJson(mockMvc, contextPath, sb.toString());
    }
}

Swagger2Markup的通用代碼
生成文檔的代碼是一樣的,因此提爲公共方法

import io.github.swagger2markup.*;
import io.github.swagger2markup.builder.Swagger2MarkupConfigBuilder;
import io.github.swagger2markup.markup.builder.MarkupLanguage;
import io.swagger.models.Path;
import io.swagger.models.Swagger;
import io.swagger.parser.Swagger20Parser;
import io.swagger.util.Json;
import org.springframework.http.MediaType;
import org.springframework.mock.web.MockHttpServletResponse;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.MvcResult;
import org.springframework.util.CollectionUtils;

import java.io.BufferedWriter;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.HashMap;
import java.util.Map;
import java.util.Optional;

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

public class Swagger2Markup {

    public static void createSpringfoxSwaggerJson(MockMvc mockMvc, String contextPath, String tagDesc) throws Exception {
        String outputDir = System.getProperty("io.springfox.staticdocs.outputDir");
        MvcResult mvcResult = mockMvc.perform(get("/v2/api-docs")
                .accept(MediaType.APPLICATION_JSON))
                .andExpect(status().isOk())
                .andReturn();

        MockHttpServletResponse response = mvcResult.getResponse();
        String nowJsonStr = response.getContentAsString();
		
        Swagger swagger = new Swagger20Parser().parse(nowJsonStr);
        swagger.setBasePath("/");
        Map<String, Path> paths = Optional.ofNullable(swagger.getPaths()).orElse(new HashMap<>());
        Map<String, Path> newPaths = new HashMap<>();
        //zuul路由端口
        int serverPort = 8770;
        //自定義URL地址
        String sb = "http://" + swagger.getHost() + ":" + serverPort + contextPath;
        paths.forEach((key,value) -> newPaths.put(sb + key, value));
        swagger.setPaths(newPaths);
        //自定義tag的描述
        if (!CollectionUtils.isEmpty(swagger.getTags())) {
            swagger.getTags().forEach(tag -> tag.setDescription(tagDesc));
        }
		
        Files.createDirectories(Paths.get(outputDir));
		//判斷是否存在其他項目的接口文檔,如果存在,讀取合併tag,path,definition
        if (Paths.get(outputDir,"swagger.json").toFile().exists()) {
            Swagger oldSwagger = new Swagger20Parser().parse(new String(Files.readAllBytes(Paths.get(outputDir,"swagger.json"))));
            if (!CollectionUtils.isEmpty(oldSwagger.getPaths())){
                oldSwagger.getPaths().forEach(swagger::path);
            }
            if (!CollectionUtils.isEmpty(oldSwagger.getDefinitions())){
                oldSwagger.getDefinitions().forEach(swagger::addDefinition);
            }
            if (!CollectionUtils.isEmpty(oldSwagger.getTags())){
                oldSwagger.getTags().forEach(swagger::addTag);
            }
        }
		//保存文檔,用於合併到下一個項目中
        try (BufferedWriter writer = Files.newBufferedWriter(Paths.get(outputDir, "swagger.json"), StandardCharsets.UTF_8)){
            writer.write(Json.pretty(swagger));
        }
		//json文檔轉換爲adoc文檔的配置
        Map<String, String> configMap = new HashMap<>();
        configMap.put("swagger2markup.extensions.springRestDocs.snippetBaseUri", outputDir);//輸出路徑
        configMap.put("swagger2markup.extensions.springRestDocs.defaultSnippets", "true");//默認值,使用代碼段文件
        configMap.put(Swagger2MarkupProperties.PATHS_GROUPED_BY, GroupBy.TAGS.name());//通過tag分組
        configMap.put(Swagger2MarkupProperties.MARKUP_LANGUAGE, MarkupLanguage.ASCIIDOC.name());//生成adoc文檔
        configMap.put(Swagger2MarkupProperties.OUTPUT_LANGUAGE, Language.ZH.name());//中文
		//輸出文件到指定路徑
        Swagger2MarkupConfig config = new Swagger2MarkupConfigBuilder(configMap).build();
        Swagger2MarkupConverter converter = Swagger2MarkupConverter.from(swagger).withConfig(config).build();
        converter.toPath(Paths.get(outputDir));

    }
}

讀取adoc文檔的配置文件

pom中有個參數爲
<asciidoctor.input.directory>${basedir}/…/fdp-base/src/main/resources/asciidoctor</asciidoctor.input.directory>
在這個參數定義的文件路徑下建立一個文件index.adoc
文件內容爲:

include::{generated}/overview.adoc[]
include::{generated}/paths.adoc[]
include::{generated}/security.adoc[]
include::{generated}/definitions.adoc[]

這個文件的主要作用爲,插件通過讀取這個文件內容,直接讀取到所有adoc文檔,就不用單獨爲每個adoc文檔寫一個配置

轉換爲HTML

調用asciidoctor插件生成HTML

插件

最後效果如下

文檔

後記

這個只是最初的一個版本,還有許多優化的地方。

  1. 如果生成新的HTML,需要把舊的adoc和json刪除可以,否則會出現問題。
  2. 如果可以不通過test,直接插件運行,使用體驗可能會更好
  3. 增加自動HTML到文檔服務器的功能
  4. 未完待續…

如果有任何疑問,請在下方留言,我會盡量作答,謝謝。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章