若依框架(RuoYi)是一套基于Java开发的快速开发框架,它提供了许多常用的功能模块和工具,包括用户管理、部门管理、角色管理、菜单管理、字典管理、系统监控、定时任务等。若依框架采用了MVC(Model-View-Controller)的架构模式,使用了Spring Boot、MyBatis等流行的开源框架,可以帮助开发者快速搭建企业级的后台管理系统。若依框架还提供了许多可视化的操作界面,使得开发者可以方便地进行系统配置和管理。

官网地址:若依框架

参考文章:

ruoyi架构那点事

若依框架(前后端分离版)

若依介绍

定义

若依(RuoYi)是一款基于Spring Boot和MyBatis的Java快速开发框架,主要用于企业级应用系统的开发。它提供了一套完整的后台管理系统解决方案,包括用户权限管理、菜单配置、数据监控等功能,同时支持代码生成器,显著提升开发效率。

核心特点

  • 模块化设计:前后端分离架构,前端可选Vue2/Vue3版本,后端基于Spring Boot。
  • 代码生成器:通过可视化配置自动生成CRUD代码,减少重复劳动。
  • 权限控制:基于RBAC(角色基于访问控制)模型,精细化管理菜单、按钮权限。
  • 多数据源支持:轻松配置动态数据源,适应复杂业务场景。

技术栈

  • 后端:Spring Boot、MyBatis、Shiro(安全框架)、Redis(缓存)。
  • 前端:Vue.js、Element UI(Vue2版本)或Ant Design Vue(Vue3版本)。
  • 数据库:支持MySQL、Oracle、SQL Server等主流数据库

环境要求

JDK8 以上,mysql,Redis(Redis 配置_redis教程),Maven,Vue

提供的功能

系统管理

用户管理:用户是系统操作者,该功能主要完成系统用户配置。
部门管理:配置系统组织机构(公司、部门、小组),树结构展现支持数据权限。
岗位管理:配置系统用户所属担任职务。
菜单管理:配置系统菜单,操作权限,按钮权限标识等。
角色管理:角色菜单权限分配、设置角色按机构进行数据范围权限划分。
字典管理:对系统中经常使用的一些较为固定的数据进行维护。
参数管理:对系统动态配置常用参数。
通知公告:系统通知公告信息发布维护。

日志管理

操作日志:系统正常操作日志记录和查询;系统异常信息日志记录和查询。
登录日志:系统登录日志记录查询包含登录异常。

系统监控

在线用户:当前系统中活跃用户状态监控。
服务监控:监视当前系统CPU、内存、磁盘、堆栈等相关信息。
缓存监控:对系统的缓存查询,查看、清理等操作。
在线构建器:拖动表单元素生成相应的HTML代码。
连接池监视:监视当期系统数据库连接池状态,可进行分析SQL找出系统性能瓶颈。

系统工具

定时任务:在线(添加、修改、删除)任务调度包含执行结果日志。
代码生成:前后端代码的生成(java、html、xml、sql)支持CRUD下载 。
系统接口:根据业务代码自动生成相关的api接口文档。


Redis简介

Redis(Remote Dictionary Server)是一个开源的高性能键值存储系统,支持多种数据结构(如字符串、哈希、列表、集合等)。它以内存存储为主,兼具持久化功能,常用于缓存、消息队列、实时数据分析等场景。

核心特点

内存存储:数据主要存储在内存中,读写速度极快(可达10万次/秒以上)。

持久化支持:提供RDB(快照)和AOF(日志)两种方式将数据保存到磁盘。

数据结构丰富:支持字符串、哈希、列表、集合、有序集合等,适应不同业务需求。

高可用性:通过哨兵(Sentinel)和集群(Cluster)模式实现故障自动转移与横向扩展。

典型应用场景

  • 缓存加速:减轻数据库压力,提升网站响应速度。
  • 会话存储:分布式系统中共享用户会话信息。
  • 实时排行榜:利用有序集合实现分数排序。
  • 消息队列:通过列表或Stream结构实现异步任务处理。

与其他数据库对比

  • Redis vs MySQL:Redis基于内存,适合高速读写;MySQL基于磁盘,适合复杂查询与事务。
  • Redis vs Memcached:Redis支持数据持久化和更多数据结构,Memcached仅支持简单键值且无持久化。

Redis的灵活性和高性能使其成为现代分布式系统中的重要组件。

Nginx介绍

