☰
RuoYi框架从单体到微服务:权限、网关与部署实战解析
2026/10/15 7:35:08 网站建设 项目流程

1. RuoYi版本图谱:为什么同一套系统要分出三套写法

RuoYi给我的第一印象是“什么都有”:用户管理、菜单管理、代码生成器、定时任务、监控大屏……但真正上手以后,你才会发现它沿三套路线走了三个方向——经典单体版、前后端分离版、微服务Cloud版。它们共用同一个名字,技术栈和架构思想差别却不小。刚接触的人很容易把这三个版本混着看,结果代码位置找不对,遇到问题也不知道该查哪份文档。

1.1 内置单页面版:适合快速交付的紧凑设计

经典RuoYi是最早的版本,也是我最早接触的版本。后端用Spring Boot搭底,前端直接用Thymeleaf模板在服务端渲染页面。登录不走当前流行的JWT,而是用传统Session配合Shiro做权限控制。它的使用方式非常直接:本地把项目一跑,浏览器访问8080端口,一套完整的后台管理页面就已经在眼前了。对追求效率的中小型企业内部系统来说,这种“前后端不分离”的写法反而省事,少维护一层接口文档,页面字段变了直接改模板。

这个版本的分层很规矩:Controller负责路由和参数接收,Service处理业务逻辑,Mapper操作数据库。一个典型业务模块通常是一个Controller对应一组路由,页面提交表单之后,由后端模板引擎渲染下一个页面。代码生成器也是从这个版本开始引入的,通过数据库表结构反向生成实体类、Mapper、Service、Controller和对应页面,几分钟就能搭出一个基础CRUD模块。

不过它的缺点也很明显:生成页面的样式是固定的,遇到复杂表单还是要手改模板,升级时生成的代码容易覆盖手工改动。所以到今天仍然有人用它,图的是“快”,不是“优雅”。

1.2 前后端分离版:接口层挑起了大梁

RuoYi-Vue的核心变化,就是前端独立成Vue工程,后端只提供JSON接口。后端认证从Shiro换成了Spring Security加JWT,不再依赖Session保持登录状态。前端路由根据后端返回的菜单结构动态生成,权限点细化到按钮级别:前端拿到按钮权限数组决定某个按钮是否渲染,后端同样用注解在接口层再做一道校验。

这套结构更接近现代企业项目的真实形态。前端团队和后端团队可以并行开发,后端不再关心页面长什么样,前端也可以按照自己的节奏迭代交互细节。我在实际项目里用它做过基底:后端沿用RuoYi-Vue的权限体系,前端在ElementUI基础上定制业务组件,整体开发效率比从零开始写要快很多。

但分离有一个代价,就是接口设计必须稳定。单体版后端改模板很方便,现在接口字段一变,前端就要跟着联调。所以RuoYi-Vue对接口规范的要求比单体版高出很多,统一返回体、分页结构、错误码这些都必须在一开始就定清楚。这一版也是目前社区里使用人数最多的版本,因为它同时抓住了“快”和“规范”两个点。

1.3 微服务版:被架构逼出来的模块化

RuoYi-Cloud不是简单地把代码复制到多台机器上。它把系统管理、认证、网关等核心能力拆成独立服务,基于Spring Cloud Alibaba把Nacos、Gateway、Sentinel这些组件组合起来。业务模块可以根据团队边界继续往下拆,也可以先保持单服务、只拆分基础设施。这套设计解决的其实不是单机性能问题,而是多团队协作的推进问题——不同小组可以按自己的节奏迭代,公共能力下沉到平台层。

从学习顺序来说,我建议先吃透经典单体版,再看Vue版的权限与接口设计,最后才进入Cloud版。原因是RuoYi-Cloud里的鉴权、数据权限、数据字典这些概念,全部继承自经典版。理解单体版那套权限表是怎么设计的,再看微服务里网关怎么认证、服务内怎么授权的,那种“原来只是换了个校验位置”的贯通感,比直接啃微服务代码要扎实得多。

版本技术栈轮廓认证方式适用场景
经典单体版Spring Boot + Shiro + MyBatis + ThymeleafSession/Cookie快速交付后台、外包项目
Vue分离版Spring Boot + Spring Security + MyBatis + VueJWT无状态前后端分离团队、中后台系统
Cloud微服务版Spring Cloud Alibaba + Gateway + NacosJWT + 网关统一鉴权多团队协作、中大规模分布式系统

2. 经典单体骨架解剖:一个请求从登录到返回数据的完整旅程

