柏竹 柏竹
首页
后端
前端
  • 应用推荐
关于
友链
  • 分类
  • 标签
  • 归档

柏竹

奋斗柏竹
首页
后端
前端
  • 应用推荐
关于
友链
  • 分类
  • 标签
  • 归档
  • Java基础

  • JavaWeb

  • 拓展技术

  • 框架技术

  • 数据库

  • 数据结构

  • Spring

  • SpringMVC

  • SpringBoot

  • SpringClound

  • Ruoyi-Vue-Plus

    • ruoyi-vue-plus-基础功能
    • ruoyi-vue-plus-权限控制
    • ruoyi-vue-plus-表格操作
    • ruoyi-vue-plus-缓存功能
    • ruoyi-vue-plus-日志功能
    • ruoyi-vue-plus-线程相关
    • ruoyi-vue-plus-OSS功能
    • ruoyi-vue-plus-代码生成功能
    • ruoyi-vue-plus-多数据源
    • ruoyi-vue-plus-任务调度
    • ruoyi-vue-plus-监控功能
    • ruoyi-vue-plus-国际化
    • ruoyi-vue-plus-XSS功能
    • ruoyi-vue-plus-防重幂&限流 功能
    • ruoyi-vue-plus-推送功能
    • ruoyi-vue-plus-序列化功能
    • ruoyi-vue-plus-数据加密
    • ruoyi-vue-plus-单元测试
    • ruoyi-vue-plus-前端插件
    • ruoyi-vue-plus-前端工具篇
    • ruoyi-vue-plus-部署篇
    • ruoyi-vue-plus-前端篇
    • ruoyi-vue-plus-后端工具篇
    • ruoyi-vue-plus-框架篇
      • 整合 MyBaits Plus
        • 元数据自动填充
        • 逻辑删除
        • 乐观锁
        • 条件构造器
      • 整合 Redis
      • 整合 Sa-Token
        • 拦截器注册
        • 排除拦截
        • 用户权限管理
        • Dao缓存
        • 拦截原理
      • 整合 日志监听
        • Slf4j
        • p6spy
      • 整合 Undertow
      • 整合 OpenApi
        • 源码
        • 重写功能
        • 使用方式
        • 初始应用
        • 模块引用
        • apifox自动导入
    • ruoyi-vue-plus-问题解决
  • 后端
  • Ruoyi-Vue-Plus
柏竹
2023-11-14
目录

ruoyi-vue-plus-框架篇

# 整合 MyBaits Plus

官方文档 : MyBaits Plus (opens new window)

# 元数据自动填充

实体类 @TableField(fill = FieldFill.xxx) 填充策略

通过 CreateAndUpdateMetaObjectHandler类 实现元数据操作填充策略 , 重写 插入/更新 方法实现

  • insertFill()
  • updateFill()
点击代码展开
@Slf4j
public class CreateAndUpdateMetaObjectHandler implements MetaObjectHandler {

   @Override
   public void insertFill(MetaObject metaObject) {
       try {
           if (ObjectUtil.isNotNull(metaObject) && metaObject.getOriginalObject() instanceof BaseEntity) {
               BaseEntity baseEntity = (BaseEntity) metaObject.getOriginalObject();
               Date current = ObjectUtil.isNotNull(baseEntity.getCreateTime())
                   ? baseEntity.getCreateTime() : new Date();
               baseEntity.setCreateTime(current);
               baseEntity.setUpdateTime(current);
               String username = StringUtils.isNotBlank(baseEntity.getCreateBy())
                   ? baseEntity.getCreateBy() : getLoginUsername();
               // 当前已登录 且 创建人为空 则填充
               baseEntity.setCreateBy(username);
               // 当前已登录 且 更新人为空 则填充
               baseEntity.setUpdateBy(username);
           }
       } catch (Exception e) {
           throw new ServiceException("自动注入异常 => " + e.getMessage(), HttpStatus.HTTP_UNAUTHORIZED);
       }
   }

   @Override
   public void updateFill(MetaObject metaObject) {
       try {
           if (ObjectUtil.isNotNull(metaObject) && metaObject.getOriginalObject() instanceof BaseEntity) {
               BaseEntity baseEntity = (BaseEntity) metaObject.getOriginalObject();
               Date current = new Date();
               // 更新时间填充(不管为不为空)
               baseEntity.setUpdateTime(current);
               String username = getLoginUsername();
               // 当前已登录 更新人填充(不管为不为空)
               if (StringUtils.isNotBlank(username)) {
                   baseEntity.setUpdateBy(username);
               }
           }
       } catch (Exception e) {
           throw new ServiceException("自动注入异常 => " + e.getMessage(), HttpStatus.HTTP_UNAUTHORIZED);
       }
   }

   /**
    * 获取登录用户名
    */
   private String getLoginUsername() {
       LoginUser loginUser;
       try {
           loginUser = LoginHelper.getLoginUser();
       } catch (Exception e) {
           log.warn("自动注入警告 => 用户未登录");
           return null;
       }
       return ObjectUtil.isNotNull(loginUser) ? loginUser.getUsername() : null;
   }

}

# 逻辑删除

自行 在库添加 删除标识字段 , 对象实体类 删除成员变量 需要 @TableLogic注解 生效

官方文档 : MyBaits Plus 逻辑删除 (opens new window)

# 乐观锁

乐观锁根据 库字段进行判断是否修改 , 每次修改仅一个线程操作

官方文档 : MyBatis Plus 乐观锁 (opens new window)

