1-SpringCloud:一套全家桶的微服务解决框架,理念就是解决我们在微服务架构中遇到的任何问题;
2-SpringCloudAlibaba:实现对SpringCloud组件进行扩展;
SpringCloud针对多种 Netflix 组件提供的开发工具包,其中包括 Eureka、Ribbon、Feign、Hystrix、Zuul、Archaius 等。
Spring Cloud Netflix 生态,到2020年,archaus/hystrix/ribbon/zuul/turbine等starter都会进入维护模式,进入维护模式意味着spring cloud团队不会再向这些模块中添加新的功能,但是仍然会修复安全问题和一些block级别的bug。只是没有新的功能迭代了,spring cloud netflix仍然可以继续使用。
进入维护模式的最根本原因还是Netflix对于zuul、ribbon等项目维护投入比较少、所以spring cloud 会在greenwich中把这些项目都进入到维护模式。
所以基本上现在如果构建新的微服务,基本都以springcloud alibaba为基准
SpringCloudAlibaba是阿里开发的一套微服务架构,目前已经纳入spring中;同Spring Cloud 一样,Spring Cloud Alibaba 也是一套微服务解决方案,包含开发分布式应用微服务的必需组件,方便开发者通过 Spring Cloud 编程模型轻松使用这些组件来开发分布式应用服务。
SpringCloudAlibaba主要阿里为了推广自家的商业服务而开发的一套微服务架构,再加上Netflix 停止了更新,所以现在更多的公司选择使用阿里系列的整体服务;
依托 Spring Cloud Alibaba,您只需要添加一些注解和少量配置,就可以将 Spring Cloud 应用接入阿里微服务解决方案,通过阿里中间件来迅速搭建分布式应用系统。
这幅图是 Spring Cloud Alibaba 系列组件,其中包含了阿里开源组件,阿里云商业化组件,以及集成 Spring Cloud 组件。
1-Nacos:一个更易于构建云原生应用的动态服务发现、配置管理和服务管理平台。
2-Gateway:API网关(webflux编程模式)
3-Sentinel:把流量作为切入点,从流量控制、熔断降级、系统负载保护等多个维度保护服务的稳定性。
4-RocketMQ:开源的分布式消息系统,基于高可用分布式集群技术,提供低延时的、高可靠的消息发布与订阅服务。
5-Dubbo:这个就不用多说了,在国内应用非常广泛的一款高性能 Java RPC 框架。
6-Seata:阿里巴巴开源产品,一个易于使用的高性能微服务分布式事务解决方案。
7-Arthas:开源的Java动态追踪工具,基于字节码增强技术,功能非常强大。
总结目前市面上的技术使用,可以看到SpringCloudNetflix 的技术基本被SpringCloudAlibaba替代了:
1-组件性能更强
2-良好的可视化界面
3-搭建简单,学习曲线低
4-文档丰富并且是中文
(1)SpringCloud Alibaba核心组件的用法及实现原理
(2)SpringCloud Alibaba结合微信小程序,从“0”学习真正开发中的使用
(3)实际工作如何避免踩坑,正确的思考问题方式
(4)SpringCloud Alibaba的进阶:代码的优化和改善,微服务监控
(1)服务发现机制就是通过一个中间件去记录服务提供者的ip地址,服务名以及心跳等数据(比如用mysql去存储这些信息,所以Nacos要配置持久化),然后服务消费者会去这个中间平台去查询相关信息,然后再去访问对应的地址,这就是服务注册和服务发现。
(2)当用户地址发生了变化也没有影响,因为服务提供方修改了用户地址,在中间件中会被更新,当服务消费方去访问中间件时就能及时获取最新的用户地址,就不会出现用户地址发生变化导致服务找不到
(1)官方文档:什么是Nacos
(2)下载安装搭建NacosServer
下载Nacos:Nacos下载
搭建Nacos:Nacos搭建
启动服务器:(单机启动模式,非集群)
下载解压后,终端进入bin目录:cd /Library/Java/AllenNacos/nacos/bin
Linux/Unix/Mac:
sh startup.sh -m standalone
Windows:
cmd startup.md
使用此地址进入界面,http://127.0.0.1:8848/nacos/
账号密码均为nacos
在终端进入bin目录使用sh shutdown.sh关闭nacos
(3)配置Nacos持久化到数据库
来到Nacos的解压目录下的conf下的application.properties文件中,将连接到外置本地数据库的代码去掉注释并修改为
spring.datasource.platform=mysql
Count of DB:
db.num=1
Connect URL of DB:
db.url.0=jdbc:mysql://127.0.0.1:3306/ry-configcharacterEncoding=utf8&connectTimeout=1000&socketTimeout=3000&autoReconnect=true&useUnicode=true&useSSL=false&serverTimezone=UTC
db.user=root
db.password=你的密码
(1)目标
1-用户中心注册到Nacos
2-内容中心注册到Nacos
(2)用户中心注册到Nacos
1-pom加依赖
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discoveryartifactId>
dependency>
2-加注解
早期在启动类上需要加上 @EnableDiscoveryClient注解,现在已经可以不需要加了
3-加yml配置
spring:
cloud:
discovery:
server-addr: localhost:8848 #指定nacos server的地址
application:
name: 服务名称 # 比如 user-center,服务名称尽量用- ,不要用_
注册成功如图所示
4-补充内容
使用DiscoverClient的相关Api可以在代码中获取Nacos提供的微服务的一些信息,调用方法如图:
cloudalibaba-provider-payment9001
父模块pom引入SpringCloudAlibaba的依赖
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-dependenciesartifactId>
<version>Hoxton.SR1version>
<type>pomtype>
<scope>importscope>
dependency>
<dependency>
<groupId>com.alibaba.cloudgroupId>
<artifactId>spring-cloud-alibaba-dependenciesartifactId>
<version>2.2.0.RELEASEversion>
<type>pomtype>
<scope>importscope>
dependency>
本模块pom引入nacos-discovery的依赖
<dependency>
<groupId>com.alibaba.cloudgroupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discoveryartifactId>
dependency>
将9001微服务注册到8848Nacos服务中心
server:
port: 9001
spring:
application:
name: nacos-payment-provider
cloud:
nacos:
discovery:
server-addr: localhost:8848 #配置nacos的地址
# 将监控的内容暴露出来
management:
endpoints:
web:
exposure:
include: '*'
添加注解@EnableDiscoveryClient
@SpringBootApplication
@EnableDiscoveryClient
public class PaymentMain9001 {
public static void main(String[] args) {
SpringApplication.run(PaymentMain9001.class,args);
}
}
@RestController
public class PaymentController {
@Value("${server.port}")
private String serverPort;
@GetMapping(value = "/payment/nacos/{id}")
public String getPayment(@PathVariable("id") Integer id)
{
return "nacos registry, serverPort: "+ serverPort+"\t id"+id;
}
}
先启动安装好的Nacos,默认端口就是8848,启动成功后进入 http://127.0.0.1:8848/nacos/ 查看控制台页面
然后启动9001服务的Application,启动成功后再看Nacos页面,可以看到新的服务注册进来了
几乎完全拷贝,只是port端口不一样
也进行启动,然后看到新的服务注册进来了
点进详情查看
cloudalibaba-consumer-nacos-order83
接下来的修改pom、修改yml配置文件、修改主启动类等等相同的步骤跳过
server:
port: 83
spring:
application:
name: nacos-order-consumer
cloud:
nacos:
discovery:
server-addr: localhost:8848
#消费者将要去访问的微服务名称(注册成功进nacos的微服务提供者)
service-url:
nacos-user-service: http://nacos-payment-provider
编写RestTemplate类的封装,加上了@LoadBalanced注解,83端口消费者根据请求的接口到Nacos服务中心去找,结果会找到两个一样的接口,分别在9001和9002,加上@LoadBalanced注解以后就会实现负载均衡,在这两个接口之间选择使用。
如果不加@LoadBalanced注解会报错,表示分不清用哪一个端口的接口
@Configuration
public class ApplicationContextConfig {
@Bean
@LoadBalanced//负载均衡
public RestTemplate getRestTemplate(){
return new RestTemplate();
}
}
@RestController
@Slf4j
public class OrderNacosController {
@Resource
private RestTemplate restTemplate;
//value注解读取的是yml配置文件里的数据,将配置和代码分开
@Value("${service-url.nacos-user-service}")
private String serverURL;
@GetMapping(value = "/consumer/payment/nacos/{id}")
public String paymentInfo(@PathVariable("id") Long id){
//访问提供者的路径
return restTemplate.getForObject(serverURL+"/payment/nacos/"+id,String.class);
}
}
启动83服务
访问83的端口:http://localhost:83/consumer/payment/nacos/13
可以看到访问的接口在9001端口和9002端口轮流调用
(1)什么是配置中心?
在微服务架构中,当系统从一个单体应用,被拆分成分布式系统上一个个服务节点后,配置文件也必须跟着迁移(分割),这样配置就分散了,不仅如此,分散中还包含着冗余,如下图:
总得来说,配置中心就是一种统一管理各种应用配置的基础服务组件。
(2)为什么使用配置中心?
配置中心将配置从各应用中剥离出来,对配置进行统一管理,应用自身不需要自己去管理配置。
(3)Nacos配置中心
Nacos是阿里巴巴开源的一个更易于构建云原生应用的动态服务发现、配置管理和服务管理平台。
配置中心的服务流程如下:
1、用户在配置中心更新配置信息。
2、服务A和服务B及时得到配置更新通知,从配置中心获取配置。
(1)新建Module
cloudalibaba-config-nacos-client3377
(2)修改pom
<dependency>
<groupId>com.alibaba.cloudgroupId>
<artifactId>spring-cloud-starter-alibaba-nacos-configartifactId>
dependency>
<dependency>
<groupId>com.alibaba.cloudgroupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discoveryartifactId>
dependency>
(3)修改yml配置文件
Nacos同springcloud-config一样,在项目初始化时,要保证先从配置中心进行配置拉取,拉取配置之后,才能保证项目的正常启动。
在这里需要由bootstrap.yml和application.yml两个配置文件,为的就是方便Nacos和springcloud的Config无缝的迁移。springboot中配置文件的加载是存在优先级顺序的,bootstrap>application。
所以我们要把重点的配置信息放在bootstrap.yml配置文件中,全局的一些配置放在bootstrap.yml,自己单独的配置放在application.yml
bootstrap.yml内容如下
spring:
application:
name: nacos-config-client
profiles:
active: dev # 表示开发环境,test表示测试环境,info
cloud:
nacos:
discovery:
server-addr: localhost:8848 #Nacos服务注册中心地址
config:
server-addr: localhost:8848 #Nacos作为配置中心地址
file-extension: yaml #指定ymal格式的配置
# 配置文件名(data id):${spring.application.name}-${spring.profile.active}.${spring.cloud.nacos.config.file-extension}
# nacos-config-client-dev.yaml
application.yml内容如下
server:
port: 3377
这两个配置文件合并起来的意思就是:到8848上面找一个名字为XXX.yaml的配置文件
(4)创建主启动类
添加@EnableDiscoveryClient的注解
(5)创建业务类
@RestController
@RefreshScope//支持Nacos的动态刷新功能
public class ConfigClientController {
//可以从8848配置中心中读取到${config.info}的值
@Value("${config.info}")
private String configInfo;
@GetMapping("/config/info")
public String getConfigInfo(){
return configInfo;
}
}
在Nacos的配置列表中添加data id,这个id名称的命名规则如下,配置文件名(data id):
${spring.application.name}-${spring.profile.active}.${spring.cloud.nacos.config.file-extension}
1-spring.application.name的值在这里就是配置文件中的nacos-config-client
2-spring.profile.active的值就是dev
3-spring.cloud.nacos.config.file-extension的值就是yaml
4-所以最终的dataId的名称就是nacos-config-client-dev.yaml
加下来开始实操在Nacos中添加配置信息
然后点击发布就行了
(7)测试
启动3377,然后调用接口查看配置信息:http://localhost:3377/config/info
(8)自带动态刷新
在Nacos配置中心中修改配置的内容,然后在不重启3377的情况,重新访问接口查看3377获取到的配置信息有没有实时刷新
可以看到修改过后的配置信息已经实时刷新了,而且不需要重新服务
(1)问题:多环境多项目管理
问题1:在实际开发中,通常一个系统会准备 dev开发环境、test测试环境、prod生产环境。如何保证指定环境启动时服务能正确读取到Nacos上相应环境的配置文件呢?
问题2:在一个大型分布式微服务系统中有很多微服务子项目,每个微服务项目又都会有相应的开发环境、测试环境、预发环境、正式环境等等。那怎么对这些微服务配置进行管理呢?
(2)Nacos的图形化管理界面
(3)Namespace+Group+DataID三者关系?为什么这么设计?
默认情况:Namespace=public, Group=DEFAULT_GROUP, 默认Cluster是DEFAULT
(4)案例
在单机模式时nacos使用嵌入式数据库实现数据的存储,不方便观察数据存储的基本情况。我们可以配置mysql数据库,可视化的查看数据的存储。
1、安装数据库,版本要求:5.6.5+
2、使用sql/ry_config_xxxx文件初始化ry-config数据库
3、修改conf/application.properties文件增加mysql支持
# db mysql
spring.datasource.platform=mysql
db.num=1
db.url.0=jdbc:mysql://localhost:3306/ry-config?characterEncoding=utf8&connectTimeout=1000&socketTimeout=3000&autoReconnect=true&useUnicode=true&useSSL=false&serverTimezone=UTC
db.user=root
db.password=password
这个application.properties指nacos的解压目录nacos/conf目录下的文件
之前在使用Eureka的时候,如果要搭建集群就要启动好几个Eureka服务,让它们之间互相注册互相守望,而且还要添加Eureka的自我保护机制。现在换成Nacos不用手动创建和启动多个服务中心了,也屏蔽了自我保护机制
集群模式适用于生产环境需要依赖mysql,单机可不必,三个及以上Nacos节点才能构成集群。
1、在nacos的解压目录nacos/conf目录下,修改配置文件cluster.conf
192.168.100.101:8848
192.168.100.102:8848
192.168.100.103:8848
2、修改bootstrap.yml中的server-addr属性,添加对应集群地址。
server-addr: 192.168.100.101:8848,192.168.100.102:8848,192.168.100.103:8848
(1)服务列表管理
(2)服务流量权重支持及流量保护
Nacos为用户提供了流量权重控制的能力,同时开放了服务流量的阈值保护,以帮助用户更好的保护服务服务提供者集群不被意外打垮。如下图所以,可以点击实例的编辑按钮,修改实例的权重。如果想增加实例的流量,可以将权重调大,如果不想实例接收流量,则可以将权重设为0。
(3)服务元数据管理
Nacos提供多个维度的服务元数据的暴露,帮助用户存储自定义的信息。这些信息都是以K-V的数据结构存储,在控制台上,会以{“version”:“1.0”,“env”:“prod”}这样的格式展示。类似的,编辑元数据可以通过相同的格式进行。例如服务的元数据编辑,首先点击服务详情页右上角的“编辑服务”按钮,然后在元数据输入框输入:{“version”:“1.0”,“env”:“prod”}。
点击确认,就可以在服务详情页面,看到服务的元数据已经更新了。
服务优雅上下线
Nacos还提供服务实例的上下线操作,在服务详情页面,可以点击实例的“上线”或者“下线”按钮,被下线的实例,将不会包含在健康的实例列表里。
Nacos支持基于Namespace和Group的配置分组管理,以便用户更灵活的根据自己的需要按照环境或者应用、模块等分组管理微服务以及Spring的大量配置,在配置管理中主要提供了配置历史版本、回滚、订阅者查询等核心管理能力。
(1)配置的版本及一键回滚
Nacos基于Namespace帮助用户逻辑隔离多个命名空间,这可以帮助用户更好的管理测试、预发、生产等多环境服务和配置,让每个环境的同一个配置(如数据库数据源)可以定义不同的值。
修改用户名和密码,将ry-config中的user表username替换成你需要的登录账户,password改成你需要的密码,密码运行即可得到加密有算法。注意盐值是随机的,所以生成密码每次可能不一样,请不要担心。
public static void main(String[] args)
{
System.out.println(new BCryptPasswordEncoder().encode("ruoyi"));
}
默认会话保持时间为30分钟。30分钟后需要重新登录认证。 暂时不支持修改该默认时间。
(1)Ribbon是什么?
可以实现负载均衡,为我们提供了丰富的负载均衡算法
(2)修改pom添加依赖
此步骤略过,因为Nacos已经结合了Ribbon
(3)加注解
在RestTemplate的Bean上加@LoadBalanced,如图所示:
@Bean
@LoadBalanced
public RestTemplate restTemplate{
return new RestTemplate()
}
使用RestTemplate
(1)在与启动类包不同路径下创建配置类,指定均衡规则
@Configuration
public class RibbonConfiguration{
@Bean
public IRule ribbonRule(){
// 随机
return new RandomRule();
}
}
@RibonClient(name = "服务名称", configuration=RibbonConfiguration.class)
public class XXXRibbonConfiguration{
}
(1)在resource目录下的application.yml中添加配置:
xxx服务名称:
ribbon:
NFLoadBalancerRuleClassName: com.netflix.loadbalancer.RandomRule # 想要的规则的类的所在全路径
属性配置方式优先级更高。尽量使用属性配置,属性方式实现不了的情况下再考虑用代码配置。
在同一个微服务内尽量保持单一性,比如统一使用属性配置,不要两种方式混用,增加定位代码的复杂性。
(1)Feign
Feign 是Spring Cloud Netflix组件中的一量级Restful的 HTTP 服务客户端,实现了负载均衡和 Rest 调用的开源框架,封装了Ribbon和RestTemplate, 实现了WebService的面向接口编程,进一步降低了项目的耦合度。
(2)什么是服务调用
顾名思义,就是服务之间的接口互相调用,在微服务架构中很多功能都需要调用多个服务才能完成某一项功能。
(3)为什么要使用Feign
Feign 旨在使编写 JAVA HTTP 客户端变得更加简单,Feign 简化了RestTemplate代码,实现了Ribbon负载均衡,使代码变得更加简洁,也少了客户端调用的代码,使用 Feign 实现负载均衡是首选方案,只需要你创建一个接口,然后在上面添加注解即可。
Feign 是声明式服务调用组件,其核心就是:像调用本地方法一样调用远程方法,无感知远程 HTTP 请求。让开发者调用远程接口就跟调用本地方法一样的体验,开发者完全无感知这是远程方法,无需关注与远程的交互细节,更无需关注分布式环境开发。
(4)Feign vs OpenFeign
Feign 内置了Ribbon,用来做客户端负载均衡调用服务注册中心的服务。
Feign 支持的注解和用法参考官方文档:https://github.com/OpenFeign/feign官方文档,使用 Feign 的注解定义接口,然后调用这个接口,就可以调用服务注册中心的服务。
Feign本身并不支持Spring MVC的注解,它有一套自己的注解,为了更方便的使用Spring Cloud孵化了OpenFeign。并且支持了Spring MVC的注解,如@RequestMapping,@PathVariable等等。
OpenFeign的@FeignClient可以解析Spring MVC的@RequestMapping注解下的接口,并通过动态代理方式产生实现类,实现类中做负载均衡调用服务。
(1)添加依赖
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-starter-openfeignartifactId>
dependency>
(2)新建RemoteUserService.java服务接口
Feign与Ribbon也可以一起整合,可以参考Feign的整合方式
/**
* 用户服务
*
* @author ruoyi
*/
@FeignClient(contextId = "remoteUserService", value = ServiceNameConstants.SYSTEM_SERVICE, fallbackFactory = RemoteUserFallbackFactory.class)
public interface RemoteUserService
{
/**
* 通过用户名查询用户信息
*
* @param username 用户名
* @return 结果
*/
@GetMapping(value = "/user/info/{username}")
public R<LoginUser> getUserInfo(@PathVariable("username") String username);
}
(3)新建RemoteUserFallbackFactory.java降级实现
/**
* 用户服务降级处理
*
* @author ruoyi
*/
@Component
public class RemoteUserFallbackFactory implements FallbackFactory<RemoteUserService>
{
private static final Logger log = LoggerFactory.getLogger(RemoteUserFallbackFactory.class);
@Override
public RemoteUserService create(Throwable throwable)
{
log.error("用户服务调用失败:{}", throwable.getMessage());
return new RemoteUserService()
{
@Override
public R<LoginUser> getUserInfo(String username)
{
return R.fail("获取用户失败:" + throwable.getMessage());
}
};
}
}
(4)消费者TestUserController.java新增info查询用户方法
@RestController
public class TestUserController
{
@Autowired
private RemoteUserService remoteUserService;
/**
* 获取当前用户信息
*/
@GetMapping("/user/{username}")
public Object info(@PathVariable("username") String username)
{
return remoteUserService.getUserInfo(username);
}
}
(5)启动类添加@EnableRyFeignClients注解,默认的@EnableRyFeignClients扫描范围com.ruoyi。
(6)启动后访问http://localhost:8888/user/admin,返回正确数据表示测试通过。
目前已经存在ruoyi-api-system系统接口模块,用于服务调用。
Feign默认集成了Ribbon,Nacos也很好的兼容了Feign,默认实现了负载均衡的效果。
Get方式传参,使用@PathVariable、@RequestParam注解接收请求参数
@GetMapping(value = "/user/info/{username}")
public R<LoginUser> getUserInfo(@PathVariable("username") String username);
Post方式传参,使用@RequestBody注解接收请求参数。
@PostMapping("/operlog")
public R<Boolean> saveLog(@RequestBody SysOperLog sysOperLog);
(1)什么是服务网关
(2)为什么要使用网关
【路由转发】
微服务的应用可能部署在不同机房,不同地区,不同域名下。此时客户端(浏览器/手机/软件工具)想 要请求对应的服务,都需要知道机器的具体 IP 或者域名 URL,当微服务实例众多时,这是非常难以记忆的,对 于客户端来说也太复杂难以维护。此时就有了网关,客户端相关的请求直接发送到网关,由网关根据请求标识 解析判断出具体的微服务地址,再把请求转发到微服务实例。这其中的记忆功能就全部交由网关来操作了。
【统一处理请求】
一个系统划分成多个不同的微服务,每个业务都会需要鉴权、限流、权限校验、跨域等逻辑处理,如果每个业务都自己实现这些功能,就会存在大量的重复劳动,完全可以把这些共同的功能抽取出来,放到一个统一的地方去做,这个地方就是网关
(3)三个核心概念
1-路由(Route):路由是网关最基础的部分,路由信息由 ID、目标 URI、一组断言和一组过滤器组成。如果断言 路由为真,则说明请求的 URI 和配置匹配。
2-断言(Predicate):Java8 中的断言函数。Spring Cloud Gateway 中的断言函数输入类型是 Spring 5.0 框架中 的 ServerWebExchange。Spring Cloud Gateway 中的断言函数允许开发者去定义匹配来自于 Http Request 中的任 何信息,比如请求头和参数等。
3-过滤器(Filter):一个标准的 Spring Web Filter。Spring Cloud Gateway 中的 Filter 分为两种类型,分别是 Gateway Filter 和 Global Filter。过滤器将会对请求和响应进行处理。
(4)项目网关的逻辑
网关的端口号是8080,在每个功能模块的配置文件中都设置了断言进行请求拦截,拦截到的请求会先进入网关模块进行处理和转发,最后根据请求路径找到对应模块中的对应Controller。
gateway网关服务,用于路由转发、异常处理、限流、降级、接口、鉴权等等。
(1)添加依赖
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-starter-gatewayartifactId>
dependency>
(2)修改yml配置文件
server:
port: 8080
spring:
application:
name: ruoyi-gateway
cloud:
gateway:
routes:
# 系统模块
- id: ruoyi-system
uri: http://localhost:9201/
predicates:
- Path=/system/**
filters:
- StripPrefix=1
(3)网关启动类
@SpringBootApplication
public class RuoYiGatewayApplication
{
public static void main(String[] args)
{
SpringApplication.run(RuoYiGatewayApplication.class, args);
System.out.println("(♥◠‿◠)ノ゙ 若依网关启动成功 ლ(´ڡ`ლ)゙ \n" +
" .-------. ____ __ \n" +
" | _ _ \\ \\ \\ / / \n" +
" | ( ' ) | \\ _. / ' \n" +
" |(_ o _) / _( )_ .' \n" +
" | (_,_).' __ ___(_ o _)' \n" +
" | |\\ \\ | || |(_,_)' \n" +
" | | \\ `' /| `-' / \n" +
" | | \\ / \\ / \n" +
" ''-' `'-' `-..-' ");
}
}
(1)Datetime
匹配日期时间之后发生的请求
spring:
application:
name: ruoyi-gateway
cloud:
gateway:
routes:
- id: ruoyi-system
uri: http://localhost:9201/
predicates:
- After=2021-02-23T14:20:00.000+08:00[Asia/Shanghai]
(2)Cookie
匹配指定名称且其值与正则表达式匹配的cookie
spring:
application:
name: ruoyi-gateway
cloud:
gateway:
routes:
- id: ruoyi-system
uri: http://localhost:9201/
predicates:
- Cookie=loginname, ruoyi
测试 curl http://localhost:8080/system/config/1 --cookie “loginname=ruoyi”
就会跳转到:http://localhost:9201/
(3)Header
匹配具有指定名称的请求头,\d+值匹配正则表达式
spring:
application:
name: ruoyi-gateway
cloud:
gateway:
routes:
- id: ruoyi-system
uri: http://localhost:9201/
predicates:
- Header=X-Request-Id, \d+
(4)Host
匹配主机名的列表
spring:
application:
name: ruoyi-gateway
cloud:
gateway:
routes:
- id: ruoyi-system
uri: http://localhost:9201/
predicates:
- Host=**.somehost.org,**.anotherhost.org
(5)Method
匹配请求methods的参数,它是一个或多个参数
spring:
application:
name: ruoyi-gateway
cloud:
gateway:
routes:
- id: ruoyi-system
uri: http://localhost:9201/
predicates:
- Method=GET,POST
(6)Path
匹配请求路径
spring:
application:
name: ruoyi-gateway
cloud:
gateway:
routes:
- id: ruoyi-system
uri: http://localhost:9201/
predicates:
- Path=/system/**
(7)Query
匹配查询参数
spring:
application:
name: ruoyi-gateway
cloud:
gateway:
routes:
- id: ruoyi-system
uri: http://localhost:9201/
predicates:
- Query=username, abc.
(8)RemoteAddr
匹配IP地址和子网掩码
spring:
application:
name: ruoyi-gateway
cloud:
gateway:
routes:
- id: ruoyi-system
uri: http://localhost:9201/
predicates:
- RemoteAddr=192.168.10.1/0
(9)Weight
匹配权重
spring:
application:
name: ruoyi-gateway
cloud:
gateway:
routes:
- id: ruoyi-system-a
uri: http://localhost:9201/
predicates:
- Weight=group1, 8
- id: ruoyi-system-b
uri: http://localhost:9201/
predicates:
- Weight=group1, 2
在spring cloud gateway中配置uri有三种方式,包括
现在断言的主要方式就是匹配请求路径
(1)websocket配置方式
spring:
cloud:
gateway:
routes:
- id: ruoyi-api
uri: ws://localhost:9090/
predicates:
- Path=/api/**
(2)http地址配置方式
spring:
application:
name: ruoyi-gateway
cloud:
gateway:
routes:
- id: ruoyi-system
uri: http://localhost:9201/
predicates:
- Path=/system/**
(3)注册中心配置方式
spring:
cloud:
gateway:
routes:
- id: ruoyi-api
uri: lb://ruoyi-api
predicates:
- Path=/api/**
通过限流,我们可以很好地控制系统的 QPS,从而达到保护系统的目的。
常见的限流算法有:计数器算法,漏桶(Leaky Bucket)算法,令牌桶(Token Bucket)算法。
Spring Cloud Gateway官方提供了RequestRateLimiterGatewayFilterFactory过滤器工厂,使用Redis 和Lua脚本实现了令牌桶的方式。
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-data-redis-reactiveartifactId>
dependency>
限流规则,根据URI限流
spring:
redis:
host: localhost
port: 6379
password:
cloud:
gateway:
routes:
# 系统模块
- id: ruoyi-system
uri: lb://ruoyi-system
predicates:
- Path=/system/**
filters:
- StripPrefix=1
- name: RequestRateLimiter
args:
redis-rate-limiter.replenishRate: 1 # 令牌桶每秒填充速率
redis-rate-limiter.burstCapacity: 2 # 令牌桶总容量
key-resolver: "#{@pathKeyResolver}" # 使用 SpEL 表达式按名称引用 bean
StripPrefix=1配置,表示网关转发到业务模块时候会自动截取前缀。
/**
* 限流规则配置类
*/
@Configuration
public class KeyResolverConfiguration
{
@Bean
public KeyResolver pathKeyResolver()
{
return exchange -> Mono.just(exchange.getRequest().getURI().getPath());
}
}
启动网关服务RuoYiGatewayApplication.java和系统服务RuoYiSystemApplication.java。
因为网关服务有认证鉴权,可以设置一下白名单/system/**在进行测试,多次请求会发现返回HTTP ERROR 429,同时在redis中会操作两个key,表示限流成功。
request_rate_limiter.{xxx}.timestamp
request_rate_limiter.{xxx}.tokens
(1)参数限流:key-resolver: “#{@parameterKeyResolver}”
@Bean
public KeyResolver parameterKeyResolver()
{
return exchange -> Mono.just(exchange.getRequest().getQueryParams().getFirst("userId"));
}
(2)IP限流:key-resolver: “#{@ipKeyResolver}”
@Bean
public KeyResolver ipKeyResolver()
{
return exchange -> Mono.just(exchange.getRequest().getRemoteAddress().getHostName());
}
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-starter-netflix-hystrixartifactId>
dependency>
spring:
redis:
host: localhost
port: 6379
password:
cloud:
gateway:
routes:
# 系统模块
- id: ruoyi-system
uri: lb://ruoyi-system
predicates:
- Path=/system/**
filters:
- StripPrefix=1
# 降级配置
- name: Hystrix
args:
name: default
# 降级接口的地址
fallbackUri: 'forward:/fallback'
上面配置包含了一个Hystrix过滤器,该过滤器会应用Hystrix熔断与降级,会将请求包装成名为fallback的路由指令RouteHystrixCommand,RouteHystrixCommand继承于HystrixObservableCommand,其内包含了Hystrix的断路、资源隔离、降级等诸多断路器核心功能,当网关转发的请求出现问题时,网关能对其进行快速失败,执行特定的失败逻辑,保护网关安全。
配置中有一个可选参数fallbackUri,当前只支持forward模式的URI。如果服务被降级,请求会被转发到该URI对应的控制器。控制器可以是自定义的fallback接口;也可以使自定义的Handler,需要实现接口org.springframework.web.reactive.function.server.HandlerFunction。
/**
* 熔断降级处理
*
* @author ruoyi
*/
@Component
public class HystrixFallbackHandler implements HandlerFunction<ServerResponse>
{
private static final Logger log = LoggerFactory.getLogger(HystrixFallbackHandler.class);
@Override
public Mono<ServerResponse> handle(ServerRequest serverRequest)
{
Optional<Object> originalUris = serverRequest.attribute(GATEWAY_ORIGINAL_REQUEST_URL_ATTR);
originalUris.ifPresent(originalUri -> log.error("网关执行请求:{}失败,hystrix服务降级处理", originalUri));
return ServerResponse.status(HttpStatus.INTERNAL_SERVER_ERROR.value()).contentType(MediaType.APPLICATION_JSON)
.body(BodyInserters.fromValue(JSON.toJSONString(R.fail("服务已被降级熔断"))));
}
}
/**
* 路由配置信息
*
* @author ruoyi
*/
@Configuration
public class RouterFunctionConfiguration
{
@Autowired
private HystrixFallbackHandler hystrixFallbackHandler;
@Autowired
private ValidateCodeHandler validateCodeHandler;
@SuppressWarnings("rawtypes")
@Bean
public RouterFunction routerFunction()
{
return RouterFunctions
.route(RequestPredicates.path("/fallback").and(RequestPredicates.accept(MediaType.TEXT_PLAIN)),
hystrixFallbackHandler)
.andRoute(RequestPredicates.GET("/code").and(RequestPredicates.accept(MediaType.TEXT_PLAIN)),
validateCodeHandler);
}
}
启动网关服务RuoYiGatewayApplication.java,访问/system/**在进行测试,会发现返回服务已被降级熔断,表示降级成功。
顾名思义,就是不能访问的地址。实现自定义过滤器BlackListUrlFilter,需要配置黑名单地址列表blacklistUrl,当然有其他需求也可以实现自定义规则的过滤器。
spring:
cloud:
gateway:
routes:
# 系统模块
- id: ruoyi-system
uri: lb://ruoyi-system
predicates:
- Path=/system/**
filters:
- StripPrefix=1
- name: BlackListUrlFilter
args:
blacklistUrl:
- /user/list
顾名思义,就是允许访问的地址。且无需登录就能访问。
在ignore中设置whites,表示允许匿名访问。
# 不校验白名单
ignore:
whites:
- /auth/logout
- /auth/login
- /*/v2/api-docs
- /csrf
全局过滤器作用于所有的路由,不需要单独配置,我们可以用它来实现很多统一化处理的业务需求,比如权限认证,IP访问限制等等。目前网关统一鉴权AuthFilter.java就是采用的全局过滤器。
单独定义只需要实现GlobalFilter, Ordered这两个接口就可以了。
/**
* 全局过滤器
*
* @author ruoyi
*/
@Component
public class AuthFilter implements GlobalFilter, Ordered
{
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain)
{
String token = exchange.getRequest().getQueryParams().getFirst("token");
if (null == token)
{
ServerHttpResponse response = exchange.getResponse();
response.getHeaders().add("Content-Type", "application/json; charset=utf-8");
String message = "{\"message\":\"请求token信息不能为空\"}";
DataBuffer buffer = response.bufferFactory().wrap(message.getBytes());
return response.writeWith(Mono.just(buffer));
}
return chain.filter(exchange);
}
@Override
public int getOrder()
{
return 0;
}
}
Sentinel 支持对 Spring Cloud Gateway、Netflix Zuul 等主流的 API Gateway 进行限流。
<dependency>
<groupId>com.alibaba.cloudgroupId>
<artifactId>spring-cloud-starter-alibaba-sentinelartifactId>
dependency>
<dependency>
<groupId>com.alibaba.cloudgroupId>
<artifactId>spring-cloud-alibaba-sentinel-gatewayartifactId>
dependency>
/**
* 网关限流配置
*
* @author ruoyi
*/
@Configuration
public class GatewayConfig
{
@Bean
@Order(Ordered.HIGHEST_PRECEDENCE)
public SentinelFallbackHandler sentinelGatewayExceptionHandler()
{
return new SentinelFallbackHandler();
}
@Bean
@Order(-1)
public GlobalFilter sentinelGatewayFilter()
{
return new SentinelGatewayFilter();
}
@PostConstruct
public void doInit()
{
// 加载网关限流规则
initGatewayRules();
}
/**
* 网关限流规则
*/
private void initGatewayRules()
{
Set<GatewayFlowRule> rules = new HashSet<>();
rules.add(new GatewayFlowRule("ruoyi-system")
.setCount(3) // 限流阈值
.setIntervalSec(60)); // 统计时间窗口,单位是秒,默认是 1 秒
// 加载网关限流规则
GatewayRuleManager.loadRules(rules);
}
}
测试验证,一分钟内访问三次系统服务出现异常提示表示限流成功。
对ruoyi-system、ruoyi-gen分组限流配置
(1)application.yml配置文件
spring:
cloud:
gateway:
routes:
# 系统模块
- id: ruoyi-system
uri: lb://ruoyi-system
predicates:
- Path=/system/**
filters:
- StripPrefix=1
# 代码生成
- id: ruoyi-gen
uri: lb://ruoyi-gen
predicates:
- Path=/code/**
filters:
- StripPrefix=1
(2)限流规则配置类
/**
* 网关限流配置
*
* @author ruoyi
*/
@Configuration
public class GatewayConfig
{
@Bean
@Order(Ordered.HIGHEST_PRECEDENCE)
public SentinelFallbackHandler sentinelGatewayExceptionHandler()
{
return new SentinelFallbackHandler();
}
@Bean
@Order(-1)
public GlobalFilter sentinelGatewayFilter()
{
return new SentinelGatewayFilter();
}
@PostConstruct
public void doInit()
{
// 加载网关限流规则
initGatewayRules();
}
/**
* 网关限流规则
*/
private void initGatewayRules()
{
Set<GatewayFlowRule> rules = new HashSet<>();
rules.add(new GatewayFlowRule("system-api")
.setCount(3) // 限流阈值
.setIntervalSec(60)); // 统计时间窗口,单位是秒,默认是 1 秒
rules.add(new GatewayFlowRule("code-api")
.setCount(5) // 限流阈值
.setIntervalSec(60));
// 加载网关限流规则
GatewayRuleManager.loadRules(rules);
// 加载限流分组
initCustomizedApis();
}
/**
* 限流分组
*/
private void initCustomizedApis()
{
Set<ApiDefinition> definitions = new HashSet<>();
// ruoyi-system 组
ApiDefinition api1 = new ApiDefinition("system-api").setPredicateItems(new HashSet<ApiPredicateItem>()
{
private static final long serialVersionUID = 1L;
{
// 匹配 /user 以及其子路径的所有请求
add(new ApiPathPredicateItem().setPattern("/system/user/**")
.setMatchStrategy(SentinelGatewayConstants.URL_MATCH_STRATEGY_PREFIX));
}
});
// ruoyi-gen 组
ApiDefinition api2 = new ApiDefinition("code-api").setPredicateItems(new HashSet<ApiPredicateItem>()
{
private static final long serialVersionUID = 1L;
{
// 只匹配 /job/list
add(new ApiPathPredicateItem().setPattern("/code/gen/list"));
}
});
definitions.add(api1);
definitions.add(api2);
// 加载限流分组
GatewayApiDefinitionManager.loadApiDefinitions(definitions);
}
}
访问:http://localhost:8080/system/user/list (触发限流 )
访问:http://localhost:8080/system/role/list (不会触发限流)
访问:http://localhost:8080/code/gen/list (触发限流)
访问:http://localhost:8080/code/gen/xxxx (不会触发限流)
为了展示更加友好的限流提示, Sentinel支持自定义异常处理。
(1)方案一:yml配置
# Spring
spring:
cloud:
sentinel:
scg:
fallback:
mode: response
response-body: '{"code":403,"msg":"请求超过最大数,请稍后再试"}'
(2)方案二:GatewayConfig注入Bean
@Bean
@Order(Ordered.HIGHEST_PRECEDENCE)
public SentinelFallbackHandler sentinelGatewayExceptionHandler()
{
return new SentinelFallbackHandler();
}
(3)SentinelFallbackHandler.java
/**
* 自定义限流异常处理
*
* @author ruoyi
*/
public class SentinelFallbackHandler implements WebExceptionHandler
{
private Mono<Void> writeResponse(ServerResponse response, ServerWebExchange exchange)
{
ServerHttpResponse serverHttpResponse = exchange.getResponse();
serverHttpResponse.getHeaders().add("Content-Type", "application/json;charset=UTF-8");
byte[] datas = "{\"code\":429,\"msg\":\"请求超过最大数,请稍后再试\"}".getBytes(StandardCharsets.UTF_8);
DataBuffer buffer = serverHttpResponse.bufferFactory().wrap(datas);
return serverHttpResponse.writeWith(Mono.just(buffer));
}
@Override
public Mono<Void> handle(ServerWebExchange exchange, Throwable ex)
{
if (exchange.getResponse().isCommitted())
{
return Mono.error(ex);
}
if (!BlockException.isBlockException(ex))
{
return Mono.error(ex);
}
return handleBlockedRequest(exchange, ex).flatMap(response -> writeResponse(response, exchange));
}
private Mono<ServerResponse> handleBlockedRequest(ServerWebExchange exchange, Throwable throwable)
{
return GatewayCallbackManager.getBlockHandler().handleRequest(exchange, throwable);
}
}
架构图
项目结构
com.ruoyi
├── ruoyi-ui // 前端框架 [80]
├── ruoyi-gateway // 网关模块 [8080]
├── ruoyi-auth // 认证中心 [9200]
├── ruoyi-api // 接口模块
│ └── ruoyi-api-system // 系统接口
├── ruoyi-common // 通用模块
│ └── ruoyi-common-core // 核心模块
│ └── ruoyi-common-datascope // 权限范围
│ └── ruoyi-common-datasource // 多数据源
│ └── ruoyi-common-log // 日志记录
│ └── ruoyi-common-redis // 缓存服务
│ └── ruoyi-common-seata // 分布式事务
│ └── ruoyi-common-security // 安全模块
│ └── ruoyi-common-swagger // 系统接口
├── ruoyi-modules // 业务模块
│ └── ruoyi-system // 系统模块 [9201]
│ └── ruoyi-gen // 代码生成 [9202]
│ └── ruoyi-job // 定时任务 [9203]
│ └── ruoyi-file // 文件服务 [9300]
├── ruoyi-visual // 图形化管理模块
│ └── ruoyi-visual-monitor // 监控中心 [9100]
├──pom.xml // 公共依赖
(1)什么是认证中心
身份认证,就是判断一个用户是否为合法用户的处理过程。最常用的简单身份认证方式是系统通过核对用户输入的用户名和口令,看其是否与系统中存储的该用户的用户名和口令一致,来判断用户身份是否正确。
(2)为什么要使用认证中心
登录请求后台接口,为了安全认证,所有请求都携带token信息进行安全认证,比如使用vue、react后者h5开发的app,用于控制可访问系统的资源。
(1)添加依赖
com.ruoyi
ruoyi-common-security
(2)认证启动类
顾名思义,就是对系统登录用户的进行认证过程。
TokenController控制器login方法会进行用户验证,如果验证通过会保存登录日志并返回token,同时缓存中会存入login_tokens:xxxxxx(包含用户、权限信息)。
用户登录接口地址 http://localhost:9200/login
请求头Content-Type - application/json,请求方式Post
{
"username": "admin",
"password": "admin123"
}
响应结果
{
"code": 200,
"data": {
"access_token": "f840488c-68a9-4272-acc9-c34d3b66a943",
"expires_in": 43200
}
}
通过用户验证登录后获取access_token,通过网关访问其他应用数据时必须携带此参数值。
顾名思义,就是对系统操作用户的进行缓存刷新,防止过期。
TokenController控制器refresh方法会在用户调用时更新令牌有效期。
刷新令牌接口地址 http://localhost:9200/refresh
请求头Authorization - f840488c-68a9-4272-acc9-c34d3b66a943,请求方式Post
(1)添加依赖
com.alibaba.cloud
spring-cloud-starter-alibaba-nacos-config
org.springframework.boot
spring-boot-starter-web
(2)在bootstrap.yml添加Nacos配置
# Spring
spring:
application:
# 应用名称
name: ruoyi-xxxx
profiles:
# 环境配置
active: dev
cloud:
nacos:
config:
# 配置中心地址
server-addr: 127.0.0.1:8848
# 配置文件格式
file-extension: yml
# 共享配置
shared-configs:
- application-${spring.profiles.active}.${spring.cloud.nacos.config.file-extension}
配置文件加载的优先级(由高到低)
bootstrap.properties ->bootstrap.yml -> application.properties -> application.yml
(3)在Application启动类加入注解@SpringBootApplication
# 测试属性
ruoyi:
# 名称
name: RuoYi
# 版本
version: 1.0.0
在Nacos Spring Cloud 中,数据集(Data Id) 的配置完整格式如下:
s p r i n g . c l o u d . n a c o s . c o n f i g . p r e f i x − {spring.cloud.nacos.config.prefix}- spring.cloud.nacos.config.prefix−{spring.profiles.active}.${spring.cloud.nacos.config.file-extension}通俗一点就是前缀-环境-扩展名
(5)编写测试类在Controller类中通过@Value注解获取配置值
@RestController
public class TestController
{
@Value("${ruoyi.name}")
private String name;
@Value("${ruoyi.version}")
private String version;
@GetMapping("info")
public String get()
{
return name + version;
}
}
访问http://localhost:9999/info,返回正确数据表示测试通过。
通常会在Controller里边用@Value取出使用,但是你要是想改变他,就要重新改代码,打包,部署,十分麻烦,我们需要让配置文件的值变得动起来,Nacos也采用了Spring Cloud原生注解@RefreshScope实现配置自动更新。
@RefreshScope //动态刷新配置
public class TestController
{
@Value("${ruoyi.name}")
private String name;
@Value("${ruoyi.version}")
private String version;
....
}