很多教程在讲RuoYi时,只挑一部分代码讲一个页面CRUD。我反而觉得最值钱的,是理清一个请求从登录开始经历的全链路:过滤器链、权限注解、数据权限SQL拼接、统一返回体。这条链路吃透了,不同版本之间的差异就只是在换壳。

2.1 统一返回体与全局异常:接口守门的两件套

RuoYi里几乎每个接口返回的都是AjaxResult或者TableDataInfo。AjaxResult自带code、msg、data三个字段,TableDataInfo则封装分页需要的total和rows。前端拿到响应后,先看code再决定走成功还是失败分支,不走HTTP状态码。刚开始我觉得这是过度设计,后来做项目才明白,统一返回体对前端太重要了,它让错误处理逻辑可以只写一遍。

配套的是全局异常处理。理论上Controller里可能出现参数校验异常、业务异常、未知异常,如果每个方法都自己try/catch,代码根本没法看。RuoYi的做法是用@RestControllerAdvice做全局兜底,业务异常在Service层主动抛出,统一由异常处理器转成返回体。后续新增错误码,也只需要在一个地方维护对应关系。

这套“统一返回体+全局异常”几乎是所有企业级后端项目的标配。如果你自己从零写项目,我建议第一件事就是把这两件套搭好。不少刚接触RuoYi的人嫌它包得太多,真去掉之后,会发现接口的错误提示形式开始五花八门,联调成本直线上升。

2.2 从Shiro到Spring Security:权限注解那点事

经典版用Shiro,权限注解长这样:@RequiresPermissions("system:user:list")。方法执行前由Shiro判断当前登录用户是否拥有对应权限标识。Vue版改成Spring Security,注解变成@PreAuthorize("@ss.hasPermi('system:user:list')")。两者思路一致:权限标识串从数据库的菜单权限表中取出来,登录时塞进用户信息,请求时通过注解校验。

需要注意的是,注解校验只是权限落地的一环。前端根据权限数组隐藏按钮,后端在接口上继续校验,两者配合才是完整的防线。实际维护中常见两类问题:

一是数据库里明明配了菜单,用户却提示没权限,多半是缓存没刷新或者权限标识串大小写写错。二是图省事把所有人都挂到系统管理员角色上,权限体系形同虚设。为了排查这类问题,我会在权限注解的切面里记录被拦截的URI、当前用户和所需权限标识,平时有点吵,但权限出问题时它帮我省了大量时间。

2.3 数据权限:不止管“能不能看”,还要管“能看到哪些行”

操作权限解决的是“某个用户能不能访问某个接口”,数据权限解决的是另一件事:“同一个列表接口,张三能看到全公司数据,李四只能看到本部门数据。”RuoYi用@DataScope注解加切面来实现。

在Mapper的SQL里你会看到类似${params.dataScope}这样的动态拼接。执行接口时,切面根据当前用户所属角色配置的数据范围,拼出“部门ID IN (...)”或“user_id = ...”条件。数据范围一般分为全部、自定义、本部门、本部门及以下、仅本人几档。第一次看这个设计会觉得有点魔术,但理解之后你会发现,它本质上就是在查询语句中注入了一个动态过滤条件,巧妙但不算复杂。

数据范围权限含义拼接SQL效果
全部数据权限可见全部数据不额外拼接条件
本部门数据权限可见本部门数据dept_id = 当前部门
本部门及以下本部门及其子部门dept_id in (本部门及子部门)
仅本人数据权限仅自己创建/所属user_id = 当前用户

这个设计的坑主要在表别名。数据权限拼接的条件里写了别名,比如d.dept_id,如果查询SQL没给部门表起别名或者别名不一致,运行时会直接报SQL错误。我遇到过好几次回退,都是去改代码生成器生成的SQL别名。所以改造数据权限之前,建议先把每张业务表和部门表、用户表的关联字段理清楚,最好画一张简单的关联矩阵再动手。

3. 从单体走向微服务:拆分、鉴权与事务的几个关键决策

把RuoYi经典版改造成微服务,最难的往往不是技术选型,而是几个关键决策点。哪些模块先拆、鉴权放哪、事务怎么处理,这三件事想不清楚,后面全是返工。

3.1 系统管理模块为什么总是第一个被拆出来

原因是几乎所有业务模块都依赖用户、角色、部门这些基础数据。拆微服务时,应该先拆“被依赖者”而不是“依赖者”。系统管理模块独立成服务之后,业务模块通过远程调用获取用户与权限信息,依赖方向变得清晰。如果先拆业务模块,而系统管理还留在大单体里,两者之间的边界就模糊了。