Nginx(发音为“engine-x”)是一款高性能的开源Web服务器、反向代理服务器、负载均衡器及HTTP缓存工具。最初由俄罗斯开发者Igor Sysoev设计,于2004年首次公开发布,现已成为全球最流行的Web服务器之一。

Nginx的核心功能

Web服务器:Nginx可以高效地处理静态内容(如HTML、CSS、图片),支持高并发连接,资源占用低,适合高流量场景。

反向代理:作为反向代理服务器,Nginx可将客户端请求转发至后端多台服务器,隐藏真实服务器信息,提升安全性和可扩展性。

负载均衡:支持多种负载均衡算法(如轮询、加权轮询、IP哈希),将流量分配到多台后端服务器,提高系统可用性和性能。

HTTP缓存:通过缓存静态或动态内容,减少后端服务器压力,加速响应速度。

Nginx的特点

  • 事件驱动架构:基于异步非阻塞模型,支持高并发连接(单机可处理数万并发请求)。

  • 模块化设计:通过模块扩展功能(如支持SSL、gzip压缩、HTTP/2)。

  • 低资源消耗:内存和CPU占用率远低于传统服务器(如Apache)。

  • 跨平台:支持Linux、Windows、macOS等操作系统。

登录验证码流程

首先通过后端生成一个表达式上传到前端,并且将验证码答案存放在Redis中,用户通过前端登录时将key值和输入答案一起传入数据库。

登录操作将内容保存在表中 sys_logininfor

进行增删该查操作

image-20250801170733602

单应用在resources目录下的application.yml,多模块ruoyi-generator中的resources目录下的generator.yml,可以自己根据实际情况调整默认配置。

1
2
3
4
5
6
7
8
9
10
# 代码生成
gen:
# 开发者姓名,生成到类注释上
author: ruoyi
# 默认生成包路径 system 需改成自己的模块名称 如 system monitor tool
packageName: com.ruoyi.system
# 自动去除表前缀,默认是false
autoRemovePre: false
# 表前缀(生成类名不会包含表前缀,多个用逗号分隔)
tablePrefix: sys_

代码生成使用

1、登录系统(系统工具 -> 代码生成 -> 导入对应表)

2、代码生成列表中找到需要表(可预览、编辑、同步、删除生成配置)

3、点击生成代码会得到一个ruoyi.zip执行sql文件,按照包内目录结构复制到自己的项目中即可

代码生成支持编辑、预览、同步

预览:对生成的代码提前预览,防止出现一些不符合预期的情况。

同步:对原表的字段进行同步,包括新增、删除、修改的字段处理。

修改:对生成的代码基本信息、字段信息、生成信息做一系列的调整。

另外多模块所有代码生成的相关业务逻辑代码在ruoyi-generator模块,不需要可以自行删除模块。

部署

后端部署

打包工程文件:在工程文件下打开执行bin/package.bat文件进行打包,生成war/jar文件

前端部署

当项目开发完毕,只需要运行一行命令就可以打包应用

1
2
3
4
# 打包正式环境
npm run build:prod
# 打包预发布环境
npm run build:stage

构建打包成功之后,会在根目录生成 dist 文件夹,里面就是构建打包好的文件,通常是 .js 、.css、index.html 等静态文件。

通常情况下 dist 文件夹的静态文件发布到你的 nginx 或者静态服务器即可,其中的 index.html 是后台服务的入口页面。

快速搭建若依

前端搭建

1
2
npm install
npm run dev

后端搭建

修改数据库和redis的配置。

代码介绍

image-20250801163351287

ruoyi-quartz

使用quartz作为定时任务管理,这里不多说,老生常谈的问题。如果想了解可以参考文章:quartz实现定时任务

ruoyi-generator

该包为代码生成器,主要的流程如下。

image-20250801163413997

GenTableServiceImpl/generatorCode代码生成

核心代码为该类的方法。主要使用org.apache.velocity.app的API。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
@Override
public void generatorCode(String tableName){
// 查询表信息
GenTable table = genTableMapper.selectGenTableByName(tableName);
// 设置主子表信息
setSubTable(table);
// 设置主键列信息
setPkColumn(table);
VelocityInitializer.initVelocity();
VelocityContext context = VelocityUtils.prepareContext(table);
// 获取模板列表
List<String> templates = VelocityUtils.getTemplateList(table.getTplCategory());
for (String template : templates) {
if (!StringUtils.containsAny(template, "sql.vm", "api.js.vm", "index.vue.vm", "index-tree.vue.vm")){
// 渲染模板
StringWriter sw = new StringWriter();
Template tpl = Velocity.getTemplate(template, Constants.UTF8);
tpl.merge(context, sw);
try {
String path = getGenPath(table, template);
FileUtils.writeStringToFile(new File(path), sw.toString(), CharsetKit.UTF_8);
} catch (IOException e) {
throw new ServiceException("渲染模板失败,表名:" + table.getTableName());
}
}
}
}

