黑马商城项目—最新SpringCloud开发实战—功能实现详细学习笔记(微服务篇)
2024黑马商城项目微服务篇
2024最新版的SpringCloud黑马商城项目
笔记对应教学视频讲解序号,并附上每小节所在的视频分p位置
笔记包含了视频讲解的核心内容及实战功能实现的详细过程
课程地址: 2024最新SpringCloud微服务开发与实战,java黑马商城项目微服务实战开发(涵盖MybatisPlus、Docker、MQ、ES、Redis高级等)
项目代码:课程地址简介中领取
系列文章目录
本笔记包含Docker、微服务、RabbitMQ、Elasticsearch等(持续更新)
黑马商城项目—最新SpringCloud开发实战—功能实现详细学习笔记(Docker篇)
黑马商城项目—最新SpringCloud开发实战—功能实现详细学习笔记(微服务篇)
黑马商城项目—最新SpringCloud开发实战—功能实现详细学习笔记(RabbitMQ篇)
黑马商城项目—最新SpringCloud开发实战—功能实现详细学习笔记(Elasticsearch篇)
目录
- 微服务
- p36 微服务01-01 导入黑马商城
- p37 微服务01-02 认识微服务-单体架构
- p38 微服务01-03 认识微服务-微服务架构
- p39 微服务01-04 认识微服务-SpringCloud
- p40 微服务01-05 微服务拆分-熟悉黑马商城
- p41 微服务01-06 微服务拆分-拆分原则
- p42 微服务01-07 微服务拆分-微服务项目结构说明
- p43 微服务01-08 微服务拆分-拆分商品服务
- p44 微服务01-09 微服务拆分-拆分购物车服务
- p45 微服务01-10 微服务拆分-远程调用
- p46 微服务01-11 服务治理-注册中心原理
- p47 微服务01-12 服务治理-搭建Nacos注册中心
- p48 微服务01-13 服务治理-服务注册
- p49 微服务01-14 服务治理-服务发现和负载均衡
- p50 微服务01-15 OpenFeign-快速入门
- p51 微服务01-16 OpenFeign-连接池
- p52 微服务01-17 OpenFeign-最佳实践
- p52 微服务01-18 OpenFeign-日志输出
- p54 微服务01-19 布置作业
- p55 微服务01-20 服务拆分作业-用户微服务
- p56 微服务01-21 服务拆分作业-交易微服务
- p57 微服务01-22 服务拆分作业-支付微服务
- p58 微服务02-01 什么是网关
- p59 微服务02-02 网关路由-快速入门
- p60 微服务02-03 网关路由-路由属性
- p61 微服务02-04 网关登录校验-思路分析
- p62 微服务02-05 网关登录校验-自定义GlobalFilter
- p63 微服务02-06 网关登录校验-自定义GatewayFilter
- p64 微服务02-07 网关登录校验-实现登录校验
- p65 微服务02-08 网关登录校验-网关传递用户到微服务
- p66 微服务02-09 网关登录校验-OpenFeign传递用户到微服务
- p67 微服务02-10 配置管理-什么是配置管理
- p68 微服务02-11 配置管理-共享配置
- p69 微服务02-12 配置管理-配置热更新
- p70 微服务02-13 配置管理-动态路由(拓展)
微服务
p36 微服务01-01 导入黑马商城
用之前的方法配置在docker中启动好mysql容器,
然后是后台项目
接下来用idea打开资料中的项目
在yaml配置文件中,这里写了用dev的profiles,在本地启动时用local,但不在这里改(不方便)
在services的编辑中把active profiles改为local就行了,
如果不能启动,可以更换jdk版本,使用jdk11
项目启动后在浏览器中可以访问到
然后启动前端项目
将nginx前端目录拷贝到一个非中文字符路径
用命令行的方式启动(方便关闭,双击exe不方便关闭)
在前端页面中登录,登录成功表明后端和数据库正常运行
虽然能够登录成功,但有一个warning输出
修改mybatis-plus的版本为3.4.2后warning消失
p37 微服务01-02 认识微服务-单体架构
单体架构的优点是架构简单,
但单体架构在开发大型项目的时候存在许多缺点,
团队协作成本高(许多人容易代码冲突),发布效率低(需要把整个项目进行打包),可用性差(所有的功能都部署在一台服务器上,性能有限)
在接口中写了一个Thread.sleep(300);模拟业务处理时间,对这个接口做性能测试
先记录一下hi接口的访问时间,300多毫秒
在jmeter中首先以200个请求每秒的速度一直访问这个接口,
然后在浏览器中继续访问hi接口,响应速度没有受到太大影响,还是300多毫秒
将jmeter的请求速度提升到300个线程每秒
此时接口的时间上升到了600毫秒,受到了影响,
因为某一个接口的并发很高,占用了很多tomcat资源,后续的请求需要等待释放,所以接口延迟变高
同时,其它的接口延迟也变高了
所以,这种单体的架构由于某一个接口出现的高并发,会影响到其它接口的性能(因为后台只有1个端口8080),
最终整个网站的延迟都很高,即系统可用性差,
还有可能由于某一个接口出现错误导致服务崩溃,那么整个项目网站也崩溃了
单体架构不适合大型、复杂、用户量较大的用户,
它适合功能简单、规模较小的用户
p38 微服务01-03 认识微服务-微服务架构
微服务架构将功能模块拆分后的好处,
粒度小(按照业务对服务进行拆分,每个服务只负责相关的功能),
团队自治(每个服务由一个团队负责,服务的功能只由一个团队开发,减少代码冲突),
服务自治(每个服务单独打包,部署后出现问题减少耦合)
p39 微服务01-04 认识微服务-SpringCloud
目前使用最广泛的微服务框架是SpringCloud
由于2022版的spring cloud需要jdk17,国内目前使用jdk17的较少(使用jdk8和11较多),教学视频以2021版来讲
Spring cloud在企业中目前使用较多的组件是spring cloud alibaba和spring cloud netflix
在pom文件中查看配置信息
在项目中指定springcloud的版本为2021版,并且这个springcloud也是一个pom文件
springcloud的pom文件中包含了许多组件
p40 微服务01-05 微服务拆分-熟悉黑马商城
由于是按照功能拆分,首先需要了解项目的功能
商城分为这些功能:登录、显示商品、加入购物车、创建订单、支付
登录功能的时序图
登录通过hutool.jwt.JWT实现,用户名密码校验通过后,生成jwt并保存(用于之后解析),返回token给前端用户,
在application的session里面可以看到登录成功后拿到的token
登录成功后的用户操作会在Headers的Authorization里面带上这个token
后台会用一个请求拦截器,对请求中的token进行解析得到用户id,并将id放入ThreadLocal中保存
接下来是购物车功能
1,商品加入购物车之后,在数据库中修改商品价格,再次刷新页面显示“比加入时便宜”等信息
2,如果发生库存不足的情况,在购物车中增加商品数量会有提示
因此,购物车当中的商品,不仅显示了商品加入时的信息(比如价格等),还显示了商品实时的最新信息(当前价格和库存等),
在查询时,后台返回了购物车的信息和最新商品信息
以上功能的实现过程就是在查询出购物车中的商品后(旧的信息),利用商品id再查一次最新信息,这两个信息放入一个VO里面返回给前端
然后是结算购物车中的商品,创建订单
最后对订单进行支付
p41 微服务01-06 微服务拆分-拆分原则
高内聚和低耦合两者是一个统一的概念,要做到服务拆分的合理
拆分有2种,简单的拆分是纵向拆分,
纵向拆分(按业务来分,分布式的),
横向拆分(水平的拆分,将多个业务内相同的处理过程抽取出来,划分为一个服务提高复用性)
p42 微服务01-07 微服务拆分-微服务项目结构说明
独立project的工程结构,
父工程是一个文件夹(它下面没有写代码),其中每个微服务是一个独立的project(在父工程下new出project),
它们有各自的git仓库,管理起来复杂,适用于大型项目
maven聚合的工程结构,
父工程是一个project包含了许多微服务(微服务都在一个project里面),每个微服务是一个module(在父工程下new出module),
都在一个project内管理起来方便,适用于小型项目,
教学视频中的微服务采用这种maven聚合的结构
p43 微服务01-08 微服务拆分-拆分商品服务
将商品服务拆分为微服务
创建一个module,
然后在pom中配置依赖,将原有的pom中的dependencies拷贝到这个module的pom中,删掉其中不必要的dependencies
然后创建包和启动类
在application中设置好端口,这里用8081,设置好微服务名称,
设置连接的数据库地址,(通常来说每个微服务需要连接到各自的数据库实例实现数据隔离,
这里为了方便教学演示,在一个连接实例中使用不同名称的数据库)
这里新建一个数据库hm-item用于存储这个微服务的数据,它其实就是单体应用的数据库中的一张表
然后是运行的代码(从单体项目拷贝过来),
拷贝domain中的dto、po、query(用于将分页对象与mybatis-plus对象互转)等,
拷贝mapper文件,拷贝service文件,拷贝controller文件等(由于这些文件是相互依赖的,按照这个顺序可以自动导入包路径(需设置))
重新加载maven工程会自动创建一个启动类,方便运行
同样地,编辑一下启动激活的profiles为local,因为需要本地启动
服务启动成功
由于配好了swagger,所以在网页中可以看到接口文档
在接口中进行分页查询调试,可以看到查询到了信息
p44 微服务01-09 微服务拆分-拆分购物车服务
购物车服务的拆分与上一节相同,首先配置yaml文件,设置启动端口8082,连接到创建的hm-cart数据库等,
然后从单体项目中拷贝购物车相关的功能代码,比如domain,mapper,service,controller等
这里在拷贝service的代码时,购物车的商品查询功能用到了商品item服务,这里先把这块代码注释掉(加一个默认值1,一会处理,
用户信息的获取也注释掉)
cart服务启动成功
在8082的端口打开swagger页面,成功查到了此购物车服务的数据库数据,
但是购物车中的商品最新价格和库存等信息还没有,功能没有实现还不完整
p45 微服务01-10 微服务拆分-远程调用
服务拆分后,购物车服务和商品服务它们的代码是分开的,数据库也是隔离的,
它们之间的访问需要通过网络发起请求,
就像前端访问后端服务得到数据一样,后端服务之间也可以相互访问得到其它服务的数据
比如前端通过一个url的get请求方式就可以获取后端返回的数据
可以利用RestTemplate工具来实现远程调用
在启动类里面创建一个bean
在实现类上加上@RequiredArgsConstructor注解,再要注入的Bean设置为final,那么它就会给必须初始化的参数加上构造方法,
如果手动用等号=初始化值了,或者没有加final,那这个参数就不会作为构造函数的一部分
类似于这样的效果,但注入类很多时会在此写很多构造方法,那么用@RequiredArgsConstructor注解比较好
在实现类的方法中就不能用单体项目的service来查询了,这里用restTeaplate来发起http请求来从其它服务查数据,
返回的类型如果是单个对象可以用反射的方式(DTO.class),如果是一个DTO数组就传入一个ParameterizedTypeReference对象来表示,
在get中的多个参数用hutool工具包的CollUtils来表示,逗号为参数分隔符
在购物车的8082端口网页中中测试接口,可以看到查到的数据中包含了商品服务的数据
购物车服务中有一条数据库查询(查询购物车信息)
商品服务中也有一条数据库查询(这个查询就是购物车服务用restTeaplate发起的http请求)
这种用RestTamplate调用其它服务的方式还存在一定问题,在之后会改进
p46 微服务01-11 服务治理-注册中心原理
以上的服务远程调用在部署多个服务进行负载均衡的时候会出现问题,
1,在写调用这个服务的代码的时候并不知道要去访问哪个端口,这里调用的方式不能写具体地址(服务调用者不知道服务提供者是谁),
2,就算知道了服务提供者的地址,也无法确定具体该访问哪一个,
3,访问的服务宕机了如何处理(无法感知服务的变更)
以上问题都是服务治理问题
服务提供者在注册中心记录可以提供的服务和端口号,
服务调用者从注册中心订阅有哪些可以提供的服务和它们的端口号,
服务调用者拿到提供者的信息后挑选一个服务(进行负载均衡算法随机轮询等),然后就可以进行远程调用,
心跳续约可以解决服务提供者出现宕机的问题,注册中心就会从服务列表中移除,并将变更推送给服务调用者
p47 微服务01-12 服务治理-搭建Nacos注册中心
由于Nacos要存储一些数据,因此它需要一个数据源,以mysql为例
执行nacos.sql脚本后会创建一个名为nacos的数据库,其中users表中的数据记录了登录的用户信息
这里Nacos采用Docker部署,这是nacos的env环境变量文件,用于docker部署(包含虚拟机地址,数据库地址等信息)
从准备好的tar包加载nacos镜像,然后创建运行nacos容器,指定env文件和打开端口等(nacos需要3个端口)
命令docker logs -f nacos查看这个容器的运行日志,可以看到它有一个控制台网页,通过ip地址加8848端口后面加nacos来访问
进入界面后要求输入用户名和密码,这个用户名和密码就是数据库中的users表中的用户名nacos和密码nacos
进入管理页面后,可以看到服务列表和订阅者列表,它们用于服务治理
p48 微服务01-13 服务治理-服务注册
在Item-service服务中引入nacos依赖
首先引入nacos依赖,可以看到nacos是阿里巴巴的产品(这里的版本号在父工程的pom文件中已经定义好了,因此省略掉不用写)
在yaml配置文件中写nacos服务的地址和端口,然后准备启动服务
启动2个Item-service实例:将ItemApplication的启动文件复制一份,添加一个启动参数来指定一个8083的端口(这个参数的优先级比yaml配置文件要高,由于8082端口已经被使用了这里写8083端口)
可以看到启动成功
在日志中可以看到连接nacos成功,然后进行服务注册
在网页控制台中可以看到服务列表中出现了item-service的服务(服务名称就是yaml文件中的spring.application.name),并且它有2个实例,
8082端口的服务没有进行配置因此没有进行服务注册
进入详情页面查看,可以看到这两个实例是刚刚设置的8081和8083端口
p49 微服务01-14 服务治理-服务发现和负载均衡
购物车服务去nacos拉取获取商品服务,叫服务发现,
消费者服务除了引入nacos依赖和配置nacos地址以外,还需要去获取服务的列表(使用nacos提供的api)
在实现类里面先注入api(同样地,定义了成员变量之后它会用构造函数自动注入,因为加了@RequiredArgsConstructor注解)
指定服务名称后可以从nacos中获取服务实例列表,然后用负载均衡来选择其中一个实例,将这个实例的url地址拼接到原来的代码中
启动完成后可以看到购物车服务也加入了服务列表中
去8082购物车服务的swagger接口执行查询请求,成功查到了商品服务的数据
两个商品服务8081和8083都查到了数据,说明负载均衡成功(这里是随机)
如果有一个实例(8081)被关闭了,那么服务列表里面也会消失(心跳机制),但购物车查询依然可以成功,因为还有一个Item服务,
当8081服务再次开启后,它又会加入到服务列表中,又可以接收到发来的请求,继续提供服务,
这样的好处是,url的地址不是写死的,它会根据可用的服务自动进行选择
p50 微服务01-15 OpenFeign-快速入门
上一节的DiscoveryClient查询方式过程太复杂,用OpenFeign可以简化操作
OpenFeign是声明式的http客户端,可以优雅的发送http请求
首先要引入依赖,这里有2个,OpenFeign和负载均衡(springcloud早期提供的是Ribbon,现在是新版的loadbalancer)
然后在启动类上要加上启动openfeign注解
编写一个openfeign接口来定义要发送的请求,这种方式配置接口后只需一行代码就可以实现http请求的发送,方便使用,
可以看到openfeign提供了类似于springMVC的注解方式(比如@GetMapping),
接口中的方法由openfeign动态代理自动实现
定义好openfeign的client接口后,同样在实现类中用final自动注入
然后在业务中用一行代码就可以进行服务调用
在接口文档中也成功查到了商品服务的数据
p51 微服务01-16 OpenFeign-连接池
先看一下openfeign发请求的代码
查看openfeign的源码,先将主机的ip填写为服务名称,拿到ip地址后将服务名称替换为ip形成完整的url链接
然后它会将地址交给一个delegate的Client去发请求,它是HttpURLConnection通过input流来发,这种方式效率比较低
为了提高项目的性能,用连接池可以减少创建连接和销毁连接的开销
其中连接发起请求的框架可以自己选择,这里以OKHttp为例
首先引入okhttp依赖
在yaml文件中配置开启连接池
再次测试发送请求,这里可以看到delegate用的是OKHttp,说明刚刚的配置成功
p52 微服务01-17 OpenFeign-最佳实践
OpenFeign的使用方式还可以再进行改进,
因为这种方式去调用商品微服务的所有调用者都需要去编写一个Client接口代码,很麻烦,
而且当商品微服务发生变化后,所有的这种Client接口的代码也需要进行修改,很不方便
这里假设order订单服务和cart购物车服务都要去调用item商品服务,
以下有2种方案:
第一种方案是,将服务提供者的业务代码从main中移动到新创建的三个子模块中,biz模块用于存放原有的业务代码,
dto和api两个模块用于写Client接口(这样服务提供者自己只需写一份Client,它由服务提供者准备好了),
服务调用者在pom文件中引入dto和api这两个模块就可以直接使用了,
优点是开发人员对服务很熟悉,因此他们编写Client接口也很熟悉(其它服务的开发者或许对此服务很陌生),
缺点是项目结构变复杂了,但从代码结构来说更合理
第二种方案是,在这些微服务同级的目录下,新创建一个子模块,它里面的client包含了其它每个微服务要向外暴露的信息,
然后服务的调用者在pom文件中引入这个api子模块,就可以调用其它服务
优点是不会让项目结构变复杂,适合于聚合的项目结构(比如说项目已经有了一个common工具模块的情况下,再增加一个api模块也比较合适)
缺点是增加了模块的耦合度(方案一的耦合度更低)
这里采用方案二比较合适(聚合的项目结构)
创建api子模块
然后在pom文件中引入openfeign和负载均衡2个依赖(并且服务的调用者pom文件中就不需要引这两个依赖了)
将cart服务的client和dto复制到api这个微服务当中,
然后在cart服务中将它们删除(在cart服务中使用Item服务的DTO原本就是为了Client接口中使用,但现在不需要了)
由于在cart服务的业务代码中使用了client和dto(此时dto不存在会报错),需要在cart服务中引入这个api依赖,
最后,在cart服务的业务代码中就可以去调api服务的client
出现了一个问题,在业务代码中使用api子模块中的client时发现没有Bean
原因是,启动cart服务时,由于itemClient并不在cart服务的扫描范围内(此时范围是com.hmall.cart而Client在com.hmall.api),
因此没有被扫描包扫描到,无法实现动态代理去交给spring去处理
有2种方式解决,这里以第一种为例
在cart服务的启动类上指定FeignClient所在的包,
然后启动成功,
在8081和8082中成功查到了数据
总结,所有的服务都需要在api模块中去写client,以后调用服务的时候就可以直接用api模块里面的client去调
p52 微服务01-18 OpenFeign-日志输出
日志很重要,在开发中可以帮助解决很多问题
输出openfeign日志的前提条件是要设置Client所在包的级别为debug
比如这里的实现类中的ItemClient所在的包属于com.hmall,那么就要在它的yaml文件中配置这个包的日志级别为debug
然后需要声明一个Bean,并让它生效,这里有2种生效方式,
可以是在Client接口上使用注解使单个Bean生效,
如果有很多Clients需要生效,可以在启动类上加注解来让所有的Bean生效
以配置日志级别为FULL为例
当某个服务需要调试的时候就在它的启动类里面加一个注解,使它生效,
(这里Feign的Config写在了另一个api模块,是为了让之后的服务都通过导入这个Config来获得日志级别,
如果在cart服务里面写那么每个服务都要单独写一个Config),
这里,在cart模块的启动类上加上注解,使它生效(指定Config类所在的位置,它会自动创建一个Bean)
在接口文档中发送请求,
可以在日志中看到api模块中的Client发送请求的详细信息,
一般情况下不需要配置openfeign的日志级别,它会影响到性能,只在调试时使用
总结openfeign的日志配置要弄清配置的位置,要分清:
yaml的位置(yaml配置包为debug)、Config类的位置(openfeign的配置类)、启动类服务是谁(启动类上加注解)。
可以简单理解为,调试哪个服务就加它的启动类注解,配置类放哪无所谓因为要import,然后启动的这个服务用到了Client因此要配yaml的日志为debug
本章总结
p54 微服务01-19 布置作业
首先拆分出微服务,
然后定义这些微服务的FeignClient,
接下来将微服务与前端联调(前端需要确定访问的微服务端口)
我写的作业:
用户微服务端口用8084(8080是未拆分的项目目前8081是Item实例1,8082是Cart,8083是Item实例2,8080是未拆分的项目)
拆分用户微服务,启动项目出现错误:
nested exception is org.springframework.beans.factory.BeanCreationException: Error creating bean with name ‘keyPair’ defined in class path resource [com/hmall/user/config/SecurityConfig.class]: Bean instantiation via factory method failed; nested exception is org.springframework.beans.BeanInstantiationException: Failed to instantiate [java.security. KeyPair]: Factory method ‘keyPair’ threw exception; nested exception is java.lang.NullPointerException
解决办法:
1,在yaml文件中加用户登录相关的配置,比如jwt的jks文件位置
2,将原有项目的jks文件也复制到用户微服务的文件夹中
项目编译通过,启动成功
遇到的问题:连接不上数据库,
解决办法:把services的Edit激活配置设为local的profile
在postman中测试登录接口,输入用户名密码,去查用户微服务的数据库,最后正常执行业务返回得到token
其中接口文档,遇到了一个问题:在接口文档中不显示用户相关接口
解决办法:发现yaml文件中的controller路径写错了,更正后接口文档中正常显示相关接口
(knife4j是对swagger的增强解决方案)
测试user服务的openfeign client调用服务
在随便一个微服务中(以cart服务为例)测试调用user服务的情况,测试user服务的login接口,输入用户名密码得到登录后的token,服务调用成功
交易微服务用8085端口,
拆分用trade微服务时遇到的问题:
FeignClient的方法中传入DTO类型的参数时,有一个参数类型选择的问题
由于订单服务被拆分出来了,因此在实现类中将IItemService改为itemClient进行调用,
这里由于client是在api模块中使用了api模块中的DTO,而提供的参数类型使用的是本服务的DTO,这两者类型不一致报错
解决办法:
修改实现类中的import类,统一使用api模块中的dto,
然后可能需要修改controller和service层接口的类import from api模块。
造成此问题的原因是存在重复的dto(api微服务和这个微服务里面都有),
应将dto抽取到api里面然后删除原有的dto,然后其它服务依赖api服务的dto就行了
测试结果:trade微服务查询数据库成功,nacos注册成功
支付微服务使用8086端口
遇到的问题:feignclient如何调用mybatisplus
在pay微服务中的实现类代码需要将orderService替换为trade微服务的tradeClient,但这里的update方法不是trade服务中的方法而是mybatis提供的
解决方法:
方法一,在trade微服务中新增一个controller接口,然后将接口定义到api模块中的Client中,
方法二,其它方法
p55 微服务01-20 服务拆分作业-用户微服务
对自己上一节的作业查漏补缺
jks文件是保存的jwt密钥,用于用户登录
p56 微服务01-21 服务拆分作业-交易微服务
对自己上一节的作业查漏补缺
微服务中的dto抽取到api模块后,删除微服务中无用的dto
p57 微服务01-22 服务拆分作业-支付微服务
支付中有一个支付流水单号的概念
支付单号用于提交给第三方支付服务(比如支付宝),而不是把业务单号提交给第三方支付,
因为可能发生支付失败,再次支付时第三方可能不允许重复,
即一个订单号可能对应多个支付单号(其中多个支付单号至多一个支付成功)
在支付微服务的实现类中有2个Service需要修改,改为FeignClient调用,
其中用户服务的deductMoney在controller里面有已经接口了,可以在api里面直接调用(向它发请求),
订单服务的updateById方法是更新订单为已支付,在controller里面也有这个接口,也可以直接使用
最后进行接口测试,成功查到了数据
目前为止,只是拆分了所有的微服务,还有一些功能未完善比如获取用户信息等,还有前端访问端口的问题
p58 微服务02-01 什么是网关
服务拆分后会遇到2个问题:
1,服务地址有很多且将来可能发生变化,前端无法确定请求的服务地址端口
2,每个服务都需要登录的用户信息,如果每个服务都去登录不仅复杂而且可能泄露密钥
以上问题可以用网关解决
前端只需要去请求网关,然后就可以访问到对应的微服务,包括3个功能,
网关还有一些优点:
网关可以避免微服务地址暴露给前端,也是一种对微服务的保护,
对于前端来说,网关的作用将后端的微服务架构变成了单体架构(前端将整个后端视为黑盒
首先微服务启动后在注册中心进行注册,然后网关会去拉取服务列表,
前端发送请求到网关,首先校验信息比如是否登录(身份校验),
然后根据服务列表确定要请求的微服务(路由),其中微服务可能有多个实例,网关需要做负载均衡(确定发给谁),
最后由网关发送这个请求到微服务实例,收到微服务的响应后再回复给前端,起到一个代理的作用
网关的作用很重要,是微服务开发中必不可少的组件
SpringCloud提供了网关组件,是目前主流使用的
p59 微服务02-02 网关路由-快速入门
单独用一个服务作为所有微服务的入口,这个服务就是网关(叫做服务网关)
主要步骤是创建网关模块然后配置路由规则
新建一个模块,引入网关、nacos和负载均衡的依赖
配置好Springboot的启动类,然后新建一个yaml文件(yml文件也行)
在cloud下配置gateway的路由规则
启动微服务和网关,测试gateway服务的运行情况
首先直接访问8081和8083两个商品服务实例的controller接口,能够正常得到数据,
然后访问8080网关的items也能得到数据,并且在8081和8083交替访问,说明路由和负载均衡功能正常
前端网页也能正常访问(网页访问的是前端nginx的18080端口,然后nginx会转发给后台的网关8080端口,网关再转给商品服务的端口)
网页也能正常登录,说明用户微服务的转发也正常
p60 微服务02-03 网关路由-路由属性
上一节只是简单配置了路由,一些特殊的需求还要配置更详细的路由属性
路由断言
https://docs.spring.io/spring-cloud-gateway/docs/3.1.9/reference/html/#gateway-request-predicates-factories
springcloud提供了详细的文档可以查看
路由过滤器,作用是对进入网关的请求和发回的响应做加工处理
StripPrefix可以去除一段前缀(比如将/api/items改为/items),此项目中的网页请求的/api由nginx已经去除过了所以不需要了
测试添加一个请求头,然后去获取
在Path的路径下添加一个名为truth的请求头,内容为anyone等
然后在controller中去获取这个头的内容并打印
发送请求后,可以在控制台看到输出的信息
如果要将所有的请求都加上请求头,那么就把它放在与routes同级的位置上并命名为defult-filters默认过滤器
p61 微服务02-04 网关登录校验-思路分析
在网关中做登录的jwt校验,校验是在转发请求之前做
以下是从源码中分析网关的处理流程
网关的请求处理里面是责任链的设计方式
首先是路由映射器,路由规则的判断是由HandlerMappering的接口完成的,默认实现是路由断言,找到匹配的路由,
然后是请求处理器,它会找到当前生效的过滤器放入集合中形成过滤器链,
过滤器链最后有一个NettyRouting过滤器用于将请求转发到微服务,收到结果后存入上下文,再依次返回给之前的过滤器
过滤器内部有2部分,pre和post,当前过滤器的pre执行成功后才会执行下一个的pre逻辑,否则会被拦截,
同样地,post部分会封装结果并返回给调用它的上一个过滤器
显然,登录校验是放在过滤器的pre阶段的,如果校验不通过就返回一个异常
在网关中的Netty过滤器(最后一个过滤器)之前定义一个过滤器用于jwt校验,并且是在pre阶段,
网关还需要将登录的用户信息传递给微服务,做法是保存用户信息到转发的请求头,
微服务收到网关的请求后将请求头中的用户信息保存,用于在微服务之间发送请求
(这里请求有不同的方式,网关发到微服务的请求是网关内置的http请求,微服务之间的请求是openfeign的方式发送的,
因此它们保存请求头的实现方式上也有差别)
p62 微服务02-05 网关登录校验-自定义GlobalFilter
在网关中定义一个过滤器用于处理登录校验的逻辑
过滤器有2种,网关过滤器(默认不生效)和全局过滤器(所有路由生效),
ServerWebExchange是网关内部的上下文对象,用于保存共享的数据,过滤器链中的过滤器可以存取数据,
GatewayFilterChain是过滤器链,用于调用其中的过滤器
由于过滤器中的post阶段在执行完pre后需要等待很长时间才有结果,过滤器链采用了非阻塞式编程,
它利用Mono定义了一个回调函数,避免了等待,等返回了结果后再调用回调函数,回调函数里面就是post阶段
GlobalFilter相对简单,以这个过滤器为例来定义一个过滤器
定义一个优先级最高的GlobalFilter来获取登录头信息,可以看到autorization头里面保存的jwt
p63 微服务02-06 网关登录校验-自定义GatewayFilter
自定义的GagewayFilter使用很少,最常见的是用GlobalFilter
这个过滤器会随着参数的变化而变化,每次变化就会有一个新的过滤器对象,
因此它需要一个工厂类,这个工厂类的作用是读取配置来创建一个对象(设计模式之工厂模式),
这里过滤器的类名后缀是固定的,而前缀是配置文件中的名字
那么这里定义的是一个过滤器工厂
apply方法是基于配置创建过滤器对象,然后new一个匿名类(实现接口)来实现filter方法,在filter方法中的方式与GlobalFIlter中相同
这里虽然定义了一个Gateway过滤器,但是没有定义它的优先级(需要实现Order接口,返回一个数字表示优先级)
但是new一个匿名类就无法用implements实现了,
2种解决方法:第一种是不用匿名类了,写一个类然后去实现Order接口
更常用的是第二种方式,用装饰模式,
它提供了一个OrderedGatewayFilter类,接收2个参数,第一个参数是刚刚的GatewayFilter,第二个参数是优先级,
这个类同时实现了这两个接口,因此OrderedGatewayFilter类用于定义带优先级的gateway过滤器
然后在yaml文件中配置PrintAnyGatewayFilterFactory的前缀PrintAny使它生效,
重启后在控制台可以看到输出
以上是定义一个无参的GatewayFilter,如果要传入参数那么要定义一个内部类来自定义配置
然后要指定参数的顺序,最后将传递内部类的字节码给父类用于加载这个类
定义3个参数(a,b,c)的Gateway过滤器
在yaml文件中给这个过滤器传入3个参数
运行,在控制台中成功输出过滤器的参数
p64 微服务02-07 网关登录校验-实现登录校验
用Global过滤器来实现登录校验的功能
jks密钥文件被加密存储了,使用的时候需要解密
password是解析这个文件的密码,从而拿到里面的jwt密钥
auth中写了不需要登录的路径,将这些请求放行
而JwtProperties用于读取jks文件
AuthProperties用于从yaml文件中读取放行的路径
然后SecurityConfig用于生成jwt密钥
JwtTool中有2个方法,创建token和解析token
首先获取请求,得到请求中的token,对token做校验,
校验不通过就拦截,否则放行
由于路径中有星号通配符,无法使用字符串匹配,这里用了Spring提供的一个AntPathMatcher路径匹配器(比如/items/list就匹配上了/items/**)
访问/carts为401错误,返回配置的未登录状态
登录后,去查看购物车,成功得到数据,说明登录拦截生效了
p65 微服务02-08 网关登录校验-网关传递用户到微服务
网关传递用户到微服务
网关拿到用户信息后需要传递给下游的微服务,传递方式是将用户信息保存到请求头中
而微服务在执行业务之前用一个拦截器(由SpringMVC提供)先保存用户信息到ThreadLocal,
这样每个业务都可以获取到用户信息,而不用每个业务都去获取请求头中的信息了
在登录校验的过滤器中,把用户信息写入到下游的请求头中
用mutate修改请求头,设置头的键值对然后build构建一个新的exchange名为swe,并传递给下一个过滤器(这样下游请求中就带上了这个请求头了)
然后在购物车接口中获取这个请求头
登录用户后访问这个接口,然后在控制台中可以看到获取到的请求头显示的用户id,
这样下游的微服务拿到了网关解析的token中的用户信息
接下来是微服务要用拦截器保存用户信息,以供此微服务的所有业务使用
由于每个微服务都要定义一个拦截器,可以把拦截器写在common模块中由其它服务去引用依赖,减少重复编写代码
这里的拦截器不做任何拦截,只用于用户登录信息的获取(真正的拦截在网关已经做过了)
定义一个用户拦截器
用户信息拦截器定义好之后,建立一个Config类把拦截器加入进去
但此时这个MvcConfig配置类并未生效(因为它没有被扫描到,它的包路径是.hmall.common,而引用它的微服务是.hmall.cart.或者.hmall.item等)
这里用到了SpringBoot自动装配的原理,
在不同包下的扫描不到的配置类想要生效,必须在resources的META-INF中定义一个文件来记录这些配置类
此时网关启动会报错,原因是网关底层并不是基于SpringMVC(而是基于非阻塞式的、响应式的SpringWebFlux),因此它不能加载MvcConfig,
但是网关也引用了common,但希望MvcConfig不在网关里生效而在其它微服务里生效
解决方法是加上一个注解,用SpringMVC的条件注解@ConditionalOnClass,它会判断当前项目下是否有DispatcherServlet
由于SpringMVC的核心api是DispatcherServlet,把它作为条件来加载,当有这个类时才加载MvcConfig,那么它在网关就不会生效
再次启动,网关正常运行
查询购物车列表,从保存的用户信息上下文中取出用户信息,实现在微服务中根据不同的用户查询购物车信息(从网关到微服务的用户传递)
p66 微服务02-09 网关登录校验-OpenFeign传递用户到微服务
OpenFeign传递用户信息,微服务之间的调用需要传递用户信息
比如下单需要3个服务来完成,交易服务、商品服务、购物车服务,
交易服务在调用购物车服务时,需要告诉购物车服务清理的是哪个用户的购物车
购物车服务中清空商品的代码存在问题
现在,测试下单后的购物车商品删除功能,
购物车服务在删除商品时从UserContext来获取用户id得到null值,update为0条删除失败(下单后购物车里面依旧有商品)
(UserContext是在common模块里面的,交易服务和购物车服务都依赖它)
购物车服务并没有从UserContext中获取到,
造成问题的原因是微服务之间的请求没有带上用户信息的请求头(而网关到微服务之间带上了),
导致购物车服务拦截器没有存入用户信息,因此在业务中的UserContext获取不到用户信息
解决方法:在微服务之间的请求头中加上用户信息
由于微服务之间的请求是由OpenFeign发送的,那就需要修改它
OpenFeign提供了拦截器接口,它发起的请求会被拦截,然后进行修改
把OpenFeign拦截器定义在api里面(api模块用于定义微服务的FeignClient),这样所有的微服务都自带这个拦截器
比如,这里交易服务在用api模块里面的FeignClient发送请求到购物车服务的时候,这个用户信息拦截器会生效,
这个用户信息拦截器可以从UserContext(依赖于common模块)中获取用户(因为网关发来请求时,交易服务的拦截器就保存好了用户信息),
然后往请求里面添加一个用户信息的请求头,这样微服务之间的请求就带上了用户信息
定义好了feign的配置类之后需要让它生效,让交易微服务的FeignClient使用这个配置,需要在它的启动类上指定这个配置类
再次进行下单后清空购物车测试,可以看到在商品微服务中这次拿到了用户id,并且删除商品成功
然后查看购物车,发现下单的商品已经被删除了,结果正确
总结:
网关的过滤器解析登录token得到的用户信息,保存到转发给微服务的请求头中,
所有的微服务都有一个SpringMVC拦截器用于获取请求头中的用户信息(保存到ThreadLocal),
由于是共有的那么放在common模块中,
所有的微服务之间的请求由OpenFeign来发送,每个服务发送的请求会被OpenFeign拦截往请求头里面添加用户信息(从ThreadLocal中获取),
由于所有微服务的FeignClient被抽取出来放在api模块中,那么OpenFeign拦截器也写在api模块中
p67 微服务02-10 配置管理-什么是配置管理
配置管理组件可以让开发更便捷
它带来2个好处:
首先,项目中用了很多组件,它们各自有很多配置文件,维护它们的成本高,
用配置管理服务实现配置共享,省去了重复的配置编写,在配置管理服务中修改配置所有微服务都可以生效
其次,与业务相关的配置(用户登录超时时间、登录失败锁定时长、 购物订单超时时长、购物车商品数量上限等)不方便在代码中写为固定值,
也不方便写在配置文件中(想要变更配置虽然不用打包但要重启项目),那就可以将配置写在配置管理服务中
配置管理服务可以推送配置变更,接收到配置的服务无需重启就能生效,
Nacos不仅具备注册中心的功能,还具备配置管理服务的功能
p68 微服务02-11 配置管理-共享配置
用nacos实现配置中心,将微服务中的一些重复的配置(比如jdbc、日志、swagger等)放到nacos的配置列表里面
打开nacos网页的配置列表,点击加号新建配置
数据分组选默认的组,选择配置格式为yaml,可以在配置文件中用${}来定义变量,其中冒号后为默认值(变量未定义时使用),最后发布配置
在配置列表中可以看到发布的配置
在nacos中配置好之后,微服务需要去nacos拉取配置
传统的SpringBoot项目启动后首先加载yml文件,然后初始化ApplicationContext(它是Spring的Bean工厂,是一个容器),
而如果项目引入了配置管理,它会加入一个SpringCloud的上下文环境,在启动时会去nacos拉取配置,然后才是做SpringBoot的上下文
但这里有个问题,nacos的地址是写在SpringBoot的yml文件里的,但它又必须先做SpringCloud的上下文去nacos拉取配置,
解决办法是将nacos的地址写在bootstrap文件中,这个yml文件会在启动后首先加载,这样就能拿到nacos的地址去做SpringCloud的上下文
为了避免重复配置,在nacos中配置了就不要在SpringBoot的配置中再配了
这里以购物车微服务为例,引入nacos的配置管理依赖和读取bootstrap文件的依赖
bootstrap文件由3部分组成:服务名称、激活的配置文件、nacos的配置管理
shared-configs指定了要拉取的共享配置文件的名称
新建一个bootstrap的yaml文件,指定启动时的必要配置信息
在原有的SpringBoot配置文件中只保留一些特定的配置信息(重复的配置信息被放到nacos中了),配置文件得到了简化
启动购物车服务,在配置文件中可以看到从nacos拉取到了配置
打开项目网页,购物车服务功能运行正常
p69 微服务02-12 配置管理-配置热更新
微服务无需重启就能使配置生效称为配置热更新
需要满足前提条件,
首先是要有一个与微服务名有关的配置文件,文件名有3个部分:服务名称、profile(比如dev或local等)、文件后缀名(比如yaml)
然后微服务需要以特定方式读取热更新的配置属性,这里有2种方式,推荐用第一种(即@ConfigurationProperties())
上一节中,在bootstrap.yaml中有一些必需配置的参数
这些参数是:服务名、profile、文件后缀
这些参数用于在启动时会默认去找这些配置文件去加载,
在启动购物车微服务时可以看到默认会加载3个配置文件(后3个是从nacos拉取的共享配置),
其中cart-service-local.yaml就包含了配置文件的3部分(服务名、profile、文件后缀)
尝试将业务相关的配置(如购物车商品数量上限)交给nacos管理
在nacos中定义一个购物车配置文件(这里不是cart-service-local.yaml那么它就会在所有环境下生效),
设置购物车中的最大商品数量为1
在购物车微服务中新建一个配置类,定义一个属性maxItems表示最大商品数量
其中SpringBoot的上下文提供了一个注解,指定了读取的配置前缀,这里是hm.cart(与nacos中的yaml中的前缀一致)
重启购物车微服务,在前端网页中测试购物车添加数量,添加失败
在控制台中显示的购物车商品上限符合nacos中的配置商品数量
在nacos中将值修改为10
无需重启购物车微服务,再次添加商品,添加成功,说明nacos中的配置热更新生效
p70 微服务02-13 配置管理-动态路由(拓展)
目前的路由是写在网关的配置文件中,网关在启动时会加载这个配置文件的内容到缓存,
如果要变更路由信息只能修改配置再重启网关,无法实现热更新,这个问题可以用nacos动态路由解决
动态路由值负责推送配置到网关,而监听路由配置并更新路由表需要自己去实现
nacos中的dataid和group唯一确定一个配置文件
网关在启动时需要拉取一次配置,然后进行监听
首先在网关中引入nacos配置管理相关的依赖
与yaml格式的路由不同,这里使用json格式的路由配置
删除网关中的配置文件路由,一会将路由配置到nacos中
当配置发生变更时会触发Listener()回调函数,然后得到路由配置并更新路由表,
具体是利用RouteDefinitionWriter来更新路由表,先删除旧的路由表再进行更新,
路由表的更新用到了Mono容器
启动网关后没有从nacos拉取到路由配置信息(此时未在nacos中配置路由)
打开网页,发现无法访问页面,因为此时没有路由信息
在nacos中添加网关路由配置信息,点击发布
在网关中监听到了路由配置信息
等待一会后再次刷新页面,成功访问到了页面,说明路由配置生效了(整个过程无需重启网关)
更多推荐
所有评论(0)