This commit is contained in:
yuhaiming
2026-04-24 14:14:41 +08:00
parent 0e02ae4bdc
commit def9b89ecd
881 changed files with 90160 additions and 0 deletions

View File

@@ -0,0 +1,41 @@
<?xml version="1.0" encoding="UTF-8"?>
<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">
<parent>
<groupId>org.dromara</groupId>
<artifactId>water-common</artifactId>
<version>${revision}</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>water-common-doc</artifactId>
<description>
water-common-doc 系统接口
</description>
<dependencies>
<dependency>
<groupId>org.dromara</groupId>
<artifactId>water-common-core</artifactId>
</dependency>
<dependency>
<groupId>org.springdoc</groupId>
<artifactId>springdoc-openapi-starter-webmvc-api</artifactId>
</dependency>
<dependency>
<groupId>com.github.therapi</groupId>
<artifactId>therapi-runtime-javadoc</artifactId>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.module</groupId>
<artifactId>jackson-module-kotlin</artifactId>
</dependency>
</dependencies>
</project>

View File

@@ -0,0 +1,138 @@
package org.dromara.common.doc.config;
import io.swagger.v3.oas.models.OpenAPI;
import io.swagger.v3.oas.models.Paths;
import io.swagger.v3.oas.models.info.Info;
import io.swagger.v3.oas.models.security.SecurityRequirement;
import lombok.RequiredArgsConstructor;
import org.dromara.common.core.utils.StringUtils;
import org.dromara.common.doc.config.properties.SpringDocProperties;
import org.dromara.common.doc.core.resolver.JavadocResolver;
import org.dromara.common.doc.core.resolver.SaTokenAnnotationMetadataJavadocResolver;
import org.dromara.common.doc.handler.OpenApiHandler;
import org.springdoc.core.configuration.SpringDocConfiguration;
import org.springdoc.core.customizers.OpenApiBuilderCustomizer;
import org.springdoc.core.customizers.OpenApiCustomizer;
import org.springdoc.core.customizers.ServerBaseUrlCustomizer;
import org.springdoc.core.properties.SpringDocConfigProperties;
import org.springdoc.core.providers.JavadocProvider;
import org.springdoc.core.service.OpenAPIService;
import org.springdoc.core.service.SecurityService;
import org.springdoc.core.utils.PropertyResolverUtils;
import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.autoconfigure.web.ServerProperties;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
import java.util.Set;
/**
* 接口文档配置
*
* @author Lion Li
*/
@RequiredArgsConstructor
@AutoConfiguration(before = SpringDocConfiguration.class)
@EnableConfigurationProperties(SpringDocProperties.class)
@ConditionalOnProperty(name = "springdoc.api-docs.enabled", havingValue = "true", matchIfMissing = true)
public class SpringDocConfig {
private final ServerProperties serverProperties;
@Bean
@ConditionalOnMissingBean(OpenAPI.class)
public OpenAPI openApi(SpringDocProperties properties) {
OpenAPI openApi = new OpenAPI();
// 文档基本信息
SpringDocProperties.InfoProperties infoProperties = properties.getInfo();
Info info = convertInfo(infoProperties);
openApi.info(info);
// 扩展文档信息
openApi.externalDocs(properties.getExternalDocs());
openApi.tags(properties.getTags());
openApi.paths(properties.getPaths());
if (properties.getComponents() != null) {
openApi.components(properties.getComponents());
Set<String> keySet = properties.getComponents().getSecuritySchemes().keySet();
List<SecurityRequirement> list = new ArrayList<>();
SecurityRequirement securityRequirement = new SecurityRequirement();
keySet.forEach(securityRequirement::addList);
list.add(securityRequirement);
openApi.security(list);
}
return openApi;
}
private Info convertInfo(SpringDocProperties.InfoProperties infoProperties) {
Info info = new Info();
info.setTitle(infoProperties.getTitle());
info.setDescription(infoProperties.getDescription());
info.setContact(infoProperties.getContact());
info.setLicense(infoProperties.getLicense());
info.setVersion(infoProperties.getVersion());
return info;
}
/**
* 自定义 openapi 处理器
*/
@Bean
public OpenAPIService openApiBuilder(Optional<OpenAPI> openAPI,
SecurityService securityParser,
SpringDocConfigProperties springDocConfigProperties, PropertyResolverUtils propertyResolverUtils,
Optional<List<OpenApiBuilderCustomizer>> openApiBuilderCustomisers,
Optional<List<ServerBaseUrlCustomizer>> serverBaseUrlCustomisers, Optional<JavadocProvider> javadocProvider,
List<JavadocResolver> javadocResolvers) {
return new OpenApiHandler(openAPI, securityParser, springDocConfigProperties, propertyResolverUtils, openApiBuilderCustomisers, serverBaseUrlCustomisers, javadocProvider, javadocResolvers);
}
/**
* 对已经生成好的 OpenApi 进行自定义操作
*/
@Bean
public OpenApiCustomizer openApiCustomizer() {
String contextPath = serverProperties.getServlet().getContextPath();
String finalContextPath;
if (StringUtils.isBlank(contextPath) || "/".equals(contextPath)) {
finalContextPath = "";
} else {
finalContextPath = contextPath;
}
// 对所有路径增加前置上下文路径
return openApi -> {
Paths oldPaths = openApi.getPaths();
if (oldPaths instanceof PlusPaths) {
return;
}
PlusPaths newPaths = new PlusPaths();
oldPaths.forEach((k, v) -> newPaths.addPathItem(finalContextPath + k, v));
openApi.setPaths(newPaths);
};
}
/**
* 注册SaToken JavaDoc权限注解解析器
*/
@Bean
public JavadocResolver saTokenAnnotationJavadocResolver() {
return new SaTokenAnnotationMetadataJavadocResolver();
}
/**
* 单独使用一个类便于判断 解决springdoc路径拼接重复问题
*
* @author Lion Li
*/
static class PlusPaths extends Paths {
public PlusPaths() {
super();
}
}
}