ruoyi-system

标准的增删改查方法。没有什么多说的。但是ruoyi项目将controller与service部分隔离开来。

ruoyi-common

image-20250801163603079

annotation

自定义注解,注解的功能在下方。

image-20250801163631130

config

获取application.yml中的配置信息,并注入项目bean中。

image-20250801163728304

constant

为项目提供常量池。

core

1.controller:所有接口层的基类,提供了分页,排序等方法,其他接口类直接继承即可。架构的常用做法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
public class BaseController {
protected final Logger logger = LoggerFactory.getLogger(this.getClass());
// 将前台传递过来的日期格式的字符串,自动转化为Date类型
@InitBinder
public void initBinder(WebDataBinder binder){
// Date 类型转换
binder.registerCustomEditor(Date.class, new PropertyEditorSupport(){
@Override
public void setAsText(String text){
setValue(DateUtils.parseDate(text));
}
});
}
// 设置请求分页数据
protected void startPage() {
PageDomain pageDomain = TableSupport.buildPageRequest();
Integer pageNum = pageDomain.getPageNum();
Integer pageSize = pageDomain.getPageSize();
if (StringUtils.isNotNull(pageNum) && StringUtils.isNotNull(pageSize)) {
String orderBy = SqlUtil.escapeOrderBySql(pageDomain.getOrderBy());
Boolean reasonable = pageDomain.getReasonable();
PageHelper.startPage(pageNum, pageSize, orderBy).setReasonable(reasonable);
}
}
// 设置请求排序数据
protected void startOrderBy() {
PageDomain pageDomain = TableSupport.buildPageRequest();
if (StringUtils.isNotEmpty(pageDomain.getOrderBy())) {
String orderBy = SqlUtil.escapeOrderBySql(pageDomain.getOrderBy());
PageHelper.orderBy(orderBy);
}
}
// 响应请求分页数据
@SuppressWarnings({ "rawtypes", "unchecked" })
protected TableDataInfo getDataTable(List<?> list) {
TableDataInfo rspData = new TableDataInfo();
rspData.setCode(HttpStatus.SUCCESS);
rspData.setMsg("查询成功");
rspData.setRows(list);
rspData.setTotal(new PageInfo(list).getTotal());
return rspData;
}
}

domain:权限等架构查询使用的实体类集合,其中BaseEntity、AjaxResult为架构设计经常使用的。BaseEntity需要其他实体类继承,提供了页码、总数等通用字段。AjaxResult是统一返回的实体类,能与前台约定固定的返回格式。{code: message: data}格式。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
public class AjaxResult extends HashMap<String, Object> {
private static final long serialVersionUID = 1L;
/** 状态码 */
public static final String CODE_TAG = "code";
/** 返回内容 */
public static final String MSG_TAG = "msg";
/** 数据对象 */
public static final String DATA_TAG = "data";
// 初始化一个新创建的 AjaxResult 对象,使其表示一个空消息。
public AjaxResult() {}
// 初始化一个新创建的 AjaxResult 对象
public AjaxResult(int code, String msg) {
super.put(CODE_TAG, code);
super.put(MSG_TAG, msg);
}
// 初始化一个新创建的 AjaxResult 对象
public AjaxResult(int code, String msg, Object data) {
super.put(CODE_TAG, code);
super.put(MSG_TAG, msg);
if (StringUtils.isNotNull(data)) {
super.put(DATA_TAG, data);
}
}
}
public class BaseEntity implements Serializable {
private static final long serialVersionUID = 1L;
/** 搜索值 */
private String searchValue;
/** 创建者 */
private String createBy;
/** 创建时间 */
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private Date createTime;
/** 更新者 */
private String updateBy;
/** 更新时间 */
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private Date updateTime;
/** 备注 */
private String remark;
/** 请求参数 */
private Map<String, Object> params;
}

exception

封装了一系列的异常,在特定时期使用即可。

image-20250801164106581

filter

过滤器,通用写法,如果有使用直接复制即可。

image-20250801164146902

utils

提供了一大波工具类。如果需要可以直接复制使用。

image-20250801164221975

ruoyi-framework

image-20250801164236304

DataScopeAspect

