目录
[二、微服务 01](#二、微服务 01)
[① 商品服务拆分](#① 商品服务拆分)
[② 购物车服务拆分](#② 购物车服务拆分)
[① RestTemplate](#① RestTemplate)
[2、📍交易微服务 - 参考](#2、📍交易微服务 - 参考)
【4】编写配置文件application.yaml,修改部分内容
一、项目部署
1、后端部署
【1】在idea打开资料包中的hmall文件
【2】更改数据库配置
【3】打包自定义镜像
【4】把下载好的jar包和dockerfile拖进虚拟机文件夹中
然后在虚拟机中输入构建docker镜像命令
先删掉之前创建的容器dd
docker rm -f dd【5】创建一个自定义网络(用处:便于直接通过容器名称进行容器间互联),如果之前创建过,直接用即可(docker network ls查看所有网络)
docker network create heima接着使用【刚刚自定义的镜像】创建【名为hm的容器】并运行
这里一定要让mysql容器和hm容器在同一个自定义网络中!否则无法通过容器名互联!
docker run -d --name hm -p 8081:8080 --network heima hmall【6】查看日志,检查是否正确运行
登录http://虚拟机IP/search/list?pageNo=1&pageSize=5,查看后端是否部署成功
如果能看到数据,则说明后端配置成功!
2、前端部署
【1】从资料包中获取nginx资料
【2】把nginx文件夹放入虚拟机目录
注意修改conf文件,ctrl+s保存
【3】创建nginx容器并完成两个数据挂载
docker run -d \ --name nginx \ -p 18080:18080 \ -p 18081:18081 \ -v /home/roye/nginx/html:/usr/share/nginx/html \ -v /home/roye/nginx/nginx.conf:/etc/nginx/nginx.conf \ --network heima \ nginx【4】访问【http://你的虚拟机ip:18080】进行测试
3、DockerCompose
部署一个简单的java项目,其中包含3个容器:
MySQL
Nginx
Java项目
而稍微复杂的项目,其中还会有各种各样的其它中间件,需要部署的东西远不止3个。如果还像之前那样手动的逐一部署,就太麻烦了。
而Docker Compose就可以帮助我们实现多个相互关联的Docker容器的快速部署。它允许用户通过一个单独的 docker-compose.yml 模板文件来定义一组相关联的应用容器。
(1)编写docker-compose文件
举个例子,用docker run部署MySQL的命令如下:
docker run -d \ --name mysql \ -p 3306:3306 \ -e TZ=Asia/Shanghai \ -e MYSQL_ROOT_PASSWORD=123 \ -v ./mysql/data:/var/lib/mysql \ -v ./mysql/conf:/etc/mysql/conf.d \ -v ./mysql/init:/docker-entrypoint-initdb.d \ --network hmall mysql如果用
docker-compose.yml文件来定义,就是这样:
version: "3.8" services: mysql: image: mysql container_name: mysql ports: - "3306:3306" environment: TZ: Asia/Shanghai MYSQL_ROOT_PASSWORD: 123 volumes: - "./mysql/conf:/etc/mysql/conf.d" - "./mysql/data:/var/lib/mysql" networks: - new networks: new: name: hmall黑马商城部署文件:
services: mysql: image: mysql container_name: mysql ports: - "3307:3306" environment: TZ: Asia/Shanghai MYSQL_ROOT_PASSWORD: 123456 volumes: - "/home/roye/mysql/conf:/etc/mysql/conf.d" - "/home/roye/data:/var/lib/mysql" - "/home/roye/mysql/init:/docker-entrypoint-initdb.d" networks: - hm-net hmall: build: context: . dockerfile: Dockerfile container_name: hmall ports: - "8081:8080" networks: - hm-net depends_on: - mysql nginx: image: nginx container_name: nginx ports: - "18080:18080" - "18081:18081" volumes: - "/home/roye/nginx/nginx.conf:/etc/nginx/nginx.conf" - "/home/roye/nginx/html:/usr/share/nginx/html" depends_on: - hmall networks: - hm-net networks: hm-net: external: true name: heima在资料包中我们获取到docker-compose.yml文件,内容就如上面的代码所示(注意把nginx.conf中容器名称hmall修改回来)
把docker-compose.yml文件拖进虚拟机目录
(2)运行docker-compose文件
docker compose命令格式如下:
docker compose [options] [commands]删掉之前部署的mysql、nginx、hm容器,再删掉镜像hmall、docker-demo
接着在命令框中执行下面代码(注意在docker-compose.yml文件目录下运行,否则需要修改命令)
docker compose up -d部署成功!
二、微服务 01
❗博主本人用的mac,部分操作实在难受,因此后面步骤只看不实战。
1、单体架构和微服务架构对比
(1)单体架构
单体架构:整个项目中所有功能模块都在一个工程中开发,项目部署时需要对所有模块一起编译、打包,项目的架构设计、开发模式都非常简单。当项目规模较小时,这种模式上手快,部署、运维也都很方便,因此早期很多小型项目都采用这种模式。
但随着项目的业务规模越来越大,团队开发人员也不断增加,单体架构就呈现出越来越多的问题:
团队协作成本高:试想一下,你们团队数十个人同时协作开发同一个项目,由于所有模块都在一个项目中,不同模块的代码之间物理边界越来越模糊。最终要把功能合并到一个分支,你绝对会陷入到解决冲突的泥潭之中。
系统发布效率低:任何模块变更都需要发布整个系统,而系统发布过程中需要多个模块之间制约较多,需要对比各种文件,任何一处出现问题都会导致发布失败,往往一次发布需要数十分钟甚至数小时。
系统可用性差:单体架构各个功能模块是作为一个服务部署,相互之间会互相影响,一些热点功能会耗尽系统资源,导致其它服务低可用。
(2)微服务架构
微服务架构:就是将单体架构中的功能模块从单体应用中拆分出来,独立部署为多个服务。
同时要满足下面的一些特点:
单一职责:一个微服务负责一部分业务功能,并且其核心数据不依赖于其它模块。
团队自治:每个微服务都有自己独立的开发、测试、发布、运维人员,团队人员规模不超过10人。
服务自治 :每个微服务都独立打包部署,++访问自己独立的数据库++,并且做好服务隔离,避免对其它服务产生影响。
例如,黑马商城项目,我们就可以把商品、用户、购物车、交易等模块拆分,交给不同的团队去开发,并独立部署:
综上所述,微服务架构解决了单体架构存在的问题,特别适合大型互联网项目的开发,因此被各大互联网公司普遍采用。
所谓的分布式架构,分布式就是服务拆分的过程,其实【微服务架构】 正是++分布式架构++ 的一种++最佳实践的方案++。
2、SpringCloud
SpringCloud框架可以说是目前Java领域最全面的微服务组件的集合。
后续项目开发用到springcloud再详细介绍
3、微服务拆分
(1)服务拆分原则
🎯【拆分目标】
微服务拆分时粒度要小,这其实是拆分的目标。具体可以从两个角度来分析:
高内聚:每个微服务的职责要尽量单一,包含的业务相互关联度高、完整度高。
低 耦合:每个微服务的功能要相对独立,尽量减少对其它微服务的依赖,或者依赖接口的稳定性要强。
🎯【拆分方式】
做服务拆分时一般有两种方式:
纵向 拆分:按照项目的++功能模块++来拆分。------ 提高服务内聚性
- 例如黑马商城中,就有用户管理功能、订单管理功能、购物车功能、商品管理功能、支付功能等。那么按照功能模块将他们拆分为一个个服务,就属于纵向拆分。这种拆分模式可以尽可能提高服务的内聚性。
横向 拆分:看各个功能模块之间有没有公共的业务部分,如果有将其抽取出来作为通用服务。
例如用户登录是需要发送消息通知,记录风控数据,下单时也要发送短信,记录风控数据。因此消息发送、风控数据记录就是通用的业务功能,因此可以将他们分别抽取为公共服务:消息中心服务、风控管理服务。
这样可以提高业务的复用性,避免重复开发。同时通用业务一般接口稳定性较强,也不会使服务之间过分耦合。
🎯【拆分结构】
一般微服务项目有两种不同的工程结构:
完全解耦:每一个微服务都创建为一个独立的工程,甚至可以使用不同的开发语言来开发,项目完全解耦。
优点:服务之间耦合度低
缺点:每个项目都有自己的独立仓库,管理起来比较麻烦
Maven聚合:整个项目为一个Project,然后每个微服务是其中的一个Module
优点:项目代码集中,管理和运维方便
缺点:服务之间耦合,编译时间较长
(2)商品、购物车服务拆分
需求:
- 将hm-service中与商品管理相关功能拆分到一个微服务module中,命名为item-service
- 将hm-service中与购物车有关的功能拆分到一个微服务module中,命名为cart-service
① 商品服务拆分
【1】创建微服务module
【2】往item-service的pom文件中导入依赖
XML<dependencies> <!--common--> <dependency> <groupId>com.heima</groupId> <artifactId>hm-common</artifactId> <version>1.0.0</version> </dependency> <!--web--> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <!--数据库--> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> </dependency> <!--mybatis--> <dependency> <groupId>com.baomidou</groupId> <artifactId>mybatis-plus-boot-starter</artifactId> </dependency> <!--redis--> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis</artifactId> </dependency> </dependencies> <build> <finalName>${project.artifactId}</finalName> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> </plugin> </plugins> </build>【3】创建启动类及相关包
java@MapperScan("com.hmall.item.mapper") @SpringBootApplication public class ItemApplication { public static void main(String[] args) { SpringApplication.run(ItemApplication.class, args); } }【4】编写配置文件,把hm-service的三个配置文件复制下来,再进行修改
application.yaml文件
javaserver: port: 8083 spring: application: name: item-service #微服务名称 profiles: active: dev datasource: url: jdbc:mysql://${hm.db.host}:3306/hm-item?useUnicode=true&characterEncoding=UTF-8&autoReconnect=true&serverTimezone=Asia/Shanghai driver-class-name: com.mysql.cj.jdbc.Driver username: root password: ${hm.db.pw} mybatis-plus: configuration: default-enum-type-handler: com.baomidou.mybatisplus.core.handlers.MybatisEnumTypeHandler global-config: db-config: update-strategy: not_null id-type: auto logging: level: com.hmall: debug pattern: dateformat: HH:mm:ss:SSS file: path: "logs/${spring.application.name}" knife4j: enable: true openapi: title: 黑马商城-商品管理-接口文档 description: "黑马商城-商品管理-接口文档" email: zhanghuyi@itcast.cn concat: 虎哥 url: https://www.itcast.cn version: v1.0.0 group: default: group-name: default api-rule: package api-rule-resources: - com.hmall.item.controller另外两个文件内容不动
【5】拆分domain类(包括po、dto等),把hm-service的domain包中与商品相关的实体类复制下来
注意:这里先复制根据名称能直接与业务相关的类,有一些类可能与业务关联但不确定的,后续复制别的包时,再及时补充
【6】拆分三层架构(controller、service、mapper)
最终目录结构如下
把hm-service与商品有关的文件都复制下来,并且更改一下导包路径
【7】添加启动服务
以后这样就可以快速启动不同服务
【8】最后我们可以进入接口文档看一下拆分是否成功
② 购物车服务拆分
步骤与商品服务类似,我们可以重新复习一遍
【1】创建微服务module
【2】pom.xml文件导入依赖
与商品服务导入的依赖相同
【3】编写启动类,创建相关包
【4】编辑配置文件application.yaml
javaserver: port: 8084 spring: application: name: cart-service #微服务名称 profiles: active: dev datasource: url: jdbc:mysql://${hm.db.host}:3306/hm-cart?useUnicode=true&characterEncoding=UTF-8&autoReconnect=true&serverTimezone=Asia/Shanghai driver-class-name: com.mysql.cj.jdbc.Driver username: root password: ${hm.db.pw} mybatis-plus: configuration: default-enum-type-handler: com.baomidou.mybatisplus.core.handlers.MybatisEnumTypeHandler global-config: db-config: update-strategy: not_null id-type: auto logging: level: com.hmall: debug pattern: dateformat: HH:mm:ss:SSS file: path: "logs/${spring.application.name}" knife4j: enable: true openapi: title: 黑马商城-购物车管理-接口文档 description: "黑马商城-购物车管理-接口文档" email: zhanghuyi@itcast.cn concat: 虎哥 url: https://www.itcast.cn version: v1.0.0 group: default: group-name: default api-rule: package api-rule-resources: - com.hmall.cart.controller【5】拆分domain类
【6】拆分三层架构
我们在拆分到CartServiceImpl时会发现,在这个实现类中调用到了ItemService接口,而购物车模块和商品模块现在被我们拆分成2个单独的服务,无法进行接口调用。
因此涉及这部分的代码需要先注释掉,后续修改:
需要获取登录用户信息,但登录校验功能目前没有复制过来,先写死固定用户id
查询购物车时需要查询商品信息,而商品信息不在当前服务,需要先将这部分代码注释
最终目录结构如下
【7】创建启动项
最后进入接口文档测试一下
(3)远程调用
在拆分的时候,我们发现一个问题:
购物车业务中需要查询商品信息,但商品信息查询的逻辑全部迁移到了
item-service服务,导致我们无法查询。最终结果就是查询到的购物车数据不完整,因此要想解决这个问题,我们就必须改造其中的代码,把原本本地方法调用,改造成跨微服务的远程调用(RPC,即R emote P roduce Call)。
因此,现在查询购物车列表的流程变成了这样:
我们该如何跨服务调用?准确的说,如何在
cart-service中获取item-service服务中的提供的商品数据呢?以前有没有实现过类似的远程查询的功能?当然有实现过,前端向服务端查询数据,其实就是从浏览器远程查询服务端数据。
比如刚才通过Swagger测试商品查询接口,就是向
http://localhost:8081/items这个接口发起的请求:这种查询就是通过http请求的方式来完成的,不仅仅可以实现远程查询,还可以实现新增、删除等各种远程请求。
假如我们在cart-service中能模拟浏览器,发送http请求到item-service,是不是就实现了跨微服务的远程调用了呢?
那么:我们该如何用Java代码发送Http的请求呢?
① RestTemplate
Spring给我们提供了一个RestTemplate的API,可以方便的实现Http请求的发送。
可以看到常见的Get、Post、Put、Delete请求都支持,如果请求参数比较复杂,还可以使用exchange方法来构造请求。
【1】先在
cart-service服务中定义一个配置类,将RestTemplate注册为一个Bean
java@Configuration public class RemoteCallConfig { @Bean public RestTemplate restTemplate() { return new RestTemplate(); } }【2】接下来,我们修改
cart-service中的CartServiceImpl的handleCartItems方法,发送http请求到item-service再把ItemDTO复制到dto包下,让代码不报错(后续优化)
下面这段就是RestTemplate发起的HTTP GET请求代码
核心作用 :根据传入的商品ID列表,向指定的服务地址发送请求,并期望得到一个
ItemDTO对象的列表作为响应。即购物车通过发送商品id列表,向商品服务端口发送请求,希望其返回商品ID对应的商品详细信息(ItemDTO)列表作为响应。
java// 发送请求,查询商品 ResponseEntity<List<ItemDTO>> response = restTemplate.exchange( "http://localhost:8083/items?ids={ids}", //请求路径 注意8083端口是item-service服务 HttpMethod.GET, // 请求方式 null, // 请求实体 new ParameterizedTypeReference<List<ItemDTO>>() {}, // 返回值类型 Map.of("ids",CollUtils.join(itemIds, ",")) // 请求参数 );下面是
handleCartItems方法的完整代码:
java/* 通过商品ID获取商品详细信息(购物车服务 → 商品服务) */ private void handleCartItems(List<CartVO> vos) { // 1.获取商品id Set<Long> itemIds = vos.stream().map(CartVO::getItemId).collect(Collectors.toSet()); // 2.查询商品 // 发送请求,查询商品 ResponseEntity<List<ItemDTO>> response = restTemplate.exchange( "http://localhost:8083/items?ids={ids}", //请求路径 HttpMethod.GET, // 请求方式 null, // 请求实体 new ParameterizedTypeReference<List<ItemDTO>>() {}, // 返回值类型 Map.of("ids",CollUtils.join(itemIds, ",")) // 请求参数 ); // 解析响应 if (!response.getStatusCode().is2xxSuccessful()) { // 查询失败,直接结束 return; } List<ItemDTO> items = response.getBody(); if (CollUtils.isEmpty(items)) { return; } /* 返回的items结构如下: [ ItemDTO(id=1, name="华为MateBook X Pro", price=8999.00, stock=100, status=1), ItemDTO(id=2, name="罗技MX Master 3鼠标", price=899.00, stock=50, status=1), ItemDTO(id=3, name="绿联Type-C扩展坞", price=199.00, stock=200, status=1) ] */ // 3.转为 id 到 item的map // 目的是基于ID快速查找商品 Map<Long, ItemDTO> itemMap = items.stream().collect( Collectors.toMap(ItemDTO::getId, Function.identity()) ); /* 生成的itemMap结构如下: itemMap = { 1: ItemDTO(id=1, name="华为MateBook X Pro", price=8999.00), 2: ItemDTO(id=2, name="罗技MX Master 3鼠标", price=899.00), 3: ItemDTO(id=3, name="绿联Type-C扩展坞", price=199.00) } */ // 4.写入vo for (CartVO v : vos) { ItemDTO item = itemMap.get(v.getItemId()); if (item == null) { continue; } v.setNewPrice(item.getPrice()); v.setStatus(item.getStatus()); v.setStock(item.getStock()); } }修改完后重启cart-service服务,就能实现【购物车服务】**远程调用【**商品服务接口】的功能了。
在这个过程中,
item-service提供了查询接口,cart-service利用Http请求调用该接口。因此
item-service可以称为服务的提供者,而cart-service则称为服务调用者。【总结】
Java发送http请求可以使用Spring提供的RestTemplate,使用的基本步骤如下:
注册RestTemplate到Spring容器
调用RestTemplate的API发送请求,常见方法有:
getForObject:发送Get请求并返回指定类型对象
PostForObject:发送Post请求并返回指定类型对象
put:发送PUT请求
delete:发送Delete请求
exchange:发送任意类型请求,返回ResponseEntity
4、服务注册和发现
之前的小节我们实现了微服务拆分,并且通过Http请求实现了跨微服务的远程调用。不过这种手动发送Http请求的方式存在一些问题:
试想一下,假如商品微服务被调用较多,为了应对更高的并发,我们进行了多实例部署,如图:
此时,每个
item-service的实例其IP或端口不同,问题来了:
item-service这么多实例,cart-service如何知道每一个实例的地址?
http请求要写url地址,
cart-service服务到底该调用哪个实例呢?如果在运行过程中,某一个
item-service实例宕机,cart-service依然在调用该怎么办?如果并发太高,
item-service临时多部署了N台实例,cart-service如何知道新实例的地址?为了解决上述问题,就必须引入注册中心的概念。
(1)注册中心原理
在微服务远程调用的过程中,包括两个角色:
服务提供者 :提供接口供其它微服务访问,比如
item-service。服务消费者 :调用其它微服务提供的接口,比如
cart-service。在大型微服务项目中,服务提供者的数量会非常多,为了管理这些服务就引入了注册中心的概念。注册中心、服务提供者、服务消费者三者间关系如下:
流程如下:
服务启动时就会注册自己的服务信息(服务名、IP、端口)到注册中心
调用者可以从注册中心订阅想要的服务,获取服务对应的实例列表(1个服务可能多实例部署)
调用者自己对实例列表负载均衡,挑选一个实例
调用者向该实例发起远程调用
当服务提供者的实例宕机或者启动新实例时,调用者如何得知呢?
服务提供者会定期向注册中心发送请求,报告自己的健康状态(心跳请求)
当注册中心长时间收不到提供者的心跳时,会认为该实例宕机,将其从服务的实例列表中剔除
当服务有新实例启动时,会发送注册服务请求,其信息会被记录在注册中心的服务实例列表
当注册中心服务列表变更时,会主动通知微服务,更新本地服务列表
(2)Nacos注册中心
需要将资料中的SQL文件导入到Docker中的MySQL容器中
【1】如何往MySQL容器中导入.sql文件?
先将sql文件拖入虚拟机根目录
进入终端,输入下面指令,将sql文件复制到容器的/tmp目录
bashdocker cp nacos.sql mysql:/tmp/然后进入mysql容器
bashdocker exec -it mysql bash mysql -u root -p导入sql文件
bashSOURCE /tmp/nacos.sql;最后查看数据库和对应表
【2】然后找到课前资料下的nacos文件夹,其中的
nacos/custom.env文件中,有一个MYSQL_SERVICE_HOST也就是mysql地址,需要修改为自己的虚拟机IP地址。【3】将课前资料中的
nacos目录上传至虚拟机的根目录。进入根目录,然后执行下面的docker命令:
bashdocker run -d \ --name nacos \ --env-file ./nacos/custom.env \ -p 8848:8848 \ -p 9848:9848 \ -p 9849:9849 \ --restart=always \ nacos/nacos-server:v2.1.0-slim启动完成后,访问下面地址:http://192.168.150.101:8848/nacos/,注意将
192.168.150.101替换为你自己的虚拟机IP地址。首次访问会跳转到登录页,账号密码都是nacos
(3)服务注册
接下来,我们把
item-service注册到Nacos,步骤如下:【1】引入依赖 :在
item-service的pom.xml中添加依赖
XML<!--nacos 服务注册发现--> <dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId> </dependency>【2】配置Nacos :在
item-service的application.yml中添加nacos地址配置
XMLspring: application: name: item-service # 服务名称 cloud: nacos: server-addr: 192.168.150.101:8848 # nacos地址【3】启动多个item-service实例 :为了测试一个服务多个实例的情况,我们再配置一个
item-service的部署实例重启
item-service的两个实例,访问nacos控制台,可以发现服务注册成功。点击详情,可以查看到
item-service服务的两个实例信息。(4)服务发现
服务的消费者要去nacos订阅服务,这个过程就是服务发现,步骤如下:
【1】引入依赖:服务发现除了要引入nacos依赖以外,由于还需要负载均衡,因此要引入SpringCloud提供的LoadBalancer依赖。
我们在
cart-service中的pom.xml中添加下面的依赖
XML<!--nacos 服务注册发现--> <dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId> </dependency>可以发现,这里Nacos的依赖与【服务注册】时一致,这个依赖中同时包含了服务注册和发现的功能。因为任何一个微服务都可以调用别人,也可以被别人调用,即可以是调用者,也可以是提供者。因此,等一会儿
cart-service启动,同样会注册到Nacos。【2】配置Nacos :在
cart-service的application.yml中添加nacos地址配置
XMLspring: cloud: nacos: server-addr: 192.168.150.101:8848【3】发现并调用服务:
接下来,服务调用者
cart-service就可以去订阅item-service服务了。不过item-service有多个实例,而真正发起调用时只需要知道一个实例的地址。因此,服务调用者必须利用负载均衡的算法,从多个实例中挑选一个去访问。常见的负载均衡算法有:
随机
轮询
IP的hash
最近最少访问
...
这里我们可以选择最简单的随机负载均衡。
另外,服务发现需要用到一个工具 ------ DiscoveryClient,SpringCloud已经帮我们自动装配,我们可以直接注入使用:
接下来,就可以对原来的远程调用做修改了,之前调用时需要写死服务提供者的IP和端口,但现在不需要了
通过DiscoveryClient发现服务实例列表,然后通过负载均衡算法,选择一个实例去调用:
java/* 通过商品ID获取商品详细信息(购物车服务 → 商品服务) */ private void handleCartItems(List<CartVO> vos) { // 1.获取商品id Set<Long> itemIds = vos.stream().map(CartVO::getItemId).collect(Collectors.toSet()); // 2.查询商品 // 2.1 发现item-service服务的实例列表 List<ServiceInstance> instances = discoveryClient.getInstances("item-service"); // 2.2 负载均衡,挑选一个实例 ServiceInstance instance = instances.get(RandomUtil.randomInt(instances.size())); // 2.3 发送请求,查询商品 ResponseEntity<List<ItemDTO>> response = restTemplate.exchange( instance.getUri() + "/items?ids={ids}", //请求路径 端口是注册中心随机挑选的item-service服务 HttpMethod.GET, // 请求方式 null, // 请求实体 new ParameterizedTypeReference<List<ItemDTO>>() {}, // 返回值类型 Map.of("ids",CollUtils.join(itemIds, ",")) // 请求参数 ); // 解析响应 if (!response.getStatusCode().is2xxSuccessful()) { // 查询失败,直接结束 return; } List<ItemDTO> items = response.getBody(); if (CollUtils.isEmpty(items)) { return; } /* 返回的items结构如下: [ ItemDTO(id=1, name="华为MateBook X Pro", price=8999.00, stock=100, status=1), ItemDTO(id=2, name="罗技MX Master 3鼠标", price=899.00, stock=50, status=1), ItemDTO(id=3, name="绿联Type-C扩展坞", price=199.00, stock=200, status=1) ] */ // 3.转为 id 到 item的map // 目的是基于ID快速查找商品 Map<Long, ItemDTO> itemMap = items.stream().collect( Collectors.toMap(ItemDTO::getId, Function.identity()) ); /* 生成的itemMap结构如下: itemMap = { 1: ItemDTO(id=1, name="华为MateBook X Pro", price=8999.00), 2: ItemDTO(id=2, name="罗技MX Master 3鼠标", price=899.00), 3: ItemDTO(id=3, name="绿联Type-C扩展坞", price=199.00) } */ // 4.写入vo for (CartVO v : vos) { ItemDTO item = itemMap.get(v.getItemId()); if (item == null) { continue; } v.setNewPrice(item.getPrice()); v.setStatus(item.getStatus()); v.setStock(item.getStock()); } }
5、OpenFeign
在上一小节,我们利用Nacos注册中心实现了服务的治理,利用RestTemplate实现了服务的远程调用。
但是远程调用的代码太复杂了!!
而且这种调用方式,与原本的本地方法调用差异太大,编程时也不统一,一会儿远程调用,一会儿本地调用。
因此,我们必须想办法改变远程调用的开发模式,让远程调用像本地方法调用一样简单,而这就要用到OpenFeign组件了。
其实远程调用的关键点就在于四个:
请求方式
请求路径
请求参数
返回值类型
所以,OpenFeign就利用SpringMVC的相关注解来声明上述4个参数,然后基于动态代理帮我们++生成远程调用的代码++,而无需我们手动再编写,非常方便。
接下来就通过一个快速入门的案例来体验一下OpenFeign的方便之处。
(1)快速入门
【1】引入依赖 :在
cart-service服务的pom.xml中引入OpenFeign的依赖和loadBalancer依赖
XML<!--openFeign--> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-openfeign</artifactId> </dependency> <!--负载均衡器--> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-loadbalancer</artifactId> </dependency>【2】启用OpenFeign :在
cart-service的CartApplication启动类上添加注解,启动OpenFeign功能【3】编写OpenFeign客户端:
java@FeignClient("item-service") // 声明服务名称 public interface ItemClient { @GetMapping("/items") // 声明请求方式和请求路径 List<ItemDTO> queryItemByIds(@RequestParam("ids") Collection<Long> ids); // 声明请求参数和返回值类型 }有了上述信息,OpenFeign就可以利用动态代理帮我们实现这个方法,并且向
http://item-service/items发送一个GET请求,携带ids为请求参数,并自动将返回值处理为List<ItemDTO>,我们只需要直接调用这个方法,就可以实现远程调用了。【4】使用FeignClient:
我们在
cart-service的CartServiceImpl中改造代码,直接调用ItemClient的方法之前那么长一段代码,现在只需要一句搞定,Feign替我们完成了服务拉取、负载均衡、发送http请求的所有工作,是不是看起来简洁多了。
而且,这里我们不再需要RestTemplate了,还省去了RestTemplate的注册。
(2)底层原理解析
OpenFeign只需要简洁的一行代码,就帮助我们实现了之前好几段代码实现的功能,那它底层是如何做到的呢?这一小节就来了解一下。
OpenFeign 的底层通过一系列步骤将定义的 Java 接口(如
ItemClient)在运行时动态转换成一个可以执行实际 HTTP 请求的代理对象。【1】启动阶段的准备
在应用启动时,OpenFeign 就已经为上述流程打好了基础:
扫描与注册 :当在启动类上添加
@EnableFeignClients注解后,Spring 会扫描所有被@FeignClient标记的接口,比如ItemClient。然后,它会将这些接口的定义信息注册到 Spring 容器中,但注册的不是普通Bean,而是一个工厂Bean------FeignClientFactoryBean。创建代理对象 :当代码中需要注入
ItemClient时,Spring 会调用FeignClientFactoryBean的getObject()方法。这个方法使用 JDK 动态代理技术,为ItemClient接口创建了一个代理实例。【2】核心组件
组件 职责 举例 ** Contract**解析接口方法上的注解(如 @GetMapping,``@RequestParam),确定HTTP方法、路径、参数等信息。解析 @GetMapping("/items")和@RequestParam("ids"),确定这是向/items发起的GET请求,参数名是ids。** Encoder**将请求体对象(如果有)编码为HTTP请求体,例如转换为JSON。 请求没有Body,主要是将 itemIds集合的值按规则拼接到URL参数中。** Client**真正发送HTTP请求的组件 。默认可能使用JDK的 HttpURLConnection,但通常我们会替换为更高效的如Apache HttpClient 或 OkHttp(后续连接池优化会讲到)。 * 负责向 http://item-service/items?ids=1,2,3,发送HTTP GET请求。 * 因为item-service启动后,会自动在Nacos注册中心注册实例列表。所以调用方cart-service启动后,OpenFeign 会向 Nacos 查询名为item-service的所有健康实例列表 ,这样调用方就获得了一个可用的服务器地址列表。 * 获取到可用的地址列表后,OpenFeign 默认集成了负载均衡器,负载均衡器会从上述实例列表中根据既定策略(如轮询、随机等)选择一个具体的实例来使用。** Decoder**将HTTP响应体(如JSON字符串)解码为Java对象。 将服务端返回的JSON数组数据转换为 List<ItemDTO>对象。(3)连接池
Feign底层发起http请求,依赖于其它的框架。其底层支持的http客户端实现包括:
HttpURLConnection:默认实现,不支持连接池(每次发起请求都需要重新创建新的连接,完成后关闭连接,效率低)
Apache HttpClient :支持连接池
OKHttp:支持连接池
因此我们通常会使用带有连接池的客户端来代替默认的HttpURLConnection。比如,我们使用OK Http。
什么是连接池?
连接池指的是一组预先创建的 HTTP 连接,这些连接可以被重复使用,而不是每次请求都创建一个新的连接。
【1】引入依赖
在
cart-service的pom.xml中引入依赖
XML<!--OK http 的依赖 --> <dependency> <groupId>io.github.openfeign</groupId> <artifactId>feign-okhttp</artifactId> </dependency>【2】开启连接池
在
cart-service的application.yml配置文件中开启Feign的连接池功能
XMLfeign: okhttp: enabled: true # 开启OKHttp功能重启服务,连接池就生效了。
【3】验证
我们可以打断点验证连接池是否生效,在
org.springframework.cloud.openfeign.loadbalancer.FeignBlockingLoadBalancerClient中的execute方法中打断点
- 按 Ctrl+Shift+N(Windows/Linux)或 Cmd+Shift+O(macOS),打开"查找文件"对话框。
- 输入类名 FeignBlockingLoadBalancerClient,选择并打开该类文件。
- 打开类文件后,按 Ctrl+F(Windows/Linux)或 Cmd+F(macOS)在该类中搜索 execute 方法
Debug方式启动cart-service,请求一次查询我的购物车方法,进入断点
可以发现这里底层的实现已经改为
OkHttpClient(4)最佳实践
将来我们要把与下单有关的业务抽取为一个独立微服务
trade-service,不过我们先来看一下hm-service中原本与下单有关的业务逻辑。hm-service中的OrderServiceImpl的createOrder方法,由于下单时前端提交了商品id,为了计算订单总价,需要查询商品信息。
也就是说,如果拆分了交易微服务(
trade-service),它也需要远程调用item-service中的根据id批量查询商品功能,这个需求与cart-service中是一样的。因此,我们就需要在trade-service中再次定义ItemClient接口,这不是重复编码吗? 有什么办法能加避免重复编码呢?避免重复编码的办法就是抽取。这里有两种抽取思路:
思路1:抽取到微服务之外的公共module
思路2:每个微服务自己抽取一个module
- 方案1抽取更加简单,工程结构也比较清晰,但缺点是整个项目耦合度偏高。
- 方案2抽取相对麻烦,工程结构相对更复杂,但服务之间耦合度降低。
由于item-service已经创建好,无法继续拆分,因此这里我们采用方案1
【1】抽取Feign客户端
在
hmall下定义一个新的module,命名为hm-api其依赖如下
XML<dependencies> <!--open feign--> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-openfeign</artifactId> </dependency> <!-- load balancer--> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-loadbalancer</artifactId> </dependency> <!-- swagger 注解依赖 --> <dependency> <groupId>io.swagger</groupId> <artifactId>swagger-annotations</artifactId> <version>1.6.6</version> <scope>compile</scope> </dependency> </dependencies>然后把ItemDTO和ItemClient都拷贝过来
现在,任何微服务要调用
item-service中的接口,只需要引入hm-api模块依赖即可,无需自己编写Feign客户端了。【2】扫描包
在
cart-service的pom.xml中引入hm-api模块
XML<!--feign模块--> <dependency> <groupId>com.heima</groupId> <artifactId>hm-api</artifactId> <version>1.0.0</version> </dependency>删除
cart-service中原来的ItemDTO和ItemClient注意:cart-service的启动类中@EnableFeignClients扫描不到
ItemClient了,因此我们需要补充声明
- 方式1:声明扫描包
- 方式2:声明要用的FeignClient
(5)日志配置
OpenFeign只会在FeignClient所在包的日志级别为DEBUG时,才会输出日志。而且其日志级别有4级:
NONE:不记录任何日志信息,这是默认值。
BASIC:仅记录请求的方法,URL以及响应状态码和执行时间
HEADERS:在BASIC的基础上,额外记录了请求和响应的头信息
FULL:记录所有请求和响应的明细,包括头信息、请求体、元数据。
Feign默认的日志级别就是NONE,所以默认我们看不到请求日志。
【1】定义日志级别
javapublic class DefaultFeignConfig { @Bean public Logger.Level feignLogLevel(){ return Logger.Level.FULL; } }【2】配置
要让日志级别生效,还需要配置这个类。有两种方式:
- 局部 生效:在某个
FeignClient中配置,只对当前FeignClient生效
java@FeignClient(value = "item-service", configuration = DefaultFeignConfig.class)
- 全局 生效:在
@EnableFeignClients中配置,针对所有FeignClient生效。
java@EnableFeignClients(defaultConfiguration = DefaultFeignConfig.class)日志格式:
java17:35:32:148 DEBUG 18620 --- [nio-8082-exec-1] com.hmall.api.client.ItemClient : [ItemClient#queryItemByIds] ---> GET http://item-service/items?ids=100000006163 HTTP/1.1 17:35:32:148 DEBUG 18620 --- [nio-8082-exec-1] com.hmall.api.client.ItemClient : [ItemClient#queryItemByIds] ---> END HTTP (0-byte body) 17:35:32:278 DEBUG 18620 --- [nio-8082-exec-1] com.hmall.api.client.ItemClient : [ItemClient#queryItemByIds] <--- HTTP/1.1 200 (127ms) 17:35:32:279 DEBUG 18620 --- [nio-8082-exec-1] com.hmall.api.client.ItemClient : [ItemClient#queryItemByIds] connection: keep-alive 17:35:32:279 DEBUG 18620 --- [nio-8082-exec-1] com.hmall.api.client.ItemClient : [ItemClient#queryItemByIds] content-type: application/json 17:35:32:279 DEBUG 18620 --- [nio-8082-exec-1] com.hmall.api.client.ItemClient : [ItemClient#queryItemByIds] date: Fri, 26 May 2023 09:35:32 GMT 17:35:32:279 DEBUG 18620 --- [nio-8082-exec-1] com.hmall.api.client.ItemClient : [ItemClient#queryItemByIds] keep-alive: timeout=60 17:35:32:279 DEBUG 18620 --- [nio-8082-exec-1] com.hmall.api.client.ItemClient : [ItemClient#queryItemByIds] transfer-encoding: chunked 17:35:32:279 DEBUG 18620 --- [nio-8082-exec-1] com.hmall.api.client.ItemClient : [ItemClient#queryItemByIds] 17:35:32:280 DEBUG 18620 --- [nio-8082-exec-1] com.hmall.api.client.ItemClient : [ItemClient#queryItemByIds] [{"id":100000006163,"name":"巴布豆(BOBDOG)柔薄悦动婴儿拉拉裤XXL码80片(15kg以上)","price":67100,"stock":10000,"image":"https://m.360buyimg.com/mobilecms/s720x720_jfs/t23998/350/2363990466/222391/a6e9581d/5b7cba5bN0c18fb4f.jpg!q70.jpg.webp","category":"拉拉裤","brand":"巴布豆","spec":"{}","sold":11,"commentCount":33343434,"isAD":false,"status":2}] 17:35:32:281 DEBUG 18620 --- [nio-8082-exec-1] com.hmall.api.client.ItemClient : [ItemClient#queryItemByIds] <--- END HTTP (369-byte body)
三、微服务拆分作业
将hm-service中的其它业务也都拆分为微服务,包括:
user-service:用户微服务,包含用户登录、管理等功能
trade-service:交易微服务,包含订单相关功能
pay-service:支付微服务,包含支付相关功能
其中交易服务、支付服务、用户服务中的业务都需要知道当前登录用户是谁,目前暂未实现,先将用户id写死。
思考:如何才能在每个微服务中都拿到用户信息?如何在微服务之间传递用户信息?
1、用户微服务
【1】新建module,user-service
【2】pom.xml文件中导入依赖(注意有Nacos)
XML<dependencies> <!--common--> <dependency> <groupId>com.heima</groupId> <artifactId>hm-common</artifactId> <version>1.0.0</version> </dependency> <!--api--> <dependency> <groupId>com.heima</groupId> <artifactId>hm-api</artifactId> <version>1.0.0</version> </dependency> <!--web--> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <!--数据库--> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> </dependency> <!--mybatis--> <dependency> <groupId>com.baomidou</groupId> <artifactId>mybatis-plus-boot-starter</artifactId> </dependency> <!--nacos 服务注册发现--> <dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId> </dependency> </dependencies> <build> <finalName>${project.artifactId}</finalName> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> </plugin> </plugins> </build>【3】创建启动类及相关包
java@EnableFeignClients(clients = {ItemClient.class}) @MapperScan("com.hmall.user.mapper") @SpringBootApplication public class UserApplication { public static void main(String[] args) { SpringApplication.run(UserApplication.class, args); } }【4】编写配置文件application.yaml,修改部分内容
htmlserver: port: 8085 spring: application: name: user-service #微服务名称 cloud: nacos: server-addr: 123.57.186.189:8848 profiles: active: dev datasource: url: jdbc:mysql://${hm.db.host}:3306/hm-user?useUnicode=true&characterEncoding=UTF-8&autoReconnect=true&serverTimezone=Asia/Shanghai driver-class-name: com.mysql.cj.jdbc.Driver username: root password: ${hm.db.pw} mybatis-plus: configuration: default-enum-type-handler: com.baomidou.mybatisplus.core.handlers.MybatisEnumTypeHandler global-config: db-config: update-strategy: not_null id-type: auto logging: level: com.hmall: debug pattern: dateformat: HH:mm:ss:SSS file: path: "logs/${spring.application.name}" knife4j: enable: true openapi: title: 黑马商城-用户管理-接口文档 description: "黑马商城-用户管理-接口文档" email: zhanghuyi@itcast.cn concat: 虎哥 url: https://www.itcast.cn version: v1.0.0 group: default: group-name: default api-rule: package api-rule-resources: - com.hmall.user.controller hm: jwt: location: classpath:hmall.jks alias: hmall password: hmall123 tokenTTL: 30m将hm-service下的hmall.jks文件拷贝到user-service下的resources目录,这是JWT加密的秘钥文件
【5】拆分项目
复制hm-service中所有与user、address、jwt有关的代码,最终项目结构如下
【6】导入数据库
user-service也需要自己的独立的database,向MySQL中导入课前资料提供的SQL
【7】配置启动项
2、📍交易微服务 - 参考
【1】新建module
【2】往pom.xml文件中导入依赖
html<dependencies> <!--common--> <dependency> <groupId>com.heima</groupId> <artifactId>hm-common</artifactId> <version>1.0.0</version> </dependency> <!--api--> <dependency> <groupId>com.heima</groupId> <artifactId>hm-api</artifactId> <version>1.0.0</version> </dependency> <!--web--> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <!--数据库--> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> </dependency> <!--mybatis--> <dependency> <groupId>com.baomidou</groupId> <artifactId>mybatis-plus-boot-starter</artifactId> </dependency> <!--nacos 服务注册发现--> <dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId> </dependency> </dependencies> <build> <finalName>${project.artifactId}</finalName> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> </plugin> </plugins> </build>【3】创建启动类及相关包
【4】编写配置文件application.yaml,修改部分内容
htmlserver: port: 8086 spring: application: name: trade-service #微服务名称 cloud: nacos: server-addr: 123.57.186.189:8848 profiles: active: dev datasource: url: jdbc:mysql://${hm.db.host}:3306/hm-trade?useUnicode=true&characterEncoding=UTF-8&autoReconnect=true&serverTimezone=Asia/Shanghai driver-class-name: com.mysql.cj.jdbc.Driver username: root password: ${hm.db.pw} mybatis-plus: configuration: default-enum-type-handler: com.baomidou.mybatisplus.core.handlers.MybatisEnumTypeHandler global-config: db-config: update-strategy: not_null id-type: auto logging: level: com.hmall: debug pattern: dateformat: HH:mm:ss:SSS file: path: "logs/${spring.application.name}" knife4j: enable: true openapi: title: 黑马商城-交易管理-接口文档 description: "黑马商城-交易管理-接口文档" email: zhanghuyi@itcast.cn concat: 虎哥 url: https://www.itcast.cn version: v1.0.0 group: default: group-name: default api-rule: package api-rule-resources: - com.hmall.trade.controller【5】拆分项目并找出调用其他服务的接口
复制hm-service中所有与trade有关的代码,最终项目结构如下:
在交易服务中,用户下单时需要做下列事情:
根据id查询商品列表 ------ item-service
计算商品总价
保存订单
扣减库存 ------ item-service
清理购物车商品 ------ cart-service
交易服务要调用他们,必须通过OpenFeign远程调用,我们需要将上述功能抽取为FeignClient。
【6】抽取要调用的服务接口至Client
++6.1 抽取ItemClient接口++
在hm-api的client包中创建ItemClient接口
【根据id查询商品列表】的接口我们之前已经实现,我们直接调用即可。
6.1.1 扣减库存
item-service的扣减库存接口如下
我们将这个接口抽取到
hm-api模块的com.hmall.api.client.ItemClient中将接口参数的
OrderDetailDTO抽取到hm-api模块的dto包下注意:现在可以删除trade-service的dto包下的OrderDetailDTO!(不删后面会报错)
++6.2 抽取CartClient接口++
6.2.1 清除购物车
cart-service的清除购物车接口如下
将这个接口抽取到
hm-api模块的com.hmall.api.client.CartClient中【7】改造 ServiceImpl代码逻辑
接下来,就可以改造OrderServiceImpl中的逻辑,将本地方法调用改造为基于FeignClient的调用
完整代码如下
javapackage com.hmall.trade.service.impl; import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; import com.hmall.api.client.CartClient; import com.hmall.api.client.ItemClient; import com.hmall.api.dto.ItemDTO; import com.hmall.api.dto.OrderDetailDTO; import com.hmall.common.exception.BadRequestException; import com.hmall.common.utils.UserContext; import com.hmall.trade.domain.dto.OrderFormDTO; import com.hmall.trade.domain.po.Order; import com.hmall.trade.domain.po.OrderDetail; import com.hmall.trade.mapper.OrderMapper; import com.hmall.trade.service.IOrderDetailService; import com.hmall.trade.service.IOrderService; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import java.time.LocalDateTime; import java.util.ArrayList; import java.util.List; import java.util.Map; import java.util.Set; import java.util.stream.Collectors; /** * <p> * 服务实现类 * </p> * * @author 虎哥 * @since 2023-05-05 */ @Service @RequiredArgsConstructor public class OrderServiceImpl extends ServiceImpl<OrderMapper, Order> implements IOrderService { private final CartClient cartClient; private final ItemClient itemClient; private final IOrderDetailService detailService; @Override @Transactional public Long createOrder(OrderFormDTO orderFormDTO) { // 1.订单数据 Order order = new Order(); // 1.1.查询商品 List<OrderDetailDTO> detailDTOS = orderFormDTO.getDetails(); // 1.2.获取商品id和数量的Map Map<Long, Integer> itemNumMap = detailDTOS.stream() .collect(Collectors.toMap(OrderDetailDTO::getItemId, OrderDetailDTO::getNum)); Set<Long> itemIds = itemNumMap.keySet(); // 1.3.查询商品 List<ItemDTO> items = itemClient.queryItemByIds(itemIds); if (items == null || items.size() < itemIds.size()) { throw new BadRequestException("商品不存在"); } // 1.4.基于商品价格、购买数量计算商品总价:totalFee int total = 0; for (ItemDTO item : items) { total += item.getPrice() * itemNumMap.get(item.getId()); } order.setTotalFee(total); // 1.5.其它属性 order.setPaymentType(orderFormDTO.getPaymentType()); order.setUserId(UserContext.getUser()); order.setStatus(1); // 1.6.将Order写入数据库order表中 save(order); // 2.保存订单详情 List<OrderDetail> details = buildDetails(order.getId(), items, itemNumMap); detailService.saveBatch(details); // 3.清理购物车商品 cartClient.deleteCartItemByIds(itemIds); // 4.扣减库存 try { itemClient.deductStock(detailDTOS); } catch (Exception e) { throw new RuntimeException("库存不足!"); } return order.getId(); } @Override public void markOrderPaySuccess(Long orderId) { Order order = new Order(); order.setId(orderId); order.setStatus(2); order.setPayTime(LocalDateTime.now()); updateById(order); } private List<OrderDetail> buildDetails(Long orderId, List<ItemDTO> items, Map<Long, Integer> numMap) { List<OrderDetail> details = new ArrayList<>(items.size()); for (ItemDTO item : items) { OrderDetail detail = new OrderDetail(); detail.setName(item.getName()); detail.setSpec(item.getSpec()); detail.setPrice(item.getPrice()); detail.setNum(numMap.get(item.getId())); detail.setItemId(item.getId()); detail.setImage(item.getImage()); detail.setOrderId(orderId); details.add(detail); } return details; } }【8】导入数据库
trade-service也需要自己的独立的database,向MySQL中导入课前资料提供的SQL
【9】配置启动项
3、支付微服务
【1】新建module
【2】往pom.xml导入依赖
java<dependencies> <!--common--> <dependency> <groupId>com.heima</groupId> <artifactId>hm-common</artifactId> <version>1.0.0</version> </dependency> <!--api--> <dependency> <groupId>com.heima</groupId> <artifactId>hm-api</artifactId> <version>1.0.0</version> </dependency> <!--web--> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <!--数据库--> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> </dependency> <!--mybatis--> <dependency> <groupId>com.baomidou</groupId> <artifactId>mybatis-plus-boot-starter</artifactId> </dependency> <!--nacos 服务注册发现--> <dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId> </dependency> </dependencies> <build> <finalName>${project.artifactId}</finalName> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> </plugin> </plugins> </build>【3】创建启动类及相关包
【4】编写配置文件application.yaml,修改部分内容
javaserver: port: 8087 spring: application: name: pay-service profiles: active: dev datasource: url: jdbc:mysql://${hm.db.host}:3306/hm-pay?useUnicode=true&characterEncoding=UTF-8&autoReconnect=true&serverTimezone=Asia/Shanghai driver-class-name: com.mysql.cj.jdbc.Driver username: root password: ${hm.db.pw} mybatis-plus: configuration: default-enum-type-handler: com.baomidou.mybatisplus.core.handlers.MybatisEnumTypeHandler global-config: db-config: update-strategy: not_null id-type: auto logging: level: com.hmall: debug pattern: dateformat: HH:mm:ss:SSS file: path: "logs/${spring.application.name}" knife4j: enable: true openapi: title: 黑马商城-支付管理-文档 description: "黑马商城-支付管理-文档" email: zhanghuyi@itcast.cn concat: 虎哥 url: https://www.itcast.cn version: v1.0.0 group: default: group-name: default api-rule: package api-rule-resources: - com.hmall.pay.controller【5】拆分项目并找出调用其他服务的接口
在支付服务中,用户支付时需要做下列事情:
查询支付订单信息
判断订单状态
扣减余额 ------ user-service
修改支付单状态
修改订单状态 ------ trade-service
支付服务要调用他们,必须通过OpenFeign远程调用,我们需要将上述功能抽取为FeignClient。
【6】抽取要调用的服务接口至Client
++6.1 抽取UserClient接口++
在hm-api的client包中创建UserClient接口
++6.1.1 扣除余额++
user-service的扣减余额接口如下
我们将这个接口抽取到
hm-api模块的com.hmall.api.client.UserClient中++6.2 抽取TradeClient接口++
在hm-api的client包中创建TradeClient接口
6.2.1 修改订单状态
trade-service的修改订单状态接口如下
我们将这个接口抽取到
hm-api模块的com.hmall.api.client.TradeClient中【7】改造ServiceImpl代码逻辑
接下来,就可以改造PayOrderServiceImpl中的逻辑,将本地方法调用改造为基于FeignClient的调用
完整代码如下:
javapackage com.hmall.pay.service.impl; import com.baomidou.mybatisplus.core.toolkit.IdWorker; import com.baomidou.mybatisplus.core.toolkit.StringUtils; import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; import com.hmall.api.client.TradeClient; import com.hmall.api.client.UserClient; import com.hmall.common.exception.BizIllegalException; import com.hmall.common.utils.BeanUtils; import com.hmall.common.utils.UserContext; import com.hmall.pay.domain.dto.PayApplyDTO; import com.hmall.pay.domain.dto.PayOrderFormDTO; import com.hmall.pay.domain.po.PayOrder; import com.hmall.pay.enums.PayStatus; import com.hmall.pay.mapper.PayOrderMapper; import com.hmall.pay.service.IPayOrderService; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import java.time.LocalDateTime; /** * <p> * 支付订单 服务实现类 * </p> * * @author 虎哥 * @since 2023-05-16 */ @Service @RequiredArgsConstructor public class PayOrderServiceImpl extends ServiceImpl<PayOrderMapper, PayOrder> implements IPayOrderService { private final UserClient userClient; private final TradeClient tradeClient; @Override public String applyPayOrder(PayApplyDTO applyDTO) { // 1.幂等性校验 PayOrder payOrder = checkIdempotent(applyDTO); // 2.返回结果 return payOrder.getId().toString(); } @Override @Transactional public void tryPayOrderByBalance(PayOrderFormDTO payOrderFormDTO) { // 1.查询支付单 PayOrder po = getById(payOrderFormDTO.getId()); // 2.判断状态 if(!PayStatus.WAIT_BUYER_PAY.equalsValue(po.getStatus())){ // 订单不是未支付,状态异常 throw new BizIllegalException("交易已支付或关闭!"); } // 3.尝试扣减余额 userClient.deductMoney(payOrderFormDTO.getPw(),po.getAmount()); // 4.修改支付单状态 boolean success = markPayOrderSuccess(payOrderFormDTO.getId(), LocalDateTime.now()); if (!success) { throw new BizIllegalException("交易已支付或关闭!"); } // 5.修改订单状态 tradeClient.markOrderPaySuccess(po.getBizOrderNo()); // 传入的是业务订单号 } public boolean markPayOrderSuccess(Long id, LocalDateTime successTime) { return lambdaUpdate() .set(PayOrder::getStatus, PayStatus.TRADE_SUCCESS.getValue()) .set(PayOrder::getPaySuccessTime, successTime) .eq(PayOrder::getId, id) // 支付状态的乐观锁判断 .in(PayOrder::getStatus, PayStatus.NOT_COMMIT.getValue(), PayStatus.WAIT_BUYER_PAY.getValue()) .update(); } private PayOrder checkIdempotent(PayApplyDTO applyDTO) { // 1.首先查询支付单 PayOrder oldOrder = queryByBizOrderNo(applyDTO.getBizOrderNo()); // 2.判断是否存在 if (oldOrder == null) { // 不存在支付单,说明是第一次,写入新的支付单并返回 PayOrder payOrder = buildPayOrder(applyDTO); payOrder.setPayOrderNo(IdWorker.getId()); save(payOrder); return payOrder; } // 3.旧单已经存在,判断是否支付成功 if (PayStatus.TRADE_SUCCESS.equalsValue(oldOrder.getStatus())) { // 已经支付成功,抛出异常 throw new BizIllegalException("订单已经支付!"); } // 4.旧单已经存在,判断是否已经关闭 if (PayStatus.TRADE_CLOSED.equalsValue(oldOrder.getStatus())) { // 已经关闭,抛出异常 throw new BizIllegalException("订单已关闭"); } // 5.旧单已经存在,判断支付渠道是否一致 if (!StringUtils.equals(oldOrder.getPayChannelCode(), applyDTO.getPayChannelCode())) { // 支付渠道不一致,需要重置数据,然后重新申请支付单 PayOrder payOrder = buildPayOrder(applyDTO); payOrder.setId(oldOrder.getId()); payOrder.setQrCodeUrl(""); updateById(payOrder); payOrder.setPayOrderNo(oldOrder.getPayOrderNo()); return payOrder; } // 6.旧单已经存在,且可能是未支付或未提交,且支付渠道一致,直接返回旧数据 return oldOrder; } private PayOrder buildPayOrder(PayApplyDTO payApplyDTO) { // 1.数据转换 PayOrder payOrder = BeanUtils.toBean(payApplyDTO, PayOrder.class); // 2.初始化数据 payOrder.setPayOverTime(LocalDateTime.now().plusMinutes(120L)); payOrder.setStatus(PayStatus.WAIT_BUYER_PAY.getValue()); payOrder.setBizUserId(UserContext.getUser()); return payOrder; } public PayOrder queryByBizOrderNo(Long bizOrderNo) { return lambdaQuery() .eq(PayOrder::getBizOrderNo, bizOrderNo) .one(); } }【8】导入数据库
pay-service也需要自己的独立的database,向MySQL中导入课前资料提供的SQL【9】配置启动项

















































































































