io.springfox
springfox-swagger2
2.9.2
io.springfox
springfox-swagger-ui
2.9.2
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/");
}
}
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());
}
}
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转换为静态文档的主要思路是,
1.通过mock调用swagger2的接口**/v2/doc,使用Springfox-swagger2生成 json文件
2.使用Swagger2markup将 json文件转换成adoc文档,并且合并多个项目的文档在一起
3.通过插件Asciidoctor把adoc文档转换为html
我的项目大概有10个小项目,因为都是继承了统一的父类进行版本管理。
插件都是在父类中,可以一步对所有项目进行统一的处理,因此生成文档的方式很快。
如果其他的项目没有使用这种方式,可能还需要改进一下,否则一个一个生成太累了。
我的项目结构大概如下:
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文档,主要考虑到合并多个服务的接口文档的问题。代码方式更好操控。
使用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));
}
}
pom中有个参数为
在这个参数定义的文件路径下建立一个文件index.adoc
文件内容为:
include::{generated}/overview.adoc[]
include::{generated}/paths.adoc[]
include::{generated}/security.adoc[]
include::{generated}/definitions.adoc[]
这个文件的主要作用为,插件通过读取这个文件内容,直接读取到所有adoc文档,就不用单独为每个adoc文档写一个配置
调用asciidoctor插件生成HTML
这个只是最初的一个版本,还有许多优化的地方。
如果有任何疑问,请在下方留言,我会尽量作答,谢谢。