1.DataScopeAspect数据权限:在执行接口时,将当前用户的组织机构等查询条件利用AOP拼接上,可以看到根据在菜单页面维护的数据权限类别进行动态的拼接sql语句。核心代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public static void dataScopeFilter(JoinPoint joinPoint, SysUser user, String deptAlias, String userAlias){
// 拼接sql
StringBuilder sqlString = new StringBuilder();
for (SysRole role : user.getRoles()) {
String dataScope = role.getDataScope();
if (DATA_SCOPE_ALL.equals(dataScope)) {
sqlString = new StringBuilder();
break;
} else if (DATA_SCOPE_CUSTOM.equals(dataScope)) { // 自定义权限拼接
sqlString.append(StringUtils.format(
" OR {}.dept_id IN ( SELECT dept_id FROM sys_role_dept WHERE role_id = {} ) ",
deptAlias, role.getRoleId()));
} else if (DATA_SCOPE_DEPT.equals(dataScope)) { // 部门权限拼接
sqlString.append(StringUtils.format(" OR {}.dept_id = {} ", deptAlias, user.getDeptId()));
} else if (DATA_SCOPE_DEPT_AND_CHILD.equals(dataScope)) { // 部门及以下权限拼接
sqlString.append(StringUtils.format(
" OR {}.dept_id IN ( SELECT dept_id FROM sys_dept WHERE dept_id = {} or find_in_set( {} , ancestors ) )",
deptAlias, user.getDeptId(), user.getDeptId()));
}
}
}

2.DataSourceAspect多数据源:利用上文DataSource注解动态的切换数据源,如果有需求可以直接使用,固定写法。

3.LogAspect日志:全局日志收集,如用户姓名、接口方法、调用ip等,并插入数据库,比较通用功能。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
protected void handleLog(final JoinPoint joinPoint, Log controllerLog, 
final Exception e, Object jsonResult){
try {
// 获取当前的用户
LoginUser loginUser = SecurityUtils.getLoginUser();
// *========数据库日志=========*//
SysOperLog operLog = new SysOperLog();
operLog.setStatus(BusinessStatus.SUCCESS.ordinal());
// 请求的地址
String ip = IpUtils.getIpAddr(ServletUtils.getRequest());
operLog.setOperIp(ip);
operLog.setOperUrl(ServletUtils.getRequest().getRequestURI());
//获取用户姓名
if (loginUser != null) {
operLog.setOperName(loginUser.getUsername());
}
if (e != null) {
operLog.setStatus(BusinessStatus.FAIL.ordinal());
operLog.setErrorMsg(StringUtils.substring(e.getMessage(), 0, 2000));
}
// 设置方法名称
String className = joinPoint.getTarget().getClass().getName();
String methodName = joinPoint.getSignature().getName();
operLog.setMethod(className + "." + methodName + "()");
// 设置请求方式
operLog.setRequestMethod(ServletUtils.getRequest().getMethod());
// 处理设置注解上的参数
getControllerMethodDescription(joinPoint, controllerLog, operLog, jsonResult);
// 保存数据库
AsyncManager.me().execute(AsyncFactory.recordOper(operLog));
} catch (Exception exp) {
// 记录本地异常日志
log.error("==前置通知异常==");
log.error("异常信息:{}", exp.getMessage());
exp.printStackTrace();
}
}
}

RateLimiterAspect限流:将每一次的调用的ip存放在redis中,然后判断本次调用和上次调用的相隔时间。短时间调用会阻止调用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
@Before("@annotation(rateLimiter)")
public void doBefore(JoinPoint point, RateLimiter rateLimiter) throws Throwable {
//redis固定的参数
String key = rateLimiter.key();
int time = rateLimiter.time();
int count = rateLimiter.count();
//获取ip+调用的方法
String combineKey = getCombineKey(rateLimiter, point);
List<Object> keys = Collections.singletonList(combineKey);
try {
// 获取一定时间内的调用次数
Long number = redisTemplate.execute(limitScript, keys, count, time);
if (StringUtils.isNull(number) || number.intValue() > count) {
throw new ServiceException("访问过于频繁,请稍候再试");
}
log.info("限制请求'{}',当前请求'{}',缓存key'{}'", count, number.intValue(), key);
} catch (ServiceException e) {
throw e;
} catch (Exception e) {
throw new RuntimeException("服务器限流异常,请稍候再试");
}
}

config

image-20250801164708417

1.DruidProperties:从application.yml中获取数据源信息。固定写法不详细描述。

2.ApplicationConfig:配置时区信息这里不详细描述。

3.CaptchaConfig:验证码配置,配置文字文本框格式等,固定写法。