应用

  1. 添加配置插件 , MybatisPlusConfig配置类

    @Bean
    public MybatisPlusInterceptor mybatisPlusInterceptor() {
        MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
        // 数据权限处理
        interceptor.addInnerInterceptor(dataPermissionInterceptor());
        // 分页插件
        interceptor.addInnerInterceptor(paginationInnerInterceptor());
        // 乐观锁插件
        interceptor.addInnerInterceptor(optimisticLockerInnerInterceptor());
        return interceptor;
    }
    
    /**
     * 乐观锁插件
     */
    public OptimisticLockerInnerInterceptor optimisticLockerInnerInterceptor() {
        return new OptimisticLockerInnerInterceptor();
    }
    
  2. 自行 在库添加 版本标识字段 , 对象实体类 版本成员变量 需要 @Version注解 生效

# 条件构造器

执行SQL 可以根据约束得到预期想要的结果 , MyBatis Plus 提供了如下两个约束的类 :

  • QueryWrapper<T>
  • LambdaQueryWrapper<T>

两者用法差不多 , 但唯独不同的就是 , 指定约束字段的参数 , LambdaQueryWrapper可根据 实体类的方法指定 , 而另一个通过 库字段名 指定 , 剩下的懂得都懂

官方文档 : MyBatis Plus 条件构造器 (opens new window)

对象属性

  • SqlSegmet (SQL约束片段)
  • TargetSql (简写 目标SQL)
  • ParamAlias (参数别名)
  • ParamNameValuePairs (参数变量&变量值)
  • CustomSqlSegment (完整SQL)

条件构造器转化

有些情况 LambdaQueryWrapper 不能满足更多功能的应用 , 因此采用 QueryWrapper , 可以通过以下方式 两种混合应用

QueryWrapper<T> qw = new QueryWrapper<>();
...
LambdaQueryWrapper<T> law = qw.lambda();

自定义Select拓展

在默认情况下查询仅查询实体类 , select 拓展其他字段应用则会查不到 , 解决方案 :

  • 在实体类添加成员变量 , 且SQL字段的 别名和该类的成员变量名一致
  • 采用Maps形式查询接收变量 , 但要自行解决对象转化环节

# 整合 Redis

采用Redisson通信 , 其优点就不用多说了

依赖

  • redisson-spring-boot-starter
  • lock4j-redisson-spring-boot-starter
  • redisson-spring-data-27

配置

映射配置文件 : RedissonProperties

配置类 : RedisConfig

应用 :

RedissonClient CLIENT = SpringUtils.getBean(RedissonClient.class);

工具类 : RedisUtils (应用实例点击跳转)

监听

订阅监听 生效 需要修改 Reids配置 notify-keyspace-events , 其配置值 :

  1. K: 开启键空间事件通知 , 发布在__keyspace@<db>__:*频道上
  2. E: 开启键事件事件通知 , 发布在__keyevent@<db>__:*频道上
  3. g: 开启一般命令事件通知, 通常是不指定对象类型的命令 , 例如DEL , RENAME等等
  4. $: 开启操作字符串命令事件通知
  5. l: 开启操作列表对象事件通知
  6. s: 开启操作集合对象事件通知
  7. h: 开启操作散列对象事件通知
  8. z: 开启操作有序集合对象事件通知
  9. x: 开启键的过期事件通知
  10. e: 开启键的淘汰事件通知
  11. A: 等价于g$lshzxe , 如果notify-keyspace-events被设置为AKE事件 , 则意味着开启所有键以及所有事件的通知。

修改配置后需要重启Reids服务生效配置

# 整合 Sa-Token

身份权限认证 等...

官方文档 : https://sa-token.cc (opens new window)

# 拦截器注册

com.ruoyi.framework.config.SaTokenConfig

对所有接口都进行登录拦截校验

@RequiredArgsConstructor
@Slf4j
@Configuration
public class SaTokenConfig implements WebMvcConfigurer {

    private final SecurityProperties securityProperties;

    /**
     * 注册sa-token的拦截器
     */
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        // 注册路由拦截器,自定义验证规则
        registry.addInterceptor(new SaInterceptor(handler -> {
            AllUrlHandler allUrlHandler = SpringUtils.getBean(AllUrlHandler.class);
            // 登录验证 -- 排除多个路径
            SaRouter
                // 获取所有的
                .match(allUrlHandler.getUrls())
                // 对未排除的路径进行检查
                .check(() -> {
                    // 检查是否登录 是否有token
                    StpUtil.checkLogin();
                });
        })).addPathPatterns("/**")
            // 排除不需要拦截的路径
            .excludePathPatterns(securityProperties.getExcludes());
    }
    
}

重写 saTokenDao 应用Bean , 优化缓存 , 改为Redis

# 排除拦截

指定路径不受认证权限相关约束 , 配置类中 指定排除拦截 securityProperties.getExcludes()

application.yml 配置文件