View File

@@ -0,0 +1,94 @@
package org.dromara.common.doc.config.properties;
import io.swagger.v3.oas.models.Components;
import io.swagger.v3.oas.models.ExternalDocumentation;
import io.swagger.v3.oas.models.Paths;
import io.swagger.v3.oas.models.info.Contact;
import io.swagger.v3.oas.models.info.License;
import io.swagger.v3.oas.models.tags.Tag;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.boot.context.properties.NestedConfigurationProperty;
import java.util.List;
/**
* swagger 配置属性
*
* @author Lion Li
*/
@Data
@ConfigurationProperties(prefix = "springdoc")
public class SpringDocProperties {
/**
* 文档基本信息
*/
@NestedConfigurationProperty
private InfoProperties info = new InfoProperties();
/**
* 扩展文档地址
*/
@NestedConfigurationProperty
private ExternalDocumentation externalDocs;
/**
* 标签
*/
private List<Tag> tags = null;
/**
* 路径
*/
@NestedConfigurationProperty
private Paths paths = null;
/**
* 组件
*/
@NestedConfigurationProperty
private Components components = null;
/**
* <p>
* 文档的基础属性信息
* </p>
*
* @see io.swagger.v3.oas.models.info.Info
*
* 为了 springboot 自动生产配置提示信息,所以这里复制一个类出来
*/
@Data
public static class InfoProperties {
/**
* 标题
*/
private String title = null;
/**
* 描述
*/
private String description = null;
/**
* 联系人信息
*/
@NestedConfigurationProperty
private Contact contact = null;
/**
* 许可证
*/
@NestedConfigurationProperty
private License license = null;
/**
* 版本
*/
private String version = null;
}
}

View File