4.DruidConfig:多数据源配置,固定写法。

5.FastJson2JsonRedisSerializer:redis序列化配置,固定写法。

6.FilterConfig:过滤器配置,@ConditionalOnProperty(value = "xss.enabled", havingValue = "true")根据application.yml是否配置xss.enabled值决定是否加载该类,也就是是否开启xss拦截器。

7.KaptchaTextCreator:验证码验证的规则,这里是计算验证码,逻辑在此类中,不详细讲解。

8.MyBatisConfig:从application.yml动态获取mybatis包的地址。并重新封装SqlSessionFactory。实现mybatis路径的可配置化。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
@Bean
public SqlSessionFactory sqlSessionFactory(DataSource dataSource) throws Exception {
//从application.yml获取配置
String typeAliasesPackage = env.getProperty("mybatis.typeAliasesPackage");
String mapperLocations = env.getProperty("mybatis.mapperLocations");
String configLocation = env.getProperty("mybatis.configLocation");
//获取实体类的包
typeAliasesPackage = setTypeAliasesPackage(typeAliasesPackage);
VFS.addImplClass(SpringBootVFS.class);

final SqlSessionFactoryBean sessionFactory = new SqlSessionFactoryBean();
//加入数据源
sessionFactory.setDataSource(dataSource);
//加入实体类地址
sessionFactory.setTypeAliasesPackage(typeAliasesPackage);
//加入mapper
sessionFactory.setMapperLocations(resolveMapperLocations(StringUtils.split(mapperLocations, ",")));
//加入配置文件地址
sessionFactory.setConfigLocation(new DefaultResourceLoader().getResource(configLocation));
return sessionFactory.getObject();
}

9.RedisConfig:redis配置固定写法,如果整合redis可以参考。

10.ResourcesConfig:通用配置,其中包括拦截器生效配置,跨域配置等 可以直接使用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
public class ResourcesConfig implements WebMvcConfigurer {
@Autowired
private RepeatSubmitInterceptor repeatSubmitInterceptor;
@Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
/** 本地文件上传路径 */
registry.addResourceHandler(Constants.RESOURCE_PREFIX + "/**")
.addResourceLocations("file:" + RuoYiConfig.getProfile() + "/");
/** swagger配置 */
registry.addResourceHandler("/swagger-ui/**")
.addResourceLocations("classpath:/META-INF/resources/webjars/springfox-swagger-ui/");
}
// 配置拦截器生效
@Override
public void addInterceptors(InterceptorRegistry registry){ //此处配置了上文点击重复的拦截器
registry.addInterceptor(repeatSubmitInterceptor).addPathPatterns("/**");
}
// 跨域配置
@Bean
public CorsFilter corsFilter() {
CorsConfiguration config = new CorsConfiguration();
config.setAllowCredentials(true);
// 设置访问源地址
config.addAllowedOriginPattern("*");
// 设置访问源请求头
config.addAllowedHeader("*");
// 设置访问源请求方法
config.addAllowedMethod("*");
// 有效期 1800秒
config.setMaxAge(1800L);
// 添加映射路径,拦截一切请求
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", config);
// 返回新的CorsFilter
return new CorsFilter(source);
}
}

11.SecurityConfig:Security的配置,如果想详细了解,请访问博主专栏:Security入门到精通。

12.ServerConfig:获取请求信息,包括:域名,端口,上下文访问路径

13.ThreadPoolConfig:线程池配置,固定配置,其中下文的manager使用了异步线程池。

datasource

多数据源固定配置,与DataSourceAspect配合使用。

interceptor