security:
  # 排除路径
  excludes:
    # 静态资源
    - /*.html
    - /**/*.html
    - /**/*.css
    - /**/*.js
    # 公共路径
    - /favicon.ico
    - /error
    # swagger 文档配置
    - /*/api-docs
    - /*/api-docs/**
    # actuator 监控配置
    - /actuator
    - /actuator/**

# 用户权限管理

Sa-Token根据 重写 StpInterface类 , 获取用户的权限信息 菜单权限标识 / 角色权限 , 以便在注解中校验使用

官方文档 : https://sa-token.cc/doc.html#/use/jur-auth (opens new window)

点击代码展开
public class SaPermissionImpl implements StpInterface {

    /**
     * 获取菜单权限列表
     */
    @Override
    public List<String> getPermissionList(Object loginId, String loginType) {
        LoginUser loginUser = LoginHelper.getLoginUser();
        UserType userType = UserType.getUserType(loginUser.getUserType());
        if (userType == UserType.SYS_USER) {
            return new ArrayList<>(loginUser.getMenuPermission());
        } else if (userType == UserType.APP_USER) {
            // 其他端 自行根据业务编写
        }
        return new ArrayList<>();
    }

    /**
     * 获取角色权限列表
     */
    @Override
    public List<String> getRoleList(Object loginId, String loginType) {
        LoginUser loginUser = LoginHelper.getLoginUser();
        UserType userType = UserType.getUserType(loginUser.getUserType());
        if (userType == UserType.SYS_USER) {
            return new ArrayList<>(loginUser.getRolePermission());
        } else if (userType == UserType.APP_USER) {
            // 其他端 自行根据业务编写
        }
        return new ArrayList<>();
    }
}

# Dao缓存

Sa-Token 持久层缓存是基于 本机内存Map存储 , 一旦重启宕机则会丢失数据

因此整合了 Redis缓存应用

点击代码展开
public class PlusSaTokenDao implements SaTokenDao {

    /**
     * 获取Value,如无返空
     */
    @Override
    public String get(String key) {
        return RedisUtils.getCacheObject(key);
    }

    /**
     * 写入Value,并设定存活时间 (单位: 秒)
     */
    @Override
    public void set(String key, String value, long timeout) {
        if (timeout == 0 || timeout <= SaTokenDao.NOT_VALUE_EXPIRE) {
            return;
        }
        // 判断是否为永不过期
        if (timeout == SaTokenDao.NEVER_EXPIRE) {
            RedisUtils.setCacheObject(key, value);
        } else {
            RedisUtils.setCacheObject(key, value, Duration.ofSeconds(timeout));
        }
    }

    /**
     * 修修改指定key-value键值对 (过期时间不变)
     */
    @Override
    public void update(String key, String value) {
        long expire = getTimeout(key);
        // -2 = 无此键
        if (expire == SaTokenDao.NOT_VALUE_EXPIRE) {
            return;
        }
        this.set(key, value, expire);
    }

    /**
     * 删除Value
     */
    @Override
    public void delete(String key) {
        RedisUtils.deleteObject(key);
    }

    /**
     * 获取Value的剩余存活时间 (单位: 秒)
     */
    @Override
    public long getTimeout(String key) {
        long timeout = RedisUtils.getTimeToLive(key);
        return timeout < 0 ? timeout : timeout / 1000;
    }

    /**
     * 修改Value的剩余存活时间 (单位: 秒)
     */
    @Override
    public void updateTimeout(String key, long timeout) {
        // 判断是否想要设置为永久
        if (timeout == SaTokenDao.NEVER_EXPIRE) {
            long expire = getTimeout(key);
            if (expire == SaTokenDao.NEVER_EXPIRE) {
                // 如果其已经被设置为永久,则不作任何处理
            } else {
                // 如果尚未被设置为永久,那么再次set一次
                this.set(key, this.get(key), timeout);
            }
            return;
        }
        RedisUtils.expire(key, Duration.ofSeconds(timeout));
    }


    /**
     * 获取Object,如无返空
     */
    @Override
    public Object getObject(String key) {
        return RedisUtils.getCacheObject(key);
    }

    /**
     * 写入Object,并设定存活时间 (单位: 秒)
     */
    @Override
    public void setObject(String key, Object object, long timeout) {
        if (timeout == 0 || timeout <= SaTokenDao.NOT_VALUE_EXPIRE) {
            return;
        }
        // 判断是否为永不过期
        if (timeout == SaTokenDao.NEVER_EXPIRE) {
            RedisUtils.setCacheObject(key, object);
        } else {
            RedisUtils.setCacheObject(key, object, Duration.ofSeconds(timeout));
        }
    }

    /**
     * 更新Object (过期时间不变)
     */
    @Override
    public void updateObject(String key, Object object) {
        long expire = getObjectTimeout(key);
        // -2 = 无此键
        if (expire == SaTokenDao.NOT_VALUE_EXPIRE) {
            return;
        }
        this.setObject(key, object, expire);
    }

    /**
     * 删除Object
     */
    @Override
    public void deleteObject(String key) {
        RedisUtils.deleteObject(key);
    }

    /**
     * 获取Object的剩余存活时间 (单位: 秒)
     */
    @Override
    public long getObjectTimeout(String key) {
        long timeout = RedisUtils.getTimeToLive(key);
        return timeout < 0 ? timeout : timeout / 1000;
    }

    /**
     * 修改Object的剩余存活时间 (单位: 秒)
     */
    @Override
    public void updateObjectTimeout(String key, long timeout) {
        // 判断是否想要设置为永久
        if (timeout == SaTokenDao.NEVER_EXPIRE) {
            long expire = getObjectTimeout(key);
            if (expire == SaTokenDao.NEVER_EXPIRE) {
                // 如果其已经被设置为永久,则不作任何处理
            } else {
                // 如果尚未被设置为永久,那么再次set一次
                this.setObject(key, this.getObject(key), timeout);
            }
            return;
        }
        RedisUtils.expire(key, Duration.ofSeconds(timeout));
    }


    /**
     * 搜索数据
     */
    @Override
    public List<String> searchData(String prefix, String keyword, int start, int size, boolean sortType) {
        Collection<String> keys = RedisUtils.keys(prefix + "*" + keyword + "*");
        List<String> list = new ArrayList<>(keys);
        return SaFoxUtil.searchList(list, start, size, sortType);
    }
}

# 拦截原理

