微服务与SpringCloud#
为了降低大型WEB应用复杂性,以及应对越来越高的并发要求,微服务出现了(大嘘)。
可以简单理解为,就是将一个大的应用,拆分成多个小的模块,每个模块都有自己的功能和职责,模块之间可以进行交互。
SpringCloud是一套微服务解决方案,包含数个组件,而这些组件或者说SpringCloud的方案最早来自于Netflix(就是你看不到的那个网飞),而现如今Netflix很多组件已经不再开源更新,就有了SpringCloudAlibaba这种新方案,有不同的组件搭配。
常见的组件有:
| 服务的注册和发现 | Eureka, Nacos, consul |
| 服务的负载均衡 | Ribbon, Dubbo |
| 服务的相互调用 | OpenFeign, Dubbo |
| 服务的容错 | Hystrix, Sentinel |
| 服务网关 | SpringCloud Gateway, Zuul |
| 服务配置的统一管理 | Config-server, Nacos, Apollo |
| 服务消息总线 | Bus |
| 服务安全组件 | Spring Security, Oauth2.0 |
而常见的实现方案有:
Dubbo+Zookeeper 老东西
SpringCloud Netflix 经典原味
SpringCloud Alibaba 更好的选择目前完成了SpringCloud Netflix的部分,并将某些组件更换为更现代化的,如网关使用Gateway和更新的OpenFeign。SpringCloud Alibaba正在路上,待我用目前的组件完成毕设再来写。
Netflix Eureka#
Eureka作为服务注册与发现的组件和其他Netflix公司的服务组件一起,被Spring Cloud社区整合为Spring Cloud Netflix模块。
Eureka不再开源更新!所以采用SpringCloud Netflix并不是一个好的选择!
CAP 定理#
指的是在一个分布式系统中:
一致性(Consistency)
多个节点里的数据需要是一致的
可用性(Availability)
如果Master(主从模式?)挂了,那么应该选一个新的Master继续维持对外服务
分区容错性(Partition tolerance)
网络与地理分布造成各节点数据的不同步需要被解决这三个要素最多只能同时实现两点,不可能三者兼顾,而分区容错性必须存在——即CP/AP二选一,Eureka为AP。
依赖#
创建SpringCloud项目仍然使用Spring Initializr。
Eureka服务器只需要添加Eureka Server,Eureka客户端则需要Eureka Discovery Client。
SpringCloud某个版本往往只能使用某几个版本的SpringBoot和各种组件,但你通常无需在意。
服务端#
在启动类前添加注解@EnableEurekaServer才会启用Eureka服务器,这时候只要启动这个SpringBoot项目就会运行起来EurekaServer。(这个服务端抱着SpringBoot大腿跑,不用另外配置部署)
在SpringBoot的配置文件中,你至少需要配置:
server:
port: 8761 #通常约定Eureka服务器端口是8761
spring:
application:
name: eureka-server #自定义服务名称
#在SpringCloud时代,你需要为应用指定一个名称,你才能找到他
#多个相同名称(通常也是相同项目)应用将会共同服务(前提是有负载均衡)这个时候浏览器访问http://localhost:8761就可以进入Eureka的信息网页。
Eureka服务端既是一个提供注册功能的服务端,也可以作为客户端注册到别的Eureka服务端,默认开启并会自己注册自己,后面将利用这个功能形成Eureka集群。
服务端常用额外配置:
#个人觉得没有必要配置这些
eureka:
client:
fetch-registry: true #是否拉取服务列表
register-with-eureka: true #是否注册自己(单机 eureka 一般关闭注册自己,集群注意打开)
server:
eviction-interval-timer-in-ms: 30000 #清除无效节点的频率(毫秒)--定期删除
enable-self-preservation: true #server 的自我保护机制,避免因为网络原因造成误剔除,生产环境建议打开
renewal-percent-threshold: 0.85 #15 分钟内有 85%的 client 没有续约,那么则可能是自己出问题了,不剔除他们
instance:
hostname: localhost # 服务主机名称
instance-id: ${eureka.instance.hostname}:${spring.application.name}:${server.port} # 实例 id
prefer-ip-address: true # (管理页面)服务列表以 ip 的形式展示
lease-renewal-interval-in-seconds: 10 # 表示Client发送心跳给Server端的频率
lease-expiration-duration-in-seconds: 20 #表示Server收到Client心跳的超时时间客户端#
在启动类前添加注解@EnableDiscoveryClient才会启动Eureka客户端,老版本为@EnableEurekaClient,建议使用前者。
类似地至少需要配置:
server:
port: 8080 #服务的端口
#注意不要与Eureka服务端端口概念混淆,服务端配置的注册中心的端口,而此处是Web服务端口
spring:
application:
name: eureka-client-114514
eureka:
client:
service-url:
defaultZone: http://localhost:8761/eureka #注册中心的地址,注意/eureka和你的实际端口
#defaultZone是大小写敏感的MagicString,实际上内容就是http://localhost:8761/eureka,一个默认的值常用额外配置:
eureka:
client:
register-with-eureka: true #注册自己
fetch-registry: true #拉取服务列表
registry-fetch-interval-seconds: 5 # 表示 eureka-client 间隔多久去拉取服务注册信息
instance:
hostname: localhost
instance-id: ${eureka.instance.hostname}:${spring.application.name}:${server.port}
prefer-ip-address: true
lease-renewal-interval-in-seconds: 10
lease-expiration-duration-in-seconds: 20客户端定期向注册中心发送心跳证明自己存活,也获取一份服务列表以便于调用其他服务。
集群#
要建立Eureka集群,需要将Eureka服务器放在不同的机器上,然后在任一服务器节点的配置文件eureka.client.service-url中添加其他所有节点,形成一个两两连接的网络。
配置如下:
# 192.168.0.1
server:
port: 8761
...
eureka:
client:
service-url:
defaultZone: http://192.168.0.2/eureka, http://192.168.0.3/eureka....
# 192.168.0.2
...
eureka:
client:
service-url:
defaultZone: http://192.168.0.1/eureka, http://192.168.0.3/eureka....
# ....在单一机器上改变端口配合hosts文件修改也可以实现,但是这没有多大意义
客户端,则在eureka.client.service-url中添加所有节点。
Eureka集群没有主机和从机的概念,节点都是对等的,集群中的服务端会交换服务列表,且只有集群里面有一个节点存活,就能保证服务的可用性。
Docker部署#
需要在SpringBoot配置文件中,把可能需要改变的参数写成EL表达式,像:
server:
port: ${PORT:8761}
# PORT是外部注入的变量,8761是默认值要注入变量,只需要运行Docker时添加-e PORT=11451。
Netflix Ribbon#
Ribbon 是一个基于 HTTP 和 TCP 的客户端负载均衡工具,Ribbon可以和RestTemplate结合使用,RestTemplate类似于OkHttp之类的Http请求库,但更常用的是和OpenFeign一起使用并且被OpenFeign集成,故Ribbon可以不怎么看。
通过RestTemplate使用Ribbon,只需要引入Ribbon,然后在创建RestTemplate的Bean方发出加上@LoadBanlanced,如下:
@Bean
@LoadBalanced
public RestTemplate restTemplate(){return new RestTemplate();}然后在需要调用其他服务的地方注入,然后访问http://服务名称//....即可,因为Ribbon已经通过注册中心拿到了服务列表,因此你只需要给服务名称。
服务名称就是 spring.application.name
OpenFeign (Netflix Feign Plus)#
Feign 是声明性(注解)Web 服务客户端,个人理解就是通过注册中心自己找服务简化Http调用。
Feign是Netflix公司的且不在开源更新,而OpenFeign是SpringCloud搞出来的,在Feign的基础上支持了SpringMVC的注解等各种改进,现在说到Feign通常都是OpenFeign。
服务提供者(被调用者)#
被调用者不需要什么额外的依赖,什么也不需要改动,只需要保证自己在注册中心注册了,并且有Web接口即可。
服务使用者(调用者)#
需要引入OpenFeign依赖(当然也需要注册中心),Spring Initializr里面就有。
然后在启动类上启用Feign客户端,添加注解@EnableFeignClients。
并且创建接口(建议放到feign包),添加@FeignClient注解,其value为服务处提供者的服务名称。
最后,将服务提供者Web接口的签名复制粘贴,如下:
package xxx.xxxxx.feign;
@FeignClient(value = "other-service-name") //你知道吗,value可以省略哦
public interface ServiceFeign{
@GetMapping("api")
String doApi();
}调用只需要:
@RestMapping
public class Controller{ //current-serivce
@Resource
private ServiceFeign serviceFeign;
@GetMapping("apif")
public String doApiFeign(){
return serviceFeign.doApi();
}
//用户->current-service.doApiFeign()->Feign,Ribbon,RPC->other-serivce-name.doApi()
}库底层通过为接口创建代理类实现,类似于装饰器之类的设计模式。
参数传递#
在Feign中想要传递参数,接口的@PathVariable、@RequestParam、@RequestBody等都不能省略,建议控制器本身和Feign接口均不省略且保持一致(包括require等参数)(这应该是规范一部分)。
通过Feign远程调用传递时间参数时,如Date对象,可能遇到时区不一致问题,可能直接换算时区过来也不正确,会出错是因为旧版本OpenFeign还在使用new Date(date.toString())这种有问题的方法,新版本POST请求@RequestBody传Date对象实测没有问题,如果你还在使用旧版本可以考虑:
- 将传递Date对象变为传递序列化后的字符串,接收方再反序列化。
- 使用JDK8的LocalDate(日期)或LocalDateTime(精度到秒的日期)对象。
- 魔改Feign。
更换连接组件#
OpenFeign或者说Ribbon默认使用HttpURLConnection实现,如果传参有Body则会强制转为Post,这将导致MethodNotAllowed,要解决可以选择放弃传递Body的Get请求或使用其他请求组件。
但使用其他请求会遇到很多问题配置可能比较复杂,这里不记录我自己也不用,等到遇到性能瓶颈不得不改时会更新这个部分。
超时#
如果通过Feign调用服务,被调用方长时间处理,可能会导致超时,进而调用方向用户报500错误。
因为OpenFeign底层使用Ribbon,因此调节超时时间应该通过Ribbon配置调节:
ribbon:
ReadTimeout: 3000 #ms,调用超时,连接上了调用时的超时时间
ConnectTimeout: 3000 #ms,连接的超时时间,用来解决服务启动时间差等问题
#这些在源代码注释里面找,并且不同版本的默认值可能不同!日志#
Feign 提供了日志打印功能,我们可以通过配置来调整日志级别。
开启日志,需要通过配置类配置日志打印级别:
@Configuration
public class FeignConfig {
@Bean
Logger.Level feignLogger() {
return Logger.Level.FULL
}
/*
NONE 默认的,不显示日志
BASE 仅记录请求方法,URL ,响应状态码及执行时间
HEADERS 在 BASE 之上增加了请求和响应头的信息
FULL 在 HEADERS 之上增加了请求和响应的正文及无数
*/然后对你的Feign接口启用debug级别日志:
logging:
level:
xxx.xxxx.feign: debugNetflix Hystrix#
服务雪崩#
用户 -> 服务A -> 服务B -> 服务C如上所示的链式服务调用(依赖)中,服务提供者C因为某种原因出现故障,那么服务调用者服务B依赖于服务C的请求便无法成功调用其提供的接口,依赖于服务C的请求越来越多导致服务B的服务器或Web容器资源耗尽,造成服务B线程阻塞,导致服务B也出现故障。紧接着,服务A依赖于服务B,由于服务B也出现了故障导致服务A出现故障。以此类推引起整个链路中的所有微服务都不可用。
通常的解决办法有:
- 超时处理:设定超时时间,请求超过一定时间没有响应就返回错误信息,不会无休止等待
- 线程隔离:限定每个业务能使用的线程数,避免耗尽整个Web容器的资源,因此也叫线程隔离。
- 熔断降级:由断路器统计业务执行的异常比例,如果超出阈值则会熔断该业务,拦截访问该业务的一切请求。
- 流量控制:制业务访问的QPS,避免服务因流量的突增而故障。
在分布式链路中,只要有一个服务不可用,都会导致整个业务或者链路瘫痪!
服务雪崩的本质是线程没有及时回收。
Hystrix是一个熔断器、断路器,保护服务不产生服务雪崩。
缓存雪崩是指缓存中数据大批量到过期时间,而查询数据量巨大,引起数据库压力过大甚至down机。
使用Hystrix#
Hystrix常与OpenFeign、Ribbon一起使用,因为有服务调用才会有服务雪崩的发生,Hystrix才能隔离服务的访问点阻止联动故障。
服务提供者依旧无需改动,除非他也要调用其他服务。
添加Hystrix依赖:
<!-- https://mvnrepository.com/artifact/org.springframework.cloud/spring-cloud-starter-netflix-hystrix -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-hystrix</artifactId>
<version>2.2.10.RELEASE</version>
</dependency>你可能需要去掉version,也可能需要version,在我这(阿里镜像)不加version无法添加依赖。
某些版本的SpringCloud,可在Initializr里的SpringCloud CircuitBreaker下找到Hystrix,但我的3.0版本只有一个Resilience4J。
然后,在配置文件中开启:
feign:
hystrix:
enabled: true实测新版本没有了这个配置,也不在spring.cloud.openfeign下。翻阅其他文档得知,是在启动类上添加@EnableCircuitBreaker注解,也可以尝试用@SpringCloudApplication和@EnableHystrix。
在网上搜索文档的时候看到很多使用@HystrixCommand注解并配合RestTemplate使用的文章,但我看的文档使用OpenFeign时写法并不相同,但流程大同小异,因此想看的自行搜索。
要提供熔断机制,或者说熔断后的降级、替代措施,需要一个实现Feign接口的实现类:
@Component //需要让Spring托管
public class OrderServiceHystrix implements OrderServiceFeign {
@Override
public String doApi() {
return "我是备胎"
}
}这时候@Autowired注入OrderServiceFeign时,会有冲突,因为存在Feign的代理类和上面这个类,你可以使用@Resource或者添加@Qualified
同时,在原Feign接口的@FeignClient中添加fallback类,指定为我们实现的类:
@FeignClient(value = "provider-service", fallback = OrderServiceHystrix.class)
public interface OrderServiceFeign{
public String doApi();
}原理#
Hystrix本质类似于AOP切入了服务调用请求的过程,在开始前判断断路器开关,在结束后判断是否要更新断路器状态,相当于用了@Around。而断路器是决定调用请求进Fallback还是正常访问一个拦截机制。
断路器状态通常有关,开,半开。当为关是不拦截服务调用;当一定时间内调用失败次数达到阈值断路器将会打开,请求将进入Fallback(熔断了);当断路器打开是会让少许请求尝试调用服务,如果成功那么断路器将关闭,此时业务恢复正常。
Sleuth#
在微服务框架中,一个由客户端发起的请求在后端系统中会经过多个不同的服务节点调用来协同产生最后的请求结果,每一个请求都会开成一条复杂的分布式服务调用链路,链路中的任何一环出现高延时或错误都会引导起整个请求最后的失败。因此我们需要一些链路跟踪监控工具来监控我们的微服务,链路追踪指的是追踪微服务的调用路径。
不建议微服务中链路调用超过3次。
Sleuth是Spring Cloud提供的分布式系统服务链追踪组件,它大量借用了Google的Dapper,Twitter的Zipkin。
通常的方案是Sleuth+Zipkin,Zipkin 是 Twitter 的一个开源项目,允许开发者收集 Twitter 各个服务上的监控数据,并提供查询接口。该系统让开发者可通过一个 Web 前端轻松的收集和分析数据,例如用户每次请求服务的处理时间等,可方便的监测系统中存在的瓶颈。
Sleuth是一个上报工具,而Zipkin是一个处理和展示Sleuth所上报信息的工具。
运行Zipkin#
根据仓库Readme中的提示下载运行Jar即可。
默认在本地的9411端口启动,用浏览器可以进入。
Zipkin中有一些关键概念:
Trace:Zipkin使用Trace结构表示对一次请求的跟踪,一次请求可能由后台的若干服务负责处理,每个服务的处理是一个Span,Span之间有依赖关系,Trace就是树结构的Span集合。
Span:每个服务的处理跟踪是一个Span,可以理解为一个基本的工作单元,包含了一些描述信息:id,parentId,name,timestamp,duration,annotations等。
整合Sleuth#
除了注册中心之类的东西,都需要添加Sleuth进行上报,添加依赖:
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-zipkin</artifactId>
</dependency>有配置:
spring:
zipkin:
base-url: http://localhost:9411 #默认启动的Zipkin服务器地址
sleuth:
sampler:
probability: 1
# 配置采样率
# 默认的采样比例为: 0.1,即 10%,所设置的值介于 0 到 1 之间
rate: 10
# 为了使用速率限制采样器,每秒间隔接受的 trace 量
# 最小数字为 0,最大值为 INT_MAXAdmin#
Spring Boot Admin 用于监控基于 Spring Boot 的应用,它是在 Spring Boot Actuator 的基础上提供简洁的可视化 WEB UI。Spring Boot Admin 提供了很多功能,如显示 name、id 和 version,显示在线状态,Loggers 的日志级别管理,Threads 线程管理,Environment 管理等。
在 Spring Boot 项目中,Spring Boot Admin 作为 Server 端,其他的要被监控的应用作为 Client 端。
监控Admin/Server端#
添加依赖:
<!-- admin server -->
<dependency>
<groupId>de.codecentric</groupId>
<artifactId>spring-boot-admin-starter-server</artifactId>
<version>2.0.6</version>
</dependency>
<!-- admin server ui -->
<dependency>
<groupId>de.codecentric</groupId>
<artifactId>spring-boot-admin-server-ui</artifactId>
<version>2.0.6</version>
</dependency>
<!--添加admin安全登录界面,可选-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<!-- 通常需要向注册中心注册, 引入eureka客户端-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>启动类需要添加注解@EnableAdminServer。
并配置注册Eureka和安全相关:
# 向注册中心注册
spring:
application:
name: admin-server
# 监控网页的端口
server:
port: 9041
# Eureka
eureka:
client:
service-url:
defaultZone: ...
# admin安全配置
spring:
security:
user:
name: admin
password: admin
eureka:
instance:
metadata-map:
user:
name: ${spring.security.user.name}
password: ${spring.security.user.password}
# 我觉得安全配置多此一举,后面通过Gateway限制就好了被监控Client端#
需要引入依赖:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<dependency>
<groupId>de.codecentric</groupId>
<artifactId>spring-boot-admin-starter-client</artifactId>
<version>2.2.2</version>
</dependency>通过Actuator暴露服务的健康数据等(实际上是一堆/actuator开头的接口)。
默认只暴露少量健康信息,想要更多信息(详细到Beans),需要配置:
management:
endpoints:
web:
exposure:
include: '*' #表示全部其实我觉得没必要整这个东西,只需要加个Actuator依赖配置下暴露的内容,用IDEA自带的调试看看就够了。
Gateway#
SpringCloud Gateway是Spring官方用来取代Netflix Zuul的新网关组件,通过它和注册中心我们可以负载均衡地访问各个服务,无需知道服务具体的端口。如果你了解过NGINX,那么可以看做它的阉割版。
举个例子,通过访问80端口网关的/provider-a/abc/method?value=...通过配置可以转发到位于6666端口的服务上调用/abc/method?value=...,而前端无需知道服务在6666端口上。
Gateway的工作流程类似Tomcat等Web容器,客户端发出请求,然后在 Gateway Handler Mapping 中找到与请求相匹配的路由,将其发送到 Gateway Web Handler,其再通过一系列指定的过滤器(PRE过滤器)来将请求发送到我们实际的服务的业务逻辑,然后返回,会再经过过滤器链(POST过滤器)回到用户手里。
Gateway是运行在Netty上基于Webflux构建的,但使用起来没有什么差别。
核心概念#
| Route 路由 | 包含ID、目标URI、断言和过滤,相当于一条反向代理设置 |
| Predicate 断言 | 通过请求中各种信息决定是否可以走这条路由 |
| Filter 过滤 | 对请求和响应进行处理 |
建立网关#
SpringCloud Gateway就像Eureka,引入依赖启动就是一个网关。
通常需要引入Gateway本身和注册中心客户端,注册中心客户端用于拿到其他服务进行动态路由。你可以通过Spring Initializr添加Gateway和Eureka Discovery Client或者手动添加:
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-gateway</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>使用Eureka客户端,当然需要在启动类前添加@EnableDiscoveryClient。
添加了Gateway依赖就会自动配置启用,但我们需要配置其他一些东西:
# 显然,我们需要连接上Eureka服务端
eureka:
client:
service-url:
defaultZone: http://localhost:8761/eureka
# 我们还需要给他一个名字
spring:
application:
name: gateway
# 网关通常在80端口上
server:
port: 80配置路由#
我们可以使用动态路由或静态路由,动态路由通过注册中心拿到具体的服务地址并有负载均衡,而静态路由只是简单地帮你转发到一个固定的地址上,通常项目中都是使用动态路由。
静态路由#
这是一个配置静态路由的例子:
spring:
cloud:
gateway:
routes:
- id: a-route #规则id或者名称,不重复即可
uri: http://dest.com #目标URI
predicates: #断言
- Path=/out/** #Path断言
#当访问gateway的/out/**时,相当于访问http://dest.com/out/**有时候我们想实现通过访问gateway的/a/doApi来调用目标的/doApi,而没有前面的/a/,这需要过滤器StripPrefix:
filters:
- StripPrefix=2 #数字表示去掉几级,填2时/a/b/doApi会在目标上变成/doApi断言和过滤器非常多,具体可以看文档查阅。
动态路由#
动态路由只需要在自身已经注册到注册中心的情况下,开启即可:
spring:
cloud:
gateway:
discovery:
locator:
enabled: true # 默认没启用
lower-case-service-id: true # 默认是全大写然后就可以通过.../服务名称/method?value....的方式访问各个服务,并有负载均衡。
但如果你想像静态路由那样给个自定义路径名称,那就需要:
spring:
cloud:
gateway:
discovery:
locator:
enabled: true # 还是需要启用的
lower-case-service-id: true
routes:
- id: a-route
uri: lb://service-name # 微服务的名称,注意lb://
predicates:
- Path=/service/** 此时访问gateway的/service/doApi就相当于访问service-name服务的/service/doApi。
注意URI的协议为lb(LoadBalance),表示启用Gateway的负载均衡功能,网关将类似使用Ribbon那样自动将服务名称转换为地址。
使用动态路由时,你实现GloabalFileter时exchange.getRequest().getPath()将会得到带有服务名称前缀的请求路径!如:/service-xxx/api/method
跨域#
现在不用在Boot项目的每个控制器前加@CrossOrigin这种东西了,只需要对网关进行配置。
可以在配置文件中添加:
spring:
cloud:
gateway:
globalcors:
corsConfigurations:
'[/**]': # 表示放行全部
allowedOrigins: "*" # 表示允许所有源
allowedMethods: "*" # 表示允许所有请求方法
allowedHeaders: "*" # 表示不关心请求头上面配置应该和下面这种配置类方式一致:
@Configuration
public class CorsConfig {
@Bean
public CorsWebFilter corsFilter() {
CorsConfiguration config = new CorsConfiguration();
config.addAllowedMethod("*");
config.addAllowedOrigin("*");
config.addAllowedHeader("*");
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(new PathPatternParser());
source.registerCorsConfiguration("/**", config);
return new CorsWebFilter(source);
}
}以上是简单的全局跨域,作用于网关下所有微服务,可能会影响到应用的其它部分。如果需要更细粒度的控制,可以使用路由规则级别的配置方法。
添加一个跨域过滤器:
spring:
cloud:
gateway:
default-filters:
- name: Cors
args:
allowedOrigins: "*"
allowedMethods: "*"
allowedHeaders: "*"像这样作用于某个路由规则:
spring:
cloud:
gateway:
routes:
- id: my_route
uri: http://example.com
predicates:
- Path=/my/path/**
filters:
- name: Cors也可以使用自定义但没必要:
public class CustomCorsFilter extends CorsWebFilter {
public CustomCorsFilter(CorsConfigurationSource configSource) {
super(configSource);
}
@Override
public Mono<Void> filter(ServerWebExchange exchange, org.springframework.web.filter.reactive.HiddenHttpMethodFilter.HiddenHttpMethodRequestWrapper request, org.springframework.web.filter.reactive.HiddenHttpMethodFilter.HiddenHttpMethodResponseWrapper response) {
if (CorsUtils.isCorsRequest(exchange.getRequest())) {
return super.filter(exchange, request, response);
} else {
return Mono.empty();
}
}
}断言工厂简单了解#
这里说的断言其实是Java的Lambda中的一种“预设”,你应该早就见过了诸如Consumer、Supplier之类的,而Predicate就是接受参数返回boolean的一个断言/判断函数,就像boolean isValueOk(Object inValue)。
通俗的说,断言就是一些布尔表达式,满足条件的返回 true,不满足的返回 false。Spring Cloud Gateway将路由作为Spring WebFlux HandlerMapping基础架构的一部分进行匹配。所有这些断言都与HTTP请求的不同属性匹配。
这些断言的作用如下图所示:

一些使用的例子:
predicates: #断言匹配
- Path=/info/** #和服务中的路径匹配,是正则匹配的模式
- After=2020-01-20T17:42:47.789-07:00[Asia/Shanghai] #此断言匹配发生在指定日期时间之后的请求,ZonedDateTime dateTime=ZonedDateTime.now()获得
- Before=2020-06-18T21:26:26.711+08:00[Asia/Shanghai] #此断言匹配发生在指定日期时间之前的请求
- Between=2020-06-18T21:26:26.711+08:00[Asia/Shanghai],2020-06-18T21:32:26.711+08:00[Asia/Shanghai] #此断言匹配发生在指定日期时间之间的请求
- Host=**.bai*.com:* #主机路由断言工厂接受一个参数:主机名模式列表。该模式是一个 ant 样式的模式。作为分隔符。此断言匹配与模式匹配的主机头
- Method=GET,POST #方法路由断言工厂接受一个方法参数,该参数是一个或多个参数:要匹配的 HTTP 方法断言也可以自定义,但是自带的已经足够,通常没人去自定义。
过滤器工厂简单了解#
过滤器分为针对某一条路由的GatewayFilter和作用于所有路由的GlobalFilter,并且这些过滤器的作用与之前用过的Web容器或者说Servlet中的基本一致,如果你还记得如何用拦截器实现登录验证功能那你应该不会有什么疑问。
与断言不同的是,过滤器通常是我们自己自定义实现的,用来实现登录之类的功能。
一个全局拦截是否有token的例子#
@Component //需要注册为Bean
public class GlobalFilterConfig implements GlobalFilter, Ordered {
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
//获取请求的信息,和HttpServletRequest很类似
String token = exchange.getRequest() //exchange包含请求和响应
.getQueryParams() //url参数
.getFirst("token"); //获取
if (token == null) {
log.error("token 为空,说明没有认证");
//设置Response的状态码
exchange.getResponse()
.setStatusCode(HttpStatus.NOT_ACCEPTABLE);
//过滤器链不继续往下传,开始返回
return exchange.getResponse().setComplete();
}
//放行,传入下一个过滤器中
return chain.filter(exchange);
}
// 在过滤器链中的优先级,越小优先级越高,越先执行
@Override
public int getOrder() {
return 0;
}
}一个限流过滤器例子#
限制一段时间内,用户访问资源的次数,减轻服务器压力,有对用户IP进行访问次数限制和一段时间内对接口请求次数的限制。
以通过令牌桶算法与Redis为例实现。
首先我们需要在使用Gateway的基础上引入Redis的依赖>spring-boot-starter-data-redis-reactive,然后在配置文件中连接Redis并配置过滤器:
server:
port: 80
spring:
application:
name: gateway
cloud:
gateway:
enabled: true
routes:
- id: user-service
uri: lb://consumer-user-service
predicates:
- Path=/info/**
filters:
- name: RequestRateLimiter # 过滤器的名称,Redis提供的,就叫这个名
args:
key-resolver: '#{@hostAddrKeyResolver}' # 限流键解析器Bean的名称,见下文
redis-rate-limiter.replenishRate: 1 # 每秒令牌补充数量
redis-rate-limiter.burstCapacity: 3 # 令牌桶的大小
redis: #redis 的配置
host: 192.168.226.128
port: 6379
database: 0
eureka:
...创建配置类,包含上文的限流解析器:
@Configuration
public class RequestRateLimiterConfig {
//IP 限流,把用户的 IP 作为限流的 Key
@Bean
@Primary //作为Bean的主候选,根据情况而加,通常不加
public KeyResolver hostAddrKeyResolver() {
return (exchange) -> Mono.just(exchange.getRequest().getRemoteAddress().getHostName());
}
//用户 id 限流,把用户 ID 作为限流的 key
@Bean
public KeyResolver userKeyResolver() {
return exchange -> Mono.just(exchange.getRequest().getQueryParams().getFirst("userId"));
}
//请求接口限流,把请求的路径作为限流 key
@Bean
public KeyResolver apiKeyResolver() {
return exchange -> Mono.just(exchange.getRequest().getPath().value());
}
}SpringCloud工程项目布局#
SpringCloud项目通常是一个Maven父模块带着几个SpringBoot子模块,而父模块一般继承SpringBoot爷爷模块。
以IDEA为例,个人习惯是用Spring Initializr先创建一个SpringBoot项目,并且顺便勾选Cloud的依赖(比如Eureka),然后用Spring Initializr创建子模块,修改pom中的父模块指向。
将得到父模块的pom中将有:
<!-- SpringBoot爷爷项目 -->
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.0.2</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<!-- 关键,父模块并无代码而是一个管理者 -->
<packaging>pom</packaging>
<!-- 拥有的子模块,子模块里也需要设置父模块 -->
<modules>
<module>EurekaServer</module>
</modules>
<!-- 一些参数,也将传递给子模块,通常是JDK版本和库版本 -->
<properties>
<maven.compiler.source>17</maven.compiler.source>
<maven.compiler.target>17</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<java.version>17</java.version>
<spring-cloud.version>2022.0.1</spring-cloud.version>
<spring-cloud.eureka.version>4.0.0</spring-cloud.eureka.version>
</properties>
<!-- 一些传递给子模块的依赖,通常是公共、大家都需要的模块 -->
<dependencies>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
</dependencies>
<!-- 依赖管理,不会真的添加依赖,只是管理子模块依赖的版本 -->
<!-- 这里使用${}读取了上面的properties,子模块中也因此无需指定版本 -->
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>${spring-cloud.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-server</artifactId>
<version>${spring-cloud.eureka.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>使用Spring Initializr创建的pom还会有build块,需要删掉,因为父模块不需要编译!
子模块的pom:
<parent>
<groupId>父模块组ID</groupId>
<artifactId>父模块工件名</artifactId>
<version>父模块版本</version>
</parent>
<!-- 打包方式就可以为jar或war了 -->
<packaging>jar</packaging>
<!-- 当前模块名,要与父模块modules中的一致 -->
<artifactId>EurekaServer</artifactId>
<!-- 当前模块的依赖,如果父模块中的dependencyManagement已经写了版本,像这里就无需再写 -->
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-server</artifactId>
</dependency>
</dependencies>
<!-- 要调用其他自己写的模块,也是在这里添加 -->因为使用Spring Initializr不能指定父模块,所以你需要自己注意所有子模块和父模块都应该在一个组下。
而子模块之间可以抽离出一些公用的东西,比如将所有实体类、DAO层、Redis操作等都抽离出来放到类似service-common的子模块中,这在使用MybatisPlus并配合MybatisX插件生成DAO层和CRUD服务层时非常方便。
此时只要其他模块在dependencies中添加你的common模块,并在使用common模块的模块都配置好数据库和Redis连接地址即可。(最好把common模块里的配置文件删掉)
据说还可以通过配置中心或者配置文件包含之类的方式统一设置,笔者对此并不了解且可能会引发问题,后面可能会更新。另外,上述做法还能让不同模块用不同的数据源。