主要功能为不允许重复点击,主要实现为将每一次的调用信息组装为key值存放到redis中,每一次调用从redis获取该key数据验证相隔时间,核心代码如下。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
public boolean isRepeatSubmit(HttpServletRequest request, RepeatSubmit annotation){
String nowParams = "";
if (request instanceof RepeatedlyRequestWrapper){
RepeatedlyRequestWrapper repeatedlyRequest = (RepeatedlyRequestWrapper) request;
nowParams = HttpHelper.getBodyString(repeatedlyRequest);
}
// body参数为空,获取Parameter的数据
if (StringUtils.isEmpty(nowParams)) {
nowParams = JSONObject.toJSONString(request.getParameterMap());
}
Map<String, Object> nowDataMap = new HashMap<String, Object>();
nowDataMap.put(REPEAT_PARAMS, nowParams);
nowDataMap.put(REPEAT_TIME, System.currentTimeMillis());
// 请求地址(作为存放cache的key值)
String url = request.getRequestURI();
// 唯一值(没有消息头则使用请求地址)
String submitKey = request.getHeader(header);
if (StringUtils.isEmpty(submitKey)) {
submitKey = url;
}
// 组装成加入redis的key值
String cacheRepeatKey = Constants.REPEAT_SUBMIT_KEY + submitKey;
//根据key值查询redsi
Object sessionObj = redisCache.getCacheObject(cacheRepeatKey);
//如果能够查询到
if (sessionObj != null) {
Map<String, Object> sessionMap = (Map<String, Object>) sessionObj;
if (sessionMap.containsKey(url)) {
Map<String, Object> preDataMap = (Map<String, Object>) sessionMap.get(url);
// 比对参数,同时比对时间
if (compareParams(nowDataMap, preDataMap)
&& compareTime(nowDataMap, preDataMap, annotation.interval())) {
return true;
}
}
}
Map<String, Object> cacheMap = new HashMap<String, Object>();
cacheMap.put(url, nowDataMap);
redisCache.setCacheObject(cacheRepeatKey, cacheMap, annotation.interval(), TimeUnit.MILLISECONDS);
return false;
}

manager

AsyncManager、ShutdownManager为异步工厂提供方法,AsyncFactory为如何使用异步,如果需要使用可以直接在类中参考编写。

security

1.JwtAuthenticationTokenFilter:主要为验证token是否正确,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public class JwtAuthenticationTokenFilter extends OncePerRequestFilter {
@Autowired
private TokenService tokenService;
@Override
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response, FilterChain chain)
throws ServletException, IOException {
// 验证用户信息
LoginUser loginUser = tokenService.getLoginUser(request);
if (StringUtils.isNotNull(loginUser) && StringUtils.isNull(SecurityUtils.getAuthentication()))
{
// 刷新token
tokenService.verifyToken(loginUser);
// 获取用户权限对象
UsernamePasswordAuthenticationToken authenticationToken = new
UsernamePasswordAuthenticationToken(loginUser, null, loginUser.getAuthorities());
authenticationToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
// 将用户权限等信息存放在SecurityContext中
SecurityContextHolder.getContext().setAuthentication(authenticationToken);
}
chain.doFilter(request, response);
}
}

2.AuthenticationEntryPointImpl、LogoutSuccessHandlerImpl:将调用信息转换为json返回的固定写法。如果有需要可以参考。

web

1.server:获取服务器信息,如cpu等,都是固定写法,如需参考直接复制即可。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
private void setCpuInfo(CentralProcessor processor) {
// CPU信息
long[] prevTicks = processor.getSystemCpuLoadTicks();
Util.sleep(OSHI_WAIT_SECOND);
long[] ticks = processor.getSystemCpuLoadTicks();
long nice = ticks[TickType.NICE.getIndex()] - prevTicks[TickType.NICE.getIndex()];
long irq = ticks[TickType.IRQ.getIndex()] - prevTicks[TickType.IRQ.getIndex()];
long softirq = ticks[TickType.SOFTIRQ.getIndex()] - prevTicks[TickType.SOFTIRQ.getIndex()];
long steal = ticks[TickType.STEAL.getIndex()] - prevTicks[TickType.STEAL.getIndex()];
long cSys = ticks[TickType.SYSTEM.getIndex()] - prevTicks[TickType.SYSTEM.getIndex()];
long user = ticks[TickType.USER.getIndex()] - prevTicks[TickType.USER.getIndex()];
long iowait = ticks[TickType.IOWAIT.getIndex()] - prevTicks[TickType.IOWAIT.getIndex()];
long idle = ticks[TickType.IDLE.getIndex()] - prevTicks[TickType.IDLE.getIndex()];
long totalCpu = user + nice + cSys + idle + iowait + irq + softirq + steal;
cpu.setCpuNum(processor.getLogicalProcessorCount());
cpu.setTotal(totalCpu);
cpu.setSys(cSys);
cpu.setUsed(user);
cpu.setWait(iowait);
cpu.setFree(idle);
}
// 设置内存信息
private void setMemInfo(GlobalMemory memory) {
mem.setTotal(memory.getTotal());
mem.setUsed(memory.getTotal() - memory.getAvailable());
mem.setFree(memory.getAvailable());
}