首先分析接口请求被拦截的过程 : (仅展示核心部分)

  1. 进入前置拦截器 SaInterceptor.preHandle()
  2. 判断方法是否注解了 @SaIgnore (跳过整个Sa-Token拦截) SaStrategy.instance.isAnnotationPresent.apply(method, SaIgnore.class)
  3. 鉴权权限注解 (所有相关注解校验) SaStrategy.instance.checkMethodAnnotation.accept(method);
    1. 对 类/方法 的所有相关注解校验
    2. 逐个判断注解对象是否存在 , 存在则进行响应的注解校验
  4. 最后校验 auth.run(handler); , 此方法会调用会 SaInterceptor.auth(构造方法中的lambda表达式)
  5. 获取所有路径 AllUrlHandler.getUrls() (在Bean中调取) 并匹配当中获取的路径
  6. 校验是否有token StpUtil.checkLogin();

Sa-Token 自定义拦截类cn.dev33.satoken.interceptor.SaInterceptor

点击代码展开
public class SaInterceptor implements HandlerInterceptor {

    public SaInterceptor(SaParamFunction<Object> auth) {
		this.auth = auth;
	}
    
	/**
	 * 每次请求之前触发的方法 
	 */
	@Override
	@SuppressWarnings("all")
	public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler)
			throws Exception {

		try {

			// 这里必须确保 handler 是 HandlerMethod 类型时,才能进行注解鉴权
			if(isAnnotation && handler instanceof HandlerMethod) {
				
				// 获取此请求对应的 Method 处理函数 
				Method method = ((HandlerMethod) handler).getMethod();

				// 如果此 Method 或其所属 Class 标注了 @SaIgnore,则忽略掉鉴权
				if(SaStrategy.instance.isAnnotationPresent.apply(method, SaIgnore.class)) {
					// 注意这里直接就退出整个鉴权了,最底部的 auth.run() 路由拦截鉴权也被跳出了
					return true;
				}

				// 执行注解鉴权
				SaStrategy.instance.checkMethodAnnotation.accept(method);
			}
			
			// Auth 路由拦截鉴权校验
			auth.run(handler);
			
		} catch (StopMatchException e) {
			// StopMatchException 异常代表:停止匹配,进入Controller

		} catch (BackResultException e) {
			// BackResultException 异常代表:停止匹配,向前端输出结果
			// 		请注意此处默认 Content-Type 为 text/plain,如果需要返回 JSON 信息,需要在 back 前自行设置 Content-Type 为 application/json
			// 		例如:SaHolder.getResponse().setHeader("Content-Type", "application/json;charset=UTF-8");
			if(response.getContentType() == null) {
				response.setContentType("text/plain; charset=utf-8"); 
			}
			response.getWriter().print(e.getMessage());
			return false;
		}
		
		// 通过验证 
		return true;
	}
}

Sa-Token 策略对象 cn.dev33.satoken.strategy.SaStrategy

  1. 鉴定注解主要入口 SaStrategy.instance.checkMethodAnnotation.accept(method);
  2. 分别校验 类 和 方法 (类能够对当前Controller中的所有接口校验生效)
  3. 调用 checkElementAnnotation 进行处理不同的校验类型
  4. ...
点击代码展开
public final class SaStrategy {
	
    /**
	 * 获取 SaStrategy 对象的单例引用
	 */
	public static final SaStrategy instance = new SaStrategy();
    
	/**
	 * 对一个 [Method] 对象进行注解校验 (注解鉴权内部实现)
	 */
	public SaCheckMethodAnnotationFunction checkMethodAnnotation = (method) -> {

		// 先校验 Method 所属 Class 上的注解
		instance.checkElementAnnotation.accept(method.getDeclaringClass());

		// 再校验 Method 上的注解
		instance.checkElementAnnotation.accept(method);
	};

	/**
	 * 对一个 [元素] 对象进行注解校验 (注解鉴权内部实现)
	 */
	public SaCheckElementAnnotationFunction checkElementAnnotation = (element) -> {

		// 校验 @SaCheckLogin 注解
		SaCheckLogin checkLogin = (SaCheckLogin) SaStrategy.instance.getAnnotation.apply(element, SaCheckLogin.class);
		if(checkLogin != null) {
			SaManager.getStpLogic(checkLogin.type(), false).checkByAnnotation(checkLogin);
		}

		// 校验 @SaCheckRole 注解
		SaCheckRole checkRole = (SaCheckRole) SaStrategy.instance.getAnnotation.apply(element, SaCheckRole.class);
		if(checkRole != null) {
			SaManager.getStpLogic(checkRole.type(), false).checkByAnnotation(checkRole);
		}

		// 校验 @SaCheckPermission 注解
		SaCheckPermission checkPermission = (SaCheckPermission) SaStrategy.instance.getAnnotation.apply(element, SaCheckPermission.class);
		if(checkPermission != null) {
			SaManager.getStpLogic(checkPermission.type(), false).checkByAnnotation(checkPermission);
		}

		// 校验 @SaCheckSafe 注解
		SaCheckSafe checkSafe = (SaCheckSafe) SaStrategy.instance.getAnnotation.apply(element, SaCheckSafe.class);
		if(checkSafe != null) {
			SaManager.getStpLogic(checkSafe.type(), false).checkByAnnotation(checkSafe);
		}

		// 校验 @SaCheckDisable 注解
		SaCheckDisable checkDisable = (SaCheckDisable) SaStrategy.instance.getAnnotation.apply(element, SaCheckDisable.class);
		if(checkDisable != null) {
			SaManager.getStpLogic(checkDisable.type(), false).checkByAnnotation(checkDisable);
		}

		// 校验 @SaCheckBasic 注解
		SaCheckBasic checkBasic = (SaCheckBasic) SaStrategy.instance.getAnnotation.apply(element, SaCheckBasic.class);
		if(checkBasic != null) {
			SaBasicUtil.check(checkBasic.realm(), checkBasic.account());
		}

		// 校验 @SaCheckOr 注解
		SaCheckOr checkOr = (SaCheckOr) SaStrategy.instance.getAnnotation.apply(element, SaCheckOr.class);
		if(checkOr != null) {
			SaStrategy.instance.checkOrAnnotation.accept(checkOr);
		}
	};
}

