若依框架(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
进行增删该查操作

单应用在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 是后台服务的入口页面。
快速搭建若依
前端搭建
后端搭建
修改数据库和redis的配置。
代码介绍

ruoyi-quartz
使用quartz作为定时任务管理,这里不多说,老生常谈的问题。如果想了解可以参考文章:quartz实现定时任务
ruoyi-generator
该包为代码生成器,主要的流程如下。

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

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

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

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()); @InitBinder public void initBinder(WebDataBinder binder){ 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"; public AjaxResult() {} public AjaxResult(int code, String msg) { super.put(CODE_TAG, code); super.put(MSG_TAG, msg); } 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
封装了一系列的异常,在特定时期使用即可。

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

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

ruoyi-framework

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){ 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 { String key = rateLimiter.key(); int time = rateLimiter.time(); int count = rateLimiter.count(); 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

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 { 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); 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() + "/"); 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("*"); config.setMaxAge(1800L); UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); source.registerCorsConfiguration("/**", config); 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); } 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()); String url = request.getRequestURI(); String submitKey = request.getHeader(header); if (StringUtils.isEmpty(submitKey)) { submitKey = url; } String cacheRepeatKey = Constants.REPEAT_SUBMIT_KEY + submitKey; 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())) { tokenService.verifyToken(loginUser); UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(loginUser, null, loginUser.getAuthorities()); authenticationToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request)); 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) { 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
| private String createToken(Map<String, Object> claims) { String token = Jwts.builder() .setClaims(claims) .signWith(SignatureAlgorithm.HS512, secret).compact(); return token; }
private Claims parseToken(String token) { return Jwts.parser() .setSigningKey(secret) .parseClaimsJws(token) .getBody(); }
|
ruoyi-admin

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 { Properties info = (Properties) redisTemplate.execute( (RedisCallback<Object>) connection -> connection.info() ); Properties commandStats = (Properties) redisTemplate.execute( (RedisCallback<Object>) connection -> connection.info("commandstats") ); Object dbSize = redisTemplate.execute((RedisCallback<Object>) connection -> connection.dbSize()); Map<String, Object> result = new HashMap<>(3); result.put("info", info); result.put("dbSize", dbSize); 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()); }
private void setCpuInfo(CentralProcessor processor) { 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")); }
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"; public AjaxResult() { } public AjaxResult(int code, String msg) { super.put(CODE_TAG, code); super.put(MSG_TAG, msg); } 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 static AjaxResult success() { return AjaxResult.success("操作成功"); } }
|
3.extends BaseController:上文介绍了BaseController,在这里使用就可以直接调用分页、排序等方法了。不用每个类都编写。
总结
ruoyi框架搭建方便,依赖组件非常少。同时提供了基本的业务功能,如用户管理、部门管理、代码生成器等,但是对于技术的深度还是不太到位,如mq的使用,安全框架等技术都没有特多的涉及。