@@ -0,0 +1,175 @@
package org.dromara.common.doc.core.model;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.annotation.JsonInclude.Include;
import lombok.Data;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;
/**
* 存储权限框架注解解析后的权限和角色信息
*
* @author AprilWind
*/
@Data
@JsonInclude(Include.NON_EMPTY)
public class SaTokenSecurityMetadata {
/**
* 权限校验信息列表(对应 @SaCheckPermission 注解)
*/
private List<AuthInfo> permissions = new ArrayList<>();
/**
* 角色校验信息列表(对应 @SaCheckRole 注解)
*/
private List<AuthInfo> roles = new ArrayList<>();
/**
* 是否忽略校验(对应 @SaIgnore 注解)
*/
private boolean ignore = false;
/**
* 添加权限信息
*
* @param values 权限值数组
* @param mode 校验模式AND/OR
* @param type 权限类型
* @param orRoles 或角色数组
*/
public void addPermission(String[] values, String mode, String type, String[] orRoles) {
if (values != null && values.length > 0) {
AuthInfo authInfo = new AuthInfo();
authInfo.setValues(values);
authInfo.setMode(mode);
authInfo.setType(type);
if (orRoles != null && orRoles.length > 0) {
authInfo.setOrValues(orRoles);
authInfo.setOrType("role");
}
this.permissions.add(authInfo);
}
}
/**
* 添加角色信息
*
* @param values 角色值数组
* @param mode 校验模式AND/OR
* @param type 角色类型
*/
public void addRole(String[] values, String mode, String type) {
if (values != null && values.length > 0) {
AuthInfo authInfo = new AuthInfo();
authInfo.setValues(values);
authInfo.setMode(mode);
authInfo.setType(type);
this.roles.add(authInfo);
}
}
/**
* 生成 Markdown 结构的权限说明
*
* @return Markdown 文本
*/
public String toMarkdownString() {
StringBuilder sb = new StringBuilder();
sb.append("<br><h3>访问权限</h3><br>");
if (ignore) {
sb.append("> **权限策略**:忽略权限检查<br>");
return sb.toString();
}
if (!ignore && permissions.isEmpty() && roles.isEmpty()){
sb.append("> **权限策略**:需要登录<br><br>");
return sb.toString();
}
if (!permissions.isEmpty()) {
sb.append("**权限校验:**<br><br>");
permissions.forEach(p -> {
String permTags = Arrays.stream(p.getValues())
.map(v -> "`" + v + "`")
.collect(Collectors.joining(p.getModeSymbol()));
sb.append("- ").append(permTags).append("<br>");
if (p.getOrValues() != null && p.getOrValues().length > 0) {
String orTags = Arrays.stream(p.getOrValues())
.map(v -> "`" + v + "`")
.collect(Collectors.joining(p.getModeSymbol()));
sb.append(" - 或角色:").append(orTags).append("<br>");
}
});
sb.append("<br>");
}
if (!roles.isEmpty()) {
sb.append("**角色校验:**<br><br>");
roles.forEach(r -> {
String roleTags = Arrays.stream(r.getValues())
.map(v -> "`" + v + "`")
.collect(Collectors.joining(r.getModeSymbol()));
sb.append("- ").append(roleTags).append("<br>");
});
}
return sb.toString().trim();
}
/**
* 认证信息
*/
@Data
@JsonInclude(Include.NON_EMPTY)
public static class AuthInfo {
/**
* 权限或角色值数组
*/
private String[] values;
/**
* 校验模式AND/OR
*/
private String mode;
/**
* 类型说明
*/
private String type;
/**
* 或权限/角色值数组(用于权限校验时的或角色校验)
*/
private String[] orValues;
/**
* 或值的类型role/permission
*/
private String orType;
/**
* 重写mode的获取方法返回符号而非文字
* @return AND→&OR→|,默认→&
*/
public String getModeSymbol() {
if (mode == null) {
return " & "; // 默认AND返回&
}
return "AND".equalsIgnoreCase(mode) ? " & " : " | ";
}
}
}

View File

