目录
- 一、前言
- 二、登录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@NoArgsConstructorprivate static class TmlyDocParamWrapper<T> {/*** 是否数组*/private Byte isArray;/*** 数组元素类型*/private String arrayType;private List<T> data;}}