This commit is contained in:
六如
2024-10-31 09:36:48 +08:00
parent 08f708d8a4
commit 0e29510d0e
58 changed files with 3569 additions and 16 deletions

View File

@@ -73,6 +73,11 @@ public class ApiInfo {
*/ */
private Integer isNeedToken; private Integer isNeedToken;
/**
* 是否有公共响应参数
*/
private Integer hasCommonResponse;
/** /**
* 注册来源1-系统注册,2-手动注册 * 注册来源1-系统注册,2-手动注册
*/ */

View File

@@ -128,6 +128,23 @@ export const tableColumns: PlusColumn[] = [
} }
] ]
}, },
{
label: "公共参数",
prop: "hasCommonResponse",
width: 100,
valueType: "select",
options: [
{
label: "无",
value: 0,
color: "red"
},
{
label: "有",
value: 1
}
]
},
{ {
label: "注册来源", label: "注册来源",
prop: "regSource", prop: "regSource",

View File

@@ -69,12 +69,6 @@
<scope>provided</scope> <scope>provided</scope>
</dependency> </dependency>
<dependency>
<groupId>io.swagger</groupId>
<artifactId>swagger-annotations</artifactId>
<version>1.6.14</version>
</dependency>
<dependency> <dependency>
<groupId>org.projectlombok</groupId> <groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId> <artifactId>lombok</artifactId>

View File

@@ -1,4 +1,4 @@
package com.gitee.sop.payment.open.impl; package com.gitee.sop.payment.impl;
import com.gitee.sop.payment.open.OpenPayment; import com.gitee.sop.payment.open.OpenPayment;
import com.gitee.sop.payment.open.req.AlipayTradeWapPayRequest; import com.gitee.sop.payment.open.req.AlipayTradeWapPayRequest;

View File

@@ -3,6 +3,7 @@ package com.gitee.sop.payment.open;
import com.gitee.sop.payment.open.req.AlipayTradeWapPayRequest; import com.gitee.sop.payment.open.req.AlipayTradeWapPayRequest;
import com.gitee.sop.payment.open.resp.AlipayTradeWapPayResponse; import com.gitee.sop.payment.open.resp.AlipayTradeWapPayResponse;
import com.gitee.sop.support.annotation.Open; import com.gitee.sop.support.annotation.Open;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation; import io.swagger.annotations.ApiOperation;
/** /**
@@ -10,6 +11,7 @@ import io.swagger.annotations.ApiOperation;
* *
* @author 六如 * @author 六如
*/ */
@Api("支付接口")
public interface OpenPayment { public interface OpenPayment {
@ApiOperation( @ApiOperation(

View File

@@ -0,0 +1,27 @@
{
// 开启推送
"enable": true,
// 扫描package多个用;隔开
"basePackage": "com.gitee.sop.payment.open",
// 推送URLIP端口对应Torna服务器
"url": "http://localhost:7700/api",
// 模块token
"token": "34ff76952462413982d21219cf099d46",
// 推送人
"author": "Jim",
// 打开调试:true/false
"debug": true,
// 是否替换文档true替换false不替换追加。默认true
"isReplace": true,
// 第三方jar中的class配置
"jarClass": {
"com.baomidou.mybatisplus.extension.plugins.pagination.Page": {
"records": { "value": "查询数据列表", "example": "" },
"total": { "value": "总数", "example": "100" },
"size": { "value": "页数", "example": "10" },
"current": { "value": "当前页", "example": "1" },
"countId": { "hidden": true },
"orders": { "hidden": true }
}
}
}

View File

@@ -1 +0,0 @@
abc,你好~!@#

View File

@@ -0,0 +1,13 @@
package com.gitee.sop.payment;
import cn.torna.swaggerplugin.SwaggerPlugin;
/**
* 推送swagger文档
* @author thc
*/
public class DocPushTest {
public static void main(String[] args) {
SwaggerPlugin.pushDoc();
}
}

View File

@@ -63,6 +63,11 @@ public class ApiInfoDTO implements Serializable {
*/ */
private Integer isNeedToken; private Integer isNeedToken;
/**
* 是否有公共响应参数
*/
private Integer hasCommonResponse;
private Integer status; private Integer status;
public String buildApiNameVersion() { public String buildApiNameVersion() {

View File

@@ -71,6 +71,11 @@ public class ApiInfo {
*/ */
private Integer isNeedToken; private Integer isNeedToken;
/**
* 是否有公共响应参数
*/
private Integer hasCommonResponse;
/** /**
* 注册来源1-系统注册,2-手动注册 * 注册来源1-系统注册,2-手动注册
*/ */

View File

@@ -0,0 +1,22 @@
package com.gitee.sop.gateway.response;
/**
* 没有公共返回结果
*
* @author 六如
*/
public class NoCommonResponse extends BaseResponse {
public static NoCommonResponse success(Object data) {
NoCommonResponse apiResponse = new NoCommonResponse();
apiResponse.setCode(SUCCESS_CODE);
apiResponse.setMsg(SUCCESS_MSG);
apiResponse.setData(data);
return apiResponse;
}
@Override
public boolean needWrap() {
return false;
}
}

View File

@@ -1,5 +1,8 @@
package com.gitee.sop.gateway.response; package com.gitee.sop.gateway.response;
import com.alibaba.fastjson2.annotation.JSONField;
import com.fasterxml.jackson.annotation.JsonIgnore;
/** /**
* @author 六如 * @author 六如
*/ */
@@ -9,4 +12,9 @@ public interface Response {
Object getData(); Object getData();
@JSONField(serialize = false)
@JsonIgnore
default boolean needWrap() {
return true;
}
} }

View File

@@ -110,8 +110,12 @@ public class ParamExecutorImpl implements ParamExecutor<HttpServletRequest, Http
FileData fileData = (FileData) data; FileData fileData = (FileData) data;
ResponseUtil.writerFile(fileData, response); ResponseUtil.writerFile(fileData, response);
} else { } else {
// 此处还可以判断charset,返回xml格式 Object responseData = apiResponse;
ResponseUtil.writerText(apiRequestContext, apiResponse, response); // 不需要公共参数
if (!apiResponse.needWrap()) {
responseData = data;
}
ResponseUtil.writerText(apiRequestContext, responseData, response);
} }
} }
} }

View File

@@ -1,10 +1,12 @@
package com.gitee.sop.gateway.service; package com.gitee.sop.gateway.service;
import com.gitee.sop.gateway.common.ApiInfoDTO; import com.gitee.sop.gateway.common.ApiInfoDTO;
import com.gitee.sop.gateway.common.enums.YesOrNoEnum;
import com.gitee.sop.gateway.config.ApiConfig; import com.gitee.sop.gateway.config.ApiConfig;
import com.gitee.sop.gateway.request.ApiRequestContext; import com.gitee.sop.gateway.request.ApiRequestContext;
import com.gitee.sop.gateway.response.ApiResponse; import com.gitee.sop.gateway.response.ApiResponse;
import com.gitee.sop.gateway.response.ApiResponseLower; import com.gitee.sop.gateway.response.ApiResponseLower;
import com.gitee.sop.gateway.response.NoCommonResponse;
import com.gitee.sop.gateway.response.Response; import com.gitee.sop.gateway.response.Response;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
@@ -22,6 +24,11 @@ public class ResultWrapperImpl implements ResultWrapper {
@Override @Override
public Response wrap(ApiRequestContext context, ApiInfoDTO apiInfoDTO, Object result) { public Response wrap(ApiRequestContext context, ApiInfoDTO apiInfoDTO, Object result) {
Integer hasCommonResponse = apiInfoDTO.getHasCommonResponse();
// 不需要公共返回参数
if (YesOrNoEnum.of(hasCommonResponse) == YesOrNoEnum.NO) {
return NoCommonResponse.success(result);
}
if (Objects.equals(apiConfig.getFieldLowercase(), true)) { if (Objects.equals(apiConfig.getFieldLowercase(), true)) {
return ApiResponseLower.success(result); return ApiResponseLower.success(result);
} else { } else {

View File

@@ -1,7 +1,5 @@
package com.gitee.sop.gateway.util; package com.gitee.sop.gateway.util;
import com.alibaba.fastjson2.JSON;
import com.fasterxml.jackson.dataformat.xml.XmlMapper;
import com.gitee.sop.gateway.request.ApiRequest; import com.gitee.sop.gateway.request.ApiRequest;
import com.gitee.sop.gateway.request.ApiRequestContext; import com.gitee.sop.gateway.request.ApiRequestContext;
import com.gitee.sop.gateway.request.RequestFormatEnum; import com.gitee.sop.gateway.request.RequestFormatEnum;
@@ -17,7 +15,6 @@ import java.io.InputStream;
* @author 六如 * @author 六如
*/ */
public class ResponseUtil { public class ResponseUtil {
private static final XmlMapper XML_MAPPER = new XmlMapper();
public static void writerFile(FileData fileData, HttpServletResponse response) throws IOException { public static void writerFile(FileData fileData, HttpServletResponse response) throws IOException {
InputStream inputStream = fileData.getInputStream(); InputStream inputStream = fileData.getInputStream();
@@ -37,11 +34,11 @@ public class ResponseUtil {
String format = apiRequest.getFormat(); String format = apiRequest.getFormat();
if (RequestFormatEnum.of(format) == RequestFormatEnum.XML) { if (RequestFormatEnum.of(format) == RequestFormatEnum.XML) {
response.setContentType(MediaType.APPLICATION_XML_VALUE); response.setContentType(MediaType.APPLICATION_XML_VALUE);
String xml = XML_MAPPER.writeValueAsString(apiResponse); String xml = XmlUtil.toXml(apiResponse);
response.getWriter().write(xml); response.getWriter().write(xml);
} else { } else {
response.setContentType(MediaType.APPLICATION_JSON_VALUE); response.setContentType(MediaType.APPLICATION_JSON_VALUE);
String json = JSON.toJSONString(apiResponse); String json = JsonUtil.toJSONString(apiResponse);
response.getWriter().write(json); response.getWriter().write(json);
} }
} }

View File

@@ -0,0 +1,18 @@
package com.gitee.sop.gateway.util;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.dataformat.xml.XmlMapper;
/**
* @author 六如
*/
public class XmlUtil {
private static final XmlMapper XML_MAPPER = new XmlMapper();
public static String toXml(Object object) throws JsonProcessingException {
return XML_MAPPER.writeValueAsString(object);
}
}

View File

@@ -11,5 +11,6 @@
<modules> <modules>
<module>sop-service-support</module> <module>sop-service-support</module>
<module>sop-spring-boot-starter</module> <module>sop-spring-boot-starter</module>
<module>sop-doc-plugin</module>
</modules> </modules>
</project> </project>

View File

@@ -0,0 +1,35 @@
# 更新日志
- 1.2.27: 新增`excludePackage`配置,用来排除对应的package,多个用;隔开
- 1.2.26: 支持Controller泛型接口
- 1.2.25支持Java17
- 1.2.24get参数使用`@ApiParam`name属性不生效问题
- 1.2.23:修复嵌套数组显示问题(`List<List<xx>>`
- 1.2.22:修复嵌套数组不显示问题(`List<List<xx>`
- 1.2.21:修复参数错乱问题 https://gitee.com/durcframework/torna/issues/I79TZ2
- 1.2.20:修复参数字段排序问题
- 1.2.19捕获ClassNotFoundException允许正常push文档; https://gitee.com/durcframework/torna/pulls/44
- 1.2.18:修复添加了@DecimalMax注解报NPE问题
- 1.2.17:优化泛型参数解析
- 1.2.16:添加插件特性,支持优先使用@Api.tags属性
- 1.2.15支持maxLength需要配合JSR-303注解`@Length(max = 55)`, `@Max(55)`
- 1.2.14:修复泛型参数解析错误问题。`List<? extends TreeVO> children`
- 1.2.13优化dubbo推送
- 1.2.12:优化枚举展示
- 1.2.11修复推送显示request类修复第三方JSONOject显示对象内容问题 https://gitee.com/durcframework/torna/issues/I4JNSX
- 1.2.10:可推送错误码
- 1.2.9可推送指定接口修复required显示问题
- 1.2.8升级SDK
- 1.2.7优化第三方jar文档展示
- 1.2.6:修复返回结果填通配符问题(`Result<?>`
- 1.2.5修复basePackage无效问题
- 1.2.4修复泛型嵌套问题优化Path参数显示
- 1.2.3修复ResponseEntity无泛型参数报NPE问题支持枚举参数
- 1.2.2:修复@RequestMapping注解没value属性值会报错 https://gitee.com/durcframework/torna/issues/I49DC0
- 1.2.1优化org.springframework.http.ResponseEntity的swagger解析逻辑及swagger中name字段优化 https://gitee.com/durcframework/torna/pulls/15
- 1.2.0优化推送支持dubbo
- 1.1.2:解决循环依赖问题
- 1.1.1:支持多环境配置
- 1.1.0:优化泛型参数显示,支持定义第三方类文档信息
- 1.0.7:修复`Result<List<T>>`不显示List问题
- 1.0.6:修复泛型参数不显示问题

View File

@@ -0,0 +1,181 @@
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.gitee.sop</groupId>
<artifactId>sop-doc-plugin</artifactId>
<version>5.0.0-SNAPSHOT</version>
<packaging>jar</packaging>
<name>swagger-plugin</name>
<description>Swagger plugin for Torna</description>
<url>https://gitee.com/durcframework/torna</url>
<licenses>
<license>
<name>MIT</name>
<url>https://mit-license.org/</url>
</license>
</licenses>
<developers>
<developer>
<name>tanghc</name>
<email>thc8719@163.com</email>
<organization>gitee</organization>
<organizationUrl>https://gitee.com/durcframework/torna</organizationUrl>
</developer>
</developers>
<scm>
<connection>scm:git:https://gitee.com/durcframework/torna.git</connection>
<developerConnection>scm:git:https://gitee.com/durcframework/torna.git</developerConnection>
<url>https://gitee.com/durcframework/torna.git</url>
<tag>HEAD</tag>
</scm>
<properties>
<!-- Generic properties -->
<java.version>1.8</java.version>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
</properties>
<dependencies>
<dependency>
<groupId>com.gitee.sop</groupId>
<artifactId>sop-service-support</artifactId>
<version>5.0.0-SNAPSHOT</version>
</dependency>
<dependency>
<groupId>cn.torna</groupId>
<artifactId>torna-sdk</artifactId>
<version>1.0.16</version>
<exclusions>
<exclusion>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.83</version>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.8</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.hibernate.validator</groupId>
<artifactId>hibernate-validator</artifactId>
<version>6.2.4.Final</version>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.34</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-beans</artifactId>
<version>5.2.9.RELEASE</version>
<scope>compile</scope>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>5.2.9.RELEASE</version>
<scope>compile</scope>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-webmvc</artifactId>
<version>5.2.9.RELEASE</version>
<scope>compile</scope>
<optional>true</optional>
</dependency>
<dependency>
<groupId>io.swagger</groupId>
<artifactId>swagger-annotations</artifactId>
<version>1.5.19</version>
<optional>true</optional>
</dependency>
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-core</artifactId>
<version>2.4.0</version>
<optional>true</optional>
</dependency>
</dependencies>
<build>
<plugins>
<!-- 打包时跳过测试 -->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<version>2.18.1</version>
<configuration>
<skipTests>true</skipTests>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<source>${java.version}</source>
<target>${java.version}</target>
<encoding>UTF-8</encoding>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-javadoc-plugin</artifactId>
<version>2.9</version>
<executions>
<execution>
<id>attach-javadocs</id>
<goals>
<goal>jar</goal>
</goals>
<configuration>
<additionalparam>-Xdoclint:none</additionalparam>
</configuration>
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-source-plugin</artifactId>
<version>2.2.1</version>
<executions>
<execution>
<id>attach-sources</id>
<goals>
<goal>jar-no-fork</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
<repositories>
<repository>
<id>maven_central</id>
<name>Maven Central</name>
<url>https://repo.maven.apache.org/maven2/</url>
</repository>
</repositories>
</project>

View File

@@ -0,0 +1,5 @@
# swagger插件
用于生成api文档
使用方式http://torna.cn/tutorial/swagger.html

View File

@@ -0,0 +1,48 @@
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.nio.charset.StandardCharsets;
/**
* 文档推送辅助类
* @author thc
*/
public class SwaggerPlugin {
/**
* 推送文档,前提:把<code>doc-plugin.json</code>文件复制到resources下
*/
public static void pushDoc() {
pushDoc("doc-plugin.json");
}
/**
* 推送swagger文档
* @param configFile 配置文件
*/
public static void pushDoc(String configFile) {
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);
new SwaggerPluginService(tornaConfig).pushDoc();
} catch (IOException e) {
e.printStackTrace();
throw new RuntimeException("推送文档出错", e);
}
}
}

View File

@@ -0,0 +1,103 @@
package cn.torna.swaggerplugin.bean;
import cn.torna.swaggerplugin.util.PluginUtil;
import io.swagger.annotations.ApiModelProperty;
import org.springframework.util.StringUtils;
import java.lang.reflect.Field;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Optional;
/**
* @author tanghc
*/
public class ApiModelPropertyWrapper {
private static final List<String> NOT_NULL_ANNOTATIONS = Arrays.asList(
"NotNull",
"NotBlank",
"NotEmpty"
);
private final Optional<ApiModelProperty> apiModelPropertyOptional;
private final Optional<Field> fieldOptional;
public ApiModelPropertyWrapper(ApiModelProperty apiModelPropertyOptional, Field field) {
this.apiModelPropertyOptional = Optional.ofNullable(apiModelPropertyOptional);
this.fieldOptional = Optional.ofNullable(field);
}
public String getName() {
String name = apiModelPropertyOptional.map(ApiModelProperty::name).orElse(null);
if (StringUtils.isEmpty(name)) {
name = fieldOptional.map(Field::getName).orElse("");
}
return name;
}
public String getValue() {
return apiModelPropertyOptional.map(ApiModelProperty::value).orElse("");
}
public String getDescription() {
String value = getValue();
String note = apiModelPropertyOptional.map(ApiModelProperty::notes).orElse("");
if (StringUtils.hasText(note)) {
value = value + "。Note" + note;
}
return value;
}
public boolean getRequired() {
if (fieldOptional.isPresent()) {
Field field = fieldOptional.get();
boolean hasAnyAnnotation = PluginUtil.hasAnyAnnotation(field, NOT_NULL_ANNOTATIONS);
// 如果有不为null相关的注解直接返回true必填
if (hasAnyAnnotation) {
return true;
}
}
return apiModelPropertyOptional.map(ApiModelProperty::required).orElse(false);
}
public String getExample() {
return apiModelPropertyOptional.map(ApiModelProperty::example).orElse("");
}
public int getPosition() {
return apiModelPropertyOptional.map(ApiModelProperty::position).orElse(0);
}
public String getDataType() {
String dataType = apiModelPropertyOptional.map(ApiModelProperty::dataType).orElse(null);
if (StringUtils.isEmpty(dataType)) {
dataType = fieldOptional.map(Field::getType).map(Class::getSimpleName).orElse("");
}
return dataType;
}
public String getMaxLength() {
if (!fieldOptional.isPresent()) {
return "-";
}
Field field = fieldOptional.get();
if (PluginUtil.hasAnyAnnotation(field, Collections.singletonList("Length"))) {
org.hibernate.validator.constraints.Length length = field.getAnnotation(org.hibernate.validator.constraints.Length.class);
return String.valueOf(length.max());
}
if (PluginUtil.hasAnyAnnotation(field, Collections.singletonList("Max"))) {
javax.validation.constraints.DecimalMax decimalMax = field.getAnnotation(javax.validation.constraints.DecimalMax.class);
if (decimalMax != null) {
return decimalMax.value();
}
javax.validation.constraints.Max max = field.getAnnotation(javax.validation.constraints.Max.class);
if (max != null) {
return String.valueOf(max.value());
}
}
return "-";
}
}

View File

@@ -0,0 +1,31 @@
package cn.torna.swaggerplugin.bean;
import lombok.Data;
/**
* @author tanghc
*/
@Data
public class ApiParamInfo {
String name;
String value;
String defaultValue;
String allowableValues;
boolean required;
String access;
boolean allowMultiple;
boolean hidden;
String example;
int position;
}

View File

@@ -0,0 +1,36 @@
package cn.torna.swaggerplugin.bean;
import io.swagger.annotations.ApiParam;
import java.util.Optional;
/**
* @author tanghc
*/
public class ApiParamWrapper {
private final Optional<ApiParam> apiParamOptional;
public ApiParamWrapper(ApiParam apiParam) {
this.apiParamOptional = Optional.ofNullable(apiParam);
}
public String getName() {
return apiParamOptional.map(ApiParam::name).orElse("");
}
public String getType() {
return apiParamOptional.map(ApiParam::type).orElse("");
}
public String getExample() {
return apiParamOptional.map(ApiParam::example).orElse("");
}
public String getDescription() {
return apiParamOptional.map(ApiParam::value).orElse("");
}
public boolean getRequired() {
return apiParamOptional.map(ApiParam::required).orElse(false);
}
}

View File

@@ -0,0 +1,17 @@
package cn.torna.swaggerplugin.bean;
public class Booleans {
public static final byte TRUE = 1;
public static final byte FALSE = 0;
public static boolean isTrue(Byte b) {
return b != null && b == TRUE;
}
public static byte toValue(Boolean b) {
if (b == null) {
return FALSE;
}
return b ? TRUE : FALSE;
}
}

View File

@@ -0,0 +1,27 @@
package cn.torna.swaggerplugin.bean;
import lombok.Getter;
import lombok.Setter;
import java.util.List;
/**
* @author tanghc
*/
@Getter
@Setter
public class CodeInfo {
/** 枚举名称 */
private String name;
/** 枚举说明 */
private String description;
/** 元素类型 */
private String itemType;
/** 枚举项 */
private List<CodeItem> items;
}

View File

@@ -0,0 +1,21 @@
package cn.torna.swaggerplugin.bean;
import lombok.Getter;
import lombok.Setter;
@Getter
@Setter
public class CodeItem {
/** 名称,字面值 */
private String name;
/** 类型 */
private String type;
/** 枚举值 */
private String value;
/** 枚举描述 */
private String description = "";
}

View File

@@ -0,0 +1,20 @@
package cn.torna.swaggerplugin.bean;
import lombok.Data;
import java.util.HashMap;
import java.util.Map;
/**
* @author tanghc
*/
@Data
public class ControllerInfo {
private String name;
private String description;
private int position;
private Class<?> controllerClass;
private Map<String, Class<?>> genericParamMap = new HashMap<>();
}

View File

@@ -0,0 +1,22 @@
package cn.torna.swaggerplugin.bean;
import lombok.Data;
/**
* @author tanghc
*/
@Data
public class DocParamInfo {
/** 示例值 */
private String example;
/** 描述 */
private String description;
/** 数据类型 */
private String type;
/** 必填 */
private Boolean required;
}

View File

@@ -0,0 +1,16 @@
package cn.torna.swaggerplugin.bean;
import lombok.AllArgsConstructor;
import lombok.Getter;
/**
* @author thc
*/
@AllArgsConstructor
@Getter
public enum ModeEnum {
MVC(0),
DUBBO(1)
;
private final int value;
}

View File

@@ -0,0 +1,9 @@
package cn.torna.swaggerplugin.bean;
/**
* @author tanghc
*/
public interface PluginConstants {
byte FALSE = 0;
byte TRUE = 1;
}

View File

@@ -0,0 +1,12 @@
package cn.torna.swaggerplugin.bean;
/**
* @author thc
*/
public enum PushFeature {
/**
* 优先使用@Api.tags属性
*/
USE_API_TAGS
}

View File

@@ -0,0 +1,108 @@
package cn.torna.swaggerplugin.bean;
import com.alibaba.fastjson.JSONObject;
import lombok.Data;
import java.util.Collections;
import java.util.List;
/**
* @author tanghc
*/
@Data
public class TornaConfig {
private String configFile;
/**
* 开启推送
*/
private Boolean enable = false;
/**
* 扫描package多个用";"隔开。不指定扫描全部
*/
private String basePackage;
/**
* 排除package多个用";"隔开
*/
private String excludePackage;
/**
* 指定扫描的API可以是某个controller也可以是某个接口方法
*/
private List<String> scanApis;
/**
* 推送URL
*/
private String url;
/**
* 模块token
*/
private String token;
/**
* 调试环境,格式:环境名称,调试路径,多个用"|"隔开
*/
private String debugEnv;
/**
* 推送人
*/
private String author;
/**
* 打开调试:true/false
*/
private Boolean debug = true;
/**
* 调试输出json格式化
*/
private Boolean debugPrintFormat = false;
/**
* 接口多个method只显示
*/
private String methodWhenMulti = "GET";
/**
* 具有body体的方法
*/
private String hasBodyMethods = "POST,PUT,DELETE";
/**
* 是否替换文档true替换false不替换追加。默认true
*/
private Boolean isReplace = true;
/**
* 0:springmvc, 1:dubbo
*/
private int mode = 1;
/**
* 全局的contentType
*/
private String globalContentType = "application/x-www-form-urlencoded";
/**
* 默认的http method
*/
private String defaultHttpMethod = "POST";
/**
* 第三方jar
*/
private JSONObject jarClass;
/**
* 定义错误码
*/
private List<CodeInfo> codes;
/**
* 只能是object类型对象
* 如com.alibaba.fastjson.JSONObject
*/
private List<String> objectClassList;
/**
* 特性
* @see PushFeature
*/
private List<String> features = Collections.emptyList();
}

View File

@@ -0,0 +1,434 @@
package cn.torna.swaggerplugin.builder;
import cn.torna.swaggerplugin.bean.ApiModelPropertyWrapper;
import cn.torna.swaggerplugin.bean.ApiParamInfo;
import cn.torna.swaggerplugin.bean.Booleans;
import cn.torna.swaggerplugin.bean.TornaConfig;
import cn.torna.swaggerplugin.util.ClassUtil;
import cn.torna.swaggerplugin.util.PluginUtil;
import com.alibaba.fastjson.JSONObject;
import io.swagger.annotations.ApiModelProperty;
import org.springframework.core.annotation.AnnotationUtils;
import org.springframework.util.ReflectionUtils;
import org.springframework.util.StringUtils;
import sun.reflect.generics.repository.ClassRepository;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.lang.reflect.Type;
import java.lang.reflect.TypeVariable;
import java.sql.Timestamp;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Date;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.concurrent.atomic.AtomicInteger;
/**
* 解析文档信息
* @author tanghc
*/
public class ApiDocBuilder {
// key: ClassA.getName() + fieldType.getName() + field.getName()
private Map<String, AtomicInteger> cycleFieldsMap = new HashMap<>();
private Map<String, Class<?>> genericParamMap = Collections.emptyMap();
private Set<String> hiddenColumns = Collections.emptySet();
private TornaConfig tornaConfig;
public ApiDocBuilder() {
}
public ApiDocBuilder(Map<String, Class<?>> genericParamMap, TornaConfig tornaConfig) {
this.tornaConfig = tornaConfig;
if (genericParamMap == null) {
genericParamMap = Collections.emptyMap();
}
this.genericParamMap = genericParamMap;
this.initHiddenColumns(tornaConfig);
}
private void initHiddenColumns(TornaConfig tornaConfig) {
JSONObject jarClass = tornaConfig.getJarClass();
if (jarClass == null) {
return;
}
hiddenColumns = new HashSet<>(8);
Set<String> classNames = jarClass.keySet();
for (String className : classNames) {
/*
"com.baomidou.mybatisplus.extension.plugins.pagination.Page": {
"countId": { "value": "xxx", "hidden": true, "required": false, "example": "" }
}
*/
JSONObject classConfig = jarClass.getJSONObject(className);
Set<String> fields = classConfig.keySet();
for (String field : fields) {
// { "value": "xxx", "hidden": true, "required": false, "example": "" }
JSONObject fieldConfig = classConfig.getJSONObject(field);
Boolean hidden = fieldConfig.getBoolean("hidden");
if (hidden != null && hidden) {
hiddenColumns.add(className + "." + field.trim());
}
}
}
}
private ApiParamInfo getApiParamInfo(Class<?> targetClass, String fieldName) {
JSONObject jarClass = tornaConfig.getJarClass();
if (jarClass == null) {
return null;
}
List<Class<?>> allClasses = ClassUtil.getAllClasses(targetClass);
for (Class<?> clazz : allClasses) {
String className = clazz.getName();
JSONObject classConfig = jarClass.getJSONObject(className);
if (classConfig == null) {
continue;
}
Set<String> fields = classConfig.keySet();
for (String field : fields) {
if (Objects.equals(field, fieldName)) {
JSONObject fieldConfig = classConfig.getJSONObject(field);
return fieldConfig == null ? null : fieldConfig.toJavaObject(ApiParamInfo.class);
}
}
}
return null;
}
/**
* 生成文档信息
* @param paramClass 参数类型
* @return 返回文档内容
*/
public List<FieldDocInfo> buildFieldDocInfo(Class<?> paramClass) {
return buildFieldDocInfosByType(paramClass, true, null);
}
/**
* 从api参数中构建
*/
protected List<FieldDocInfo> buildFieldDocInfosByType(Class<?> clazz, boolean root, Class<?> generic) {
Class<?> targetClassRef = PluginUtil.isCollectionOrArray(clazz) ? getCollectionElementClass(clazz) : clazz;
// 查找泛型
if(targetClassRef.getName().equals("org.springframework.http.ResponseEntity")){
Field body = ReflectionUtils.findField(targetClassRef, "body");
String typeName = ((TypeVariable<?>) body.getGenericType()).getName();
targetClassRef = getGenericParamClass(targetClassRef, typeName);
// 没有指定泛型类型
if (targetClassRef == null) {
System.out.println("Warning: Use ResponseEntity but not specified generic parameter.");
return Collections.emptyList();
}
}
final Class<?> targetClass = targetClassRef;
// 如果是基本类型
if (!PluginUtil.isPojo(targetClass) || isObjectClass(targetClass)) {
return Collections.emptyList();
}
final List<FieldDocInfo> fieldDocInfos = new ArrayList<>();
// 遍历参数对象中的属性
ReflectionUtils.doWithFields(targetClass, field -> {
ApiModelProperty apiModelProperty = AnnotationUtils.findAnnotation(field, ApiModelProperty.class);
if (root) {
resetCycle();
}
FieldDocInfo fieldDocInfo;
Class<?> fieldType = field.getType();
Type genericType = field.getGenericType();
Class<?> realClass = null;
// 如果是未知泛型private T data
// 获取真实的类型
if (genericType instanceof TypeVariable) {
String typeName = ((TypeVariable<?>) genericType).getName();
realClass = getGenericParamClass(targetClass, typeName);
}
if (fieldType.isEnum()) {
fieldDocInfo = buildFieldDocInfoByEnumClass((Class<Enum>) fieldType, apiModelProperty, field);
} else if (realClass != null) {
fieldDocInfo = buildFieldDocInfoByClass(apiModelProperty, realClass, field);
} else if (PluginUtil.isPojo(fieldType)) {
// 如果是自定义类
fieldDocInfo = buildFieldDocInfoByClass(apiModelProperty, fieldType, field);
} else {
fieldDocInfo = buildFieldDocInfo(apiModelProperty, field, generic);
}
formatDataType(fieldDocInfo, realClass != null ? realClass : fieldType);
fieldDocInfos.add(fieldDocInfo);
}, field -> {
if (PluginUtil.isTransientField(field) || Modifier.isStatic(field.getModifiers()) || isClassFieldHidden(targetClass, field)) {
return false;
}
ApiModelProperty apiModelProperty = AnnotationUtils.findAnnotation(field, ApiModelProperty.class);
return apiModelProperty == null || !apiModelProperty.hidden();
});
this.bindJarClassFields(targetClass, fieldDocInfos);
return fieldDocInfos;
}
protected void formatDataType(FieldDocInfo fieldDocInfo, Class<?> fieldType) {
if (isObjectClass(fieldType)) {
fieldDocInfo.setType(DataType.OBJECT.getValue());
fieldDocInfo.setChildren(Collections.emptyList());
}
}
protected boolean isObjectClass(Class<?> targetClass) {
List<String> objectClassList = this.tornaConfig.getObjectClassList();
if (objectClassList == null) {
return false;
}
String name = targetClass.getName();
return objectClassList.contains(name);
}
protected void bindJarClassFields(Class<?> targetClass, List<FieldDocInfo> fieldDocInfos) {
for (FieldDocInfo child : fieldDocInfos) {
ApiParamInfo apiParamInfo = this.getApiParamInfo(targetClass, child.getName());
if (apiParamInfo != null) {
child.setRequired(Booleans.toValue(apiParamInfo.isRequired()));
child.setExample(apiParamInfo.getExample());
child.setDescription(apiParamInfo.getValue());
child.setOrderIndex(apiParamInfo.getPosition());
if(!StringUtils.isEmpty(apiParamInfo.getName())){
child.setName(apiParamInfo.getName());
}
}
}
}
protected Class<?> getCollectionElementClass(Class<?> clazz) {
if (clazz.isArray()) {
return clazz.getComponentType();
}
Class<?> targetClass = getGenericParamClass(clazz, clazz.getName());
// Class<?> targetClass = clazz;
// Class<? extends Class> rawTypeClass = clazz.getClass();
// Method getGenericInfo = ReflectionUtils.findMethod(rawTypeClass, PluginUtil.METHOD_GET_GENERIC_INFO);
// if (getGenericInfo != null) {
// ReflectionUtils.makeAccessible(getGenericInfo);
// ClassRepository classRepository = (ClassRepository) ReflectionUtils.invokeMethod(getGenericInfo, clazz);
// if (classRepository != null) {
// TypeVariable<?>[] typeParameters = classRepository.getTypeParameters();
// for (TypeVariable<?> typeParameter : typeParameters) {
// Class<?> realClass = getGenericParamClass(clazz, typeParameter.getName());
// if (realClass != null) {
// targetClass = realClass;
// break;
// }
// }
// }
// }
return targetClass;
}
protected FieldDocInfo buildFieldDocInfo(ApiModelProperty apiModelProperty, Field field, Type generic) {
ApiModelPropertyWrapper apiModelPropertyWrapper = new ApiModelPropertyWrapper(apiModelProperty, field);
Class<?> fieldType = field.getType();
String type = getFieldType(field, apiModelPropertyWrapper);
// 优先使用注解中的字段名
String fieldName = getFieldName(field, apiModelPropertyWrapper);
String description = getFieldDescription(apiModelProperty);
boolean required = apiModelPropertyWrapper.getRequired();
String example = apiModelPropertyWrapper.getExample();
FieldDocInfo fieldDocInfo = new FieldDocInfo();
fieldDocInfo.setName(fieldName);
fieldDocInfo.setType(type);
fieldDocInfo.setRequired(getRequiredValue(required));
fieldDocInfo.setExample(example);
fieldDocInfo.setDescription(description);
fieldDocInfo.setOrderIndex(apiModelPropertyWrapper.getPosition());
fieldDocInfo.setMaxLength(apiModelPropertyWrapper.getMaxLength());
boolean isList = PluginUtil.isCollectionOrArray(fieldType);
Type genericType = field.getGenericType();
Type elementClass = null;
if (generic != null && isList) {
elementClass = generic;
}
// 如果有明确的泛型如List<Order>
if (elementClass == null && PluginUtil.isGenericType(genericType)) {
elementClass = PluginUtil.getGenericType(genericType);
if (elementClass instanceof TypeVariable) {
TypeVariable<?> typeVariable = (TypeVariable<?>) elementClass;
Class<?> genericDeclaration = (Class<?>)typeVariable.getGenericDeclaration();
Class<?> realClass = this.getGenericParamClass(genericDeclaration, typeVariable.getName());
if (realClass != null) {
elementClass = realClass;
} else {
elementClass = Object.class;
}
}
} else if (fieldType.isArray()) {
elementClass = fieldType.getComponentType();
}
if (elementClass != null && elementClass != Object.class && elementClass != Void.class) {
// 如果是嵌套泛型类型List<List<String>>
if (PluginUtil.isGenericType(elementClass)) {
Type genericRawType = PluginUtil.getGenericRawType(elementClass);
if (PluginUtil.isCollectionOrArray((Class<?>) genericRawType)) {
Type genericElType = PluginUtil.getGenericType(elementClass);
if (isList) {
Class<?> elType = (Class<?>) genericElType;
boolean primitive = ClassUtil.isPrimitive(elType.getName());
fieldDocInfo.setType("List<List<"+ (primitive ? elType.getSimpleName() : "Object") +">>");
List<FieldDocInfo> fieldDocInfos = buildFieldDocInfosByType(elType, true, null);
fieldDocInfo.setChildren(fieldDocInfos);
}
}
} else {
Class<?> clazz = (Class<?>) elementClass;
if (PluginUtil.isPojo(clazz)) {
List<FieldDocInfo> fieldDocInfos = isCycle(clazz, field)
? Collections.emptyList()
: buildFieldDocInfosByType(clazz, false, null);
fieldDocInfo.setChildren(fieldDocInfos);
}
}
}
return fieldDocInfo;
}
protected Class<?> getGenericParamClass(Class<?> clazz, String name) {
String genericParamKey = PluginUtil.getGenericParamKey(clazz, name);
return genericParamMap.get(genericParamKey);
}
protected FieldDocInfo buildFieldDocInfoByClass(ApiModelProperty apiModelProperty, Class<?> clazz, Field field) {
ApiModelPropertyWrapper apiModelPropertyWrapper = new ApiModelPropertyWrapper(apiModelProperty, field);
Objects.requireNonNull(clazz);
Objects.requireNonNull(field);
String name = getFieldName(field, apiModelPropertyWrapper);
String type = PluginUtil.getDataType(clazz);
String description = apiModelPropertyWrapper.getDescription();
boolean required = apiModelPropertyWrapper.getRequired();
String example = apiModelPropertyWrapper.getExample();
FieldDocInfo fieldDocInfo = new FieldDocInfo();
fieldDocInfo.setName(name);
fieldDocInfo.setType(type);
fieldDocInfo.setRequired(getRequiredValue(required));
fieldDocInfo.setExample(example);
fieldDocInfo.setDescription(description);
fieldDocInfo.setOrderIndex(apiModelPropertyWrapper.getPosition());
fieldDocInfo.setMaxLength(apiModelPropertyWrapper.getMaxLength());
if (PluginUtil.isCollection(clazz)) {
Class<?> elementClass = this.getCollectionElementClass(clazz);
if (elementClass != null) {
clazz = elementClass;
}
}
// 解决循环依赖问题
boolean cycle = isCycle(clazz, field);
Type genericType = PluginUtil.getGenericType(field);
Class<?> generic = genericType instanceof Class<?> && genericType != clazz? (Class<?>) genericType : null;
List<FieldDocInfo> children = cycle ? Collections.emptyList()
: buildFieldDocInfosByType(clazz, false, generic);
fieldDocInfo.setChildren(children);
return fieldDocInfo;
}
protected FieldDocInfo buildFieldDocInfoByEnumClass(Class<Enum> enumClass, ApiModelProperty apiModelProperty, Field field) {
ApiModelPropertyWrapper apiModelPropertyWrapper = new ApiModelPropertyWrapper(apiModelProperty, field);
Enum[] enumConstants = enumClass.getEnumConstants();
FieldDocInfo fieldDocInfo = new FieldDocInfo();
fieldDocInfo.setName(apiModelPropertyWrapper.getName());
fieldDocInfo.setType(DataType.ENUM.getValue());
fieldDocInfo.setRequired(Booleans.toValue(apiModelPropertyWrapper.getRequired()));
fieldDocInfo.setOrderIndex(apiModelPropertyWrapper.getPosition());
List<String> examples = new ArrayList<>(enumConstants.length);
for (Enum enumConstant : enumConstants) {
examples.add(enumConstant.name());
}
if (examples.size() > 0) {
fieldDocInfo.setExample(examples.get(0));
}
fieldDocInfo.setDescription(String.join("", examples));
fieldDocInfo.setMaxLength(apiModelPropertyWrapper.getMaxLength());
fieldDocInfo.setMaxLength(apiModelPropertyWrapper.getMaxLength());
return fieldDocInfo;
}
protected boolean isCycle(Class<?> clazz, Field field) {
String key = clazz.getName() + field.getType().getName() + field.getName();
AtomicInteger atomicInteger = cycleFieldsMap.computeIfAbsent(key, k -> new AtomicInteger());
return atomicInteger.getAndIncrement() >= 1;
}
private boolean isClassFieldHidden(Class<?> clazz, Field field) {
String key = clazz.getName() + "." + field.getName();
return hiddenColumns.contains(key);
}
private static byte getRequiredValue(boolean b) {
return (byte) (b ? 1 : 0);
}
private static String getFieldType(Field field, ApiModelPropertyWrapper apiModelProperty) {
String dataType = apiModelProperty.getDataType();
if (StringUtils.hasText(dataType)) {
return dataType;
}
Class<?> type = field.getType();
String rawTypeName = type.getSimpleName();
String multipartFile = "MultipartFile";
if (rawTypeName.contains(multipartFile)) {
return type.isArray() ? DataType.FILES.getValue() : DataType.FILE.getValue();
}
if (Collection.class.isAssignableFrom(type)) {
Type genericType = field.getGenericType();
return genericType.getTypeName().contains(multipartFile) ?
DataType.FILES.getValue() :
DataType.ARRAY.getValue();
}
if (type.isArray()) {
return DataType.ARRAY.getValue();
}
if (type == Date.class) {
return DataType.DATE.getValue();
}
if (type == Timestamp.class) {
return DataType.DATETIME.getValue();
}
return PluginUtil.isPojo(type) ? DataType.OBJECT.getValue() : field.getType().getSimpleName().toLowerCase();
}
private static String getFieldName(Field field, ApiModelPropertyWrapper apiModelPropertyWrapper) {
String name = apiModelPropertyWrapper.getName();
if (StringUtils.hasText(name)) {
return name;
}
return field.getName();
}
private static String getFieldDescription(ApiModelProperty apiModelProperty) {
if (apiModelProperty == null) {
return "";
}
String desc = apiModelProperty.value();
if (StringUtils.isEmpty(desc)) {
desc = apiModelProperty.notes();
}
return desc;
}
private void resetCycle() {
cycleFieldsMap.clear();
}
}

View File

@@ -0,0 +1,25 @@
package cn.torna.swaggerplugin.builder;
/**
* 数据类型
* @author tanghc
*/
public enum DataType {
BYTE("byte"), SHORT("short"), INT("integer"), LONG("long"), FLOAT("float"), DOUBLE("double")
, CHAR("char"), BOOLEAN("boolean"), ARRAY("array"), OBJECT("object"),STRING("string")
, FILE("file"), FILES("file[]"), DATE("date"),DATETIME("datetime"), ENUM("enum");
DataType(String v) {
val = v;
}
private String val;
public String getValue() {
return this.val;
}
public static DataType parse(String name) {
return DataType.valueOf(DataType.class, name);
}
}

View File

@@ -0,0 +1,42 @@
package cn.torna.swaggerplugin.builder;
import lombok.Data;
import java.util.List;
/**
* 文档参数字段信息
*
* @author tanghc
*
*/
@Data
public class FieldDocInfo {
/** 字段名称 */
private String name;
/** 字段类型 */
private String type;
/** 是否必须10否 */
private Byte required;
/** 最大长度 */
private String maxLength;
/** 示例值 */
private String example;
/** 描述 */
private String description;
/** 父节点, 没有填空字符串 */
private String parentId;
/** 排序 */
private Integer orderIndex;
/** 子节点 */
private List<FieldDocInfo> children;
}

View File

@@ -0,0 +1,65 @@
package cn.torna.swaggerplugin.builder;
import cn.torna.swaggerplugin.bean.ControllerInfo;
import cn.torna.swaggerplugin.bean.TornaConfig;
import io.swagger.annotations.ApiOperation;
import org.springframework.core.annotation.AnnotatedElementUtils;
import org.springframework.util.StringUtils;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import java.lang.reflect.Method;
/**
* @author tanghc
*/
public abstract class HttpMethodInfoBuilder implements RequestInfoBuilder {
private ControllerInfo controllerInfo;
private TornaConfig tornaConfig;
private Method method;
private ApiOperation apiOperation;
public HttpMethodInfoBuilder(ControllerInfo controllerInfo, Method method, TornaConfig tornaConfig) {
this.controllerInfo = controllerInfo;
this.tornaConfig = tornaConfig;
this.method = method;
this.apiOperation = method.getAnnotation(ApiOperation.class);
}
@Override
public String getHttpMethod() {
String httpMethod = apiOperation.httpMethod();
if (StringUtils.hasText(httpMethod)) {
return 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();
}
public TornaConfig getTornaConfig() {
return tornaConfig;
}
@Override
public Method getMethod() {
return method;
}
@Override
public ApiOperation getApiOperation() {
return apiOperation;
}
public ControllerInfo getControllerInfo() {
return controllerInfo;
}
}

View File

@@ -0,0 +1,55 @@
package cn.torna.swaggerplugin.builder;
import cn.torna.swaggerplugin.bean.ControllerInfo;
import cn.torna.swaggerplugin.bean.TornaConfig;
import com.gitee.sop.support.annotation.Open;
import org.springframework.core.annotation.AnnotatedElementUtils;
import org.springframework.core.annotation.AnnotationUtils;
import org.springframework.http.MediaType;
import org.springframework.web.bind.annotation.RequestMapping;
import java.lang.reflect.Method;
/**
* @author tanghc
*/
public class MvcRequestInfoBuilder extends HttpMethodInfoBuilder {
public MvcRequestInfoBuilder(ControllerInfo controllerInfo, Method method, TornaConfig tornaConfig) {
super(controllerInfo, method, tornaConfig);
}
@Override
public String buildUrl() {
Method method = getMethod();
Open open = AnnotationUtils.findAnnotation(method, Open.class);
if (open == null) {
throw new RuntimeException("接口[" + method + "]未定义@Open注解");
}
return open.value();
}
@Override
public String getVersion() {
Method method = getMethod();
Open open = AnnotationUtils.findAnnotation(method, Open.class);
if (open == null) {
throw new RuntimeException("接口[" + method + "]未定义@Open注解");
}
return open.version();
}
@Override
public String buildContentType() {
return MediaType.APPLICATION_JSON_VALUE;
}
private String[] getConsumes() {
RequestMapping requestMapping = AnnotatedElementUtils.findMergedAnnotation(getMethod(), RequestMapping.class);
if (requestMapping != null) {
return requestMapping.consumes();
}
return null;
}
}

View File

@@ -0,0 +1,33 @@
package cn.torna.swaggerplugin.builder;
import cn.torna.swaggerplugin.bean.ControllerInfo;
import io.swagger.annotations.ApiOperation;
import java.lang.reflect.Method;
/**
* Copyright © 2021 DHF Info. Tech Ltd. All rights reserved.
* <p></p>
*
* @author tanghc
* @version 1.0.0
* @description
* @date 2021/7/14/014
*/
public interface RequestInfoBuilder {
String buildUrl();
String getHttpMethod();
String buildContentType();
Method getMethod();
ApiOperation getApiOperation();
ControllerInfo getControllerInfo();
default String getVersion() {
return "";
}
}

View File

@@ -0,0 +1,11 @@
package cn.torna.swaggerplugin.exception;
/**
* @author tanghc
*/
public class HiddenException extends Exception {
public HiddenException(String message) {
super(message);
}
}

View File

@@ -0,0 +1,10 @@
package cn.torna.swaggerplugin.exception;
/**
* @author tanghc
*/
public class IgnoreException extends Exception {
public IgnoreException(String message) {
super(message);
}
}

View File

@@ -0,0 +1,110 @@
package cn.torna.swaggerplugin.scaner;
import java.io.IOException;
import java.lang.annotation.Annotation;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Set;
import org.springframework.core.io.Resource;
import org.springframework.core.io.support.PathMatchingResourcePatternResolver;
import org.springframework.core.io.support.ResourcePatternResolver;
import org.springframework.core.type.classreading.CachingMetadataReaderFactory;
import org.springframework.core.type.classreading.MetadataReader;
import org.springframework.core.type.classreading.MetadataReaderFactory;
import org.springframework.core.type.filter.AnnotationTypeFilter;
import org.springframework.core.type.filter.AssignableTypeFilter;
import org.springframework.core.type.filter.TypeFilter;
import org.springframework.util.ClassUtils;
/**
* class扫描
* @author tanghc
*/
public class ClassScanner {
private static final String RESOURCE_PATTERN = "/**/*.class";
private ResourcePatternResolver resourcePatternResolver = new PathMatchingResourcePatternResolver();
private List<String> packagesList = new LinkedList<String>();
private List<TypeFilter> typeIncludes = new LinkedList<TypeFilter>();
private Set<Class<?>> classSet = new HashSet<Class<?>>();
public ClassScanner(String[] entityPackage) {
this(entityPackage, null);
}
@SuppressWarnings("unchecked")
public ClassScanner(String[] entityPackage, Class<?> scanClass) {
for (String packagePath : entityPackage) {
this.packagesList.add(packagePath);
}
if (scanClass != null) {
// scanClass是注解类型
if(scanClass.isAnnotation()) {
// AnnotationTypeFilter 有scanClass注解的类将被过滤出来,不能过滤接口
typeIncludes.add(new AnnotationTypeFilter((Class<? extends Annotation>) scanClass, false));
}else {
// AssignableTypeFilter 继承或实现superClass的类将被过滤出来
// superClass可以是接口
typeIncludes.add(new AssignableTypeFilter(scanClass));
}
}
}
/**
* 将符合条件的Bean以Class集合的形式返回
*
* @return 返回Mapper的class集合
* @throws IOException IO异常
* @throws ClassNotFoundException 文件找不到异常
*/
public Set<Class<?>> getClassSet() throws IOException, ClassNotFoundException {
this.classSet.clear();
if (!this.packagesList.isEmpty()) {
for (String pkg : this.packagesList) {
// classpath*:com/xx/dao/**/*.class
String pattern = ResourcePatternResolver.CLASSPATH_ALL_URL_PREFIX
+ ClassUtils.convertClassNameToResourcePath(pkg) + RESOURCE_PATTERN;
Resource[] resources = this.resourcePatternResolver.getResources(pattern);
MetadataReaderFactory readerFactory = new CachingMetadataReaderFactory(this.resourcePatternResolver);
for (Resource resource : resources) {
if (resource.isReadable()) {
MetadataReader reader = readerFactory.getMetadataReader(resource);
if (matchesEntityTypeFilter(reader, readerFactory)) {
String className = reader.getClassMetadata().getClassName();
this.classSet.add(Class.forName(className));
}
}
}
}
}
return this.classSet;
}
/**
* 检查当前扫描到的Bean含有任何一个指定的注解标记
*
* @param reader
* @param readerFactory
* @return 返回true表示它是一个Mapper
* @throws IOException
*/
private boolean matchesEntityTypeFilter(MetadataReader reader, MetadataReaderFactory readerFactory)
throws IOException {
if (!this.typeIncludes.isEmpty()) {
for (TypeFilter filter : this.typeIncludes) {
if (filter.match(reader, readerFactory)) {
return true;
}
}
}
return false;
}
}

View File

@@ -0,0 +1,151 @@
package cn.torna.swaggerplugin.util;
import cn.torna.swaggerplugin.scaner.ClassScanner;
import org.springframework.http.HttpMethod;
import org.springframework.web.context.request.WebRequest;
import org.springframework.web.multipart.MultipartRequest;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.Reader;
import java.io.Writer;
import java.security.Principal;
import java.time.ZoneId;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Locale;
import java.util.Objects;
import java.util.Set;
import java.util.TimeZone;
/**
* 扫描2包下的所有类
* <p>Title: ClassUtil.java</p>
* <p>Description: </p>
*
* @author lichao1
* @version 1.0
* @date 2018年12月3日
*/
public class ClassUtil {
/**
* 从包package中获取所有的Class
*
* @param packageName
* @return 返回class集合
*/
public static Set<Class<?>> getClasses(String packageName, Class<?> scanClass) {
try {
return new ClassScanner(new String[]{packageName}, scanClass).getClassSet();
} catch (IOException | ClassNotFoundException e) {
e.printStackTrace();
}
return Collections.emptySet();
}
/**
* 获取自身以及上层父类class父类的父类也算不包括Object
*
* @param clazz 当前class
* @return 没有返回空
*/
public static List<Class<?>> getAllClasses(Class<?> clazz) {
List<Class<?>> allClasses = new ArrayList<>();
allClasses.add(clazz);
findSuperclass(clazz, allClasses);
return allClasses;
}
private static void findSuperclass(Class<?> clazz, List<Class<?>> allClasses) {
if (clazz == null) {
return;
}
Class<?> superclass = clazz.getSuperclass();
if (superclass != Object.class) {
allClasses.add(superclass);
findSuperclass(superclass, allClasses);
}
}
public static boolean isSpecialType(Class<?> paramType) {
// 特殊参数
boolean special = (
WebRequest.class.isAssignableFrom(paramType) ||
MultipartRequest.class.isAssignableFrom(paramType) ||
Principal.class.isAssignableFrom(paramType) ||
InputStream.class.isAssignableFrom(paramType) ||
Reader.class.isAssignableFrom(paramType) ||
HttpMethod.class == paramType ||
Locale.class == paramType ||
TimeZone.class == paramType ||
ZoneId.class == paramType ||
OutputStream.class.isAssignableFrom(paramType) ||
Writer.class.isAssignableFrom(paramType)
);
String name = paramType.getName();
return name.startsWith("javax") ||
name.startsWith("jakarta")
|| special;
}
/**
* Check if it is the basic data type of json data
*
* @param type0 java class name
* @return boolean
*/
public static boolean isPrimitive(String type0) {
if (Objects.isNull(type0)) {
return true;
}
String type = type0.contains("java.lang") ? type0.substring(type0.lastIndexOf(".") + 1, type0.length()) : type0;
type = type.toLowerCase();
switch (type) {
case "string":
case "integer":
case "int":
case "object":
case "void":
case "long":
case "double":
case "float":
case "short":
case "bigdecimal":
case "char":
case "character":
case "number":
case "boolean":
case "byte":
case "uuid":
case "biginteger":
case "java.sql.timestamp":
case "java.util.date":
case "java.time.localdatetime":
case "java.time.localtime":
case "localtime":
case "date":
case "localdatetime":
case "localdate":
case "zoneddatetime":
case "java.time.localdate":
case "java.time.zoneddatetime":
case "java.math.bigdecimal":
case "java.math.biginteger":
case "java.util.uuid":
case "java.io.serializable":
return true;
default:
return false;
}
}
// public static void main(String[] args) {
// Set<Class<?>> classes = getClasses("cn.torna.swaggerplugin", Api.class);
// classes.forEach(System.out::println);
// }
}

View File

@@ -0,0 +1,407 @@
package cn.torna.swaggerplugin.util;
import cn.torna.swaggerplugin.bean.ControllerInfo;
import cn.torna.swaggerplugin.bean.PushFeature;
import cn.torna.swaggerplugin.bean.TornaConfig;
import cn.torna.swaggerplugin.builder.DataType;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.BeansException;
import org.springframework.beans.FatalBeanException;
import org.springframework.util.Assert;
import org.springframework.util.ClassUtils;
import org.springframework.util.ReflectionUtils;
import java.beans.PropertyDescriptor;
import java.beans.Transient;
import java.lang.annotation.Annotation;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.lang.reflect.Parameter;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.lang.reflect.TypeVariable;
import java.lang.reflect.WildcardType;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.concurrent.atomic.AtomicInteger;
/**
* @author tanghc
*/
public class PluginUtil {
public static final String METHOD_GET_GENERIC_INFO = "getGenericInfo";
private static final List<String> SYSTEM_PACKAGE_LIST = Arrays.asList(
"java.", "sun.", "org.springframework.", "javax.", "jakarta."
);
public static String getParameterType(Parameter parameter) {
Class<?> type = parameter.getType();
String simpleName = type.getSimpleName().toLowerCase();
if ("MultipartFile".equalsIgnoreCase(simpleName)) {
return "file";
}
if ("MultipartFile[]".equalsIgnoreCase(simpleName)) {
return "file[]";
}
return simpleName;
}
/**
* 是否普通的java类
*
* @param clazz 类class
* @return true是
*/
public static boolean isPojo(Class<?> clazz) {
if (clazz.isArray() || Collection.class.isAssignableFrom(clazz) || clazz.isPrimitive()) {
return false;
}
String name = clazz.getName();
for (String prefix : SYSTEM_PACKAGE_LIST) {
if (name.startsWith(prefix)) {
return false;
}
}
return true;
}
/**
* 字段是否被transient关键字修饰或有@Transient注解
*
* @param field
* @return 是返回true
*/
public static boolean isTransientField(Field field) {
Transient transientAnno = field.getAnnotation(Transient.class);
return transientAnno != null || Modifier.isTransient(field.getModifiers());
}
public static <T extends Annotation> boolean isExistAnnotation(Annotation[] annotations, Class<T> annoClass) {
if (annotations == null) {
return false;
}
for (Annotation annotation : annotations) {
if (annotation.getClass() == annoClass) {
return true;
}
}
return false;
}
public static Type getGenericType(Field field) {
ReflectionUtils.makeAccessible(field);
Method[] allDeclaredMethods = ReflectionUtils.getAllDeclaredMethods(Field.class);
for (Method allDeclaredMethod : allDeclaredMethods) {
if (allDeclaredMethod.getName().equals("getGenericSignature")) {
ReflectionUtils.makeAccessible(allDeclaredMethod);
Object o = ReflectionUtils.invokeMethod(allDeclaredMethod, field);
// Lcn/torna/tornaexample/controller/eg/vo/CommonPage<Lcn/torna/tornaexample/controller/eg/vo/LockingDto;>;
String signature = String.valueOf(o);
if (signature.startsWith("L") && signature.endsWith(";>;")) {
String className = signature.replace(";", "");
// Lcn/torna/tornaexample/controller/eg/vo/LockingDto
className = StringUtil.findBetweenChar(className, '<', '>');
if (className.startsWith("L")) {
// cn/torna/tornaexample/controller/eg/vo/LockingDto
className = className.substring(1);
}
className = StringUtil.splashToDot(className);
try {
return ClassUtils.forName(className, Thread.currentThread().getContextClassLoader());
} catch (ClassNotFoundException e) {
return null;
}
}
}
}
return field.getGenericType();
}
/**
* 获取泛型参数类型List{@literal <String> } 返回String
*
* @param type 泛型参数
* @return 返回泛型参数类型
*/
public static Type getGenericType(Type type) {
Type[] params = ((ParameterizedType) type).getActualTypeArguments();
if (params.length == 0) {
return Object.class;
}
Type param = params[0];
// List<? extends Pojo>,
if (param instanceof WildcardType) {
WildcardType wildcardType = (WildcardType) param;
Type[] upperBounds = wildcardType.getUpperBounds();
if (upperBounds != null && upperBounds.length > 0) {
// Pojo.class
return upperBounds[0];
}
Type[] lowerBounds = wildcardType.getLowerBounds();
if (lowerBounds != null && lowerBounds.length > 0) {
return lowerBounds[0];
}
}
return param;
}
/**
* 获取泛型参数的原理类型List{@literal <String> } 返回List
*
* @param type 泛型参数
* @return 返回泛型参数原始类型
*/
public static Type getGenericRawType(Type type) {
Type rawType = ((ParameterizedType) type).getRawType();
if (rawType == null) {
return Object.class;
}
return rawType;
}
public static Type getGenericTypeDeeply(Type type) {
if (isGenericType(type)) {
Type genericType = getGenericType(type);
return getGenericTypeDeeply(genericType);
} else {
return type;
}
}
/**
* 是否是泛型类型
*
* @param type 类型
* @return true是泛型类型
*/
public static boolean isGenericType(Type type) {
return type instanceof ParameterizedType;
}
public static String getGenericParamKey(Class<?> rawType, String name) {
// return rawType.getName() + "." + name;
return rawType.getName();
}
public static boolean isCollectionOrArray(Class<?> type) {
return type.isArray() || isCollection(type);
}
public static boolean isCollection(Class<?> type) {
return Collection.class.isAssignableFrom(type);
}
public static String getDataType(Class<?> type) {
if (isCollectionOrArray(type)) {
return DataType.ARRAY.getValue();
}
return isPojo(type) ? DataType.OBJECT.getValue() : type.getSimpleName().toLowerCase();
}
public static List<String> getClassGenericParamName(Class<?> clazz) {
TypeVariable<? extends Class<?>>[] typeParameters = clazz.getTypeParameters();
List<String> paramNames = new ArrayList<>();
for (TypeVariable<?> typeParameter : typeParameters) {
paramNames.add(typeParameter.getName());
}
return paramNames;
}
/**
* 将泛型实际参数存储到genericParamMap中。<br>
* <pre>
* {@literal
* Result<Page<Order>>
* key: xxx.Result.T value: Page.class
* key: xxx.Page.T value: Order.class
* }
* </pre>
*
* @param genericParamMap 存储泛型信息
* @param genericParameterType 泛型类型
*/
public static void appendGenericParamMap1(Map<String, Class<?>> genericParamMap, Type genericParameterType) {
// 如果有泛型
if (PluginUtil.isGenericType(genericParameterType)) {
ParameterizedType parameterType = (ParameterizedType) genericParameterType;
// 泛型参数
Type[] actualTypeArguments = parameterType.getActualTypeArguments();
// 原始类型
Class<?> rawType = (Class<?>) parameterType.getRawType();
Class<? extends Class> rawTypeClass = rawType.getClass();
TypeVariable<?>[] typeParameters = rawTypeClass.getTypeParameters();
for (int i = 0; i < typeParameters.length; i++) {
String key = getGenericParamKey(rawType, typeParameters[i].getName());
Type actualTypeArgument = actualTypeArguments[i];
// 如果泛型填的?,即Result<?>
if (actualTypeArgument instanceof WildcardType) {
genericParamMap.put(key, Object.class);
continue;
}
boolean isGeneric = PluginUtil.isGenericType(actualTypeArgument);
Class<?> value = isGeneric ?
(Class<?>) ((ParameterizedType) actualTypeArgument).getRawType() : (Class<?>) actualTypeArgument;
genericParamMap.put(key, value);
if (isGeneric) {
appendGenericParamMap1(genericParamMap, actualTypeArgument);
}
}
}
}
/**
* 将泛型实际参数存储到genericParamMap中。<br>
* <pre>
* {@literal
* Result<Page<Order>>
* key: xxx.Result value: Page.class
* key: xxx.Page value: Order.class
* }
* </pre>
*
* @param genericParamMap 存储泛型信息
* @param genericParameterType 泛型类型
*/
public static void appendGenericParamMap(ControllerInfo controllerInfo, Map<String, Class<?>> genericParamMap, Type genericParameterType) {
// 如果有泛型
if (PluginUtil.isGenericType(genericParameterType)) {
ParameterizedType parameterizedType = (ParameterizedType) genericParameterType;
Type rawType = parameterizedType.getRawType();
Type target = parameterizedType.getActualTypeArguments()[0];
Class targetClass;
if (target instanceof ParameterizedType) {
ParameterizedType parameterizedTypeTarget = (ParameterizedType) target;
targetClass = (Class) parameterizedTypeTarget.getRawType();
genericParamMap.put(rawType.getTypeName(), targetClass);
appendGenericParamMap(controllerInfo, genericParamMap, target);
} else if (target instanceof TypeVariable) {
TypeVariable parameterizedTypeTarget = (TypeVariable) target;
Class<?> genericDeclaration = (Class<?>) parameterizedTypeTarget.getGenericDeclaration();
String key = genericDeclaration.getName() + "#" + parameterizedTypeTarget.getName();
Class<?> realClass = controllerInfo.getGenericParamMap().get(key);
if (realClass != null) {
targetClass = realClass;
genericParamMap.put(rawType.getTypeName(), targetClass);
}
} else {
if (target instanceof WildcardType) {
return;
}
targetClass = (Class) target;
genericParamMap.put(rawType.getTypeName(), targetClass);
}
}
}
/**
* 属性拷贝,第一个参数中的属性值拷贝到第二个参数中<br>
* 注意:当第一个参数中的属性有null值时,不会拷贝进去
*
* @param from 源对象
* @param to 目标对象
* @throws BeansException
*/
public static void copyPropertiesIgnoreNull(Object from, Object to, String... ignoreProperties)
throws BeansException {
Assert.notNull(from, "Source must not be null");
Assert.notNull(to, "Target must not be null");
Class<?> actualEditable = to.getClass();
PropertyDescriptor[] targetPds = BeanUtils.getPropertyDescriptors(actualEditable);
List<String> ignoreList = (ignoreProperties != null ? Arrays.asList(ignoreProperties) : null);
for (PropertyDescriptor targetPd : targetPds) {
Method writeMethod = targetPd.getWriteMethod();
if (writeMethod != null && (ignoreList == null || !ignoreList.contains(targetPd.getName()))) {
PropertyDescriptor sourcePd = BeanUtils.getPropertyDescriptor(from.getClass(), targetPd.getName());
if (sourcePd != null) {
Method readMethod = sourcePd.getReadMethod();
if (readMethod != null &&
ClassUtils.isAssignable(writeMethod.getParameterTypes()[0], readMethod.getReturnType())) {
try {
if (!Modifier.isPublic(readMethod.getDeclaringClass().getModifiers())) {
readMethod.setAccessible(true);
}
Object value = readMethod.invoke(from);
if (!Modifier.isPublic(writeMethod.getDeclaringClass().getModifiers())) {
writeMethod.setAccessible(true);
}
// 这里判断value是否为空 当然这里也能进行一些特殊要求的处理
// 例如绑定时格式转换等等
if (value != null) {
if (!Modifier.isPublic(writeMethod
.getDeclaringClass().getModifiers())) {
writeMethod.setAccessible(true);
}
writeMethod.invoke(to, value);
}
} catch (Throwable ex) {
throw new FatalBeanException(
"Could not copy property '" + targetPd.getName() + "' from source to target", ex);
}
}
}
}
}
}
public static boolean isFileParameter(Parameter parameter) {
Class<?> type = parameter.getType();
boolean pojo = PluginUtil.isPojo(type);
if (pojo) {
AtomicInteger fileCount = new AtomicInteger();
ReflectionUtils.doWithFields(type, field -> {
String fieldType = field.getType().getSimpleName();
if (fieldType.contains("MultipartFile")) {
fileCount.incrementAndGet();
}
});
return fileCount.get() > 0;
} else {
String parameterType = PluginUtil.getParameterType(parameter);
return parameterType.equals("file") || parameterType.equals("file[]");
}
}
/**
* 字段是否包含某些注解
*
* @param field 字段
* @param annotationClassname 注解名称
* @return 如果有返回true
*/
public static boolean hasAnyAnnotation(Field field, List<String> annotationClassname) {
Annotation[] annotations = field.getAnnotations();
for (Annotation annotation : annotations) {
String annotationClassName = annotation.annotationType().getName();
for (String name : annotationClassname) {
if (annotationClassName.contains(name)) {
return true;
}
}
}
return false;
}
public static boolean isEnableFeature(TornaConfig config, PushFeature feature) {
List<String> features = config.getFeatures();
if (features == null) {
return false;
}
for (String feat : features) {
if (feature.name().equalsIgnoreCase(feat)) {
return true;
}
}
return false;
}
}

View File

@@ -0,0 +1,77 @@
package cn.torna.swaggerplugin.util;
import java.net.URL;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
public class StringUtil {
private StringUtil() {
}
/**
* "file:/home/whf/cn/fh" -> "/home/whf/cn/fh"
* "jar:file:/home/whf/foo.jar!cn/fh" -> "/home/whf/foo.jar"
*/
public static String getRootPath(URL url) {
String fileUrl = url.getFile();
int pos = fileUrl.indexOf('!');
if (-1 == pos) {
return fileUrl;
}
return fileUrl.substring(5, pos);
}
/**
* "cn.fh.lightning" -> "cn/fh/lightning"
* @param name
* @return
*/
public static String dotToSplash(String name) {
return name.replaceAll("\\.", "/");
}
/**
* "cn/fh/lightning" -> "cn.fh.lightning"
* @param name
* @return
*/
public static String splashToDot(String name) {
return name.replace('/', '.');
}
/**
* "Apple.class" -> "Apple"
*/
public static String trimExtension(String name) {
int pos = name.indexOf('.');
if (-1 != pos) {
return name.substring(0, pos);
}
return name;
}
/**
* /application/home -> /home
* @param uri
* @return
*/
public static String trimURI(String uri) {
String trimmed = uri.substring(1);
int splashIndex = trimmed.indexOf('/');
return trimmed.substring(splashIndex);
}
public static String findBetweenChar(String input, char begin, char end) {
String regex = begin + "(.*?)" + end;
Pattern pattern = Pattern.compile(regex);
Matcher matcher = pattern.matcher(input);
if (matcher.find()) {
return matcher.group(1);
}
return input;
}
}

View File

@@ -0,0 +1,50 @@
package cn.torna.swaggerplugin;
import cn.torna.swaggerplugin.util.StringUtil;
import org.junit.Assert;
import org.junit.Test;
import java.util.ArrayList;
import java.util.List;
public class ClassTest {
@Test
public void test() {
List<Class<?>> parents = getSuperclass(RedApple.class);
List<Class<?>> parents2 = getSuperclass(Apple.class);
List<Class<?>> parents3 = getSuperclass(Food.class);
System.out.println(parents);
System.out.println(parents2);
System.out.println(parents3);
}
@Test
public void test2() {
String s = "CommonPage<Lcn/torna/tornaexample/controller/eg/vo/LockingDto;>";
String content = StringUtil.findBetweenChar( s, '<', '>');
Assert.assertEquals("Lcn/torna/tornaexample/controller/eg/vo/LockingDto;", content);
}
private static List<Class<?>> getSuperclass(Class<?> clazz) {
List<Class<?>> parents = new ArrayList<>();
findSuperclass(clazz, parents);
return parents;
}
private static void findSuperclass(Class<?> clazz, List<Class<?>> parents) {
if (clazz == null) {
return;
}
Class<?> superclass = clazz.getSuperclass();
if (superclass != Object.class) {
parents.add(superclass);
findSuperclass(superclass, parents);
}
}
static class Food {}
static class Apple extends Food {}
static class RedApple extends Apple {}
}

View File

@@ -0,0 +1,29 @@
package cn.torna.swaggerplugin;
import io.swagger.annotations.ApiModelProperty;
import org.junit.Test;
public class EnumTest {
@Test
public void test() {
Class<GenderEnum> genderEnumClass = GenderEnum.class;
Enum[] enumConstants = genderEnumClass.getEnumConstants();
for (Enum enumConstant : enumConstants) {
System.out.println(enumConstant.name() + "=" + enumConstant.ordinal());
}
}
public enum GenderEnum {
@ApiModelProperty(value = "")
MALE(1),
FEMALE(0)
;
private final int gender;
GenderEnum(int gender) {
this.gender = gender;
}
}
}

View File

@@ -0,0 +1,68 @@
package cn.torna.swaggerplugin;
import cn.torna.swaggerplugin.util.PluginUtil;
import junit.framework.TestCase;
import lombok.Data;
import org.junit.Test;
import org.springframework.util.ReflectionUtils;
import sun.reflect.generics.reflectiveObjects.ParameterizedTypeImpl;
import java.lang.reflect.Method;
import java.lang.reflect.Type;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* @author 六如
*/
public class GenericParadigmUtilTest extends TestCase {
@Test
public void testA() {
Class<Controller> controllerClass = Controller.class;
Method method = ReflectionUtils.findMethod(controllerClass, "page");
Type genericReturnType = method.getGenericReturnType();
ParameterizedTypeImpl parameterizedType = (ParameterizedTypeImpl) genericReturnType;
Type rawType = parameterizedType.getRawType();
Type target = parameterizedType.getActualTypeArguments()[0];
Class targetClass;
if (target instanceof ParameterizedTypeImpl) {
ParameterizedTypeImpl parameterizedTypeTarget = (ParameterizedTypeImpl) target;
targetClass = parameterizedTypeTarget.getRawType();
System.out.println(rawType.getTypeName() + ":" + targetClass.getName());
} else {
targetClass = (Class) target;
}
}
public static class Controller {
public Result<Page<User>> page() {
return null;
}
}
@Data
public static class Page<T> {
private List<T> list;
}
@Data
public static class User {
private Integer id;
}
public static class Result<T> {
}
}

View File

@@ -0,0 +1,36 @@
package cn.torna.swaggerplugin;
import cn.torna.swaggerplugin.bean.TornaConfig;
import cn.torna.swaggerplugin.util.ClassUtil;
import io.swagger.annotations.Api;
import org.junit.Test;
import java.lang.reflect.Method;
import java.util.Arrays;
import java.util.Set;
/**
* @author 六如
*/
public class SwaggerPluginServiceTest {
@Test
public void exclude() {
TornaConfig tornaConfig = new TornaConfig();
tornaConfig.setExcludePackage("cn.torna.swaggerplugin.pkg1.pkg2;cn.torna.swaggerplugin.pkg1.pkg3");
tornaConfig.setBasePackage("cn.torna.swaggerplugin.pkg1");
tornaConfig.setScanApis(Arrays.asList("cn.torna.swaggerplugin.pkg1.UserInterface"));
SwaggerPluginService swaggerPluginService = new SwaggerPluginService(tornaConfig);
Set<Class<?>> clazzs = ClassUtil.getClasses(tornaConfig.getBasePackage(), Api.class);
for (Class<?> clazz : clazzs) {
for (Method method : clazz.getMethods()) {
if (swaggerPluginService.match(method)) {
System.out.println(method);
}
}
}
}
}

View File

@@ -0,0 +1,14 @@
package cn.torna.swaggerplugin.pkg1;
import io.swagger.annotations.Api;
/**
* @author 六如
*/
@Api
public interface UserInterface {
String getUserName();
String getAge(Long userId);
}

View File

@@ -0,0 +1,11 @@
package cn.torna.swaggerplugin.pkg1.pkg2;
import io.swagger.annotations.Api;
/**
* @author 六如
*/
@Api
public interface OrderInterface {
String getOrder();
}

View File

@@ -0,0 +1,12 @@
package cn.torna.swaggerplugin.pkg1.pkg3;
import io.swagger.annotations.Api;
/**
* @author 六如
*/
@Api
public interface AddressInterface {
String getAddress(String a);
}

View File

@@ -20,6 +20,11 @@
<artifactId>transmittable-thread-local</artifactId> <artifactId>transmittable-thread-local</artifactId>
<version>2.14.5</version> <version>2.14.5</version>
</dependency> </dependency>
<dependency>
<groupId>io.swagger</groupId>
<artifactId>swagger-annotations</artifactId>
<version>1.6.14</version>
</dependency>
<dependency> <dependency>
<groupId>org.apache.dubbo</groupId> <groupId>org.apache.dubbo</groupId>
<artifactId>dubbo</artifactId> <artifactId>dubbo</artifactId>

View File

@@ -27,7 +27,7 @@ public @interface Open {
String version() default "1.0"; String version() default "1.0";
/** /**
* 指定接口是否需要授权才能访问可在admin中进行修改 * 指定接口是否需要授权才能访问可在admin中进行授权
*/ */
boolean permission() default false; boolean permission() default false;
@@ -36,4 +36,27 @@ public @interface Open {
*/ */
boolean needToken() default false; boolean needToken() default false;
/**
* 是否有公共响应参数,默认true
* <pre>
* 如果设置true,返回结果如下
* {
* "code": "0",
* "msg": "",
* "sub_code": "",
* "sub_msg": "",
* "data": {
* "id": 1,
* "name": "Jim"
* }
* }
* 如果设置false,只返回data部分:
* {
* "id": 1,
* "name": "Jim"
* }
* </pre>
*/
boolean hasCommonResponse() default true;
} }

View File

@@ -5,6 +5,7 @@ import com.gitee.sop.support.annotation.Open;
import com.gitee.sop.support.service.ApiRegisterService; import com.gitee.sop.support.service.ApiRegisterService;
import com.gitee.sop.support.service.dto.RegisterDTO; import com.gitee.sop.support.service.dto.RegisterDTO;
import com.gitee.sop.support.service.dto.RegisterResult; import com.gitee.sop.support.service.dto.RegisterResult;
import io.swagger.annotations.ApiOperation;
import lombok.Data; import lombok.Data;
import org.apache.commons.logging.Log; import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory; import org.apache.commons.logging.LogFactory;
@@ -109,6 +110,11 @@ public class ApiRegister {
registerDTO.setParamInfo(JSON.toJSONString(paramInfos)); registerDTO.setParamInfo(JSON.toJSONString(paramInfos));
registerDTO.setIsPermission(parseBoolean(open.permission())); registerDTO.setIsPermission(parseBoolean(open.permission()));
registerDTO.setIsNeedToken(parseBoolean(open.needToken())); registerDTO.setIsNeedToken(parseBoolean(open.needToken()));
registerDTO.setHasCommonResponse(parseBoolean(open.hasCommonResponse()));
ApiOperation apiOperation = method.getAnnotation(ApiOperation.class);
if (apiOperation != null) {
registerDTO.setDescription(apiOperation.value());
}
LOG.info("注册开放接口, apiInfo=" + registerDTO); LOG.info("注册开放接口, apiInfo=" + registerDTO);
RegisterResult result = apiRegisterService.register(registerDTO); RegisterResult result = apiRegisterService.register(registerDTO);
if (!result.getSuccess()) { if (!result.getSuccess()) {

View File

@@ -61,5 +61,10 @@ public class RegisterDTO implements Serializable {
*/ */
private Integer isNeedToken; private Integer isNeedToken;
/**
* 是否有公共响应参数
*/
private Integer hasCommonResponse;
} }

View File

@@ -20,6 +20,11 @@
<artifactId>sop-service-support</artifactId> <artifactId>sop-service-support</artifactId>
<version>5.0.0-SNAPSHOT</version> <version>5.0.0-SNAPSHOT</version>
</dependency> </dependency>
<dependency>
<groupId>com.gitee.sop</groupId>
<artifactId>sop-doc-plugin</artifactId>
<version>5.0.0-SNAPSHOT</version>
</dependency>
<dependency> <dependency>
<groupId>org.springframework.boot</groupId> <groupId>org.springframework.boot</groupId>