【微服务 Day2】SpringCloud实战开发(微服务拆分步骤 + Nacos注册中心 + OpenFeign + 微服务拆分作业)

目录

一、项目部署

1、后端部署

2、前端部署

3、DockerCompose

(1)编写docker-compose文件

(2)运行docker-compose文件

[二、微服务 01](#二、微服务 01)

1、单体架构和微服务架构对比

(1)单体架构

(2)微服务架构

2、SpringCloud

3、微服务拆分

(1)服务拆分原则

(2)商品、购物车服务拆分

[① 商品服务拆分](#① 商品服务拆分)

[② 购物车服务拆分](#② 购物车服务拆分)

(3)远程调用

[① RestTemplate](#① RestTemplate)

4、服务注册和发现

(1)注册中心原理

(2)Nacos注册中心

(3)服务注册

(4)服务发现

5、OpenFeign

(1)快速入门

(2)底层原理解析

(3)连接池

(4)最佳实践

(5)日志配置

三、微服务拆分作业

1、用户微服务

[2、📍交易微服务 - 参考](#2、📍交易微服务 - 参考)

【1】新建module

【2】往pom.xml文件中导入依赖

【3】创建启动类及相关包

【4】编写配置文件application.yaml,修改部分内容

【5】拆分项目并找出调用其他服务的接口

【6】抽取要调用的服务接口至Client

【7】改造ServiceImpl代码逻辑

【8】导入数据库

【9】配置启动项

3、支付微服务


一、项目部署

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文件

java 复制代码
server:
  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

java 复制代码
server:
  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中的CartServiceImplhandleCartItems方法,发送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目录

bash 复制代码
docker cp nacos.sql mysql:/tmp/

然后进入mysql容器

bash 复制代码
docker exec -it mysql bash

mysql -u root -p

导入sql文件

bash 复制代码
SOURCE /tmp/nacos.sql;

最后查看数据库和对应表

【2】然后找到课前资料下的nacos文件夹,其中的nacos/custom.env文件中,有一个MYSQL_SERVICE_HOST也就是mysql地址,需要修改为自己的虚拟机IP地址。

【3】将课前资料中的nacos目录上传至虚拟机的目录。

进入根目录,然后执行下面的docker命令:

bash 复制代码
docker 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-servicepom.xml中添加依赖

XML 复制代码
<!--nacos 服务注册发现-->
<dependency>
    <groupId>com.alibaba.cloud</groupId>
    <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>

【2】配置Nacos :在item-serviceapplication.yml中添加nacos地址配置

XML 复制代码
spring:
  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-serviceapplication.yml中添加nacos地址配置

XML 复制代码
spring:
  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-serviceCartApplication启动类上添加注解,启动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-serviceCartServiceImpl中改造代码,直接调用ItemClient的方法

之前那么长一段代码,现在只需要一句搞定,Feign替我们完成了服务拉取、负载均衡、发送http请求的所有工作,是不是看起来简洁多了。

而且,这里我们不再需要RestTemplate了,还省去了RestTemplate的注册。

(2)底层原理解析

OpenFeign只需要简洁的一行代码,就帮助我们实现了之前好几段代码实现的功能,那它底层是如何做到的呢?这一小节就来了解一下。

OpenFeign 的底层通过一系列步骤将定义的 Java 接口(如 ItemClient)在运行时动态转换成一个可以执行实际 HTTP 请求的代理对象。

【1】启动阶段的准备

在应用启动时,OpenFeign 就已经为上述流程打好了基础:

  • 扫描与注册 :当在启动类上添加 @EnableFeignClients 注解后,Spring 会扫描所有被 @FeignClient 标记的接口,比如 ItemClient。然后,它会将这些接口的定义信息注册到 Spring 容器中,但注册的不是普通Bean,而是一个工厂Bean------FeignClientFactoryBean

  • 创建代理对象 :当代码中需要注入 ItemClient时,Spring 会调用 FeignClientFactoryBeangetObject()方法。这个方法使用 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-servicepom.xml中引入依赖

XML 复制代码
<!--OK http 的依赖 -->
<dependency>
  <groupId>io.github.openfeign</groupId>
  <artifactId>feign-okhttp</artifactId>
</dependency>

【2】开启连接池

cart-serviceapplication.yml配置文件中开启Feign的连接池功能

XML 复制代码
feign:
  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-servicepom.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】定义日志级别

java 复制代码
public 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)

日志格式:

java 复制代码
17: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,修改部分内容

html 复制代码
server:
  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,修改部分内容
html 复制代码
server:
  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的调用

完整代码如下

java 复制代码
package 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,修改部分内容

java 复制代码
server:
  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的调用

完整代码如下:

java 复制代码
package 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】配置启动项

相关推荐
wniuniu_2 小时前
blob是啥
java·服务器·网络
.生产的驴2 小时前
DockerCompoe 部署注册中心Nacos 一键部署 单机+Mysql8
java·linux·运维·spring boot·缓存·docker·doc
hgz07102 小时前
MyBatis插件(拦截器)
java·tomcat
Sunsets_Red2 小时前
待修改莫队与普通莫队优化
java·c++·python·学习·算法·数学建模·c#
葡萄城技术团队2 小时前
在 Java 中优化 MySQL 查询以提升性能
java·开发语言·mysql
杀死那个蝈坦2 小时前
短链接生成-基于布隆过滤器和唯一索引
java·数据库·微服务·oracle·rocketmq
慕白Lee2 小时前
Java foreach在lambda的foreach遍历中退出操作(lambda foreach break)
java
winfield8212 小时前
Java 中大量闲置 MySQL 连接的解决方案(从根因到落地)
java·mysql
moxiaoran57532 小时前
Java开发中VO的使用
java·开发语言