@@ -0,0 +1,163 @@
package org.dromara.common.doc.core.resolver;
import cn.hutool.core.annotation.AnnotationUtil;
import cn.hutool.core.util.ClassLoaderUtil;
import io.swagger.v3.oas.models.Operation;
import org.springframework.web.method.HandlerMethod;
import java.lang.annotation.Annotation;
import java.lang.reflect.AnnotatedElement;
import java.util.Map;
import java.util.function.Supplier;
/**
* 抽象元数据 Javadoc 解析器
*
* @param <M> 元数据类型
* @author 秋辞未寒
*/
public abstract class AbstractMetadataJavadocResolver<M> implements JavadocResolver {
public static final int HIGHEST_PRECEDENCE = Integer.MIN_VALUE;
public static final int LOWEST_PRECEDENCE = Integer.MAX_VALUE;
private final Supplier<M> metadataProvider;
private final int order;
public AbstractMetadataJavadocResolver(Supplier<M> metadataProvider) {
this(metadataProvider, LOWEST_PRECEDENCE);
}
public AbstractMetadataJavadocResolver(Supplier<M> metadataProvider, int order) {
this.metadataProvider = metadataProvider;
this.order = order;
}
@Override
public int getOrder() {
return order;
}
@Override
public String resolve(HandlerMethod handlerMethod, Operation operation) {
return resolve(handlerMethod, operation, metadataProvider.get());
}
/**
* 执行解析并返回解析到的 Javadoc 内容
* @param handlerMethod 处理器方法
* @param operation Swagger Operation实例
* @param metadata 元信息
* @return 解析到的 Javadoc 内容
*/
public abstract String resolve(HandlerMethod handlerMethod, Operation operation, M metadata);
/**
* 检查处理器方法所属的类上是否存在注解
* @param handlerMethod 处理器方法
* @param annotationClass 注解类
* @return 是否存在注解
*/
public boolean hasClassAnnotation(HandlerMethod handlerMethod,Class<? extends Annotation> annotationClass){
return AnnotationUtil.hasAnnotation(handlerMethod.getBeanType(), annotationClass);
}
/**
* 检查处理器方法所属的类上是否存在注解
* @param handlerMethod 处理器方法
* @param annotationTypeName 注解类名称
* @return 是否存在注解
*/
public boolean hasClassAnnotation(HandlerMethod handlerMethod, String annotationTypeName){
return AnnotationUtil.hasAnnotation(handlerMethod.getBeanType(), annotationTypeName);
}
/**
* 检查处理器方法上是否存在注解
* @param handlerMethod 处理器方法
* @param annotationClass 注解类
* @return 是否存在注解
*/
public boolean hasMethodAnnotation(HandlerMethod handlerMethod,Class<? extends Annotation> annotationClass){
return AnnotationUtil.hasAnnotation(handlerMethod.getMethod(), annotationClass);
}
/**
* 检查处理器方法上是否存在注解
* @param handlerMethod 处理器方法
* @param annotationTypeName 注解类名称
* @return 是否存在注解
*/
public boolean hasMethodAnnotation(HandlerMethod handlerMethod, String annotationTypeName){
return AnnotationUtil.hasAnnotation(handlerMethod.getMethod(), annotationTypeName);
}
/**
* 检查处理器方法上是否存在注解
* @param handlerMethod 处理器方法
* @param annotationClass 注解类
* @return 是否存在注解
*/
public boolean hasAnnotation(HandlerMethod handlerMethod,Class<? extends Annotation> annotationClass){
return this.hasClassAnnotation(handlerMethod, annotationClass) || this.hasMethodAnnotation(handlerMethod, annotationClass);
}
/**
* 检查处理器方法上是否存在注解
* @param handlerMethod 处理器方法
* @param annotationTypeName 注解类名称
* @return 是否存在注解
*/
public boolean hasAnnotation(HandlerMethod handlerMethod, String annotationTypeName){
return this.hasClassAnnotation(handlerMethod, annotationTypeName) || this.hasMethodAnnotation(handlerMethod, annotationTypeName);
}
/**
* 获取处理器方法所属类上的注解的值
* @param handlerMethod 处理器方法
* @param annotationClass 注解类
* @return 注解的值
*/
public Map<String, Object> getClassAnnotationValueMap(HandlerMethod handlerMethod, Class<? extends Annotation> annotationClass) {
return AnnotationUtil.getAnnotationValueMap(handlerMethod.getBeanType(), annotationClass);
}
/**
* 获取处理器方法所属类上的注解的值
* @param handlerMethod 处理器方法
* @param annotationClassName 注解类名称
* @return 注解的值
*/
@SuppressWarnings("unchecked")
public Map<String, Object> getClassAnnotationValueMap(HandlerMethod handlerMethod, String annotationClassName) {
Class<? extends Annotation> annotationClass = (Class<? extends Annotation>) ClassLoaderUtil.loadClass(annotationClassName, false);
return AnnotationUtil.getAnnotationValueMap(handlerMethod.getBeanType(), annotationClass);
}
/**
* 获取处理器方法上的注解的值
* @param handlerMethod 处理器方法
* @param annotationClass 注解类
* @return 注解的值
*/
public Map<String, Object> getMethodAnnotationValueMap(HandlerMethod handlerMethod, Class<? extends Annotation> annotationClass) {
return AnnotationUtil.getAnnotationValueMap(handlerMethod.getMethod(), annotationClass);
}
/**
* 获取处理器方法所属类上的注解的值
* @param handlerMethod 处理器方法
* @param annotationClassName 注解类名称
* @return 注解的值
*/
@SuppressWarnings("unchecked")
public Map<String, Object> getMethodAnnotationValueMap(HandlerMethod handlerMethod, String annotationClassName) {
Class<? extends Annotation> annotationClass = (Class<? extends Annotation>) ClassLoaderUtil.loadClass(annotationClassName, false);
return AnnotationUtil.getAnnotationValueMap(handlerMethod.getMethod(), annotationClass);
}
private Map<String, Object> getAnnotationValueMap(AnnotatedElement annotatedElement, Class<? extends Annotation> annotationClass) {
return AnnotationUtil.getAnnotationValueMap(annotatedElement, annotationClass);
}
}

View File

@@ -0,0 +1,51 @@
package org.dromara.common.doc.core.resolver;
import io.swagger.v3.oas.models.Operation;
import org.jetbrains.annotations.NotNull;
import org.springframework.core.Ordered;
import org.springframework.web.method.HandlerMethod;
/**
* Javadoc解析器接口
*
* @author echo
* @author 秋辞未寒
*/
public interface JavadocResolver extends Comparable<JavadocResolver>, Ordered {
/**
* 检查解析器是否支持解析 HandlerMethod
* @param handlerMethod 处理器方法
* @return 是否支持解析
*/
boolean supports(HandlerMethod handlerMethod);
/**
* 执行解析并返回解析到的 Javadoc 内容
* @param handlerMethod 处理器方法
* @param operation Swagger Operation实例
* @return 解析到的 Javadoc 内容
*/
String resolve(HandlerMethod handlerMethod, Operation operation);
/**
* 获取解析器优先级
*/
default int getOrder() {
return Ordered.LOWEST_PRECEDENCE;
}
/**
* 获取解析器的名称
*
* @return 解析器名称
*/
default String getName() {
return this.getClass().getSimpleName();
}
@Override
default int compareTo(@NotNull JavadocResolver o) {
return Integer.compare(getOrder(), o.getOrder());
}
}