拆的时候还要处理公共模块。原来的公共模块里可能同时存在工具类、实体类、常量、配置类,如果拆分时不控制它膨胀,很快公共模块就变成“公共垃圾堆”。我的经验是给公共模块定一条铁律:只放无业务语义的通用能力,比如字符串处理、日期工具、统一返回体、基础常量。一旦出现某个业务枚举或者业务Service跑到公共模块里,代码评审阶段直接打回。

3.2 网关统一鉴权如何替代本地拦截器

单体时代,认证逻辑分散在Servlet过滤器或方法注解里。微服务之后,认证职责上收到网关。RuoYi-Cloud里网关做的事情是:拦截请求,解析JWT,校验有效性和时效,把用户信息塞进Header,再路由到下游服务。下游服务从Header里恢复登录用户上下文,继续使用方法级权限注解。

这里最容易犯的错,是以为网关校验过JWT,服务内部就不用再做权限校验了。网关只负责“这个人是谁”,不负责“这个人能不能做这件事”。按钮级别的权限校验必须继续在服务内方法注解上做。如果试图把权限判断全部收拢到网关层,很快网关就会变成塞满业务规则的上帝服务,维护成本直线上升。

3.3 本地事务失效后的取舍:分布式事务三种方案

单体里一个Service方法加@Transactional就能保证原子性。拆成服务之后,跨服务操作时本地事务必然失效。RuoYi-Cloud没有内置完整的分布式事务组件,更多是给你留了选型空间。我实际项目里用过三种做法:

方案一是业务规避。重新梳理业务边界,让一次操作尽量不跨服务,或者只在一个库的多张表间完成。比如删除用户时同步删除其关联数据,如果链路必须跨服务,就用补偿逻辑。方案二是本地消息表加MQ。上游业务落库时同时写一条本地消息表事件,事务提交后投递MQ,下游消费成功再回执,达到最终一致。方案三是引入Seata的AT模式,代码侵入小,但对数据库连接和事务执行时间有要求。

方案一致性强度代码侵入适用场景
业务规避与补偿弱一致中业务链路可重新划分
本地消息表 + MQ最终一致中异步可接受的业务场景
Seata AT模式接近强一致低同步一致性要求高的链路

我后来的项目里用得多的是方案一和方案二,方案三等核心链路验证稳定之后才逐步引入。聊分布式事务时一定要明白,跨服务事务肯定比本地事务慢,不要试图把所有链路都做成强一致。先接受部分场景是最终一致,系统才能继续往前走。

4. RuoYi-Cloud的微服务协作骨架:Nacos、Gateway与Feign的配合细节

进入微服务部分,光看官方文档是不够的。Nacos、Gateway、Feign这三样东西配合起来,踩坑点和性能瓶颈往往藏在细节里。

4.1 Nacos一人分饰两角:注册中心与配置中心

Nacos在RuoYi-Cloud里不只是服务注册中心,还兼任配置中心。服务启动时先从配置中心拉取配置,然后注册到注册中心,之后通过注册中心发现其他服务。配置中心里的配置按dataId和group组织,不同环境放在不同Namespace下做隔离。

实际操作中最大的坑是“配置放错位置”。配置中心文件里缩进写错、或者同一个环境配置误改影响其他环境,都是常见事故。我的建议是:配置中心只放需要动态调整的配置,比如开关、阈值、限流值;数据源这种静态配置放在服务本地。因为@RefreshScope刷新配置时可能触发连接重连,如果数据源也塞进配置中心,刷新一次就会造成数据库连接抖动。

Nacos本身也是需要运维的组件。本地开发时用单机模式启动:startup -m standalone。生产环境至少部署成集群,配合MySQL做持久化,避免注册中心挂掉后服务发现整体瘫痪。我碰到的不少“服务连不上”的故障,追到最后都是Nacos配置问题,而不是服务本身的问题。

4.2 Gateway统一JWT鉴权与路由白名单

RuoYi-Cloud的网关基于Spring Cloud Gateway,核心是一组过滤器。请求进来先判断是否在白名单里:登录接口、验证码接口这些不需要JWT的,直接放行;其余请求必须携带有效的Authorization头,否则直接返回401。JWT校验在网关做一次,解析出的用户信息转成Header下发到下游服务。

白名单是个长期维护的清单。每次新增开放接口,都要记得往白名单里加。我有一个项目漏配了内网回调接口,导致服务间回调一直被拦,排查了很久才发现是网关白名单的问题。这类问题通常不是框架不行,而是配置维护的速度跟不上接口增长速度。建议新增开放接口时,在代码评审阶段同步检查网关白名单。

