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

}
  • 13
    点赞
  • 15
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
Spring Boot是一个用于创建独立的、基于Spring的应程序的框架。它简化了Spring应用程序的配置和部署过程,并提供了一套强大的开发工具和约定,使开发人员能够更快地构建高效的应用程序。 MyBatis是一个开源的持久层框架,它可以将SQL语句和数据库操作映射到Java对象中,使得开发人员可以通过简单的配置来实现数据访问层的操作。在Spring Boot中集成MyBatis可以通过添加相应的依赖和配置来实现。 PageHelper是一个用于实现分页功能的MyBatis插件。它可以通过拦截SQL语句并自动添加分页查询的相关信息,从而简化了分页查询的操作。在Spring Boot中集成PageHelper可以通过添加相应的依赖和配置来实现。 Swagger是一个用于生成、描述、调用和可视化RESTful风格的Web服务的工具集。它可以根据代码注解自动生成接口文档,并提供了一个用户友好的界面来测试和调试接口。在Spring Boot中集成Swagger可以通过添加相应的依赖和配置来实现。 下面是集成Spring Boot、MyBatis、PageHelper和Swagger的步骤: 1. 在pom.xml文件中添加Spring Boot、MyBatis、PageHelper和Swagger的依赖: ```xml <dependencies> <!-- Spring Boot --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <!-- MyBatis --> <dependency> <groupId>org.mybatis.spring.boot</groupId> <artifactId>mybatis-spring-boot-starter</artifactId> </dependency> <!-- PageHelper --> <dependency> <groupId>com.github.pagehelper</groupId> <artifactId>pagehelper-spring-boot-starter</artifactId> </dependency> <!-- Swagger --> <dependency> <groupId>io.springfox</groupId> <artifactId>springfox-swagger2</artifactId> <version>2.9.2</version> </dependency> <dependency> <groupId>io.springfox</groupId> <artifactId>springfox-swagger-ui</artifactId> <version>2.9.2</version> </dependency> </dependencies> ``` 2. 在application.properties或application.yml文件中配置数据库连接和MyBatis的相关配置: ```properties # 数据库连接配置 spring.datasource.url=jdbc:mysql://localhost:3306/mydatabase spring.datasource.username=root spring.datasource.password=123456 spring.datasource.driver-class-name=com.mysql.jdbc.Driver # MyBatis配置 mybatis.mapper-locations=classpath:mapper/*.xml mybatis.type-aliases-package=com.example.model # PageHelper配置 pagehelper.helper-dialect=mysql pagehelper.reasonable=true pagehelper.support-methods-arguments=true # Swagger配置 swagger.enabled=true ``` 3. 创建MyBatis的Mapper接口和对应的XML文件,定义数据库操作的SQL语句和映射关系。 4. 创建Spring Boot的Controller类,定义接口的请求路径和处理方法。 5. 在启动类上添加@EnableSwagger2注解,启用Swagger。 ```java @SpringBootApplication @EnableSwagger2 public class Application { public static void main(String[] args) { SpringApplication.run(Application.class, args); } } ``` 6. 启动应用程序,访问http://localhost:8080/swagger-ui.html可以查看生成的接口文档,并进行接口的测试和调试。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

原来是小雨啊

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值