这篇文章的目的不是为了探究各种轮子的底层实现,是为了理解一个主流的微服务框架是如何优雅高效的集成思路,尽可能的展示若依网关从项目目录到微服务集成配置的每一个细节💯,从前端开发者的视角展开讨论,解开微服务网关的面纱。
若依springcloud安装文档
网关文件目录结构
上面的结构来自于若依网关仓库
- src->main->java目录是SpringCloud项目在生成项目后的固定结构,
-
***.ruoyi.gateway为项目自定义,包含了若依网关的业务实现,
1、config 为项目配置文件
2、filter为网关过滤器
3、handler为网关的处理
4、service为业务层实现 - resources目录下为本地配置,
- target为编译后的文件目录。
什么是网关
先看若依框架的架构图:
网关模块在Spring Cloud通常中又叫做Spring Cloud Gateway ,虽然是 Spring Cloud 生态系统的一部分,专为在微服务架构中工作而设计,但并不意味着它只能在 Spring Cloud 项目或环境中实现或使用。
一个完整健全的Spring Cloud微服务系统需要网关(Spring Cloud Gateway )。🤪
上面的架构图完美的解释了下面网关所扮演的角色和发挥的作用:
角色和作用:
- 路由转发:
网关可以根据请求的不同路径或其他属性将请求路由到不同的下游服务。这样客户端只需要知道网关的地址,而无需知道后端服务的复杂结构。
- 跨域处理:
在微服务架构中,服务可能部署在不同的域中,Spring Cloud Gateway 可以处理跨域请求,避免前端直接与多个服务交互时出现的跨域问题。
- 权限验证:
在请求转发之前,通过身份验证和授权功能,确保只有合法请求被代理到下游服务。
- 限流:
通过集成限流组件如Redis RateLimiter等,对流量进行控制,预防下游服务因为过载而崩溃。
- 熔断:
集成熔断机制,当下游服务不可用时自动切断请求,保护系统稳定性。
- API监控与指标:
提供对网关路由、过滤器等的监控,并且可以收集和展示各种指标数据。
- 响应式编程支持:
使用响应式编程模型来处理请求和响应,提高了系统的吞吐量和伸缩性。
网关的核心概念
- Route(路由):路由是网关的基本构建块,它由一个ID、一个目标URI、一系列的断言和一系列的过滤器组成,如果断言为true,则匹配该路由。
- Predicate(断言):这是一个Java 8的函数式接口,输入类型是ServerWebExchange。可以使用它来匹配来自HTTP请求中的任何信息,比如头信息、请求参数等。
- Filter(过滤器):分为Gateway Filter和Global Filter。过滤器可以修改进入的HTTP请求和返回的HTTP响应。Gateway Filters仅作用于特定路由上,而GlobalFilters则会影响所有的路由。
下面解析若依是如何实现一个微服务网关系统的 ⛹️♂️
网关过滤器的实现
- 网关鉴权
找到位于在filter下AuthFilter.java文件,该文件在头部引入了一些网关jar包,包括日志(slf4j)、自动注解(Autowired)、全局过滤器(GlobalFilter)、JWT(Claims)等等,还有一些引用其他模块封装的类和方法。
这里面最核心的在于全局过滤器(GlobalFilter),所有实现了此接口的类都会成为Spring Cloud网关中的全局过滤器组件,作用于所有的路由。
看一下具体的实现:
public class AuthFilter implements GlobalFilter, Ordered {
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
// 获取请求对象
ServerHttpRequest request = exchange.getRequest();
//用来创建 ServerHttpRequest.Builder的一个可变实例,以便对请求做出改动
ServerHttpRequest.Builder mutate = request.mutate();
//提取请求路径:比如返回 /api/data
String url = request.getURI().getPath();
String token = getToken(request);
.....
//调用 chain.filter(...) 并传递更新后的交换对象(exchange)来调用链中的下一个过滤器
return chain.filter(exchange.mutate().request(mutate.build()).build());
}
@Override
public int getOrder() {
return -200;
}
}
上面只展示了AuthFilter 过滤器的实现思路,省略的部分是验证请求是否包含token,token是否过期,以及请求是否在白名单内等等。
在这段代码中重写了全局过滤器(GlobalFilter)的filter方法,其中**Mono**是 一个核心概念,这里的 filter 方法返回的 Mono 表示的是一个可能会进行一些副作用操作但不返回任何值的异步序列,这样如果还有其他过滤器就可以链式调用.下面是一个简略的过滤器流程图:
- 网关自动配置
找到位于在config->properties下CaptchaProperties.java文件,该文件是一个在登陆的时候配置验证码的配置文件,这里面涉及到了微服务的自动下发配置,而无需重启服务器,文件中的业务代码并不多来看下:
@Configuration
@RefreshScope
@ConfigurationProperties(prefix = "security.captcha")
public class CaptchaProperties
{
private Boolean enabled;
private String type;
public Boolean getEnabled()
{
return enabled;
}
public void setEnabled(Boolean enabled)
{
this.enabled = enabled;
}
public String getType()
{
return type;
}
public void setType(String type)
{
this.type = type;
}
}
上面的注解@RefreshScope 和 @ConfigurationProperties 一起构成了微服务下发配置文件的注入,prefix = “security.captcha” 表明来自于配置文件的目录结构,找到resources文件夹下面的bootstrap.yml配置文件发现并没有security.captcha,其实这个配置来自于Nacos,下面是来自于Nacos配置文件的截图:
可以看到关于security.captcha的配置, 在点击发布之后,网关会自动更新相关配置,无须重启服务。
其他的配置文件也是类似的道理。
微服务集成网关
若依微服务采用的是Nacos,上面提到的自动注入配置无须启动服务就是由Nacos实现的,下面是Nacos启动之后的客户端界面
Nacos可以做什么事情:
- 服务发现和服务健康监测:Nacos 支持基于 DNS 和基于 RPC
的服务发现。它可以自动检测服务实例的健康状态,并在服务实例不健康时将其从服务列表中移除。 - 动态配置服务:动态配置服务让你可以在运行时动态地调整配置信息,而不需要重启服务。配置更新能够被自动推送到服务实例。
- 动态 DNS 服务:支持权重路由,使得 DNS 解析可以根据服务实例的权重和负载进行智能分配。
- 服务和元数据管理:可以管理微服务架构中的各种运行时元数据。
网关集成Nacos:
若依的配置文件中已经标明了配置信息:
shared-configs:
- application-
s
p
r
i
n
g
.
p
r
o
f
i
l
e
s
.
a
c
t
i
v
e
.
{spring.profiles.active}.
spring.profiles.active.{spring.cloud.nacos.config.file-extension}
这里的共享配置在maven构建后会指的是ruoyi-gateway-dev.yml,这个可以在Nacos配置列表中找到。
在Nacos的网关配置文件中有关于路由的配置如下:
```yaml
routes:
# 认证中心
- id: ruoyi-auth
uri: lb://ruoyi-auth
predicates:
- Path=/auth/**
filters:
# 验证码处理
- CacheRequestFilter
- ValidateCodeFilter
- StripPrefix=1
# 代码生成
- id: ruoyi-gen
uri: lb://ruoyi-gen
predicates:
- Path=/code/**
filters:
- StripPrefix=1
# 定时任务
- id: ruoyi-job
uri: lb://ruoyi-job
predicates:
- Path=/schedule/**
filters:
- StripPrefix=1
# 系统模块
- id: ruoyi-system
uri: lb://ruoyi-system
predicates:
- Path=/system/**
filters:
- StripPrefix=1
# 文件服务
- id: ruoyi-file
uri: lb://ruoyi-file
predicates:
- Path=/file/**
filters:
- StripPrefix=1
举例说明用法:
认证中心
- id: 路由的唯一标识符
- uri:
- predicates:路由谓词,定义了请求匹配的条件,比如- Path=/auth/**表明只匹配以/auth/开头的请求,。
- Path 比如当请求路径匹配/auth/**时,该请求会被转发到名为ruoyi-auth的服务实例上
- filters:应用于当前路由的过滤器列表
业务层实现
在网关中有一个关于生成验证码的业务:具体的实现逻辑如下:
@Service
public class ValidateCodeServiceImpl implements ValidateCodeService {
//通过name匹配Bean
@Resource(name = "captchaProducer")
private Producer captchaProducer;
@Resource(name = "captchaProducerMath")
private Producer captchaProducerMath;
//通过类型匹配Bean
@Autowired
private RedisService redisService;
@Autowired
private CaptchaProperties captchaProperties;
/**
* 生成验证码
*/
@Override
public AjaxResult createCaptcha() throws IOException, CaptchaException {
AjaxResult ajax = AjaxResult.su***ess();
boolean captchaEnabled = captchaProperties.getEnabled();// 当前是否要产生验证码,该配置在nacos中可以在不重启服务的情况下修改
ajax.put("captchaEnabled", captchaEnabled);
if (!captchaEnabled) {
return ajax;
}
// 保存验证码信息
String uuid = IdUtils.simpleUUID(); // 生成唯一的uuid用于验证;
String verifyKey = CacheConstants.CAPTCHA_CODE_KEY + uuid;
String capStr = null, code = null;
BufferedImage image = null;
String captchaType = captchaProperties.getType();
if ("math".equals(captchaType)) {
String capText = captchaProducerMath.createText(); // 生成文本
capStr = capText.substring(0, capText.lastIndexOf("@"));
code = capText.substring(capText.lastIndexOf("@") + 1);
image = captchaProducerMath.createImage(capStr);
} else if ("char".equals(captchaType)) {
capStr = code = captchaProducer.createText();
image = captchaProducer.createImage(capStr);
}
redisService.setCacheObject(verifyKey, code, Constants.CAPTCHA_EXPIRATION, TimeUnit.MINUTES);
FastByteArrayOutputStream os = new FastByteArrayOutputStream(); // 在内存中创建一个缓冲区,所有发送到输出流的数据都保存在该缓冲区中
try {
ImageIO.write(image, "jpg", os); // 尝试将BufferedImage对象以jpg格式写入输出流
} catch (IOException e) {
return AjaxResult.error(e.getMessage());
}
ajax.put("uuid", uuid);
ajax.put("img", Base64.encode(os.toByteArray()));
return ajax;
}
/**
* 校验验证码
*/
@Override
public void checkCaptcha(String code, String uuid) throws CaptchaException {
if (StringUtils.isEmpty(code)) {
throw new CaptchaException("验证码不能为空");
}
if (StringUtils.isEmpty(uuid)) {
throw new CaptchaException("验证码已失效");
}
String verifyKey = CacheConstants.CAPTCHA_CODE_KEY + uuid;
String captcha = redisService.getCacheObject(verifyKey);
redisService.deleteObject(verifyKey);
if (!code.equalsIgnoreCase(captcha)) {
throw new CaptchaException("验证码错误");
}
}
}
上面的业务代码涉及到的注解有:
@Resource:这是一个用于匹配Bean的注解,比如@Resource(name = “captchaProducer”)表示当前的service中引入captchaProducer依赖,在网关中captchaProducer指向了CaptchaConfig类,这个类中实现了字符串验证码方法
@Autowired:同样的也是个匹配Bean的注解,但是和@Resource不同的地方在于该注解通过类型匹配,而且**@Autowired**是spring特有的注解,在spring cloud中这两种方法可以替换使用。
上面的代码已经做了必要的注释,可以对照若依网关文件查看。