# 整合 日志监听

# Slf4j

SLF4j官方文档 :

  • https://springdoc.cn/spring-boot/features#features.logging (opens new window)
  • https://www.slf4j.org/manual.html (opens new window)
  • https://www.slf4j.org/apidocs/index.html (opens new window)

Maven依赖

<dependency>
    <groupId>org.projectlombok</groupId>
    <artifactId>lombok</artifactId>
    <version>1.18.30</version>
</dependency>

日志配置

# 日志配置
logging:
  level:
    com.ruoyi: @logging.level@
    org.springframework: warn
    com.bozhu: trace
  config: classpath:logback-plus.xml

环境变量配置

点击展开 **pom.xml**
<!-- 其他省略 -->
<profiles>
    <profile>
        <id>local</id>
        <properties>
            <!-- 环境标识,需要与配置文件的名称相对应 -->
            <profiles.active>local</profiles.active>
            <logging.level>debug</logging.level>
        </properties>
    </profile>
    <profile>
        <id>dev</id>
        <properties>
            <!-- 环境标识,需要与配置文件的名称相对应 -->
            <profiles.active>dev</profiles.active>
            <logging.level>debug</logging.level>
        </properties>
        <activation>
            <!-- 默认环境 -->
            <activeByDefault>true</activeByDefault>
        </activation>
    </profile>
    <profile>
        <id>prod</id>
        <properties>
            <profiles.active>prod</profiles.active>
            <logging.level>warn</logging.level>
        </properties>
    </profile>
</profiles>

日志打印样式 , 以及文件存储路径

点击展开 **resources/logback-plus.xml**
<?xml version="1.0" encoding="UTF-8"?>
<configuration>
    <!-- 存储路径 -->
    <property name="log.path" value="./logs"/>
    <!-- 红色日期 绿色线程 高亮级别(5个字符空间) 洋红色类名 日志信息 -->
    <property name="console.log.pattern"
              value="%red(%d{yyyy-MM-dd HH:mm:ss}) %green([%thread]) %highlight(%-5level) %boldMagenta(%logger{36}%n) - %msg%n"/>
    <!-- 去掉高亮颜色 -->
    <property name="log.pattern" value="%d{yyyy-MM-dd HH:mm:ss} [%thread] %-5level %logger{36} - %msg%n"/>

    <!-- appender日志输出组件 -->
    <!-- 控制台输出 -->
    <appender name="console" class="ch.qos.logback.core.ConsoleAppender">
        <!-- 控制台输出格式(引用上面格式) -->
        <encoder>
            <pattern>${console.log.pattern}</pattern>
            <charset>utf-8</charset>
        </encoder>
    </appender>

    <!-- 控制台输出 -->
    <appender name="file_console" class="ch.qos.logback.core.rolling.RollingFileAppender">
        <file>${log.path}/sys-console.log</file>
        <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
            <!-- 日志文件名格式 -->
            <fileNamePattern>${log.path}/sys-console.%d{yyyy-MM-dd}.log</fileNamePattern>
            <!-- 日志最大 1天 -->
            <maxHistory>1</maxHistory>
        </rollingPolicy>
        <encoder>
            <pattern>${log.pattern}</pattern>
            <charset>utf-8</charset>
        </encoder>
        <filter class="ch.qos.logback.classic.filter.ThresholdFilter">
            <!-- 过滤的级别 -->
            <level>INFO</level>
        </filter>
    </appender>

    <!-- 系统日志输出 -->
    <appender name="file_info" class="ch.qos.logback.core.rolling.RollingFileAppender">
        <file>${log.path}/sys-info.log</file>
        <!-- 循环政策:基于时间创建日志文件 -->
        <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
            <!-- 日志文件名格式 -->
            <fileNamePattern>${log.path}/sys-info.%d{yyyy-MM-dd}.log</fileNamePattern>
            <!-- 日志最大的历史 60天 -->
            <maxHistory>60</maxHistory>
        </rollingPolicy>
        <encoder>
            <pattern>${log.pattern}</pattern>
        </encoder>
        <filter class="ch.qos.logback.classic.filter.LevelFilter">
            <!-- 过滤的级别 -->
            <level>INFO</level>
            <!-- 匹配时的操作:接收(记录) -->
            <onMatch>ACCEPT</onMatch>
            <!-- 不匹配时的操作:拒绝(不记录) -->
            <onMismatch>DENY</onMismatch>
        </filter>
    </appender>

    <appender name="file_error" class="ch.qos.logback.core.rolling.RollingFileAppender">
        <file>${log.path}/sys-error.log</file>
        <!-- 循环政策:基于时间创建日志文件 -->
        <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
            <!-- 日志文件名格式 -->
            <fileNamePattern>${log.path}/sys-error.%d{yyyy-MM-dd}.log</fileNamePattern>
            <!-- 日志最大的历史 60天 -->
            <maxHistory>60</maxHistory>
        </rollingPolicy>
        <encoder>
            <pattern>${log.pattern}</pattern>
        </encoder>
        <filter class="ch.qos.logback.classic.filter.LevelFilter">
            <!-- 过滤的级别 -->
            <level>ERROR</level>
            <!-- 匹配时的操作:接收(记录) -->
            <onMatch>ACCEPT</onMatch>
            <!-- 不匹配时的操作:拒绝(不记录) -->
            <onMismatch>DENY</onMismatch>
        </filter>
    </appender>

    <!-- info异步打印追加日志 -->
    <appender name="async_info" class="ch.qos.logback.classic.AsyncAppender">
        <!-- 不丢失日志.默认的,如果队列的80%已满,则会丢弃TRACT、DEBUG、INFO级别的日志 -->
        <discardingThreshold>0</discardingThreshold>
        <!-- 更改默认的队列的深度,该值会影响性能.默认值为256 -->
        <queueSize>512</queueSize>
        <!-- 添加附加的appender,最多只能添加一个 -->
        <appender-ref ref="file_info"/>
    </appender>

    <!-- error异步打印追加日志 -->
    <appender name="async_error" class="ch.qos.logback.classic.AsyncAppender">
        <!-- 不丢失日志.默认的,如果队列的80%已满,则会丢弃TRACT、DEBUG、INFO级别的日志 -->
        <discardingThreshold>0</discardingThreshold>
        <!-- 更改默认的队列的深度,该值会影响性能.默认值为256 -->
        <queueSize>512</queueSize>
        <!-- 添加附加的appender,最多只能添加一个 -->
        <appender-ref ref="file_error"/>
    </appender>

    <!-- 整合 skywalking 控制台输出 tid -->
