RuoYi-Cloud 后端笔记

又说要把这 RuoYi 单体项目迁移到微服务项目,只得继续学习了。RuoYi 单体项目是比较无聊的,没啥学习价值(只有权限系统让人眼前一亮,然后数据权限很依赖部门,其实现又让人两眼一黑),但其微服务项目是值得学习的,学习它不止是为了学习它本身,也是增长一下对微服务项目的感性经验,将来自己搭微服务项目的时候也好做参照。

带着问题学习:

  1. 网关,认证,System 分别是做啥的,它们之间的依赖关系如何
  2. 微服务之间互相调用是怎么实现的,用户信息是怎么传递的,调用异常默认是怎么处理的
  3. 客户端的请求的全流程是怎样的
  4. 开发一个新的微服务模块的全流程(见 https://doc.ruoyi.vip/ruoyi-cloud/document/htsc.html#新建子模块

中间件部署

首先处理开发环境的部署。中间件均使用 Docker 部署。为了避免各种愚蠢的 IP、host 不对应的问题,同时避免哪个闲的蛋疼的中间件不想看到 localhost,也为了后面可以给中间件做真正的集群,利用 V-YOP/docker-network,使宿主机能通过 IP 访问容器(linux 系统的 docker 有此功能,其他系统需要耍一些手段),使得容器和宿主机就像真的同处在一个网络下。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# 事先定义 network ykinet,网段为 172.19.0.0/16,所有容器都丢到这个网段下,通过 host 互相访问
docker network create --subnet 172.19.0.0/16 ykinet

# mysql
docker run --network ykinet --ip 172.19.100.10 --name some-mysql --hostname some-mysql -e MYSQL_ROOT_PASSWORD=root -e MYSQL_DATABASE=db -d mysql:8

# redis,默认无账号无密码
docker run --network ykinet --ip 172.19.100.11 --name some-redis --hostname some-redis --name some-redis -d redis

# nacos,默认账号密码是 nacos,这里根据 ruoyi 的文档,指定其使用 mysql 去持久化配置,这些环境变量的名字见 conf/application.properties
docker run --network ykinet --ip 172.19.100.12 --name some-nacos --hostname some-nacos --name some-nacos -d \
-e SPRING_DATASOURCE_PLATFORM=mysql \
-e MYSQL_SERVICE_HOST=some-mysql \
-e MYSQL_SERVICE_DB_NAME=ry-config \
-e MYSQL_SERVICE_DB_PARAM=useUnicode=true\&characterEncoding=utf8\&zeroDateTimeBehavior=convertToNull\&useSSL=true\&serverTimezone=GMT%2B8 \
-e MYSQL_SERVICE_USER=root \
-e MYSQL_SERVICE_PASSWORD=root \
-e MODE=standalone \
nacos/nacos-server:2.0.2

为了保持一致,宿主机也用 host 去访问中间件,但这要求编辑 hosts…反正就一趟。

ry-config数据库中执行的 sql 是 RuoYi 的配置,这些表应该默认是 nacos 自己去生成的。所有配置文件都在 nacos 里面。

部署完中间件后,创建数据库,执行 SQL,然后编辑项目中的各个 bootstrap.yml 文件配置 nacos 地址,然后在 nacos 中配置各 yml 中 redis 和 mysql 地址即可。

如果 nacos 地址没配或配错了,它似乎是没有任何错误消息,只不过是只用本地的配置。JDBC 配错时亦然,会是 Mybatis 报错Invalid Bound ...

注册到 nacos 的微服务默认会监听[spring.application.name][spring.application.name].yml[spring.application.name]-[spring.profiles.active].yml三个配置文件,且比本地配置文件优先。

项目结构

一般架构

  • ruoyi-common:系统模块和各微服务模块直接依赖,本地调用的模块,提供框架的底层功能,如常量,工具类,注解,redis 等……离谱的是其中的ruoyi-common-security依赖了ruoyi-api-system来编写权限相关逻辑;ruoyi-common中包含 8 个子模块,但他们看上去似乎每个都是必要的,不如每次都全部依赖(反正它们本来就互相依赖)
  • ruoyi-auth:认证服务,其提供登陆接口供前端,用户登录时,调用 ruoyi-system 中的用户接口获取用户的所有信息包括角色,权限信息并在 redis 中存储,供网关和其他微服务模块使用,并返回令牌(JWT格式)给前端。令牌中只存储用户的标识符和名称,权限和角色信息仍旧是在微服务中主动从 redis 中取
  • ruoyi-gateway:网关服务,前端唯一能看到的东西,负责鉴权——从 Token 中解析出用户信息,添加信息到请求头,并转发给业务模块;网关同时也内置 sentinel 用来做限流,但目前估计不需要研究它。网关通过配置文件去明确哪些 url 要转发给哪些微服务,见 nacos 中的ruoyi-gateway-dev.yml
  • ruoyi-api:即微服务的接口层。业务模块如果想要调用其他微服务,就必须引入ruoyi-api包下的对应接口模块。注意和公司项目不同,该模块下的接口对应的是微服务实现模块中的控制器层而非服务层,这有利于解耦,而且是一般来说的更“正宗”的微服务的架构。
  • ruoyi-modules:微服务实现,每个模块代表一个特定的微服务,每个微服务可以独立启动。网关(经过相应配置后)保证打到微服务的请求是鉴权后的,微服务自身需要检查用户权限和角色,这里和单体不一样,使用了自定义的注解来做的鉴权,实现见com.ruoyi.common.security.aspect.PreAuthorizeAspect
  • ruoyi-api/ruoyi-system-apiruoyi-systemruoyi-file两模块的接口,但只暴露了一些最必要的方法,比如查询用户信息,上传文件等。也就是说自定义的微服务是无法访问系统的其他功能的,需要另外开发
  • ruoyi-modules/ruoyi-system:核心模块,提供用户,角色,权限,字典等功能,它是必须的,被认证服务依赖

如果要新增微服务模块,在ruoyi-api中添加模块存放微服务的接口以及 VO,在ruoyi-modules包中添加模块存放微服务的实现,它要依赖它的接口,以及ruoyi-common

单体迁移到微服务可能要做的修改

下面的前提是把所有业务代码都迁移到RuoYi-Cloud项目中。如果不做此迁移,那只需要把文件管理部分和有状态的地方做一些修改以允许横向扩展。

微服务拆分

为了尽可能减少改动,应当尽量避免服务间调用,让每个微服务之间的功能尽可能分离,同时尽量避免在实现上的修改,只动接口。值得庆幸的是,Openfeign 利用的是 Spring MVC 的注解,而 Spring MVC 的注解是可以继承的,这意味着只需要在接口上进行标注便可把服务暴露出来,而且保证 Service 和 Client 的接口是对应的。

对于没有任何依赖也不被依赖的服务,他们可以不暴露给外部,和控制器绑在一起,只进行本地调用。

对于那些有依赖其他服务的服务,递归找到它依赖以及被它依赖的所有服务,如果服务数量较少,可能这些服务也可以绑在一起,不暴露出来。

如果必须要暴露出来,则找到需要暴露的服务,考虑进行下面的操作:

  1. 在服务的接口上标注@FeignClient,指定 value,即 serviceId(供 nacos 找到服务实际地址),以及指定 primary 为 false。注意该接口上不能标注 @RequestMapping
  2. 在需要暴露出来(给其他微服务调用)的接口中标注@RequestMapping或其他 Mapping,给参数标注相应注解(注意@RequestParam@RequestPart必须指定名称),就如 RestController 一般
  3. 把服务的实现类上的@Service替换为@RestController并标注@Primary

此法需要手动指定实现类为@Primary,以避免注入本地实现时注入了 OpenFeign 的代理对象,实际上官方文档 https://docs.spring.io/spring-cloud-openfeign/docs/current/reference/html/#spring-cloud-feign-inheritance 是不建议这样做的,@FeignClient 注解不应当让客户端(即服务的接口)和服务共用,解决方案有:

  1. 另外分开定义客户端类,让它继承接口类并标注 @FeignClient,这是官方文档中的建议
  2. 在注入实现处使用@Qualifier,根据Bean名称明确使用实现类(想单独只使用@Resource也是不行的,它会优先注入@Primary的Bean)
  3. 注意到实现模块的包名起始为com.ruoyi.XXX,接口模块的包名起始为com.ruoyi.api.XXX,可以在启动类上排除com.ruoyi.api.XXX包路径,使其不被扫描到,示例:@ComponentScan(excludeFilters = @ComponentScan.Filter(type = FilterType.REGEX, pattern = "com.ruoyi.api.myfile.*"))

需注意,如果该接口依赖分页,或者其他 ThreadLocal 参数,就必须得修改实现了,考虑修改原接口或另外定义接口去接受分页参数,如果这种情况极其多,考虑研究把分页参数带到请求头等处使其能够在服务间调用时传递,这个逻辑加到请求端的 FeignRequestInterceptor,然后被请求端的逻辑考虑加个 MVC 的 filter 或者 interceptor 去读请求头,设置相应 ThreadLocal 参数。

以及,微服务接口不应该直接暴露在公网上,因为其内部是没有任何鉴权的。RuoYi-Cloud 提供了一个注解@InnerAuth以仅允许内部调用,但其实现是通过一个请求头参数确定是否是内部调用,如果被直接调用的话仍旧可以被绕过。

如果接口接受 MultipartFile(虽然该项目没有这样的接口),使用@RequestPart去接受相应参数,在 Mapping 注解中标注consumes = MediaType.MULTIPART_FORM_DATA_VALUE,(且必须指定参数名),然后,使用spring-test中的MockMultipartFile去传参。

异常处理

OpenFeign 默认是没做异常处理的,服务消费者直接把服务提供者返回的东西按照方法的返回类型进行解码,无论服务提供者是否发生异常。

一个解决方案是始终返回R<T>(即单体项目的AjaxResult),RuoYi 发生异常时 MVC 的全局异常处理器会返回该类型的值且响应码仍旧是 200(??),因此成功和失败均会得到该类型,只需检查其 statusCode 便知是否失败。但在这里此法无法使用,因为该项目所有服务的接口均未返回R<T>

考虑参考 https://blog.csdn.net/Kevin_King1992/article/details/135370839,在服务提供者处增加切面,检查如果是内部调用且抛出异常,就把异常序列化,丢到响应头里,在服务消费者处反序列化异常,重新抛出,或者使用类似的方式。

登陆和鉴权

RuoYi 单体和微服务登陆的流程基本都是一样的逻辑——调用后端接口,保存用户的所有信息包括权限和角色到 redis,然后返回给前端一个 JWT。单体项目中登陆流程依赖 Spring Security,该部分的迁移考虑是在ruoyi-auth中负责所有登录相关业务,然后微服务部分提供根据用户名获取用户信息的接口。

鉴权也是类似,在单体中,前端将 token 置于请求头中的Authorization字段,后端根据 token 从 redis 中获取对应用户信息,并设置在一些上下文中供后续使用。

微服务中,前端同样将 token 置于请求头中的Authorization字段然后请求网关,网关校验合法性和过期时间后,把用户标识置于请求头的user_key,再转发给微服务,微服务得到user_key后根据它去获取完整用户信息并设置到上下文中。此外,微服务进行内部调用时,也会在请求头上带上user_key供服务提供者获取调用的用户。

鉴权部分可能可以直接依赖RuoYi-Cloud的逻辑。

权限

这里的权限指的是提供给前端的接口的权限。

单体项目中鉴权是使用 Spring Security 的注解@PreAuthorize("@ss.xxx(xxx)"),倘若参照微服务的文档 https://doc.ruoyi.vip/ruoyi-cloud/document/htsc.html#权限注解,需要改为@RequiresLogin@RequiresPermissions@RequiresRoles注解。但考虑到单体中的PermissionService,即 ss 是直接从上下文中取得的用户信息并进行检查,没有任何外部依赖,**考虑在ruoyi-common中重新实现PermissionService**,以避免修改。

文件管理

项目中自定义处理文件的逻辑的地方较少,这些地方考虑手动修改使其依赖ruoyi-file,并给ruoyi-file添加下载等逻辑(它默认只有个上传)。但考虑到ruoyi-file当前的实现内容不多,在common包下编写依赖分布式文件系统的文件服务并直接依赖它也是个选择。

并发

项目中没有同步操作。

定时任务

有一个定时任务…需要改为使用 quartz 或 xxljob 等分布式任务调度框架。

事务

… 不存在的。

开发微服务模块全过程

微服务版本的代码生成功能和单体版本的一致。

添加接口和实现模块

  1. ruoyi-api 下添加接口模块,构件名为ruoyi-api-XXX,包名为com.ruoyi(代码实际起始包为com.ruoyi.api.XXX),引入ruoyi-common-core依赖
  2. ruoyi-modules 下添加实现模块,构件名为ruoyi-modules-XXX,包名为com.ruoyi(代码实际起始包为com.ruoyi.XXX),拷贝 ruoyi-system 的依赖,并引入对应接口模块的依赖
  3. 拷贝ruoyi-system的启动类的内容过来,拷贝ruoyi-systembootstrap.yml到实现模块中,编辑spring.application.nameruoyi-XXX(该名称用于服务发现),同时在com.ruoyi.common.core.constant.ServiceNameConstants中添加该名称供后续在 @FeignClient 中引用
  4. 登入 nacos,克隆ruoyi-system-dev.yml,命名为ruoyi-XXX-dev.yml,编辑其中 mybatis 的配置
  5. 如果该微服务需要暴露接口给前端,在网关配置中照葫芦画瓢添加 routes

这时候应当能正常启动了。

添加业务代码

  1. 在接口模块下定义相应服务的接口,路径为com.ruoyi.api.XXX.XXXService。在接口上标注@FeignClient,其中 value 引用服务名;在方法和参数上标注相应注解(如果该服务需要暴露接口给其他微服务的话,否则在实现模块下定义,且不需要标注注解)
  2. 定义实现类,路径为com.ruoyi.XXX.service.XXXServiceImpl,标注 @RestController
  3. ……其余同单体一致

本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 协议 ,转载请注明出处!