跨域配置也要谨慎。前后端分离场景下,前端访问网关需要允许跨域,但allowedOriginPatterns别写得太宽。如果允许所有来源,等于把保护层拆掉了,任何网页都能触发你的后端请求。

4.3 服务间Feign调用时Token传递的坑

这是微服务改造里踩到最多人脚后跟的坑。RuoYi-Cloud的默认做法是:在Feign的RequestInterceptor里,从当前线程的SecurityContextHolder取登录用户,放进自定义Header。下游服务收到请求后,再从Header恢复用户上下文。听起来简单,但一翻车就报“用户信息不存在”或者“没有权限”。

丢用户上下文的原因常见有三类:

第一类是异步线程丢失。@Async方法或者手动new Thread之后,SecurityContextHolder在线程池里不会自动传递,子线程拿不到用户信息。第二类是内部调用又走了一遍网关,导致二次鉴权,内网请求缺少某些头被拦截下来。第三类是Feign拦截器没生效,最常见的原因就是自定义Header名冲突,或者被其他过滤器覆盖。

我当时的处理思路是:给Feign拦截器加上日志,每次调用都记录是否存在用户上下文;同时把跨服务调用统一封装成Feign接口,避免有人直接写RestTemplate绕过拦截器。后来在项目里又加了一个TraceId,和用户信息一起透传,排起问题来容易得多。

5. 数据层改造:代码生成器、数据字典与分布式主键

数据层往往是微服务改造里最容易被忽略、却最难啃的部分。代码生成器、数据字典、主键策略这三件事,在分布式环境里都有不少细节要处理。

5.1 代码生成器生成的代码只能算半成品

RuoYi的代码生成器能根据表结构生成一套CRUD,但生成的代码离业务可用还有一段距离。它处理的是标准单表查询,遇到多表关联、复杂查询、自定义校验就要手写改造。很多用生成器的人有个误区,觉得生成之后再改动不好迁移,就硬扛着默认代码继续往上堆逻辑,最后代码结构越来越别扭。

我的建议是把代码生成器定位成“半自动脚手架”。生成之后先用,等业务复杂度上来,再在Service层建立独立的DTO和VO模型。常见的目录吐槽点就在这个地方,如果直接把实体类往外抛,字段会污染接口语义。改造方向是锁住Mapper层,隔离查询结构,再对外输出页面真正需要的字段。这样开发速度保持了,代码质量也比直接用生成的实体类强不少。

5.2 数据字典在分布式环境下的同步问题

单体版里数据字典就是一张表,登录时加载到缓存,需要的时候直接从缓存读取,比如性别、状态、类型这些。拆成微服务之后,如果每个业务服务仍然连接同一个数据库,字典还能勉强查询。一旦分库,每个业务服务就要解决“字典数据从哪来”的问题。

常见的有三条路。第一种,把字典做成独立微服务,业务服务通过Feign拉取,本地缓存,定期失效。第二种,字典变更通过MQ广播,业务服务监听之后刷新本地缓存。第三种,启动时全量拉取,配合定时任务刷新。三种都能用,关键是字典变更的入口必须收敛在同一个后台页面,避免多个服务各自直改字典表。

缓存一致性是这里的隐藏问题。字典变更后缓存不刷新,页面就会保留旧文案。我在后台字典编辑逻辑里加了一个“发布”动作,只有点发布才发送广播事件,未发布的修改不生效。这个设计看起来多了一步操作,实际反而让每次变更都可控、可追溯。

5.3 自增主键到雪花算法:分库带来的主键问题

单体场景里自增ID很好用:简单、有序、人眼容易识别。但分库之后就麻烦了,不同库各自生成自增ID会重复。RuoYi-Cloud默认提供了MyBatis Plus集成的ID策略,但如果你沿用经典MyBatis的写法,就得自己解决主键生成问题。

雪花算法是最常见的选择。它生成的ID是64位整数,由时间戳、机器标识、序列号组成,既能全局唯一又趋势递增。不过经典的雪花算法依赖机器标识,在运维环境里可能存在时钟回拨问题,极端情况下会造成重复ID。我在做分布式部署时,把工作节点ID从Nacos配置中心下发,避免各节点手写配置不一致,并在启动时校验服务器时间回拨。

上线之后再出现ID重复的概率几乎为零。虽然偶尔会有同事吐槽雪花ID没有自增ID好读,但分库场景里这个取舍是必须的。如果业务上对ID可读性有要求,也可以考虑把时间信息单独冗余到业务字段里,不要为了可读性回到自增主键的老路。