<!--    <appender name="console" class="ch.qos.logback.core.ConsoleAppender">-->
<!--        <encoder class="ch.qos.logback.core.encoder.LayoutWrappingEncoder">-->
<!--            <layout class="org.apache.skywalking.apm.toolkit.log.logback.v1.x.TraceIdPatternLogbackLayout">-->
<!--                <pattern>[%tid] ${console.log.pattern}</pattern>-->
<!--            </layout>-->
<!--            <charset>utf-8</charset>-->
<!--        </encoder>-->
<!--    </appender>-->

    <!-- 整合 skywalking 推送采集日志 -->
<!--    <appender name="sky_log" class="org.apache.skywalking.apm.toolkit.log.logback.v1.x.log.GRPCLogClientAppender">-->
<!--        <encoder class="ch.qos.logback.core.encoder.LayoutWrappingEncoder">-->
<!--            <layout class="org.apache.skywalking.apm.toolkit.log.logback.v1.x.TraceIdPatternLogbackLayout">-->
<!--                <pattern>[%tid] ${console.log.pattern}</pattern>-->
<!--            </layout>-->
<!--            <charset>utf-8</charset>-->
<!--        </encoder>-->
<!--    </appender>-->

    <!--系统操作日志-->
    <root level="info">
        <!-- 分别引用上面的日志输出组件 -->
        <appender-ref ref="console" />
        <appender-ref ref="async_info" />
        <appender-ref ref="async_error" />
        <appender-ref ref="file_console" />
<!--        <appender-ref ref="sky_log"/>-->
    </root>

</configuration>

# p6spy

日志打印通过 p6spy实现日志打印 , 通过数据源层级进行代理监听SQL , 代理驱动 com.mysql.cj.jdbc.Driver

maven依赖

<dependency>
    <groupId>p6spy</groupId>
    <artifactId>p6spy</artifactId>
    <version>3.9.1</version>
</dependency>

配置

数据源配置 spring.datasource.dynamic.p6spy 设置为 true (生产环境不建议启用该功能)

更多细节配置 spy.properties配置文件

点击展开
# p6spy 性能分析插件配置文件
# mybatis-plus 也实现了支持p6spy的日志打印
modulelist=com.baomidou.mybatisplus.extension.p6spy.MybatisPlusLogFactory,com.p6spy.engine.outage.P6OutageFactory
# 自定义日志打印
logMessageFormat=com.baomidou.mybatisplus.extension.p6spy.P6SpyLogger
# 日志输出到控制台
appender=com.baomidou.mybatisplus.extension.p6spy.StdoutLogger
# 使用日志系统记录 sql
#appender=com.p6spy.engine.spy.appender.Slf4JLogger
# 设置 p6spy driver 代理
#deregisterdrivers=true
# 取消JDBC URL前缀
useprefix=true
# 配置记录 Log 例外,可去掉的结果集有error,info,batch,debug,statement,commit,rollback,result,resultset.
excludecategories=info,debug,result,commit,resultset
# 日期格式
dateformat=yyyy-MM-dd HH:mm:ss
# SQL语句打印时间格式
databaseDialectTimestampFormat=yyyy-MM-dd HH:mm:ss
# 实际驱动可多个
#driverlist=org.h2.Driver
# 是否开启慢SQL记录 (超过2秒记录问题)
outagedetection=true
# 慢SQL记录标准 2 秒
outagedetectioninterval=2
# 是否过滤 Log
filter=true
# 过滤 Log 时所排除的 sql 关键字,以逗号分隔 (排除检测数据库存活的SQL)
exclude=SELECT 1

# 整合 Undertow

Undertow是Java开发的高性能Web容器 , SpringBoot框架内置有Web容器 (默认采用的是 Tomcat)

Undertow对比文章 : https://cloud.tencent.com (opens new window)

引入依赖

依赖排除 Tomcat , 并且引入 undertow 依赖

ruoyi-framework模块中的 pom.xml