View File

@@ -0,0 +1,164 @@
package org.dromara.common.doc.core.resolver;
import cn.hutool.core.convert.Convert;
import cn.hutool.core.util.ClassLoaderUtil;
import io.swagger.v3.oas.models.Operation;
import lombok.extern.slf4j.Slf4j;
import org.dromara.common.doc.core.model.SaTokenSecurityMetadata;
import org.springframework.web.method.HandlerMethod;
import java.lang.annotation.Annotation;
import java.util.List;
import java.util.Map;
import java.util.function.Supplier;
/**
* 基于JavaDoc的SaToken权限解析器
*
* @author echo
* @author 秋辞未寒
*/
@SuppressWarnings("unchecked")
@Slf4j
public class SaTokenAnnotationMetadataJavadocResolver extends AbstractMetadataJavadocResolver<SaTokenSecurityMetadata> {
/**
* 默认元数据提供者,每次解析都会创建一个新的元数据对象
*/
public static final Supplier<SaTokenSecurityMetadata> DEFAULT_METADATA_PROVIDER = SaTokenSecurityMetadata::new;
private static final String BASE_CLASS_NAME = "cn.dev33.satoken.annotation";
private static final String SA_CHECK_ROLE_CLASS_NAME = BASE_CLASS_NAME + ".SaCheckRole";
private static final String SA_CHECK_PERMISSION_CLASS_NAME = BASE_CLASS_NAME + ".SaCheckPermission";
private static final String SA_IGNORE_CLASS_NAME = BASE_CLASS_NAME + ".SaIgnore";
private static final String SA_CHECK_LOGIN_NAME = BASE_CLASS_NAME + ".SaCheckLogin";
private static final Class<? extends Annotation> SA_CHECK_ROLE_CLASS;
private static final Class<? extends Annotation> SA_CHECK_PERMISSION_CLASS;
private static final Class<? extends Annotation> SA_IGNORE_CLASS;
private static final Class<? extends Annotation> SA_CHECK_LOGIN_CLASS;
static {
// 通过类加载器去加载注解类Class实例
SA_CHECK_ROLE_CLASS = (Class<? extends Annotation>) ClassLoaderUtil.loadClass(SA_CHECK_ROLE_CLASS_NAME, false);
SA_CHECK_PERMISSION_CLASS = (Class<? extends Annotation>) ClassLoaderUtil.loadClass(SA_CHECK_PERMISSION_CLASS_NAME, false);
SA_IGNORE_CLASS = (Class<? extends Annotation>) ClassLoaderUtil.loadClass(SA_IGNORE_CLASS_NAME, false);
SA_CHECK_LOGIN_CLASS = (Class<? extends Annotation>) ClassLoaderUtil.loadClass(SA_CHECK_LOGIN_NAME, false);
if (log.isDebugEnabled()) {
log.debug("SaTokenAnnotationJavadocResolver init success, load annotation class: {}", List.of(SA_CHECK_ROLE_CLASS, SA_CHECK_PERMISSION_CLASS, SA_IGNORE_CLASS, SA_CHECK_LOGIN_CLASS));
}
}
public SaTokenAnnotationMetadataJavadocResolver() {
this(DEFAULT_METADATA_PROVIDER);
}
public SaTokenAnnotationMetadataJavadocResolver(Supplier<SaTokenSecurityMetadata> metadataProvider) {
super(metadataProvider);
}
public SaTokenAnnotationMetadataJavadocResolver(int order) {
this(DEFAULT_METADATA_PROVIDER,order);
}
public SaTokenAnnotationMetadataJavadocResolver(Supplier<SaTokenSecurityMetadata> metadataProvider, int order) {
super(metadataProvider,order);
}
@Override
public boolean supports(HandlerMethod handlerMethod) {
return hasAnnotation(handlerMethod, SA_CHECK_ROLE_CLASS) || hasAnnotation(handlerMethod, SA_CHECK_PERMISSION_CLASS) || hasAnnotation(handlerMethod, SA_IGNORE_CLASS);
}
@Override
public String resolve(HandlerMethod handlerMethod, Operation operation, SaTokenSecurityMetadata metadata) {
// 检查是否忽略校验
if(hasAnnotation(handlerMethod, SA_IGNORE_CLASS_NAME)){
metadata.setIgnore(true);
return metadata.toMarkdownString();
}
// 解析权限校验
resolvePermissionCheck(handlerMethod, metadata);
// 解析角色校验
resolveRoleCheck(handlerMethod, metadata);
return metadata.toMarkdownString();
}
/**
* 解析权限校验
*/
private void resolvePermissionCheck(HandlerMethod handlerMethod, SaTokenSecurityMetadata metadata) {
// 解析获取方法上的注解角色信息
if (hasMethodAnnotation(handlerMethod, SA_CHECK_PERMISSION_CLASS_NAME)) {
Map<String, Object> annotationValueMap = getMethodAnnotationValueMap(handlerMethod, SA_CHECK_PERMISSION_CLASS);
resolvePermissionAnnotation(metadata, annotationValueMap);
}
// 解析获取类上的注解角色信息
if (hasClassAnnotation(handlerMethod, SA_CHECK_PERMISSION_CLASS_NAME)) {
Map<String, Object> annotationValueMap = getClassAnnotationValueMap(handlerMethod, SA_CHECK_PERMISSION_CLASS);
resolvePermissionAnnotation(metadata, annotationValueMap);
}
}
/**
* 解析权限注解
*/
private void resolvePermissionAnnotation(SaTokenSecurityMetadata metadata, Map<String, Object> annotationValueMap) {
try {
// 反射获取注解属性
Object value = annotationValueMap.get( "value");
Object mode = annotationValueMap.get( "mode");
Object type = annotationValueMap.get( "type");
Object orRole = annotationValueMap.get( "orRole");
String[] values = Convert.toStrArray(value);
String modeStr = mode != null ? mode.toString() : "AND";
String typeStr = type != null ? type.toString() : "";
String[] orRoles = Convert.toStrArray(orRole);
metadata.addPermission(values, modeStr, typeStr, orRoles);
} catch (Exception ignore) {
// 忽略解析错误
}
}
/**
* 解析角色校验
*/
private void resolveRoleCheck(HandlerMethod handlerMethod, SaTokenSecurityMetadata metadata) {
// 解析获取方法上的注解角色信息
if (hasMethodAnnotation(handlerMethod, SA_CHECK_ROLE_CLASS_NAME)) {
Map<String, Object> annotationValueMap = getMethodAnnotationValueMap(handlerMethod, SA_CHECK_ROLE_CLASS);
resolveRoleAnnotation(metadata, annotationValueMap);
}
// 解析获取类上的注解角色信息
if (hasClassAnnotation(handlerMethod, SA_CHECK_ROLE_CLASS_NAME)) {
Map<String, Object> annotationValueMap = getClassAnnotationValueMap(handlerMethod, SA_CHECK_ROLE_CLASS);
resolveRoleAnnotation(metadata, annotationValueMap);
}
}
/**
* 解析角色注解
*/
private void resolveRoleAnnotation(SaTokenSecurityMetadata metadata, Map<String, Object> annotationValueMap) {
try {
// 反射获取注解属性
Object value = annotationValueMap.get("value");
Object mode = annotationValueMap.get("mode");
Object type = annotationValueMap.get("type");
String[] values = Convert.toStrArray(value);
String modeStr = mode != null ? mode.toString() : "AND";
String typeStr = type != null ? type.toString() : "";
metadata.addRole(values, modeStr, typeStr);
} catch (Exception ignore) {
// 忽略解析错误
}
}
}