6. 部署与运维实战:上线之后才会注意到的细节

代码能跑起来只是开始,真正让人头疼的是上线之后的事。服务启动顺序、连接池参数、日志排查方式,这些看着不起眼,却决定了你在生产环境是被动救火还是从容应对。

6.1 服务启动顺序与健康检查

单体应用启动只要数据库和Redis能用就行。微服务不一样,要先把Nacos拉起来,再启动网关和认证服务,然后是系统服务,最后是业务服务。顺序颠倒了,启动日志里就会冒出一大片连接报错,虽然可能过一会儿自己恢复,但看着烦,也容易掩盖真正的问题。

用docker-compose编排时,不能只依赖depends_on。它只决定容器启动先后,并不保证容器内部服务真正就绪。更稳妥的做法是配合健康检查接口,比如持续请求/actuator/health,返回UP之后再启动下游。K8s环境就用readinessProbe配置真实就绪探针。这种方式比在脚本里加一堆sleep要靠谱得多。

我本地用docker-compose跑整套环境的习惯是:先启动MySQL和Redis,再启动Nacos,等Nacos控制台的服务列表里有名字了,再启动后端服务。多次操作下来就成了肌肉记忆,踩过启动顺序的坑,才知道脚本里那些等待逻辑不是在浪费生命。

6.2 压测中被忽略的连接池与线程池参数

很多用RuoYi上线的项目,直接拿默认配置就跑。Spring Boot默认的HikariCP最大连接数是10,单机压个两三百并发,数据库连接池先被打满,应用响应时间飙升。这时候监控里看起来是大量线程卡在等待数据库连接,而不是SQL本身慢。

我建议正式上线前做一次简单压测,把连接池从默认的10调到50左右,同时根据业务实例数量反推数据库的最大连接数上限,避免调得太大把数据库连接占满。除了连接池,还有Tomcat线程池和Feign的超时时间。默认的Feign连接超时比较短,服务间慢查询容易先触发超时重试。这些参数最好放在配置中心,压测时直接调整,不用一遍遍重新部署。

参数项常见默认值调优方向注意事项
HikariCP maximumPoolSize10按实例数调整到合理范围防止数据库连接被占满
Feign连接超时较短根据实际接口耗时调整慢查询容易先触发超时
Tomcat最大线程数200左右结合压测结果调整数值放配置中心便于实验

压测数据要留档。我有一次调完连接池性能提升非常明显,后来某次上线回滚配置后接口又变慢了,大家凭印象猜原因。所以配置变更要在需求单里留下记录,压测结论同步到项目文档,这个习惯很低成本,但遇上问题排查时特别管用。

6.3 日志链路追踪:从“查日志靠grep”到TraceId

单体时代的排查方式是,一个请求的所有日志都在同一个应用日志里,用线程ID基本就能串起来。微服务之后,一次请求可能经过网关、认证服务、系统服务、业务服务,每个服务都输出独立日志文件,想靠grep定位一条完整调用链变得非常痛苦。

我在项目里一开始就是这种状态。后来花半天时间接了一套轻量版的TraceId方案:网关生成TraceId放进MDC和请求头,Feign拦截器自动透传,下游服务在日志Pattern里输出TraceId。这样只要grep一个TraceId,就能把整个链路的日志全部拉出来,排查效率提升非常明显。

不一定一开始就引入完整链路追踪系统,先让日志带上TraceId解决日常排查就够了。真正难的地方在于存量服务没有接入点,所以如果你正在新建或者正在改造的RuoYi项目,建议一开始就把TraceId埋进去。等后续需要更完整的调用链监控时,再渐进式升级,从成本和时间上看都是最划算的路径。

这几段经验是我从自己使用RuoYi经典版开始,到后来团队一起迁移到RuoYi-Cloud的过程中慢慢累积下来的。我记得印象最深的一件事是:框架本身边界其实很清楚,但工程里大多数麻烦都出在边界衔接的地方——网关和服务之间的Header、Feign调用里的用户上下文、配置中心里的权限开关、日志里的TraceId,这些地方一个没顾上,线上就要折腾半天。不管是刚开始看RuoYi的目录结构,还是已经走在微服务改造的路上,遇到问题时先回到“一个请求从哪里来、经过哪些环节、到哪里去”这个基本思路上,基本都能快速定位。你经手的项目多了会发现,真正能长期顺畅跑起来的技术方案,往往是尊重了边界,也愿意为边界付出配置和维护成本的那一套。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询