目录
- 一、前言
- 二、登录torna
- 三、创建/选择空间
- 四、创建/选择项目
- 五、创建/选择应用
- 六、获取应用的token
- 七、服务推送
- 7.1 引入maven依赖
- 7.2 test下面按照如下方式新建文件
一、前言
Torna作为一款企业级文档管理系统,支持了很多种接口文档的推送方式。官方比较推荐的一种方式,就是使用smart-doc插件推送,该插件需要完善接口代码中的javadoc,相对来说,代码规范性要求较高。
使用方式如下:
接口文档管理解决方案调研及Torna+Smart-doc的使用
这里,由于某些老项目,javadoc并不规范,而且某些接口连swagger注解都没有。所以,在这里提供了一种基于swagger插件的方式,利用main方法推送文档至torna的方式。
二、登录torna
三、创建/选择空间
这里空间可以配置为某个具体的环境,例如:开发环境、测试环境。
四、创建/选择项目
五、创建/选择应用
六、获取应用的token
七、服务推送
说明:
由于默认的swagger插件只支持扫描带有@Api的Controller以及只带有@ApiOperation的接口方法,这里兼容了无swagger注解的接口推送。
7.1 引入maven依赖
<dependency>
<groupId>cn.torna</groupId>
<artifactId>swagger-plugin</artifactId>
<version>1.2.14</version>
<scope>test</scope>
</dependency>
7.2 test下面按照如下方式新建文件
- torna.json
{
// 开启推送
"enable": true,
// 扫描package,多个用;隔开
"basePackage": "com.product",
// 推送URL,IP端口对应Torna服务器
"url": "http://test.xxx.com:7700/torna/api",
// 模块token,复制应用的token
"token": "xxxxxxxxxxxxxxxxxxxxxxxxxx",
"debugEnv": "test,https://test.xxx.com/product",
// 推送人
"author": "author",
// 打开调试:true/false
"debug": true,
// 是否替换文档,true:替换,false:不替换(追加)。默认:true
"isReplace": false
}
- DocPushTest.java
import cn.torna.swaggerplugin.TmlySwaggerPlugin;
public class DocPushTest {
public static void main(String[] args) {
TmlySwaggerPlugin.pushDoc();
}
}
- TmlySwaggerPlugin.java
package cn.torna.swaggerplugin;
import cn.torna.swaggerplugin.bean.TornaConfig;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import org.springframework.core.io.ClassPathResource;
import org.springframework.util.StreamUtils;
import java.io.IOException;
import java.io.InputStream;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import java.nio.charset.StandardCharsets;
public class TmlySwaggerPlugin {
/**
* 推送文档,前提:把<code>torna.json</code>文件复制到resources下
*/
public static void pushDoc() {
pushDoc("torna.json");
}
/**
* 推送swagger文档
*
* @param configFile 配置文件
*/
public static void pushDoc(String configFile) {
pushDoc(configFile, TmlySwaggerPluginService.class);
}
public static void pushDoc(String configFile, Class<? extends SwaggerPluginService> swaggerPluginServiceClazz) {
ClassPathResource classPathResource = new ClassPathResource(configFile);
if (!classPathResource.exists()) {
throw new IllegalArgumentException("找不到文件:" + configFile + ",请确保resources下有torna.json");
}
System.out.println("加载Torna配置文件:" + configFile);
try {
InputStream inputStream = classPathResource.getInputStream();
String json = StreamUtils.copyToString(inputStream, StandardCharsets.UTF_8);
JSONObject jsonObject = JSON.parseObject(json);
TornaConfig tornaConfig = jsonObject.toJavaObject(TornaConfig.class);
Constructor<? extends SwaggerPluginService> constructor = swaggerPluginServiceClazz.getConstructor(TornaConfig.class);
SwaggerPluginService swaggerPluginService = constructor.newInstance(tornaConfig);
swaggerPluginService.pushDoc();
} catch (IOException | InstantiationException | IllegalAccessException | NoSuchMethodException |
InvocationTargetException e) {
e.printStackTrace();
throw new RuntimeException("推送文档出错", e);
}
}
}
- TmlySwaggerPluginService.java
package cn.torna.swaggerplugin;
import cn.torna.sdk.param.DocItem;
import cn.torna.swaggerplugin.bean.Booleans;
import cn.torna.swaggerplugin.bean.ControllerInfo;
import cn.torna.swaggerplugin.bean.PluginConstants;
import cn.torna.swaggerplugin.bean.TornaConfig;
import cn.torna.swaggerplugin.builder.MvcRequestInfoBuilder;
import cn.torna.swaggerplugin.builder.RequestInfoBuilder;
import cn.torna.swaggerplugin.exception.HiddenException;
import cn.torna.swaggerplugin.exception.IgnoreException;
import cn.torna.swaggerplugin.util.ClassUtil;
import cn.torna.swaggerplugin.util.PluginUtil;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.springframework.beans.BeanUtils;
import org.springframework.core.annotation.AnnotatedElementUtils;
import org.springframework.core.annotation.AnnotationUtils;
import org.springframework.http.MediaType;
import org.springframework.util.CollectionUtils;
import org.springframework.util.ReflectionUtils;
import org.springframework.util.StringUtils;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RestController;
import springfox.documentation.annotations.ApiIgnore;
import java.lang.reflect.Method;
import java.lang.reflect.Parameter;
import java.util.*;
import java.util.stream.Collectors;
public class TmlySwaggerPluginService extends SwaggerPluginService {
private final TornaConfig tornaConfig;
public TmlySwaggerPluginService(TornaConfig tornaConfig) {
super(tornaConfig);
this.tornaConfig = tornaConfig;
}
public void pushDoc() {
if (!tornaConfig.getEnable()) {
return;
}
String basePackage = tornaConfig.getBasePackage();
if (StringUtils.isEmpty(basePackage)) {
throw new IllegalArgumentException("basePackage can not empty.");
}
this.doPush();
this.pushCode();
}
protected void doPush() {
String packageConfig = tornaConfig.getBasePackage();
String[] pkgs = packageConfig.split(";");
Set<Class<?>> classes = new HashSet<>();
for (String basePackage : pkgs) {
// Set<Class<?>> clazzs = ClassUtil.getClasses(basePackage, Api.class);
// 把带有RestController的控制层抽取出来
Set<Class<?>> clazzs = ClassUtil.getClasses(basePackage, RestController.class);
classes.addAll(clazzs);
}
Map<ControllerInfo, List<DocItem>> controllerDocMap = new HashMap<>(32);
for (Class<?> clazz : classes) {
ControllerInfo controllerInfo;
try {
controllerInfo = buildControllerInfo(clazz);
} catch (HiddenException | IgnoreException e) {
System.out.println(e.getMessage());
continue;
}
List<DocItem> docItems = controllerDocMap.computeIfAbsent(controllerInfo, k -> new ArrayList<>());
ReflectionUtils.doWithMethods(clazz, method -> {
try {
DocItem apiInfo = this.buildDocItem(new MvcRequestInfoBuilder(method, tornaConfig));
docItems.add(apiInfo);
} catch (HiddenException | IgnoreException e) {
System.out.println(e.getMessage());
} catch (Exception e) {
System.out.printf("Create doc error, method:%s%n", method);
throw new RuntimeException(e.getMessage(), e);
}
}, this::match);
}
List<DocItem> docItems = mergeSameFolder(controllerDocMap);
this.push(docItems);
}
private ControllerInfo buildControllerInfo(Class<?> controllerClass) throws HiddenException, IgnoreException {
Api api = AnnotationUtils.findAnnotation(controllerClass, Api.class);
ApiIgnore apiIgnore = AnnotationUtils.findAnnotation(controllerClass, ApiIgnore.class);
if (api != null && api.hidden()) {
throw new HiddenException("Hidden doc(@Api.hidden=true):" + api.value());
}
if (apiIgnore != null) {
throw new IgnoreException("Ignore doc(@ApiIgnore):" + controllerClass.getName());
}
String name, description;
int position = 0;
if (api == null) {
name = controllerClass.getSimpleName();
description = "";
} else {
name = api.value();
if (StringUtils.isEmpty(name) && api.tags().length > 0) {
name = api.tags()[0];
}
description = api.description();
position = api.position();
}
ControllerInfo controllerInfo = new ControllerInfo();
controllerInfo.setName(name);
controllerInfo.setDescription(description);
controllerInfo.setPosition(position);
return controllerInfo;
}
/**
* 合并控制层文档
* 按照控制层类的顺序及名称(@Api为value,否则类的getSimpleName),合并为一个有序的文档数组
*
* @param controllerDocMap 控制层->文档集合
* @return
*/
private List<DocItem> mergeSameFolder(Map<ControllerInfo, List<DocItem>> controllerDocMap) {
// key:文件夹,value:文档
Map<String, List<DocItem>> folderDocMap = new HashMap<>();
controllerDocMap.forEach((key, value) -> {
List<DocItem> docItems = folderDocMap.computeIfAbsent(key.getName(), k -> new ArrayList<>());
docItems.addAll(value);
});
List<ControllerInfo> controllerInfoList = controllerDocMap.keySet()
.stream()
.sorted(Comparator.comparing(ControllerInfo::getPosition))
.collect(Collectors.toList());
List<DocItem> folders = new ArrayList<>(controllerDocMap.size());
for (Map.Entry<String, List<DocItem>> entry : folderDocMap.entrySet()) {
String name = entry.getKey();
ControllerInfo info = controllerInfoList
.stream()
.filter(controllerInfo -> name.equals(controllerInfo.getName()))
.findFirst()
.orElse(null);
if (info == null) {
continue;
}
DocItem docItem = new DocItem();
docItem.setName(name);
docItem.setDefinition(info.getDescription());
docItem.setOrderIndex(info.getPosition());
docItem.setIsFolder(Booleans.TRUE);
List<DocItem> items = entry.getValue();
items.sort(Comparator.comparing(DocItem::getOrderIndex));
docItem.setItems(items);
folders.add(docItem);
}
return folders;
}
protected DocItem buildDocItem(RequestInfoBuilder requestInfoBuilder) throws HiddenException, IgnoreException {
Method method = requestInfoBuilder.getMethod();
ApiOperation apiOperation = method.getAnnotation(ApiOperation.class);
ApiIgnore apiIgnore = method.getAnnotation(ApiIgnore.class);
if (apiOperation != null && apiOperation.hidden()) {
throw new HiddenException("Hidden API(@ApiOperation.hidden=true):" + apiOperation.value());
}
if (apiIgnore != null) {
throw new IgnoreException("Ignore API(@ApiIgnore):" + apiOperation.value());
}
return this.doBuildDocItem(requestInfoBuilder);
}
/**
* 兼容方法名上@ApiOperation为空的情况
*
* @param requestInfoBuilder
* @return
*/
protected DocItem doBuildDocItem(RequestInfoBuilder requestInfoBuilder) {
ApiOperation apiOperation = requestInfoBuilder.getApiOperation();
Method method = requestInfoBuilder.getMethod();
DocItem docItem = new DocItem();
String httpMethod = getHttpMethod(requestInfoBuilder);
docItem.setAuthor(apiOperation != null ? buildAuthor(apiOperation) : "");
docItem.setName(apiOperation != null ? apiOperation.value() : method.getName());
docItem.setDescription(apiOperation != null ? apiOperation.notes() : "");
docItem.setOrderIndex(apiOperation != null ? buildOrder(apiOperation, method) : 0);
docItem.setUrl(requestInfoBuilder.buildUrl());
String contentType = buildContentType(requestInfoBuilder);
docItem.setHttpMethod(httpMethod);
docItem.setContentType(contentType);
docItem.setIsFolder(PluginConstants.FALSE);
docItem.setPathParams(buildPathParams(method));
docItem.setHeaderParams(buildHeaderParams(method));
docItem.setQueryParams(buildQueryParams(method, httpMethod));
TmlyDocParamWrapper reqWrapper = new TmlyDocParamWrapper();
BeanUtils.copyProperties(buildRequestParams(method, httpMethod), reqWrapper);
TmlyDocParamWrapper respWrapper = new TmlyDocParamWrapper();
BeanUtils.copyProperties(buildResponseParams(method), respWrapper);
docItem.setRequestParams(reqWrapper.getData());
docItem.setResponseParams(respWrapper.getData());
docItem.setIsRequestArray(reqWrapper.getIsArray());
docItem.setRequestArrayType(reqWrapper.getArrayType());
docItem.setIsResponseArray(respWrapper.getIsArray());
docItem.setResponseArrayType(respWrapper.getArrayType());
docItem.setErrorCodeParams(apiOperation != null ? buildErrorCodes(apiOperation) : new ArrayList<>(0));
return docItem;
}
private String getHttpMethod(RequestInfoBuilder requestInfoBuilder) {
ApiOperation apiOperation = requestInfoBuilder.getApiOperation();
Method method = requestInfoBuilder.getMethod();
if (apiOperation != null && StringUtils.hasText(apiOperation.httpMethod())) {
return apiOperation.httpMethod();
}
RequestMapping requestMapping = AnnotatedElementUtils.findMergedAnnotation(method, RequestMapping.class);
if (requestMapping != null) {
RequestMethod[] methods = requestMapping.method();
if (methods.length == 0) {
return this.tornaConfig.getMethodWhenMulti();
} else {
return methods[0].name();
}
}
return tornaConfig.getDefaultHttpMethod();
}
private String buildContentType(RequestInfoBuilder requestInfoBuilder) {
ApiOperation apiOperation = requestInfoBuilder.getApiOperation();
Method method = requestInfoBuilder.getMethod();
if (apiOperation != null && StringUtils.hasText(apiOperation.consumes())) {
return apiOperation.consumes();
}
String[] consumeArr = getConsumes(method);
if (consumeArr != null && consumeArr.length > 0) {
return consumeArr[0];
}
Parameter[] methodParameters = method.getParameters();
if (methodParameters.length == 0) {
return "";
}
for (Parameter methodParameter : methodParameters) {
RequestBody requestBody = methodParameter.getAnnotation(RequestBody.class);
if (requestBody != null) {
return MediaType.APPLICATION_JSON_VALUE;
}
if (PluginUtil.isFileParameter(methodParameter)) {
return MediaType.MULTIPART_FORM_DATA_VALUE;
}
}
return getTornaConfig().getGlobalContentType();
}
private String[] getConsumes(Method method) {
RequestMapping requestMapping = AnnotatedElementUtils.findMergedAnnotation(method, RequestMapping.class);
if (requestMapping != null) {
return requestMapping.consumes();
}
return null;
}
public boolean match(Method method) {
List<String> scanApis = this.tornaConfig.getScanApis();
if (CollectionUtils.isEmpty(scanApis)) {
// return method.getAnnotation(ApiOperation.class) != null;
return AnnotatedElementUtils.hasAnnotation(method, RequestMapping.class);
}
for (String scanApi : scanApis) {
String methodName = method.toString();
if (methodName.contains(scanApi)) {
return true;
}
}
return false;
}
@Data
@AllArgsConstructor
@NoArgsConstructor
private static class TmlyDocParamWrapper<T> {
/**
* 是否数组
*/
private Byte isArray;
/**
* 数组元素类型
*/
private String arrayType;
private List<T> data;
}
}