View File

@@ -0,0 +1,277 @@
package org.dromara.common.doc.handler;
import cn.hutool.core.io.IoUtil;
import io.swagger.v3.core.jackson.TypeNameResolver;
import io.swagger.v3.core.util.AnnotationsUtils;
import io.swagger.v3.oas.annotations.tags.Tags;
import io.swagger.v3.oas.models.Components;
import io.swagger.v3.oas.models.OpenAPI;
import io.swagger.v3.oas.models.Operation;
import io.swagger.v3.oas.models.Paths;
import io.swagger.v3.oas.models.tags.Tag;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.dromara.common.core.utils.StreamUtils;
import org.dromara.common.doc.core.resolver.JavadocResolver;
import org.springdoc.core.customizers.OpenApiBuilderCustomizer;
import org.springdoc.core.customizers.ServerBaseUrlCustomizer;
import org.springdoc.core.properties.SpringDocConfigProperties;
import org.springdoc.core.providers.JavadocProvider;
import org.springdoc.core.service.OpenAPIService;
import org.springdoc.core.service.SecurityService;
import org.springdoc.core.utils.PropertyResolverUtils;
import org.springframework.context.ApplicationContext;
import org.springframework.core.annotation.AnnotatedElementUtils;
import org.springframework.util.CollectionUtils;
import org.springframework.web.method.HandlerMethod;
import java.io.StringReader;
import java.lang.reflect.Method;
import java.util.*;
import java.util.stream.Collectors;
import java.util.stream.Stream;
/**
* 自定义 openapi 处理器
* 对源码功能进行修改 增强使用
*/
@Slf4j
@SuppressWarnings("all")
public class OpenApiHandler extends OpenAPIService {
/**
* The Basic error controller.
*/
private static Class<?> basicErrorController;
/**
* The Security parser.
*/
private final SecurityService securityParser;
/**
* The Mappings map.
*/
private final Map<String, Object> mappingsMap = new HashMap<>();
/**
* The Springdoc tags.
*/
private final Map<HandlerMethod, Tag> springdocTags = new HashMap<>();
/**
* The Open api builder customisers.
*/
private final Optional<List<OpenApiBuilderCustomizer>> openApiBuilderCustomisers;
/**
* The server base URL customisers.
*/
private final Optional<List<ServerBaseUrlCustomizer>> serverBaseUrlCustomizers;
/**
* The Spring doc config properties.
*/
private final SpringDocConfigProperties springDocConfigProperties;
/**
* The Cached open api map.
*/
private final Map<String, OpenAPI> cachedOpenAPI = new HashMap<>();
/**
* The Property resolver utils.
*/
private final PropertyResolverUtils propertyResolverUtils;
/**
* Javadoc解析器接口
*/
private final List<JavadocResolver> javadocResolvers;
/**
* The javadoc provider.
*/
private final Optional<JavadocProvider> javadocProvider;
/**
* The Context.
*/
private ApplicationContext context;
/**
* The Open api.
*/
private OpenAPI openAPI;
/**
* The Is servers present.
*/
private boolean isServersPresent;
/**
* The Server base url.
*/
private String serverBaseUrl;
/**
* Instantiates a new Open api builder.
*
* @param openAPI the open api
* @param securityParser the security parser
* @param springDocConfigProperties the spring doc config properties
* @param propertyResolverUtils the property resolver utils
* @param openApiBuilderCustomizers the open api builder customisers
* @param serverBaseUrlCustomizers the server base url customizers
* @param javadocProvider the javadoc provider
*/
public OpenApiHandler(Optional<OpenAPI> openAPI, SecurityService securityParser,
SpringDocConfigProperties springDocConfigProperties, PropertyResolverUtils propertyResolverUtils,
Optional<List<OpenApiBuilderCustomizer>> openApiBuilderCustomizers,
Optional<List<ServerBaseUrlCustomizer>> serverBaseUrlCustomizers,
Optional<JavadocProvider> javadocProvider,
List<JavadocResolver> javadocResolvers) {
super(openAPI, securityParser, springDocConfigProperties, propertyResolverUtils, openApiBuilderCustomizers, serverBaseUrlCustomizers, javadocProvider);
if (openAPI.isPresent()) {
this.openAPI = openAPI.get();
if (this.openAPI.getComponents() == null)
this.openAPI.setComponents(new Components());
if (this.openAPI.getPaths() == null)
this.openAPI.setPaths(new Paths());
if (!CollectionUtils.isEmpty(this.openAPI.getServers()))
this.isServersPresent = true;
}
this.propertyResolverUtils = propertyResolverUtils;
this.securityParser = securityParser;
this.springDocConfigProperties = springDocConfigProperties;
this.openApiBuilderCustomisers = openApiBuilderCustomizers;
this.serverBaseUrlCustomizers = serverBaseUrlCustomizers;
this.javadocProvider = javadocProvider;
this.javadocResolvers = javadocResolvers == null ? new ArrayList<>() : javadocResolvers;
if (springDocConfigProperties.isUseFqn())
TypeNameResolver.std.setUseFqn(true);
}
@Override
public Operation buildTags(HandlerMethod handlerMethod, Operation operation, OpenAPI openAPI, Locale locale) {
Set<Tag> tags = new HashSet<>();
Set<String> tagsStr = new HashSet<>();
buildTagsFromMethod(handlerMethod.getMethod(), tags, tagsStr, locale);
buildTagsFromClass(handlerMethod.getBeanType(), tags, tagsStr, locale);
if (!CollectionUtils.isEmpty(tagsStr))
tagsStr = tagsStr.stream()
.map(str -> propertyResolverUtils.resolve(str, locale))
.collect(Collectors.toSet());
if (springdocTags.containsKey(handlerMethod)) {
io.swagger.v3.oas.models.tags.Tag tag = springdocTags.get(handlerMethod);
tagsStr.add(tag.getName());
if (openAPI.getTags() == null || !openAPI.getTags().contains(tag)) {
openAPI.addTagsItem(tag);
}
}
if (!CollectionUtils.isEmpty(tagsStr)) {
if (CollectionUtils.isEmpty(operation.getTags()))
operation.setTags(new ArrayList<>(tagsStr));
else {
Set<String> operationTagsSet = new HashSet<>(operation.getTags());
operationTagsSet.addAll(tagsStr);
operation.getTags().clear();
operation.getTags().addAll(operationTagsSet);
}
}
if (isAutoTagClasses(operation)) {
if (javadocProvider.isPresent()) {
String description = javadocProvider.get().getClassJavadoc(handlerMethod.getBeanType());
if (StringUtils.isNotBlank(description)) {
io.swagger.v3.oas.models.tags.Tag tag = new io.swagger.v3.oas.models.tags.Tag();
// 自定义部分 修改使用java注释当tag名
List<String> list = IoUtil.readLines(new StringReader(description), new ArrayList<>());
// tag.setName(tagAutoName);
tag.setName(list.get(0));
operation.addTagsItem(list.get(0));
tag.setDescription(description);
if (openAPI.getTags() == null || !openAPI.getTags().contains(tag)) {
openAPI.addTagsItem(tag);
}
}
} else {
String tagAutoName = splitCamelCase(handlerMethod.getBeanType().getSimpleName());
operation.addTagsItem(tagAutoName);
}
}
if (!CollectionUtils.isEmpty(tags)) {
// Existing tags
List<io.swagger.v3.oas.models.tags.Tag> openApiTags = openAPI.getTags();
if (!CollectionUtils.isEmpty(openApiTags))
tags.addAll(openApiTags);
openAPI.setTags(new ArrayList<>(tags));
}
// Handle SecurityRequirement at operation level
io.swagger.v3.oas.annotations.security.SecurityRequirement[] securityRequirements = securityParser
.getSecurityRequirements(handlerMethod);
if (securityRequirements != null) {
if (securityRequirements.length == 0)
operation.setSecurity(Collections.emptyList());
else
securityParser.buildSecurityRequirement(securityRequirements, operation);
}
if (javadocProvider.isPresent()) {
String description = javadocProvider.get().getMethodJavadocDescription(handlerMethod.getMethod());
String summary = javadocProvider.get().getFirstSentence(description);
if (StringUtils.isNotBlank(description)){
operation.setSummary(summary);
}
// 调用解析器提取JavaDoc中的权限信息
if (javadocResolvers != null && !javadocResolvers.isEmpty()) {
for (JavadocResolver resolver : javadocResolvers) {
String desc = resolver.resolve(handlerMethod, operation);
description = description + desc;
}
operation.setDescription(description);
}
}
return operation;
}
private void buildTagsFromMethod(Method method, Set<io.swagger.v3.oas.models.tags.Tag> tags, Set<String> tagsStr, Locale locale) {
// method tags
Set<Tags> tagsSet = AnnotatedElementUtils
.findAllMergedAnnotations(method, Tags.class);
Set<io.swagger.v3.oas.annotations.tags.Tag> methodTags = tagsSet.stream()
.flatMap(x -> Stream.of(x.value())).collect(Collectors.toSet());
methodTags.addAll(AnnotatedElementUtils.findAllMergedAnnotations(method, io.swagger.v3.oas.annotations.tags.Tag.class));
if (!CollectionUtils.isEmpty(methodTags)) {
tagsStr.addAll(StreamUtils.toSet(methodTags, tag -> propertyResolverUtils.resolve(tag.name(), locale)));
List<io.swagger.v3.oas.annotations.tags.Tag> allTags = new ArrayList<>(methodTags);
addTags(allTags, tags, locale);
}
}
private void addTags(List<io.swagger.v3.oas.annotations.tags.Tag> sourceTags, Set<io.swagger.v3.oas.models.tags.Tag> tags, Locale locale) {
Optional<Set<io.swagger.v3.oas.models.tags.Tag>> optionalTagSet = AnnotationsUtils
.getTags(sourceTags.toArray(new io.swagger.v3.oas.annotations.tags.Tag[0]), true);
optionalTagSet.ifPresent(tagsSet -> {
tagsSet.forEach(tag -> {
tag.name(propertyResolverUtils.resolve(tag.getName(), locale));
tag.description(propertyResolverUtils.resolve(tag.getDescription(), locale));
if (tags.stream().noneMatch(t -> t.getName().equals(tag.getName())))
tags.add(tag);
});
});
}
}

View File

@@ -0,0 +1 @@
org.dromara.common.doc.config.SpringDocConfig