2.exception@RestControllerAdvice、@ExceptionHandler全局的拦截异常,当系统中有异常时,该类会直接获取异常并输出,很多类就不用特意try catch了。架构的常用写法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
@RestControllerAdvice
public class GlobalExceptionHandler {
private static final Logger log = LoggerFactory.getLogger(GlobalExceptionHandler.class);
// 权限校验异常
@ExceptionHandler(AccessDeniedException.class)
public AjaxResult handleAccessDeniedException(AccessDeniedException e, HttpServletRequest request) {
String requestURI = request.getRequestURI();
log.error("请求地址'{}',权限校验失败'{}'", requestURI, e.getMessage());
return AjaxResult.error(HttpStatus.FORBIDDEN, "没有权限,请联系管理员授权");
}
// 请求方式不支持
@ExceptionHandler(HttpRequestMethodNotSupportedException.class)
public AjaxResult handleHttpRequestMethodNotSupported(HttpRequestMethodNotSupportedException e,
HttpServletRequest request) {
String requestURI = request.getRequestURI();
log.error("请求地址'{}',不支持'{}'请求", requestURI, e.getMethod());
return AjaxResult.error(e.getMessage());
}
}

3.service:权限相关的service。大部分都是curd的业务,这里详细说TokenService。TokenService使用jwt生成token、从token中获取数据等。大部分为jwt固定写法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 从数据声明生成令牌 @param claims 数据声明 @return 令牌
private String createToken(Map<String, Object> claims) {
String token = Jwts.builder()
.setClaims(claims)
.signWith(SignatureAlgorithm.HS512, secret).compact();
return token;
}
// 从令牌中获取数据声明 @param token 令牌 @return 数据声明
private Claims parseToken(String token) {
return Jwts.parser()
.setSigningKey(secret)
.parseClaimsJws(token)
.getBody();
}

ruoyi-admin

image-20250801165556243

common通用方法

CaptchaController:获取验证码通过API生成验证码。详细代码不进行讲解。

CommonController:通用的上传下载接口,如果需要直接使用即可。

monitor监控

1.CacheController监控redis

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
public class CacheController {
@Autowired
private RedisTemplate<String, String> redisTemplate;
@PreAuthorize("@ss.hasPermi('monitor:cache:list')")
@GetMapping()
public AjaxResult getInfo() throws Exception {
// redis的常用信息
Properties info = (Properties) redisTemplate.execute(
(RedisCallback<Object>) connection -> connection.info()
);
Properties commandStats = (Properties) redisTemplate.execute(
(RedisCallback<Object>) connection -> connection.info("commandstats")
);
// rediskey数量
Object dbSize = redisTemplate.execute((RedisCallback<Object>) connection -> connection.dbSize());
Map<String, Object> result = new HashMap<>(3);
result.put("info", info);
result.put("dbSize", dbSize);
// key的详细信息
List<Map<String, String>> pieList = new ArrayList<>();
commandStats.stringPropertyNames().forEach(key -> {
Map<String, String> data = new HashMap<>(2);
String property = commandStats.getProperty(key);
data.put("name", StringUtils.removeStart(key, "cmdstat_"));
data.put("value", StringUtils.substringBetween(property, "calls=", ",usec"));
pieList.add(data);
});
result.put("commandStats", pieList);
return AjaxResult.success(result);
}
}