<!-- SpringBoot Web容器 -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
     <exclusions>
         <exclusion>
             <artifactId>spring-boot-starter-tomcat</artifactId>
             <groupId>org.springframework.boot</groupId>
         </exclusion>
     </exclusions>
</dependency>
<!-- web 容器使用 undertow 性能更强 -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-undertow</artifactId>
</dependency>

配置

ruoyi-admin模块中的 application.yml









 
 
 
 
 
 
 
 
 
 
 
 
 

# 开发环境配置
server:
  # 服务器的HTTP端口,默认为8080
  port: ${sys.port:8080}
  servlet:
    # 应用的访问路径
    context-path: /
  # undertow 配置
  undertow:
    # HTTP post内容的最大大小。当值为-1时,默认值为大小是无限的
    max-http-post-size: -1
    # 以下的配置会影响buffer,这些buffer会用于服务器连接的IO操作,有点类似netty的池化内存管理
    # 每块buffer的空间大小,越小的空间被利用越充分
    buffer-size: 512
    # 是否分配的直接内存
    direct-buffers: true
    threads:
      # 设置IO线程数, 它主要执行非阻塞的任务,它们会负责多个连接, 默认设置每个CPU核心一个线程
      io: 8
      # 阻塞任务线程池, 当执行类似servlet请求阻塞操作, undertow会从这个线程池中取得线程,它的值设置取决于系统的负载
      worker: 256

ruoyi-framework模块中的 UndertowConfig配置类

@Configuration
public class UndertowConfig implements WebServerFactoryCustomizer<UndertowServletWebServerFactory> {

    /**
     * 设置 Undertow 的 websocket 缓冲池
     */
    @Override
    public void customize(UndertowServletWebServerFactory factory) {
        // 默认不直接分配内存 如果项目中使用了 websocket 建议直接分配
        factory.addDeploymentInfoCustomizers(deploymentInfo -> {
            WebSocketDeploymentInfo webSocketDeploymentInfo = new WebSocketDeploymentInfo();
            webSocketDeploymentInfo.setBuffers(new DefaultByteBufferPool(false, 512));
            deploymentInfo.addServletContextAttribute("io.undertow.websockets.jsr.WebSocketDeploymentInfo", webSocketDeploymentInfo);
        });
    }

}

# 整合 OpenApi

采用 SpringDoc框架 , 以 OpenAPI规范生成接口信息 , 基于 Javadoc无注解形式实现

OpenAPI文档 : https://openapi.apifox.cn (opens new window)

SpringDoc文档 : https://springdoc.org/ (opens new window)

若依文档 : https://plus-doc.dromara.org (opens new window)

javadoc使用方式 (基础应用注释即可 , 更多功能注解实现)

springdoc javadoc
@Tag(name = "xxx") java类注释第一行
@Tag(description= "xxx") java类注释
@Operation java方法注释
@Hidden 无
@Parameter java方法@param参数注释
@Parameter java方法@param参数注释
@Parameters 多个@param参数注释
@Schema java实体类注释
@Schema java属性注释
@Schema(accessMode = READ_ONLY) 无
@ApiResponse java方法@return返回值注释

# 源码

涉及类

类 说明
SpringDocProperties / SpringDocConfigProperties 配置属性类 (重叠配置)
SpringDocConfig 配置类
OpenApiHandler 自定义OpenAPI处理

pom.xml依赖

<!-- 核心依赖 -->
<dependency>
    <groupId>org.springdoc</groupId>
    <artifactId>springdoc-openapi-webmvc-core</artifactId>
    <version>1.6.15</version>
</dependency>
<!-- 注释生成文档 -->
<dependency>
    <groupId>org.springdoc</groupId>
    <artifactId>springdoc-openapi-javadoc</artifactId>
    <version>1.6.15</version>
</dependency>

pom.xml插件处理

点击展开
<plugins>
    <plugin>
        <groupId>org.apache.maven.plugins</groupId>
        <artifactId>maven-compiler-plugin</artifactId>
        <version>3.9.0</version>
        <configuration>
            <source>${java.version}</source>
            <target>${java.version}</target>
            <encoding>${project.build.sourceEncoding}</encoding>
            <!-- 注解处理路径 (一旦使用上 annotationProcessorPaths 就一定要为引用中写上 , 如:lombok) -->
            <annotationProcessorPaths>
                <path>
                    <groupId>com.github.therapi</groupId>
                    <artifactId>therapi-runtime-javadoc-scribe</artifactId>
                    <version>0.15.0</version>
                </path>
                <path>
                    <groupId>org.projectlombok</groupId>
                    <artifactId>lombok</artifactId>
                    <version>${lombok.version}</version>
                </path>
                <path>
                    <groupId>org.springframework.boot</groupId>
                    <artifactId>spring-boot-configuration-processor</artifactId>
                    <version>${spring-boot.version}</version>
                </path>
            </annotationProcessorPaths>
        </configuration>
    </plugin>
    <!-- 单元测试使用 -->
    <plugin>
        <groupId>org.apache.maven.plugins</groupId>
        <artifactId>maven-surefire-plugin</artifactId>
        <version>2.22.2</version>
        <configuration>
            <argLine>-Dfile.encoding=UTF-8</argLine>
            <!-- 根据打包环境执行对应的@Tag测试方法 -->
            <groups>${profiles.active}</groups>
            <!-- 排除标签 -->
            <excludedGroups>exclude</excludedGroups>
        </configuration>
    </plugin>
</plugins>

application.yml配置文件

属性对应的类属性是重叠两个类使用 : SpringDocProperties、SpringDocConfigProperties

