基于swagger插件的方式推送接口文档至torna

一、前言

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;
    }

}

相关推荐

最近更新

  1. docker php8.1+nginx base 镜像 dockerfile 配置

    2024-07-12 12:42:02       66 阅读
  2. Could not load dynamic library ‘cudart64_100.dll‘

    2024-07-12 12:42:02       70 阅读
  3. 在Django里面运行非项目文件

    2024-07-12 12:42:02       57 阅读
  4. Python语言-面向对象

    2024-07-12 12:42:02       68 阅读

热门阅读

  1. Backend - C# 基础知识

    2024-07-12 12:42:02       18 阅读
  2. 网络编程:数据包的封装----libnet库

    2024-07-12 12:42:02       26 阅读
  3. 使用定时器消除抖动

    2024-07-12 12:42:02       21 阅读
  4. Spring框架(三)——AOP--实现方式

    2024-07-12 12:42:02       18 阅读
  5. vue-grid-layout详解

    2024-07-12 12:42:02       23 阅读
  6. linux,docker,k8s常见命令

    2024-07-12 12:42:02       21 阅读
  7. TensorFlow 和 PyTorch 显示模型参数方法详解

    2024-07-12 12:42:02       20 阅读
  8. 【go学习合集】进阶数据类型2 -------->切片

    2024-07-12 12:42:02       20 阅读
  9. 扩展欧几里得c++

    2024-07-12 12:42:02       24 阅读
  10. elementui的table的@selection-change阻止事件改变

    2024-07-12 12:42:02       22 阅读