前言:接到任务需要在春天云项目中生成离线api,刚开始也是不清楚如何下手,于是查阅的相关的博客,但是并没有找到在春天云项目中完成离线api相关的博客,经过差不多三天的时间,慢慢的完成了这样一个功能,也是踩了坑,走过弯路,在这里记录一下,分享给大家。
前提:项目是基于spring boot搭建的spring cloud微服务项目,使用swagger对实体类,接口进行相应的注解。在父工程下分为了:服务注册+网关服务+其他功能服务。
介绍:此功能是在网关服务中实现,以下均为Zuul服务器中代码。
1.多服务在线swagger api中首先我们需要实现接口SwaggerResourcesProvider以完成api的切换,离线api需要通过这个接口获取相应的SwaggerResource中的位置作为获取对应服务的api json数据。
import org.springframework.context.annotation.Primary;
import org.springframework.stereotype.Component;
import springfox.documentation.swagger.web.SwaggerResource;
import springfox.documentation.swagger.web.SwaggerResourcesProvider;
@Component
@Primary
public class DocumentationConfig implements SwaggerResourcesProvider {
@Override
public List<SwaggerResource> get() {
List<SwaggerResource> resources = new ArrayList<>();
resources.add(swaggerResource("服务1", "/serverName1/v2/api-docs", "1.0"));
resources.add(swaggerResource("服务2", "/serverName2/v2/api-docs", "1.0"));
resources.add(swaggerResource("服务3", "/serverName3/v2/api-docs", "1.0"));
return resources;
}
private SwaggerResource swaggerResource(String name, String location, String version) {
SwaggerResource swaggerResource = new SwaggerResource();
swaggerResource.setName(name);
swaggerResource.setLocation(location);
swaggerResource.setSwaggerVersion(version);
return swaggerResource;
}
}
2.接下来我们建一个测试类,当然你也可以不建测试类,只要在程序中能够获取到其他服务的api json数据都行。生成json数据和通过索引.adoc文件生成overview.adoc,paths .adoc,definitions.adoc这一块网上很多。因为POM文件中只能给出一个生成PDF和HTML文件的目录,所以每生成完一个服务的API文档就需要将它转移到其他目录中,并且删除原目录下文件以备下一次文档的生成,目标目录如:destPdfDirs,destHtmlDirs数组。中间通过调用代码方式执行mvn test指令,过程中会通过overview.adoc,paths.adoc,definitions.adoc三个文件生成pdf和HTML文件。
import com.wode.config.DocumentationConfig;
import com.wode.swagger.MavenUtil;
import io.github.robwin.markup.builder.MarkupLanguage;
import io.github.robwin.swagger2markup.GroupBy;
import io.github.robwin.swagger2markup.Swagger2MarkupConverter;
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.boot.test.autoconfigure.restdocs.AutoConfigureRestDocs;
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.http.MediaType;
import org.springframework.test.context.junit4.SpringRunner;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.util.ResourceUtils;
import springfox.documentation.staticdocs.SwaggerResultHandler;
import springfox.documentation.swagger.web.SwaggerResource;
import springfox.documentation.swagger.web.SwaggerResourcesProvider;
import java.io.File;
import java.util.List;
import static org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders.get;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
@AutoConfigureMockMvc
@AutoConfigureRestDocs
@RunWith(SpringRunner.class)
@SpringBootTest
public class SwaggerTest {
private static Logger logger = LoggerFactory.getLogger(SwaggerTest.class);
private String snippetDir = "target/generated-snippets";
private String outputDir = "target/asciidoc";
private String pdfFileDir = "target/asciidoc/pdf/index.pdf";
private String htmlFileDir = "target/asciidoc/html/index.html";
private String[] destPdfDirs = {"server1/pdf", "server2/pdf", "server3/pdf"};
private String[] destHtmlDirs = {"server1/html", "server2/html", "server3/html"};
private String baseDir = "src/main/resources/api/";
@Autowired
private MockMvc mockMvc;
@Test
public void test() throws Exception {
SwaggerResourcesProvider provider = new DocumentationConfig();
List<SwaggerResource> resources = provider.get();
for (int i = 0; i < resources.size(); i++) {
SwaggerResource resource = resources.get(i);
// 得到swagger.json,写入outputDir目录中
mockMvc.perform(get(resource.getLocation()).accept(MediaType.APPLICATION_JSON))
.andDo(SwaggerResultHandler.outputDirectory(outputDir).build())
.andExpect(status().isOk())
.andReturn();
Swagger2MarkupConverter.from(outputDir + "/swagger.json")
.withPathsGroupedBy(GroupBy.TAGS)// 按tag排序
.withMarkupLanguage(MarkupLanguage.ASCIIDOC)//格式
.withExamples(snippetDir)
.build()
.intoFolder(outputDir);//输出
MavenUtil mavenUtil = new MavenUtil();
mavenUtil.createPdfAndHtmlFile();//执行mvn指令
long start = System.currentTimeMillis();
int restrict = 60000;//时间限制1分钟
while (true) {
long end = System.currentTimeMillis();
if (restrict <= (end - start)) {
logger.warn("create pdf or html time out. error msg: {}", "path: [" + destPdfDirs[i] + "] or [" + destHtmlDirs[i] + "];");
return;
}
String pdfPath = mavenUtil.returnPath(pdfFileDir);
String htmlPath = mavenUtil.returnPath(htmlFileDir);
File pdfFile = new File(pdfPath);
File htmlFile = new File(htmlPath);
if (pdfFile != null && pdfFile.exists() && htmlFile != null && htmlFile.exists()) {
File staticPdfFile = ResourceUtils.getFile(mavenUtil.returnPath(baseDir + destPdfDirs[i]) + "/index.pdf");
File staticHtmlFile = ResourceUtils.getFile(mavenUtil.returnPath(baseDir + destHtmlDirs[i]) + "/index.html");
if (staticPdfFile != null && staticHtmlFile.exists()) {
staticPdfFile.delete();
}
if (staticHtmlFile != null && staticHtmlFile.exists()) {
staticHtmlFile.delete();
}
String destPdfPath = mavenUtil.returnPath(baseDir + destPdfDirs[i]);
String destHtmlPath = mavenUtil.returnPath(baseDir + destHtmlDirs[i]);
mavenUtil.moveFile(pdfPath, destPdfPath);
mavenUtil.moveFile(htmlPath, destHtmlPath);
//清空target/asciidoc文件下的文件为下一次生产api准备
File file = new File("target/asciidoc");
mavenUtil.deleteFile(file);
break;
}
}
}
logger.info("create pdf and html of static api success.");
}
}
3.pom.xml: “$ {}” 是index.adoc文档生成配置的目录,生成PDF格式的目录,生成HTML的目录注意导入依赖的版本,若因为版本不一致可能导致一些NoSuchMethod的异常, swagger的依赖要使用2.6.1版本,否则也会有冲突。
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<java.version>1.8</java.version>
<snippetsDirectory>${project.build.directory}/generated-snippets</snippetsDirectory>
<asciidoctor.input.directory>${project.basedir}/docs/asciidoc</asciidoctor.input.directory>
<generated.asciidoc.directory>${project.build.directory}/asciidoc</generated.asciidoc.directory>
<asciidoctor.html.output.directory>${project.build.directory}/asciidoc</asciidoctor.html.output.directory>
<asciidoctor.pdf.output.directory>${project.build.directory}/asciidoc</asciidoctor.pdf.output.directory>
</properties>
<!-- 以下为增加的依赖 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
</dependency>
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger2</artifactId>
</dependency>
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger-ui</artifactId>
</dependency>
<!--离线文档-->
<dependency>
<groupId>org.springframework.restdocs</groupId>
<artifactId>spring-restdocs-mockmvc</artifactId>
<version>1.1.2.RELEASE</version>
<scope>test</scope>
</dependency>
<!--springfox-staticdocs 生成静态文档-->
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-staticdocs</artifactId>
<version>2.6.1</version>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
<version>3.8.1</version>
</dependency>
<dependency>
<groupId>io.github.robwin</groupId>
<artifactId>swagger2markup</artifactId>
<version>0.9.2</version>
</dependency>
<dependency>
<groupId>io.swagger</groupId>
<artifactId>swagger-models</artifactId>
<version>1.5.10</version>
</dependency>
<!-- 下面的内容网上很多很多很多 -->
<build>
<plugins>
<!-- 通过mvn指令执行单元测试 -->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
</plugin>
<plugin>
<groupId>org.asciidoctor</groupId>
<artifactId>asciidoctor-maven-plugin</artifactId>
<version>1.5.3</version>
<!--生成PDF-->
<dependencies>
<dependency>
<groupId>org.asciidoctor</groupId>
<artifactId>asciidoctorj-pdf</artifactId>
<version>1.5.0-alpha.14</version>
</dependency>
<dependency>
<groupId>org.jruby</groupId>
<artifactId>jruby-complete</artifactId>
<version>1.7.21</version>
</dependency>
</dependencies>
<!--文档生成配置-->
<configuration>
<sourceDirectory>${asciidoctor.input.directory}</sourceDirectory>
<sourceDocumentName>index.adoc</sourceDocumentName>
<attributes>
<doctype>book</doctype>
<toc>left</toc>
<toclevels>3</toclevels>
<numbered></numbered>
<hardbreaks></hardbreaks>
<sectlinks></sectlinks>
<sectanchors></sectanchors>
<generated>${generated.asciidoc.directory}</generated>
</attributes>
</configuration>
<executions>
<!--html5-->
<execution>
<id>output-html</id>
<phase>test</phase>
<goals>
<goal>process-asciidoc</goal>
</goals>
<configuration>
<backend>html5</backend>
<outputDirectory>${asciidoctor.html.output.directory}/html</outputDirectory>
</configuration>
</execution>
<!--pdf-->
<execution>
<id>output-pdf</id>
<phase>test</phase>
<goals>
<goal>process-asciidoc</goal>
</goals>
<configuration>
<backend>pdf</backend>
<outputDirectory>${asciidoctor.pdf.output.directory}/pdf</outputDirectory>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>
4.index.adoc:这个是文档生成配置文件,POM配置中要能找到这个文件就行。
include::{generated}/overview.adoc[]
include::{generated}/definitions.adoc[]
include::{generated}/paths.adoc[]
5.maven指令文件:这两个文件是调用代码是生成的指令文件。
6.MavenUtil:执行maven指令和一些文件的操作。注意执行的maven指令必须是在当前项目下也就是在网关服务中执行mvn test。
import java.io.*;
import java.nio.file.Paths;
public class MavenUtil {
public void createPdfAndHtmlFile() throws Exception {
//获取pom.xml的绝对路径
String pomPath = returnPath("pom.xml");
//生成bat文件
StringBuilder batFileContent = new StringBuilder(pomPath.substring(0, 2)).append("\r\n");
batFileContent.append("cd ").append(pomPath.substring(0, pomPath.length() - 7)).append("\r\n");
batFileContent.append("mvn test");
//将batFileContent写入bat文件中
writeInBatFile("src/cmd/bat.bat", batFileContent.toString().replace("/", "\\"));
//生成vbs文件内容
StringBuilder vbsFileContent = new StringBuilder("Set ws = CreateObject(\"Wscript.Shell\")").append("\r\n");
vbsFileContent.append("ws.run \"cmd /c ").append(returnPath("src/cmd/bat.bat").replace("/", "\\")).append("\",vbhide");
//写入vbs文件
writeInBatFile("src/cmd/vbs.vbs", vbsFileContent.toString());
//生成执行vbs文件命令(用vbs隐藏bat文件执行时的窗口)
StringBuilder cmd = new StringBuilder("cmd /c CScript ").append(returnPath("src/cmd/vbs.vbs"));
System.out.println(cmd.toString());
Process process = Runtime.getRuntime().exec(cmd.toString());
System.out.println(process.waitFor());
}
public String returnPath(String filePath) {
return Paths.get(filePath).toAbsolutePath().toString();
}
private void writeInBatFile(String fileName, String content) {
File file = new File(returnPath(fileName));
//如果文件存在则删除
if (file != null && file.exists()) {
file.delete();
}
try {
//创建GBK格式的bat文件,utf-8中文在cmd中乱码
OutputStreamWriter out = new OutputStreamWriter(new FileOutputStream(file), "GBK");
BufferedWriter bw = new BufferedWriter(out);
bw.write(content);
bw.close();
out.close();
} catch (IOException e) {
e.printStackTrace();
}
}
public boolean moveFile(String fileName, String destinationFloderUrl) {
File file = new File(fileName);
File destFloder = new File(destinationFloderUrl);
if (destFloder.exists()) {
if (destFloder.isFile()) {
return false;
}
} else {
if (!destFloder.mkdirs()) {
return false;
}
}
if (file.isFile() && file.exists()) {
String destinationFile = destinationFloderUrl + "/" + file.getName();
if (!file.renameTo(new File(destinationFile))) {
return false;
}
} else {
return false;
}
return true;
}
public void deleteFile(File file) {
if (file.isDirectory()) {
File[] files = file.listFiles();
for (File f : files) {
if (f.isFile()) {
f.delete();
} else if (file.isDirectory()) {
deleteFile(f);
}
}
file.delete();
}
}
}
7.代码完了,要生成所有服务的离线API文档,需要先将注册服务跑起来,再将需要生产离线API的服务跑起来,最后跑网关服务中的测试类就行了。下面是其中一个HTML,暂时还没有去解决PDF中文缺失的问题,大家可以参考一下其他伙伴们的经验