点击展开
springdoc:
  api-docs:
    # 是否开启接口文档
    enabled: true
#  swagger-ui:
#    # 持久化认证数据
#    persistAuthorization: true
  info:
    # 标题
    title: '标题:${ruoyi.name}后台管理系统_接口文档'
    # 描述
    description: '描述:用于管理集团旗下公司的人员信息,具体包括XXX,XXX模块...'
    # 版本
    version: '版本号: ${ruoyi-vue-plus.version}'
    # 作者信息
    contact:
      name: Lion Li
      email: crazylionli@163.com
      url: https://gitee.com/dromara/RuoYi-Vue-Plus
  components:
    # 鉴权方式配置
    security-schemes:
      apiKey:
        type: APIKEY
        in: HEADER
        name: ${sa-token.token-name}
  #这里定义了两个分组,可定义多个,也可以不定义
  group-configs:
    - group: 1.演示模块
      packages-to-scan: com.ruoyi.demo
    - group: 2.系统模块
      packages-to-scan: com.ruoyi.web
    - group: 3.代码生成模块
      packages-to-scan: com.ruoyi.generator

SpringDocCqonfig配置类

点击展开
@RequiredArgsConstructor
@Configuration
@AutoConfigureBefore(SpringDocConfiguration.class)
@ConditionalOnProperty(name = "springdoc.api-docs.enabled", havingValue = "true", matchIfMissing = true)
public class SpringDocConfig {

    private final ServerProperties serverProperties;

    /**
     * 注册 OpenAPI Bean
     * @param properties 参数注入Bean
     */
    @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());
        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) {
        return new OpenApiHandler(openAPI, securityParser, springDocConfigProperties, propertyResolverUtils, openApiBuilderCustomisers, serverBaseUrlCustomisers, javadocProvider);
    }

    /**
     * 对已经生成好的 OpenApi 进行自定义操作
     * 对含有前缀的路径进行拼接处理
     */
    @Bean
    public OpenApiCustomiser openApiCustomiser() {
        // 获取 server.servlet.context-path 配置指
        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);
        };
    }

    /**
     * 单独使用一个类便于判断 解决springdoc路径拼接重复问题
     *
     * @author Lion Li
     */
    static class PlusPaths extends Paths {

        public PlusPaths() {
            super();
        }
    }

}

# 重写功能

问题 : 导出的接口信息 , 默认采用当前类名进行作为标识名称 . 在一些情况下查阅会显得费劲

解决思路 : 继承类并重写方法 OpenAPIService#buildTags() , 替换默认形式的名称即可

OpenApiHandler类

点击展开














































































































 






















































 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 




















































@SuppressWarnings("all")
public class OpenApiHandler extends OpenAPIService {

    /**
     * The constant LOGGER.
     */
    private static final Logger LOGGER = LoggerFactory.getLogger(OpenAPIService.class);

    /**
     * The Context.
     */
    private ApplicationContext context;

    /**
     * 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, io.swagger.v3.oas.models.tags.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 Open api.
     */
    private OpenAPI openAPI;

    /**
     * The Cached open api map.
     */
    private final Map<String, OpenAPI> cachedOpenAPI = new HashMap<>();

    /**
     * The Is servers present.
     */
    private boolean isServersPresent;

    /**
     * The Server base url.
     */
    private String serverBaseUrl;

    /**
     * The Property resolver utils.
     */
    private final PropertyResolverUtils propertyResolverUtils;

    /**
     * The javadoc provider.
     */
    private final Optional<JavadocProvider> javadocProvider;

    /**
     * The Basic error controller.
     */
    private static Class<?> basicErrorController;

    static {
        try {
            //spring-boot 2
            basicErrorController = Class.forName("org.springframework.boot.autoconfigure.web.servlet.error.BasicErrorController");
        } catch (ClassNotFoundException e) {
            //spring-boot 1
            try {
                basicErrorController = Class.forName("org.springframework.boot.autoconfigure.web.BasicErrorController");
            } catch (ClassNotFoundException classNotFoundException) {
                //Basic error controller class not found
                LOGGER.trace(classNotFoundException.getMessage());
            }
        }
    }

    /**
     * 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) {
        // 保留父类具有的信息
        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;
        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
                    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);
        }

        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(methodTags.stream().map(tag -> propertyResolverUtils.resolve(tag.name(), locale)).collect(Collectors.toSet()));
            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);
            });
        });
    }

}

# 使用方式

# 初始应用

大致流程

  1. 引入依赖
  2. 配置基础信息
  3. 启用访问 https://localhost:8080/v3/api-docs (opens new window)

# 模块引用

大致流程

  1. 新建模块 , 关联模块 (opens new window) 并引入 common模块依赖
  2. 配置模块 , admin模块配置文件中 , 配置 ==springdoc.group-configs==包路径指定模块
  3. 启用访问 https://localhost:8080/v3/api-docs/{模块名} (opens new window) https://localhost:8080/v3/api-docs/1.演示模块 (opens new window)

# apifox自动导入

大致流程

打开 apifox (opens new window) -> 进入项目 -> 项目设置 -> 定时导入 -> 新建

根据 模块URL形式进行定时导入

这种形式模块化分类明确!

#ruoyi-vue-plus

← ruoyi-vue-plus-后端工具篇 ruoyi-vue-plus-问题解决→

最近更新
01
HTTPS自动续签
10-21
02
博客搭建-简化版(脚本)
10-20
03
ruoyi-vue-plus-部署篇
07-13
更多文章>
Theme by Vdoing | Copyright © 2019-2024 | 桂ICP备2022009417号-1
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式