2.ServerController服务器监控:主要监控正在运行服务器的信息,核心代码如下,如果需要使用直接复制即可。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
public void copyTo() throws Exception {
SystemInfo si = new SystemInfo();
HardwareAbstractionLayer hal = si.getHardware();
setCpuInfo(hal.getProcessor());
setMemInfo(hal.getMemory());
setSysInfo();
setJvmInfo();
setSysFiles(si.getOperatingSystem());
}
// 设置CPU信息
private void setCpuInfo(CentralProcessor processor) {
// CPU信息
long[] prevTicks = processor.getSystemCpuLoadTicks();
Util.sleep(OSHI_WAIT_SECOND);
long[] ticks = processor.getSystemCpuLoadTicks();
long nice = ticks[TickType.NICE.getIndex()] - prevTicks[TickType.NICE.getIndex()];
long irq = ticks[TickType.IRQ.getIndex()] - prevTicks[TickType.IRQ.getIndex()];
long softirq = ticks[TickType.SOFTIRQ.getIndex()] - prevTicks[TickType.SOFTIRQ.getIndex()];
long steal = ticks[TickType.STEAL.getIndex()] - prevTicks[TickType.STEAL.getIndex()];
long cSys = ticks[TickType.SYSTEM.getIndex()] - prevTicks[TickType.SYSTEM.getIndex()];
long user = ticks[TickType.USER.getIndex()] - prevTicks[TickType.USER.getIndex()];
long iowait = ticks[TickType.IOWAIT.getIndex()] - prevTicks[TickType.IOWAIT.getIndex()];
long idle = ticks[TickType.IDLE.getIndex()] - prevTicks[TickType.IDLE.getIndex()];
long totalCpu = user + nice + cSys + idle + iowait + irq + softirq + steal;
cpu.setCpuNum(processor.getLogicalProcessorCount());
cpu.setTotal(totalCpu);
cpu.setSys(cSys);
cpu.setUsed(user);
cpu.setWait(iowait);
cpu.setFree(idle);
}
// 设置内存信息
private void setMemInfo(GlobalMemory memory) {
mem.setTotal(memory.getTotal());
mem.setUsed(memory.getTotal() - memory.getAvailable());
mem.setFree(memory.getAvailable());
}
// 设置服务器信息
private void setSysInfo() {
Properties props = System.getProperties();
sys.setComputerName(IpUtils.getHostName());
sys.setComputerIp(IpUtils.getHostIp());
sys.setOsName(props.getProperty("os.name"));
sys.setOsArch(props.getProperty("os.arch"));
sys.setUserDir(props.getProperty("user.dir"));
}
// 设置Java虚拟机
private void setJvmInfo() throws UnknownHostException {
Properties props = System.getProperties();
jvm.setTotal(Runtime.getRuntime().totalMemory());
jvm.setMax(Runtime.getRuntime().maxMemory());
jvm.setFree(Runtime.getRuntime().freeMemory());
jvm.setVersion(props.getProperty("java.version"));
jvm.setHome(props.getProperty("java.home"));
}
// 设置磁盘信息
private void setSysFiles(OperatingSystem os) {
FileSystem fileSystem = os.getFileSystem();
List<OSFileStore> fsArray = fileSystem.getFileStores();
for (OSFileStore fs : fsArray)
{
long free = fs.getUsableSpace();
long total = fs.getTotalSpace();
long used = total - free;
SysFile sysFile = new SysFile();
sysFile.setDirName(fs.getMount());
sysFile.setSysTypeName(fs.getType());
sysFile.setTypeName(fs.getName());
sysFile.setTotal(convertFileSize(total));
sysFile.setFree(convertFileSize(free));
sysFile.setUsed(convertFileSize(used));
sysFile.setUsage(Arith.mul(Arith.div(used, total, 4), 100));
sysFiles.add(sysFile);
}
}

3.SysLogininforController,SysOperlogController 登录日志、操作日志:主要查询前文AOP生成的日志表,普通的增删改查。

4.SysUserOnlineController在线用户管理:主要功能为在线用户监控与强踢下线。通过查询和删除redis缓存即可实现。

system业务代码

这里都是系统管理业务代码,写法比较统一,但是在编写过程中有部分架构的规定,下面一一说明。

**1.@PreAuthorize(“@ss.hasPermi(‘system:dict:list’)”)**:权限注解,上文已经详解

2.AjaxResult:提供了 结果编码/调用信息/数据的返回格式,为前台提供了统一的返回格式。架构的基础组成部分。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
public class AjaxResult extends HashMap<String, Object> {
private static final long serialVersionUID = 1L;
/** 状态码 */
public static final String CODE_TAG = "code";
/** 返回内容 */
public static final String MSG_TAG = "msg";
/** 数据对象 */
public static final String DATA_TAG = "data";
// 初始化一个新创建的 AjaxResult 对象,使其表示一个空消息。
public AjaxResult() { }
// 初始化一个新创建的 AjaxResult 对象 @param code 状态码 @param msg 返回内容
public AjaxResult(int code, String msg) {
super.put(CODE_TAG, code);
super.put(MSG_TAG, msg);
}
// 初始化一个新创建的 AjaxResult 对象 @param code 状态码 @param msg 返回内容 @param data 数据对象
public AjaxResult(int code, String msg, Object data) {
super.put(CODE_TAG, code);
super.put(MSG_TAG, msg);
if (StringUtils.isNotNull(data)) {
super.put(DATA_TAG, data);
}
}
// 返回成功消息 @return 成功消息
public static AjaxResult success() {
return AjaxResult.success("操作成功");
}
}

3.extends BaseController:上文介绍了BaseController,在这里使用就可以直接调用分页、排序等方法了。不用每个类都编写。

总结

ruoyi框架搭建方便,依赖组件非常少。同时提供了基本的业务功能,如用户管理、部门管理、代码生成器等,但是对于技术的深度还是不太到位,如mq的使用,安全框架等技术都没有特多的涉及。