1.前言
最近项目交付需要提交接口文档,由我来完成这个任务,但是接口文档的编写绝对是很折磨人的一件事情,就比如我要完成的接口文档就有几百个接口,如果手动一个个去写的话,不仅浪费时间,还很容易写错,所以我就想能不能有什么工具可以帮忙导出接口word文档,比如swagger、apifox等,但是发现它们都不能直接导出word,需要一定的转换,并且导出的模板是固定的,后续可能还会花大把时间来调整格式,所以就决定自己写代码来导出文档
最终我选择根据apifox的接口json格式文件来导出文档,它就和swagger的json格式内容类似,都包含了所有接口的信息,实际上swagger或apifox生成的文档也是根据这些接口信息生成的,而apifox的文档比swagger的包含更多信息,swagger只会读取swagger相关注解的内容,而apifox的会包含一些其他注解和注释的信息,再加上我们目前前后端联调也是用的apifox
2.实现
2.1 获取接口信息文件
首先右键选择导出
然后选择Apifox格式的文件,选择要导出的接口点击导出
此时就会获得mianshiya.apifox.json
这样一个.apifox.json
格式的文件
2.2 文件格式分析
首先对前面得到的文件进行分析,主要使用的apiCollection
和schemaCollection
字段中的数据,分别为接口信息和数据对象模型
apiCollection
字段实际上是一个数组,分别对应不同目录下的数据,但我在使用的时候只会在根目录下进行操作,在根目录下一般在根据不同controller又建立不同的子目录,对应的就是items
字段
items
字段同样是一个数组,每一个元素就代表一个controller里的接口,每个元素里目前对我有用的就是name
字段和items
字段,分别对应controller的名称和每一个具体接口的信息
这里的嵌套的itesm
字段里可用的信息就很多了,比如name
以及api
字段里的method, path, paramters, responses, responseExamples, requestBody
等,所以主要解析的内容就是这些字段
而上面这些字段可能存在$ref
字段,它的值是一开始说的schemaCollection
数据模型中数据的id,并且存在嵌套的关系,就是schemaCollection
里的一个数据模型同样会使用$ref
字段
2.3 代码实现
首先因为需要解析json文件以及创建word文档,所以需要引入一些依赖,解析json使用的是fastjson,操作word则是poi
<!-- POI基础依赖 -->
<dependency>
<groupId>org.apache.poi</groupId>
<artifactId>poi</artifactId>
<version>5.3.0</version>
</dependency>
<!-- POI对OOXML格式的支持 -->
<dependency>
<groupId>org.apache.poi</groupId>
<artifactId>poi-ooxml</artifactId>
<version>5.3.0</version>
</dependency>
<!-- 必要的XML解析库 -->
<dependency>
<groupId>org.apache.xmlbeans</groupId>
<artifactId>xmlbeans</artifactId>
<version>5.1.1</version>
</dependency>
<!-- POI对图表等功能的支持(可选,根据需求添加) -->
<dependency>
<groupId>org.apache.poi</groupId>
<artifactId>poi-ooxml-schemas</artifactId>
<version>4.1.2</version> <!-- 可选版本,根据需要调整 -->
</dependency>
<!-- fastjson -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.83</version>
</dependency>
接着是一些原文档对应的数据模型类
@Data
public class Item {
private String name;
private String id;
private Api api;
private Schema schema;
private List<Item> items;
}
@Data
public class Schema {
private JsonSchema jsonSchema;
}
@Data
public class JsonSchema {
private String type;
// 保证原数据的顺序
private LinkedHashMap<String, JSONObject> properties;
private List<String> required;
@JSONField(name = "$ref")
private String ref;
private String description;
}
@Data
public class Api {
private String id;
private String method;
private String path;
private Parameter parameters;
private List<Response> responses;
private List<ResponseExample> responseExamples;
private RequestBody requestBody;
}
@Data
public class Parameter {
private List<Query> query;
}
@Data
public class Response {
private JsonSchema jsonSchema;
}
@Data
public class ResponseExample {
private String name;
private String data;
}
@Data
public class RequestBody {
private String type;
private JsonSchema jsonSchema;
private List<Query> parameters;
}
@Data
public class Query {
private String name;
private Boolean required;
private String description;
private String type;
}
接着是两个生成表格用的数据对象
@Data
public class RequestData {
private String name;
private String position;
private String type;
private String required;
private String description;
public List<String> list() {
return Arrays.asList(name, position, type, required, description);
}
}
@Data
public class ResponseData {
private String name;
private String type;
private String description;
public List<String> list() {
return Arrays.asList(name, type, description);
}
}
接着就是主要的生成文档代码
public class JustTest {
/**
* 核心的接口信息数据
*/
private static List<Item> apiItems;
/**
* 核心的数据对象数据
*/
private static List<Item> schemaItems;
/**
* 解析的apifox.json文件地址
*/
private static final String filePath = "d://desktop/mianshiya.apifox.json";
/**
* 输出文件名,也可以修改为绝对地址
*/
public static final String outputFileName = "output-mianshiya.docx";
public static void main(String[] args) throws Exception {
long start = System.currentTimeMillis();
// 解析apifox.json文件数据
parseApifoxJson();
// 转换为word文旦
translateToWord();
System.out.printf("总耗时: %s s\n", (System.currentTimeMillis() - start) / 1000.0);
}
/**
* 解析json文件
*/
private static void parseApifoxJson() {
long start = System.currentTimeMillis();
StringBuilder content = new StringBuilder();
try (BufferedReader reader = new BufferedReader(new FileReader(filePath))) {
String line;
while ((line = reader.readLine()) != null) {
content.append(line);
}
String json = content.toString();
System.out.println("json总长度: " + json.length());
JSONObject jsonObject = JSONObject.parseObject(json);
// 获取取apiCollection[0].items
JSONObject apiCollection0 = (JSONObject)jsonObject.getJSONArray("apiCollection").get(0);
String jsonApiCollectionItems = apiCollection0.getString("items");
apiItems = JSONObject.parseObject(jsonApiCollectionItems, new TypeReference<List<Item>>() {});
// 获取schemaCollection[0].items
JSONObject schemaCollection0 = (JSONObject)jsonObject.getJSONArray("schemaCollection").get(0);
String jsonSchemaCollection0Items = schemaCollection0.getString("items");
schemaItems = JSONObject.parseObject(jsonSchemaCollection0Items, new TypeReference<List<Item>>() {});
long end = System.currentTimeMillis();
System.out.printf("解析apifox.json总耗时: %s s\n", (end - start) / 1000.0);
} catch (IOException e) {
e.printStackTrace();
}
}
/**
* 将数据转换为word文档
*
* @throws Exception
*/
private static void translateToWord() throws Exception {
// 创建一个新的Word文档
XWPFDocument document = new XWPFDocument();
// 标题的1级序号
int no = 1;
// 标题的2级序号
int categoryNo = 0;
for (Item apiItem : apiItems) {
categoryNo++;
// 获取分类名(一般对应的是controller的名称)
String categoryName = apiItem.getName();
List<Item> items = apiItem.getItems();
// 创建一级标题
XWPFParagraph heading1 = document.createParagraph();
// 设置大纲级别为1(一级标题)
setHeadingLevel(heading1, 1);
XWPFRun run1 = heading1.createRun();
String title1 = String.format("%s.%s %s", no, categoryNo, categoryName);
run1.setText(title1);
// 加粗
run1.setBold(true);
// 字体大小
run1.setFontSize(16);
// 字体
run1.setFontFamily("宋体");
// todo
System.out.printf("-----------%s.%s %s-----------\n", no, categoryNo, categoryName);
// 标题的三级序号
int itemNo = 0;
for (Item item : items) {
itemNo++;
// 获取接口名称
String itemName = item.getName();
Api api = item.getApi();
// 获取请求方法
String method = api.getMethod();
// 获取请求路径
String path = api.getPath();
// 创建二级标题
XWPFParagraph heading2 = document.createParagraph();
// 设置大纲级别为2(二级标题)
setHeadingLevel(heading2, 2);
XWPFRun run2 = heading2.createRun();
String title2 = String.format("%s.%s.%s %s\n", no, categoryNo, itemNo, itemName);
run2.setText(title2);
// 加粗
run2.setBold(true);
// 字体大小
run2.setFontSize(14);
// 字体
run2.setFontFamily("宋体");
createNormalContent(document, "接口地址", true);
createNormalContent(document, String.format("%s %s", method.toUpperCase(), path), false);
// todo
System.out.printf("%s.%s.%s %s\n", no, categoryNo, itemNo, itemName);
System.out.println("接口地址");
System.out.printf("%s %s\n", method.toUpperCase(), path);
// 创建请求参数表格数据
RequestData requestDataHead = new RequestData();
requestDataHead.setName("名称");
requestDataHead.setPosition("位置");
requestDataHead.setType("类型");
requestDataHead.setRequired("必须");
requestDataHead.setDescription("描述");
List<RequestData> requestDataList = new ArrayList<>();
requestDataList.add(requestDataHead);
Parameter parameters = api.getParameters();
List<Query> querys = parameters.getQuery();
RequestBody requestBody = api.getRequestBody();
createNormalContent(document, "请求参数", true);
// todo
System.out.println("请求参数");
// 如果有query参数
if (!CollectionUtils.isEmpty(querys)) {
for (Query query : querys) {
RequestData requestData = new RequestData();
requestData.setName(query.getName());
requestData.setPosition("query");
requestData.setType(query.getType());
requestData.setRequired(query.getRequired() ? "是" : "否");
requestData.setDescription(query.getDescription());
requestDataList.add(requestData);
}
}
// 如果为body传输数据
if ("application/json".equals(requestBody.getType())) {
JsonSchema jsonSchema = requestBody.getJsonSchema();
String ref = jsonSchema.getRef();
requestDataList.addAll(getSchemaRequestData(ref));
}
// 其他情况如文件上传
if (!CollectionUtils.isEmpty(requestBody.getParameters())) {
for (Query query : requestBody.getParameters()) {
RequestData requestData = new RequestData();
requestData.setName(query.getName());
requestData.setPosition("body");
requestData.setType(query.getType());
requestData.setRequired(query.getRequired() ? "是" : "否");
requestData.setDescription(query.getDescription());
requestDataList.add(requestData);
}
}
// 创建表格
createTable(document, requestDataList.stream().map(RequestData::list).collect(Collectors.toList()));
for (RequestData requestData : requestDataList) {
// todo
System.out.printf("%s %s %s %s %s\n", requestData.getName(), requestData.getPosition(),
requestData.getType(), requestData.getRequired(), requestData.getDescription());
}
createNormalContent(document, "响应参数", true);
// todo
System.out.println("响应参数");
// 创建响应参数表格数据
ResponseData responseDataHead = new ResponseData();
responseDataHead.setName("名称");
responseDataHead.setType("类型");
responseDataHead.setDescription("描述");
List<ResponseData> responseDataList = new ArrayList<>();
responseDataList.add(responseDataHead);
List<Response> responses = api.getResponses();
if (!CollectionUtils.isEmpty(responses)) {
// 只取第一个成功响应
Response response = responses.get(0);
// 一般来说对于一个项目都是有统一返回类的,所以响应参数基本都是取jsonSchema字段里引用的id,这个id对应SchemaCollection中item的id
JsonSchema jsonSchema = response.getJsonSchema();
String ref = jsonSchema.getRef();
if (ref != null) {
getSchemaResponseData(ref, responseDataList, 0);
}
}
// 创建表格
createTable(document, responseDataList.stream().map(ResponseData::list).collect(Collectors.toList()));
for (ResponseData responseData : responseDataList) {
// todo
System.out.printf("| %s | %s | %s |\n", responseData.getName(), responseData.getType(),
responseData.getDescription());
}
createNormalContent(document, "响应示例", true);
// todo
System.out.println("响应示例");
List<ResponseExample> responseExamples = api.getResponseExamples();
if (!CollectionUtils.isEmpty(responseExamples)) {
// 只取第一个成功响应示例
ResponseExample responseExample = responseExamples.get(0);
responseExample.getData();
createCodeContent(document, responseExample.getData());
// todo
System.out.println(responseExample.getData());
}
}
}
// 将文档保存到文件
try (FileOutputStream out = new FileOutputStream(outputFileName)) {
document.write(out);
} catch (IOException e) {
e.printStackTrace();
}
// 关闭文档
document.close();
System.out.println("Word文档创建成功!");
}
/**
* 创建文档内容
*
* @param document document
* @param content 内容
* @param bold 是否加粗
*/
private static void createNormalContent(XWPFDocument document, String content, boolean bold) {
XWPFParagraph pathContent = document.createParagraph();
XWPFRun pathRunContent = pathContent.createRun();
pathRunContent.setText(content);
pathRunContent.setBold(bold);
pathRunContent.setFontFamily("宋体");
}
/**
* 创建代码块
*
* @param document document
* @param content 代码块内容
*/
private static void createCodeContent(XWPFDocument document, String content) {
// 按行拆分内容,因为换行符\n在word里显示是一个空格,会导致格式混乱
String[] lines = content.split("\n");
for (String line : lines) {
// 为每一行创建一个段落
XWPFParagraph paragraph = document.createParagraph();
XWPFRun run = paragraph.createRun();
run.setText(line); // 添加文本
run.setFontFamily("Consolas"); // 设置等宽字体
run.setFontSize(10);
// 设置代码块背景颜色
CTShd shd = paragraph.getCTP().addNewPPr().addNewShd();
shd.setFill("F0F0F0"); // 设置背景颜色为浅灰色
shd.setVal(STShd.CLEAR);
}
}
/**
* 创建表格
*
* @param document document
* @param list 表格内容
*/
private static void createTable(XWPFDocument document, List<List<String>> list) {
int row = list.size();
int col = list.get(0).size();
// 只有一行说明仅有head,不创建表格,展示内容为无
if (row == 1) {
createNormalContent(document, "无", false);
return;
}
// 创建表格
XWPFTable table = document.createTable(row, col);
table.setWidth("100%");
for (int i = 0; i < row; i++) {
for (int j = 0; j < col; j++) {
table.getRow(i).getCell(j).setText(list.get(i).get(j));
}
}
}
/**
* 获取请求表格数据
*
* @param schemaId 引用的schemaId
* @return
*/
private static List<RequestData> getSchemaRequestData(String schemaId) {
List<RequestData> list = new ArrayList<>();
if (schemaId == null) {
return list;
}
Item item = getSchemaItemById(schemaId);
Schema schema = item.getSchema();
JsonSchema jsonSchema = schema.getJsonSchema();
List<String> required = new ArrayList<>();
HashSet<String> requiredSet = new HashSet<>();
if (!CollectionUtils.isEmpty(jsonSchema.getRequired())) {
requiredSet.addAll(jsonSchema.getRequired());
}
LinkedHashMap<String, JSONObject> properties = jsonSchema.getProperties();
for (Map.Entry<String, JSONObject> entry : properties.entrySet()) {
String key = entry.getKey();
JSONObject value = entry.getValue();
RequestData requestData = new RequestData();
// 获取type字段,如果没有该字段说明是引用,并且type字段可能为数组也可能为字符串
String typeString = value.getString("type");
if (typeString != null) {
String type;
if (typeString.startsWith("[")) {
JSONArray typeArray = value.getJSONArray("type");
// typeArray的值如果是数组,如: ["string", "null"],只取第一个
type = (String)typeArray.get(0);
} else {
type = typeString;
}
requestData.setName(key);
requestData.setPosition("body");
requestData.setType(type);
requestData.setDescription(value.getString("description"));
if (requiredSet.contains(key)) {
requestData.setRequired("是");
} else {
requestData.setRequired("否");
}
list.add(requestData);
}
}
return list;
}
/**
* 获取响应表格数据,涉及到多层引用,使用递归处理
*
* @param schemaId 引用的schemaId
* @param list 响应数据
* @param level 递归层级
*/
private static void getSchemaResponseData(String schemaId, List<ResponseData> list, int level) {
if (schemaId == null) {
return;
}
// 子字段添加前缀
StringBuilder builder = new StringBuilder();
for (int i = 0; i < level; i++) {
builder.append(" ");
}
if (level != 0) {
builder.append("└");
}
String namePrefix = builder.toString();
Item item = getSchemaItemById(schemaId);
Schema schema = item.getSchema();
JsonSchema jsonSchema = schema.getJsonSchema();
LinkedHashMap<String, JSONObject> properties = jsonSchema.getProperties();
for (Map.Entry<String, JSONObject> entry : properties.entrySet()) {
String key = entry.getKey();
JSONObject value = entry.getValue();
ResponseData responseData = new ResponseData();
// 获取type字段,如果没有该字段说明是引用,并且type字段可能为数组也可能为字符串
String typeString = value.getString("type");
if (typeString != null) {
String type;
if (typeString.startsWith("[")) {
JSONArray typeArray = value.getJSONArray("type");
// typeArray的值如果是数组,如: ["string", "null"],只取第一个
type = (String)typeArray.get(0);
} else {
type = typeString;
}
responseData.setName(namePrefix + key);
responseData.setType(type);
responseData.setDescription(value.getString("description"));
list.add(responseData);
// 数组类型为引用需要继续递归
if ("array".equals(type)) {
JSONObject items = value.getJSONObject("items");
String ref = items.getString("$ref");
getSchemaResponseData(ref, list, level + 1);
}
} else {
String ref = value.getString("$ref");
String description = value.getString("description");
responseData.setName(namePrefix + key);
responseData.setType("object");
responseData.setDescription(description);
list.add(responseData);
// 引用类型需要继续递归
getSchemaResponseData(ref, list, level + 1);
}
}
}
/**
* 根据schemaId获取Item
*
* @param schemaId 引用的schemaId
* @return
*/
private static Item getSchemaItemById(String schemaId) {
for (Item schemaItem : schemaItems) {
if (schemaItem.getId().equals(schemaId)) {
return schemaItem;
}
}
throw new RuntimeException("找不到id为" + schemaId + "的schema,请检查源文件数据");
}
/**
* 设置段落的大纲级别
*
* @param paragraph 段落
* @param level 大纲级别(1为一级标题,2为二级标题,以此类推)
*/
private static void setHeadingLevel(XWPFParagraph paragraph, int level) {
CTP ctp = paragraph.getCTP();
CTPPr ppr = ctp.isSetPPr() ? ctp.getPPr() : ctp.addNewPPr();
ppr.addNewOutlineLvl().setVal(BigInteger.valueOf(level - 1));
}
}
2.4 文档示例
导出的文档示例如下
3.结尾
我目前也只是根据有的几个项目的json文件分析的,可能有些类型没考虑到,比如ai对话常用的流式输出,还有的项目post请求却使用form表单传输字段数据等;文档的样式也可以自行修改代码poi操作更改样式或者用wps、word工具打开修改样式;文档的格式也可以根据代码自行调整
最后别忘了检查一下文档,以免错误