SpringBoot2.0.4+Swagger2.9.2+asciidoctor1.5.7.1生成静态HTML的API文档

文章目录

  • 说明
    • 术语
  • Springboot整合Swagger2
    • Swagger2的jar包引用
    • SpringBoot 的拦截器配置
    • Swagger2自身展示内容配置
    • 通用返回消息的修改
  • Swagger2转换为静态文档
    • jar包
    • 插件
    • 代码
      • 生成adoc文档
      • 读取adoc文档的配置文件
      • 转换为HTML
  • 最后效果如下
  • 后记

说明

  • 目前的公司项目的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包引用

        
            io.springfox
            springfox-swagger2
            2.9.2
        
        
            io.springfox
            springfox-swagger-ui
            2.9.2
        

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;

/**
 * 〈一句话功能简述〉
* 〈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;

/**
 * 〈一句话功能简述〉
* 〈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>类,解析为内部引用类型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 */ 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;

/**
 * 〈一句话功能简述〉
* 〈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个小项目,因为都是继承了统一的父类进行版本管理。
插件都是在父类中,可以一步对所有项目进行统一的处理,因此生成文档的方式很快。
如果其他的项目没有使用这种方式,可能还需要改进一下,否则一个一个生成太累了。
我的项目结构大概如下:

SpringBoot2.0.4+Swagger2.9.2+asciidoctor1.5.7.1生成静态HTML的API文档_第1张图片

jar包

        
            io.github.swagger2markup
            swagger2markup
            1.3.3
        
        
            io.github.swagger2markup
            swagger2markup-spring-restdocs-ext
            1.3.3
            test
        
        
            org.springframework.restdocs
            spring-restdocs-mockmvc
        
        
            io.springfox
            springfox-staticdocs
            2.6.1
        

插件

    
        UTF-8
        UTF-8
        1.8
        Finchley.RELEASE
        2.0.4.RELEASE
        1.3.7
        ${basedir}/../fdp-base/src/main/resources/asciidoctor
        ${project.basedir}/../fdp-docs/swagger
        ${project.basedir}/../fdp-docs/html
        ${project.basedir}/../fdp-docs/war
    

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

    
    
        
            jcenter-snapshots
            jcenter
            http://oss.jfrog.org/artifactory/oss-snapshot-local/
        
        
            
                false
            
            jcenter-releases
            jcenter
            http://jcenter.bintray.com
        
    

    
        
            jcentral
            bintray
            http://jcenter.bintray.com
            
                false
            
        
        
            jcenter-snapshots
            jcenter
            http://oss.jfrog.org/artifactory/oss-snapshot-local/
        
    
    
        
            
                
                    org.apache.maven.plugins
                    maven-compiler-plugin
                    3.8.0
                    
                        1.8
                        1.8
                        UTF-8
                    
                
                
                    maven-source-plugin
                    3.0.1
                    
                        
                            ${project.name}
                            
                                jar-no-fork
                            
                        
                    
                
                
                    maven-war-plugin
                    3.2.2
                    
                        false
                        UTF-8
                    
                
                
                    maven-surefire-plugin
                    2.20
                    
                        once
                        -Dfile.encoding=UTF-8
                        
                       		 
                            ${swagger.output.dir}
                        
                    
                
                
                
                    org.asciidoctor
                    asciidoctor-maven-plugin
                    1.5.7.1
                    
                        
                            org.asciidoctor
                            asciidoctorj-pdf
                            1.5.0-alpha.16
                        
                        
                            org.jruby
                            jruby-complete
                            9.1.17.0
                        
                    
                    
                    	
                        ${asciidoctor.input.directory}
                        index.adoc
                        
                        ${asciidoctor.html.output.directory}
                        fdp.html
                        html5
                        
                        
                            book
                            left
                            3
                            
                            
                            
                            
                            ${swagger.output.dir}
                        
                    
                
            
        
    

代码

代码分为几部分说明
  • 生成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中有个参数为
${basedir}/…/fdp-base/src/main/resources/asciidoctor
在这个参数定义的文件路径下建立一个文件index.adoc
文件内容为:

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

这个文件的主要作用为,插件通过读取这个文件内容,直接读取到所有adoc文档,就不用单独为每个adoc文档写一个配置

转换为HTML

调用asciidoctor插件生成HTML

SpringBoot2.0.4+Swagger2.9.2+asciidoctor1.5.7.1生成静态HTML的API文档_第2张图片

最后效果如下

SpringBoot2.0.4+Swagger2.9.2+asciidoctor1.5.7.1生成静态HTML的API文档_第3张图片

后记

这个只是最初的一个版本,还有许多优化的地方。

  1. 如果生成新的HTML,需要把旧的adoc和json删除可以,否则会出现问题。
  2. 如果可以不通过test,直接插件运行,使用体验可能会更好
  3. 增加自动HTML到文档服务器的功能
  4. 未完待续…

如果有任何疑问,请在下方留言,我会尽量作答,谢谢。

你可能感兴趣的:(SpringBoot)