微服务开发与实战Day03

news2024/11/27 10:31:47

一、导入黑马商城项目

资料文档:Docs

1. 安装MySQL

①删除root目录下的mysql

 rm -rf mysql/

②把课前资料里的mysql目录上传到root目录下

③创建一个通用网络

docker network create hm-net

④使用下面的命令安装MySQL

docker run -d \
  --name mysql \
  -p 3306:3306 \
  -e TZ=Asia/Shanghai \
  -e MYSQL_ROOT_PASSWORD=123456 \
  -v /root/mysql/data:/var/lib/mysql \
  -v /root/mysql/conf:/etc/mysql/conf.d \
  -v /root/mysql/init:/docker-entrypoint-initdb.d \
  --network hm-net\
  mysql

⑤客户端连接测试

2. 后端

①把hmall项目复制到IDEA工作空间,打开。修改配置文件里的信息(在Day02中导入过的不用删除)

②ALT + 8键打开services窗口,新增一个启动项

在弹出的窗口中找到Spring Boot

在弹出窗口中配置SpringBoot的启动环境为local:

③接着运行项目,访问 http://localhost:8080/hi

3. 前端

①把Day03课前资料中的hmall-nginx目录拷贝到一个不带中文的路径下,

②运行nginx.exe

如果启动失败,查看错误日志如下,把nginx.conf中的端口号改成其他的,或者停止占用80端口号的程序。

2024/06/07 10:50:15 [emerg] 42452#46764: bind() to 0.0.0.0:80 failed (10013: An attempt was made to access a socket in a way forbidden by its access permissions)

③访问 http://localhost:18080,查看是否成功

二、认识微服务

1. 单体架构

单体架构:将业务的所有功能集中在一个项目中开发,打成一个包部署

优点:架构简单,部署成本低。

缺点:团队协作成本高,系统发布效率低,系统可用性差

总结:单价架构适合开发功能相对简单,规模较小的项目。

JMeter官网:Apache JMeter - Download Apache JMeter

安装教程:JMeter软件的安装(超详细教程)_jmeter安装-CSDN博客

①把课前资料中的“黑马商城测试.jmx”拖拽入JMeter中测试。

2. 微服务

微服务架构,是服务化思想指导下的一套最佳实践架构方案。服务化,就是把单体架构中的功能模块拆分为多个独立项目。

  • 粒度小
  • 团队自治
  • 服务自治

3. SpringCloud

SpringCloud是目前国内使用最广泛的微服务架构。SpringCloud集成了各种微服务功能组件,并基于SpringBoot实现了这些组件的自动装配,从而提供了良好的开箱即用体验。

官网地址:Spring Cloud

SpringCloud版本SpringBoot版本
2023.0.x aka Leyton3.2.x
2022.0.x aka Kilburn3.0.x, 3.1.x(Starting with 2022.0.3)
2021.0.x aka Jubilee2.6.x, 2.7.x (Starting with 2021.0.3)
2020.0.x aka Ilford2.4.x, 2.5.x (Starting with 2020.0.3)
Hoxton2.2.x, 2.3.x (Starting with SR5)
Greenwich2.1.x
Finchley2.0.x
Edgware1.5.x
Dalston1.5.x

三、微服务拆分

1. 熟悉黑马商城

在登录时如果报错如下,在pom.xml里把MyBatis Plus的版本改成3.4.2或其他版本

org.mybatis.spring.MyBatisSystemException: nested exception is org.apache.ibatis.builder.BuilderException: Error evaluating expression 'ew.sqlSegment != null and ew.sqlSegment != '' and ew.nonEmptyOfWhere'. Cause: org.apache.ibatis.ognl.OgnlException: sqlSegment [java.lang.ExceptionInInitializerError]
    at org.mybatis.spring.MyBatisExceptionTranslator.translateExceptionIfPossible(MyBatisExceptionTranslator.java:96) ~[mybatis-spring-2.0.6.jar:2.0.6]
    at org.mybatis.spring.SqlSessionTemplate$SqlSessionInterceptor.invoke(SqlSessionTemplate.java:441) 

2. 服务拆分原则

1. 什么时候拆分

  • 创业型项目:先采用单体架构,快速开发,快速试错。随着规模扩大,逐渐拆分
  • 确定的大型项目:资金充足,目标明确,可以直接选择微服务架构,避免后续拆分的麻烦。

2. 怎么拆分

从拆分目标来说,要做到:

  • 高内聚:每个微服务的职责要尽量单一,包含的业务相关关联度高、完整度高
  • 低耦合:每个微服务的功能要相对独立,尽量减少对其他微服务的依赖。

从拆分方式来说,一般包含两种方式:

  • 纵向拆分:按照业务模块来拆分
  • 横向拆分:抽取公共服务,提高复用性

3. 拆分购物车、商品服务

工程结构有两种:

  • 独立Project
  • Maven聚合

案例1:拆分服务

需求:

  • 将hm-service中与商品管理相关功能代码拆分到一个微服务module中,命名为item-service
  • 将hm-service中与购物车有关的功能拆分成一个微服务moudle中,命名为cart-service

(1)拆分商品管理模块 item-service

步骤:

①创建item-servicemodule

如果创建新module时,出现Error adding module to project: null的错误(JDK版本过高),可以下载一个JDK11。

安装教程:超详细JDK下载与安装步骤_jdk下载与安装教程-CSDN博客

JDK 安装与环境变量配置(Win10详细版)_jdk环境变量配置-CSDN博客

②修改pom.xml文件

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <parent>
        <artifactId>hmall</artifactId>
        <groupId>com.heima</groupId>
        <version>1.0.0</version>
    </parent>
    <modelVersion>4.0.0</modelVersion>

    <artifactId>item-service</artifactId>

    <properties>
        <maven.compiler.source>11</maven.compiler.source>
        <maven.compiler.target>11</maven.compiler.target>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    </properties>

    <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>
    </dependencies>
    <build>
        <finalName>${project.artifactId}</finalName>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>

</project>

③添加启动类ItemApplication

package com.hmall.item;

import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@MapperScan("com.hmall.item.mapper")
@SpringBootApplication
public class ItemApplication {
    public static void main(String[] args) {
        SpringApplication.run(ItemApplication.class, args);
    }
}

④执行SQL脚本,更改配置文件

application.yaml

server:
  port: 8081
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

⑤从hm-service拷贝下图所示文件到item-service当中,报错的把import删掉,设置自动导入

自动导入设置

⑥修改ItemServiceImpl.java中sqlStatement字符串

⑦启动类使用的配置文件设置为local

⑧启动项目进行测试

http://localhost:8081/doc.html#/home

小Tips:IDEA同时打开多个Database

(2)拆分购物车模块 cart-service

①创建cart-service模块

②添加启动类CartApplication

package com.hmall.cart;

import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@MapperScan("com.hmall.cart.mapper")
@SpringBootApplication
public class CartApplication {
    public static void main(String[] args) {
        SpringApplication.run(CartApplication.class, args);
    }
}

③从item-service拷贝并修改配置文件application.yaml

server:
  port: 8082
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

④执行脚本hm-cart.sql

⑤从hm-service中拷贝如下文件到cart-service中

⑥将CartServiceImpl.java中目前报错的代码先注释掉

package com.hmall.cart.service.impl;

import cn.hutool.core.util.StrUtil;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.hmall.cart.domain.dto.CartFormDTO;
import com.hmall.cart.domain.po.Cart;
import com.hmall.cart.domain.vo.CartVO;
import com.hmall.cart.mapper.CartMapper;
import com.hmall.cart.service.ICartService;
import com.hmall.common.exception.BizIllegalException;
import com.hmall.common.utils.BeanUtils;
import com.hmall.common.utils.CollUtils;
import com.hmall.common.utils.UserContext;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;

import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.function.Function;
import java.util.stream.Collectors;

/**
 * <p>
 * 订单详情表 服务实现类
 * </p>
 *
 * @author 虎哥
 * @since 2023-05-05
 */
@Service
@RequiredArgsConstructor
public class CartServiceImpl extends ServiceImpl<CartMapper, Cart> implements ICartService {

    // private final IItemService itemService;

    @Override
    public void addItem2Cart(CartFormDTO cartFormDTO) {
        // 1.获取登录用户
        Long userId = UserContext.getUser();

        // 2.判断是否已经存在
        if(checkItemExists(cartFormDTO.getItemId(), userId)){
            // 2.1.存在,则更新数量
            baseMapper.updateNum(cartFormDTO.getItemId(), userId);
            return;
        }
        // 2.2.不存在,判断是否超过购物车数量
        checkCartsFull(userId);

        // 3.新增购物车条目
        // 3.1.转换PO
        Cart cart = BeanUtils.copyBean(cartFormDTO, Cart.class);
        // 3.2.保存当前用户
        cart.setUserId(userId);
        // 3.3.保存到数据库
        save(cart);
    }

    @Override
    public List<CartVO> queryMyCarts() {
        // 1.查询我的购物车列表
        List<Cart> carts = lambdaQuery().eq(Cart::getUserId,  1L/* TODO UserContext.getUser()*/).list();
        if (CollUtils.isEmpty(carts)) {
            return CollUtils.emptyList();
        }

        // 2.转换VO
        List<CartVO> vos = BeanUtils.copyList(carts, CartVO.class);

        // 3.处理VO中的商品信息
        handleCartItems(vos);

        // 4.返回
        return vos;
    }

    private void handleCartItems(List<CartVO> vos) {
        // TODO 1.获取商品id
//        Set<Long> itemIds = vos.stream().map(CartVO::getItemId).collect(Collectors.toSet());
//        // 2.查询商品
//        List<ItemDTO> items = itemService.queryItemByIds(itemIds);
//        if (CollUtils.isEmpty(items)) {
//            return;
//        }
//        // 3.转为 id 到 item的map
//        Map<Long, ItemDTO> itemMap = items.stream().collect(Collectors.toMap(ItemDTO::getId, Function.identity()));
//        // 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());
//        }
    }

    @Override
    public void removeByItemIds(Collection<Long> itemIds) {
        // 1.构建删除条件,userId和itemId
        QueryWrapper<Cart> queryWrapper = new QueryWrapper<Cart>();
        queryWrapper.lambda()
                .eq(Cart::getUserId, UserContext.getUser())
                .in(Cart::getItemId, itemIds);
        // 2.删除
        remove(queryWrapper);
    }

    private void checkCartsFull(Long userId) {
        int count = lambdaQuery().eq(Cart::getUserId, userId).count();
        if (count >= 10) {
            throw new BizIllegalException(StrUtil.format("用户购物车课程不能超过{}", 10));
        }
    }

    private boolean checkItemExists(Long itemId, Long userId) {
        int count = lambdaQuery()
                .eq(Cart::getUserId, userId)
                .eq(Cart::getItemId, itemId)
                .count();
        return count > 0;
    }
}

⑦拷贝相关依赖到pom.xml

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <parent>
        <artifactId>hmall</artifactId>
        <groupId>com.heima</groupId>
        <version>1.0.0</version>
    </parent>
    <modelVersion>4.0.0</modelVersion>

    <artifactId>cart-service</artifactId>

    <properties>
        <maven.compiler.source>11</maven.compiler.source>
        <maven.compiler.target>11</maven.compiler.target>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    </properties>

    <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>
    </dependencies>
    <build>
        <finalName>${project.artifactId}</finalName>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>

</project>

⑧在Maven面板刷新一下,ALT + 8键打开services面板,加载启动类CartApplication

⑨运行测试 http://localhost:8082/doc.html#/home

4. 远程调用

Spring给我们提供了一个RestTemplate工具,可以方便的实现Http请求的发送。使用步骤如下:

①注入RestTemplate到Spring容器

@Bean
public RestTemplate restTemplate() {
    return new RestTemplate();
}

②发起远程调用

public <T> ResponseEntity<T> exchange(
    String url, // 请求路径
    HttpMethod method, // 请求方式
    @Nullable HttpEntity<?> requestEntity,  // 请求实体,可以为空
    Class<T> responseType, // 返回值类型
    Map<String, ?> uriVariables  // 请求参数
)

步骤:

①CartApplication

package com.hmall.cart;

import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Bean;
import org.springframework.web.client.RestTemplate;

@MapperScan("com.hmall.cart.mapper")
@SpringBootApplication
public class CartApplication {
    public static void main(String[] args) {
        SpringApplication.run(CartApplication.class, args);
    }

    @Bean
    public RestTemplate restTemplate() {
        return new RestTemplate();
    }
}

②CartServiceImpl

package com.hmall.cart.service.impl;

import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.util.StrUtil;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.hmall.cart.domain.dto.CartFormDTO;
import com.hmall.cart.domain.dto.ItemDTO;
import com.hmall.cart.domain.po.Cart;
import com.hmall.cart.domain.vo.CartVO;
import com.hmall.cart.mapper.CartMapper;
import com.hmall.cart.service.ICartService;
import com.hmall.common.exception.BizIllegalException;
import com.hmall.common.utils.BeanUtils;
import com.hmall.common.utils.CollUtils;
import com.hmall.common.utils.UserContext;
import lombok.RequiredArgsConstructor;
import org.springframework.core.ParameterizedTypeReference;
import org.springframework.http.HttpMethod;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Service;
import org.springframework.web.client.RestTemplate;

import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.function.Function;
import java.util.stream.Collectors;

/**
 * <p>
 * 订单详情表 服务实现类
 * </p>
 *
 * @author 虎哥
 * @since 2023-05-05
 */
@Service
@RequiredArgsConstructor // 必备参数的构造函数
public class CartServiceImpl extends ServiceImpl<CartMapper, Cart> implements ICartService {

    private final RestTemplate restTemplate;
    // private final IItemService itemService;

    // ... ...

    private void handleCartItems(List<CartVO> vos) {
        // TODO 1.获取商品id
        Set<Long> itemIds = vos.stream().map(CartVO::getItemId).collect(Collectors.toSet());
        // 2.查询商品
        // List<ItemDTO> items = itemService.queryItemByIds(itemIds);
        // 2.1 利用RestTemplate发起http请求,得到http响应
        ResponseEntity<List<ItemDTO>> response = restTemplate.exchange(
                "http://localhost:8081/items?ids={ids}",
                HttpMethod.GET,
                null,
                new ParameterizedTypeReference<List<ItemDTO>>() {
                },
                Map.of("ids", CollUtil.join(itemIds, ","))
        );
        // 2.2 解析响应
        if(!response.getStatusCode().is2xxSuccessful()) {
            // 查询失败,直接结束
            return;
        }
        List<ItemDTO> items = response.getBody();
        if (CollUtils.isEmpty(items)) {
            return;
        }
        // 3.转为 id 到 item的map
        Map<Long, ItemDTO> itemMap = items.stream().collect(Collectors.toMap(ItemDTO::getId, Function.identity()));
        // 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());
        }
    }

    // ... ...
}

同时启动ItemApplication和CartApplication

④调试  http://localhost:8082/doc.html#/home

⑤打开console,查询id为100000006163的商品,修改其价格

 

SELECT * FROM item WHERE id = 100000006163;

重新调试

5. 总结

1. 什么时候拆分微服务?

答:初创型公司或项目尽量采用单体项目,快速试错。随着项目发展到达一定规模再做拆分。

2. 如何拆分微服务?

答:目标:高内聚、低耦合。方式:纵向拆分、横向拆分。

3. 拆分后碰到的第一个问题是什么,如何解决?

答:拆分后,某些数据在不同服务,无法直接调用本地方法查寻数据。利用RestTemplate发送Http请求,实现远程调用。

四、服务治理

1. 服务远程调用时存在的问题

2. 注册中心原理

1. 服务治理中的三个角色分别是什么?

  • 服务提供者:暴露服务接口,供其他服务调用
  • 服务消费者:调用其他服务提供的接口
  • 注册中心:记录并监控微服务各实例状态,推送服务变更信息

2. 消费者如何指导提供者的地址?

  • 服务提供者会在启动时注册自己的信息到注册中心,消费者可以从注册中心订阅和拉取服务信息

3. 消费者如何知道服务状态变更?

  • 服务提供者通过心跳机制向注册中心报告自己的健康状态,当心跳异常时注册中心会将异常服务剔除,并通知订阅了该服务的消费者。

4. 当提供者有多个实例时,消费者该选择哪一个?

  • 消费者可以通过负载均衡算法,从多个实例中选择一个

3. Nacos注册中心

Nacos是目前国内企业中占比最多的注册中心组件。它是阿里巴巴的产品,目前已经加入SpringCloudAlibaba中。

官网:Nacos官网| Nacos 配置中心 | Nacos 下载| Nacos 官方社区 | Nacos

课程资料:Docs

步骤:

①执行nacos.sql脚本

②根据自己的情况修改课前资料里的custom.env文件

PREFER_HOST_MODE=hostname
MODE=standalone
SPRING_DATASOURCE_PLATFORM=mysql
MYSQL_SERVICE_HOST=192.168.126.151
MYSQL_SERVICE_DB_NAME=nacos
MYSQL_SERVICE_PORT=3306
MYSQL_SERVICE_USER=root
MYSQL_SERVICE_PASSWORD=123456
MYSQL_SERVICE_DB_PARAM=characterEncoding=utf8&connectTimeout=1000&socketTimeout=3000&autoReconnect=true&useSSL=false&allowPublicKeyRetrieval=true&serverTimezone=Asia/Shanghai

③上传nacos文件夹以及nacos.tar到虚拟机的root目录下

④加载nacos镜像

docker load -i nacos.tar

⑤创建并运行nacos

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

⑥查看nacos的运行日志

docker logs -f nacos

⑦访问测试 http://192.168.126.151:8848/nacos/#/login

用户名和密码都是:nacos

4. 服务注册

服务注册步骤如下:

①引入nacos discovery依赖:

<dependency>
    <groupId>com.alibaba.cloud</groupId>
    <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>

②配置nacos地址

spring:
  application:
    name: item-service # 服务名称
  cloud:
    nacos:
      servcer-addr: 192.168.126.151:8848 # nacos地址

步骤:item-service模块

①引入依赖

pom.xml(item-service)

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

②配置nacos地址

spring:
  application:
    name: item-service  # 微服务名称
  cloud:
    nacos:
      server-addr: 192.168.126.151:8848

③右键点击ItemApplication,点击Copy Configuration

在弹框中点击“Modify options”,选择“Add VM options”

填写如下:

④启动ItemApplication以及ItemApplication2,查看nacos信息

5. 服务发现 

消费者需要连接nacos以拉取和订阅服务,因此服务发现的前两步与服务注册是一样的,后面再加上服务调用即可:

①引入nacos discovery依赖

<dependency>
    <groupId>com.alibaba.cloud</groupId>
    <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>

②配置nacos地址

spring:
  application:
    name: item-service  # 微服务名称
  cloud:
    nacos:
      server-addr: 192.168.126.151:8848

③服务发现

private final DiscoveryClient discoveryClient;

private void handleCartItems(List<CartVO> vos) {
    // 1. 根据服务名称,拉取服务的实例列表
    List<ServiceInstance> instances = discoveryClient.getInstances("item-service");
    // 2. 负载均衡,挑选一个实例
    ServiceInstance instance = instances.get(RandomUtil.randomInt(instances.size()));
    // 3. 获取实例的IP和窗口
    URI uri = instance.getUri();
    // ... ... 略
}

步骤:cart-service模块

①引入nacos discovery依赖 pom.xml(cart-service)

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

②application.yaml

spring:
  application:
    name: cart-service  # 微服务名称
  cloud:
    nacos:
      server-addr: 192.168.126.151:8848

③CartServiceImpl

package com.hmall.cart.service.impl;

import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.util.RandomUtil;
import cn.hutool.core.util.StrUtil;
// ... ...
import com.hmall.common.utils.CollUtils;
import com.hmall.common.utils.UserContext;
import lombok.RequiredArgsConstructor;
import org.springframework.cloud.client.ServiceInstance;
import org.springframework.cloud.client.discovery.DiscoveryClient;
import org.springframework.core.ParameterizedTypeReference;
import org.springframework.http.HttpMethod;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Service;
import org.springframework.web.client.RestTemplate;

/**
 * <p>
 * 订单详情表 服务实现类
 * </p>
 *
 * @author 虎哥
 * @since 2023-05-05
 */
@Service
@RequiredArgsConstructor // 必备参数的构造函数
public class CartServiceImpl extends ServiceImpl<CartMapper, Cart> implements ICartService {

    private final RestTemplate restTemplate;

    private final DiscoveryClient discoveryClient;

    // ... ...

    private void handleCartItems(List<CartVO> vos) {
        // TODO 1.获取商品id
        Set<Long> itemIds = vos.stream().map(CartVO::getItemId).collect(Collectors.toSet());
        // 2.查询商品
        // List<ItemDTO> items = itemService.queryItemByIds(itemIds);
        // 2.1 根据服务名称获取服务的实例列表
        List<ServiceInstance> instances = discoveryClient.getInstances("item-service");
        if(CollUtil.isEmpty(instances)) {
            return;
        }
        // 2.2 手写负载均衡,从实例列表中挑选一个实例
        ServiceInstance instance = instances.get(RandomUtil.randomInt(instances.size()));
        // 2.3 利用RestTemplate发起http请求,得到http响应
        ResponseEntity<List<ItemDTO>> response = restTemplate.exchange(
                instance.getUri() + "/items?ids={ids}",
                HttpMethod.GET,
                null,
                new ParameterizedTypeReference<List<ItemDTO>>() {},
                Map.of("ids", CollUtil.join(itemIds, ","))
        );
        // 2.4 解析响应
        if (!response.getStatusCode().is2xxSuccessful()) {
            // 查询失败,直接结束
            return;
        }
        List<ItemDTO> items = response.getBody();
        if (CollUtils.isEmpty(items)) {
            return;
        }
        // 3.转为 id 到 item的map
        Map<Long, ItemDTO> itemMap = items.stream().collect(Collectors.toMap(ItemDTO::getId, Function.identity()));
        // 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());
        }
    }

    // ... ...
}

④同时启动CartApplication、ItemApplication以及ItemApplication2在接口文档(查询购物车)以及nacos中进行测试

http://localhost:8082/doc.html#/home

五、OpenFeign

1. 快速入门

OpenFeign是一个声明式的http客户端,是SpringCloud在Eureka公司开源的Feign基础上改造而来。

官方地址:GitHub - OpenFeign/feign: Feign makes writing java http clients easier

其作用就是基于SpringMVC的常见注解,帮我们优雅的实现http请求的发送。

复杂:

OpenFeign已经被SpringCloud自动装配,实现起来非常简单

①引入依赖,包括OpenFeign和负载均衡组件SpringCloudLoadBalancer

<!--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>

②通过@EnableFeignClients注解,开启OpenFeign功能

@EnableFeignClients
@SpringBootApplication
public class CartApplication { // ... ... }

③编写FeignClient

@FeignClient(value="item-service")
public interface ItemClient {
    @GetMapping("/items")
    List<ItemDTO> queryItemByIds(@RequestParam("ids") Collection<Long> ids);
}

④使用FeignClient,实现远程调用

List<ItemDTO> items = itemClient.queryItemByIds(List.of(1,2,3));

步骤:cart-service模块

①pom.xml(cart-service)

  <!--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>

②CartApplication

package com.hmall.cart;

import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.openfeign.EnableFeignClients;
import org.springframework.context.annotation.Bean;
import org.springframework.web.client.RestTemplate;

@EnableFeignClients
@MapperScan("com.hmall.cart.mapper")
@SpringBootApplication
public class CartApplication {
    public static void main(String[] args) {
        SpringApplication.run(CartApplication.class, args);
    }

    @Bean
    public RestTemplate restTemplate() {
        return new RestTemplate();
    }
}

③ItemClient

package com.hmall.cart.client;

import com.hmall.cart.domain.dto.ItemDTO;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;

import java.util.Collection;
import java.util.List;

@FeignClient("item-service")
public interface ItemClient {
    @GetMapping("/items")
    List<ItemDTO> queryItemByIds(@RequestParam("ids") Collection<Long> ids);
}

④CartServiceImpl

package com.hmall.cart.service.impl;

import cn.hutool.core.util.StrUtil;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.hmall.cart.client.ItemClient;
import com.hmall.cart.domain.dto.CartFormDTO;
import com.hmall.cart.domain.dto.ItemDTO;
import com.hmall.cart.domain.po.Cart;
import com.hmall.cart.domain.vo.CartVO;
import com.hmall.cart.mapper.CartMapper;
import com.hmall.cart.service.ICartService;
import com.hmall.common.exception.BizIllegalException;
import com.hmall.common.utils.BeanUtils;
import com.hmall.common.utils.CollUtils;
import com.hmall.common.utils.UserContext;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;


import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.function.Function;
import java.util.stream.Collectors;

/**
 * <p>
 * 订单详情表 服务实现类
 * </p>
 *
 * @author 虎哥
 * @since 2023-05-05
 */
@Service
@RequiredArgsConstructor // 必备参数的构造函数
public class CartServiceImpl extends ServiceImpl<CartMapper, Cart> implements ICartService {
    private final ItemClient itemClient;

    // ... ...
    private void handleCartItems(List<CartVO> vos) {
        // TODO 1.获取商品id
        Set<Long> itemIds = vos.stream().map(CartVO::getItemId).collect(Collectors.toSet());
        // 2.查询商品
        List<ItemDTO> items = itemClient.queryItemByIds(itemIds);
        if (CollUtils.isEmpty(items)) {
            return;
        }
        // 3.转为 id 到 item的map
        Map<Long, ItemDTO> itemMap = items.stream().collect(Collectors.toMap(ItemDTO::getId, Function.identity()));
        // 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());
        }
    }

   // ... ...
}

2. 连接池

OpenFeign对Http请求做了优雅的伪装,不过其底层发起http请求,依赖于其他的框架。这些框架可以自己选择,包括以下三种:

  • HttpURLConnection:默认实现,不支持连接池
  • Apache HttpClient:支持连接池
  • OKHttp:支持连接池

具体源码可以参考FeighBlockingLoadBalancerClient类中的delegate成员变量。

OpenFeign整合OKHttp的步骤如下:cart-service模块

①引入依赖pom.xml

<!--OK http 的依赖 -->
<dependency>
  <groupId>io.github.openfeign</groupId>
  <artifactId>feign-okhttp</artifactId>
</dependency>

②开启连接池功能application.yaml

feign:
  okhttp:
    enabled: true # 开启OKHttp功能

3. 最佳实践

  • 方案1抽取更加简单,工程结构也比较清晰,但缺点是整个项目耦合度偏高。
  • 方案2抽取相对麻烦,工程结构相对更复杂,但服务之间耦合度降低。

由于item-service已经创建好,无法继续拆分,因此这里我们采用方案1.

步骤:hm-api模块

①新建hm-api模块

②把pom.xml(cart-service)里有关OpenFeign的依赖剪切到pom.xml(hm-api)

<!--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>

引入swagger的依赖

<dependency>
  <groupId>io.swagger</groupId>
  <artifactId>swagger-annotations</artifactId>
  <version>1.5.22</version>
  <scope>compile</scope>
</dependency>

③拷贝cart-service下 domain里的ItemDTO 以及 client下的ItemClient 到hm-api模块下

④修改pom.xml(cart-service),导入hm-api依赖

<!--hm-api-->
<dependency>
  <groupId>com.heima</groupId>
  <artifactId>hm-api</artifactId>
  <version>1.0.0</version>
</dependency>

⑤修改CartServiceImpl,注意别导错包(可以把cart-service下的ItemDTO和ItemClient删除)

import com.hmall.api.client.ItemClient;
import com.hmall.api.dto.ItemDTO;

⑥在cart-service的启动类上添加声明即可(两种方式),否则会报错。

方式一:声明扫描包

package com.hmall.cart;

import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.openfeign.EnableFeignClients;
import org.springframework.context.annotation.Bean;
import org.springframework.web.client.RestTemplate;

@EnableFeignClients(basePackages = "com.hmall.api.client")
@MapperScan("com.hmall.cart.mapper")
@SpringBootApplication
public class CartApplication {
    public static void main(String[] args) {
        SpringApplication.run(CartApplication.class, args);
    }

    @Bean
    public RestTemplate restTemplate() {
        return new RestTemplate();
    }
}

方式二:声明要用的FeignClient

package com.hmall.cart;

import com.hmall.api.client.ItemClient;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.openfeign.EnableFeignClients;
import org.springframework.context.annotation.Bean;
import org.springframework.web.client.RestTemplate;

//@EnableFeignClients(basePackages = "com.hmall.api.client")
@EnableFeignClients(clients = {ItemClient.class})
@MapperScan("com.hmall.cart.mapper")
@SpringBootApplication
public class CartApplication {
    public static void main(String[] args) {
        SpringApplication.run(CartApplication.class, args);
    }

    @Bean
    public RestTemplate restTemplate() {
        return new RestTemplate();
    }
}

4. 日志

OpenFeign只会在FeignClient所在包的日志级别为DEBUG时,才会输出日志。而且其日志级别有4级:

  • NONE:不记录任何日志信息,这是默认值。
  • BASIC:仅记录请求的方法,URL以及响应状态码和执行时间
  • HEADERS:在BASIC的基础上,额外记录了请求和响应的头信息
  • FULL:记录所有请求和响应的明细,包括头信息、请求体、元数据。

由于Feign默认的日志级别就是NONE,所以,我们默认看不到请求日志。

步骤:

①要自定义日志级别需要声明一个类型为Logger.Level的Bean,在其中定义日志级别:

public class DefaultFeignConfig {
    @Bean
    public Logger.level feignLogLevel() {
        return Logger.Level.FULL;
    }
}

②此时这个Bean并未生效,要想配置某个FeignClient的日志,可以在@FeignClient注解中声明

@FeignClient(value = "item-service", configuration = DefaultFeignConfig.class)

如果想要全局配置,让所有FeignClient都按照这个日志配置,则需要在@EnableFeignClients注解中声明

@EnableFeignClients(defaultConfiguration = DefaultFeignConfig.class)

步骤:

①DefaultFeignConfig

package com.hmall.api.config;

import feign.Logger;
import org.springframework.context.annotation.Bean;

public class DefaultFeignConfig {
    @Bean
    public Logger.Level feignLoggerLevel() {
        return Logger.Level.FULL;
    }
}

②CartApplication

package com.hmall.cart;

import com.hmall.api.client.ItemClient;
import com.hmall.api.config.DefaultFeignConfig;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.openfeign.EnableFeignClients;
import org.springframework.context.annotation.Bean;
import org.springframework.web.client.RestTemplate;

//@EnableFeignClients(basePackages = "com.hmall.api.client")
@EnableFeignClients(clients = {ItemClient.class}, defaultConfiguration = DefaultFeignConfig.class)
@MapperScan("com.hmall.cart.mapper")
@SpringBootApplication
public class CartApplication {
    public static void main(String[] args) {
        SpringApplication.run(CartApplication.class, args);
    }

    @Bean
    public RestTemplate restTemplate() {
        return new RestTemplate();
    }
}

③重启CartApplication进行测试

5. 总结

1. 如何利用OpenFeign实现远程调用?

  • 引入OpenFeign和SpringCloudLoadBalancer依赖
  • 利用@EnableFeignClients注解开启OpenFeign功能
  • 编写FeignClient

2. 如何配置OpenFeign的连接池?

  • 引入http客户端依赖,例如OKHttp、HttpClient
  • 配置yaml文件,打开OpenFeign连接池开关

3. OpenFeign使用的最佳实践方式是什么?

  • 由服务提供者编写独立module,将FeignClient及DTO抽取

4. 如何配置OpenFeign输出日志的级别?

  • 声明类型为Logger.Level的Bean
  • 在@FeignClient或@EnableFeignClients注解上使用

六、微服务拆分

1. 用户服务

①执行SQL脚本

②新建模块user-service

③pom.xml(user-service)

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <parent>
        <artifactId>hmall</artifactId>
        <groupId>com.heima</groupId>
        <version>1.0.0</version>
    </parent>
    <modelVersion>4.0.0</modelVersion>

    <artifactId>user-service</artifactId>

    <properties>
        <maven.compiler.source>11</maven.compiler.source>
        <maven.compiler.target>11</maven.compiler.target>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    </properties>

    <dependencies>
        <!--common-->
        <dependency>
            <groupId>com.heima</groupId>
            <artifactId>hm-common</artifactId>
            <version>1.0.0</version>
        </dependency>
        <!--hm-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>

</project>

④从cart-service模块中拷贝三个配置文件到user-service模块

修改application.yaml

server:
  port: 8084
spring:
  application:
    name: user-service  # 微服务名称
  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}
  cloud:
    nacos:
      server-addr: 192.168.126.151:8848
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

⑤添加启动类UserApplication

package com.hmall.user;

import com.hmall.api.client.ItemClient;
import com.hmall.api.config.DefaultFeignConfig;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.openfeign.EnableFeignClients;

@EnableFeignClients(clients = {ItemClient.class}, defaultConfiguration = DefaultFeignConfig.class)
@MapperScan("com.hmall.user.mapper")
@SpringBootApplication
public class UserApplication {
    public static void main(String[] args) {
        SpringApplication.run(UserApplication.class, args);
    }
}

⑥conhm-service中拷贝以下文件(导包错误的重新导一下)

⑦配置启动项

⑧启动UserApplication进行测试

http://localhost:8084/doc.html#/home

2. 交易服务

①创建一个新的模块trade-service

②pom.xml引入依赖

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <parent>
        <artifactId>hmall</artifactId>
        <groupId>com.heima</groupId>
        <version>1.0.0</version>
    </parent>
    <modelVersion>4.0.0</modelVersion>

    <artifactId>trade-service</artifactId>

    <properties>
        <maven.compiler.source>11</maven.compiler.source>
        <maven.compiler.target>11</maven.compiler.target>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    </properties>

    <dependencies>
        <!--common-->
        <dependency>
            <groupId>com.heima</groupId>
            <artifactId>hm-common</artifactId>
            <version>1.0.0</version>
        </dependency>

        <!--hm-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>
</project>

③从user-service拷贝三个配置文件,并修改application.yaml

server:
  port: 8085
spring:
  application:
    name: trade-service  # 微服务名称
  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}
  cloud:
    nacos:
      server-addr: 192.168.126.151:8848
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

④添加启动类TradeApplication

package com.hmall.trade;

import com.hmall.api.client.ItemClient;
import com.hmall.api.config.DefaultFeignConfig;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.openfeign.EnableFeignClients;

@EnableFeignClients(basePackages = "com.hmall.api.client", defaultConfiguration = DefaultFeignConfig.class)
@MapperScan("com.hmall.trade.mapper")
@SpringBootApplication
public class TradeApplication {
    public static void main(String[] args) {
        SpringApplication.run(TradeApplication.class, args);
    }
}

⑤从hm-service拷贝相关文件到trader-service

⑥从hm-service模块拷贝OrderDetailDTO到hm-api模块

修改ItemClient

package com.hmall.api.client;

import com.hmall.api.dto.ItemDTO;
import com.hmall.api.dto.OrderDetailDTO;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestParam;

import java.util.Collection;
import java.util.List;

@FeignClient("item-service")
public interface ItemClient {
    @GetMapping("/items")
    List<ItemDTO> queryItemByIds(@RequestParam("ids") Collection<Long> ids);

    @PutMapping("/items/stock/deduct")
    public void deductStock(@RequestBody List<OrderDetailDTO> items);
}

添加一个CartClient

package com.hmall.api.client;

import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.RequestParam;

import java.util.Collection;

@FeignClient("cart-service")
public interface CartClient {
    @DeleteMapping("/carts")
    void deleteCartItemByIds(@RequestParam("ids") Collection<Long> ids);
}

⑦修改OrderServiceImpl

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 ItemClient itemClient;
    private final IOrderDetailService detailService;
    private final CartClient cartClient;

    @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;
    }
}

⑧OrderFormDTO重新导包

package com.hmall.trade.domain.dto;

import com.hmall.api.dto.OrderDetailDTO;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;

import java.util.List;

@Data
@ApiModel(description = "交易下单表单实体")
public class OrderFormDTO {
    @ApiModelProperty("收货地址id")
    private Long addressId;
    @ApiModelProperty("支付类型")
    private Integer paymentType;
    @ApiModelProperty("下单商品列表")
    private List<OrderDetailDTO> details;
}

⑨删除item-service中的两个dto

在pom.xml(item-service)中引入hm-api依赖

<!--hm-api-->
<dependency>
  <groupId>com.heima</groupId>
  <artifactId>hm-api</artifactId>
  <version>1.0.0</version>
</dependency>

ItemController、ItemService、ItemServiceImpl、ItemMapper重新导包

⑩修改运行配置,启动trade-service服务进行测试

http://localhost:8085/doc.html#/home

3. 支付服务

步骤:pay-service

①创建模块pay-service

②pom.xml(pay-service)

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <parent>
        <artifactId>hmall</artifactId>
        <groupId>com.heima</groupId>
        <version>1.0.0</version>
    </parent>
    <modelVersion>4.0.0</modelVersion>

    <artifactId>pay-service</artifactId>

    <properties>
        <maven.compiler.source>11</maven.compiler.source>
        <maven.compiler.target>11</maven.compiler.target>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    </properties>

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

</project>

③添加启动类PayApplication

package com.hmall.pay;

import com.hmall.api.config.DefaultFeignConfig;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.openfeign.EnableFeignClients;

@EnableFeignClients(basePackages = "com.hmall.api.client", defaultConfiguration = DefaultFeignConfig.class)
@MapperScan("com.hmall.pay.mapper")
@SpringBootApplication
public class PayApplication {
    public static void main(String[] args) {
        SpringApplication.run(PayApplication.class, args);
    }
}

④从hm-service模块拷贝三个配置文件到pay-service,修改application.yaml

server:
  port: 8086
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}
  cloud:
    nacos:
      server-addr: 192.168.126.151:8848
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

⑤在hm-api模块中新增一个UserClient

package com.hmall.api.client;

import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestParam;

@FeignClient("user-service")
public interface UserClient {
    @PutMapping("/users/money/deduct")
    void deductMoney(@RequestParam("pw") String pw, @RequestParam("amount") Integer amount);
}

⑥在hm-api模块中新增一个TradeClient

package com.hmall.api.client;

import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PutMapping;

@FeignClient("trade-service")
public interface TradeClient {
    @PutMapping("/orders/{orderId}")
    public void markOrderPaySuccess(@PathVariable("orderId") Long orderId);
}

⑦从hm-service拷贝以下文件到pay-service

PayOrderServiceImpl

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());
    }

    // ... ... 省略
}

⑧在PayController中添加一个接口方便测试

@ApiOperation("查询支付单")
@GetMapping
public List<PayOrderVO> queryPayOrders(){
    return BeanUtils.copyList(payOrderService.list(), PayOrderVO.class);
}

⑨启动项配置

⑩启动测试

http://localhost:8086/doc.html#/home

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1802969.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

CMakeLists如何多行注释

在使用Visual Studio编写CMakeLists的时候你可能会遇到需要多行注释的情况&#xff0c;可又不知道快捷键是什么。。。 其实你只需要敲个 #[[ 就行了&#xff0c;另外一般方括号VS会自动帮你补全&#xff0c;之后将需要注释的内容放在第二个方括号与第三个方括号之间就完成注释…

今天是放假带娃的一天

端午节放假第一天 早上5点半宝宝就咔咔乱叫了&#xff0c;几乎每天都这个点醒&#xff0c;准时的很&#xff0c;估计他是个勤奋的娃吧&#xff0c;要早起锻炼婴语&#xff0c;哈哈 醒来后做饭、洗锅、洗宝宝的衣服、给他吃D3&#xff0c;喂200ml奶粉、给他洗澡、哄睡&#xff0…

【上篇】从 YOLOv1 到 YOLOv8 的 YOLO 物体检测模型历史

YOLO 型号之所以闻名遐迩,主要有两个原因:其速度和准确性令人印象深刻,而且能够快速、可靠地检测图像中的物体。 在本文中,我将与大家分享我在阅读一篇长达 30 页的综合性论文时获得的见解,该论文深入探讨了 YOLO 模型的进步。 这篇评论全面概述了 YOLO 框架的演变过程,…

Python教程:Python操作MySQL基础使用

8、Python操作MySQL基础使用 8.1 安装pymysql pip install pymysql8.2 测试连接 测试代码 from pymysql import Connection# 获取到MySQL数据库的链接对象 conn Connection(# 主机名hostlocalhost,# 端口号,默认3306port3306,# 账户名userroot,# 密码password3535 )# 打印…

实现Ingress-Nginx Controller高可用方案

文章目录 前提准备1.修改Ingress-Controller 运行模式为hostNetwork并生效2.给部署ingress-controller的节点打标签3.查看ingress-controller的部署情况 方式一&#xff1a;LVSKeepalivedNginxIngress一、部署ipvsadm和keepalived二、配置keepalived1.配置lvs01(keepalived mas…

3.haproxy负载均衡

haproxy负载均衡 一、haproxy介绍1、负载均衡类型 二、haproxy配置文件1、backend2、frontend 三、haproxy实现MySQL负载均衡 —— 4层1、后端两台MySQL配置双主复制2、安装配置haproxy2.1 安装软件2.2 编辑配置2.3 启动haproxy2.4 测试通过haproxy正常连接MySQL 四、haproxy实…

机器学习----奥卡姆剃刀定律

奥卡姆剃刀定律&#xff08;Occam’s Razor&#xff09;是一条哲学原则&#xff0c;通常表述为“如无必要&#xff0c;勿增实体”&#xff08;Entities should not be multiplied beyond necessity&#xff09;或“在其他条件相同的情况下&#xff0c;最简单的解释往往是最好的…

容器化部署fastdfs文件存储

目录 一、软件信息 二、构建fastdfs镜像 三、docker 启动fdfs服务 四、k8s部署fdfs服务 1、fdfs部署文件 五、外部服务访问 一、软件信息 fastdfs版本&#xff1a;fastdfs:V5.11 libfastcommon版本: V1.0.36 fastdfs-nginx-module版本&#xff1a;V1.20 nginx版本&am…

【python】OpenCV—Blob Detection(11)

学习来自OpenCV基础&#xff08;10&#xff09;使用OpenCV进行Blob检测 文章目录 1、cv2.SimpleBlobDetector_create 中文文档2、默认 parameters3、配置 parameters附录——cv2.drawKeypoints 1、cv2.SimpleBlobDetector_create 中文文档 cv2.SimpleBlobDetector_create 是 O…

LeetCode72编辑距离

题目描述 解析 一般这种给出两个字符串的动态规划问题都是维护一个二维数组&#xff0c;尺寸和这两个字符串的长度相等&#xff0c;用二维做完了后可以尝试优化空间。这一题其实挺类似1143这题的&#xff0c;只不过相比1143的一种方式&#xff0c;变成了三种方式&#xff0c;就…

Linux基础2-基本指令4(cp,mv,cat,tac)

上篇文章我们说到了rmdir,rm,man,echo.重定向等知识。 Linux基础1-基本指令3-CSDN博客 本文继续梳理其他基础指令 1.本章重点 1.使用cp命令拷贝文件 2.使用mv命令移动文件 3.使用cat&#xff0c;tac查看小文本文件 2.cp命令 在linux中使用cp命令来拷贝粘贴文件 cp src(原文…

信息学奥赛初赛天天练-23-CSP-J2023基础题-指针、链表、哈夫曼树与哈夫曼编码的实战应用与技巧大揭秘

PDF文档公众号回复关键字:20240608 单项选择题&#xff08;共15题&#xff0c;每题2分&#xff0c;共计30分&#xff1a;每题有且仅有一个正确选项&#xff09; 4 假设有一个链表的节点定义如下&#xff1a; struct Node {int data; Node* next; };现在有一个指向链表头部…

【深度学习】CICIDS 2019,入侵检测,SVM支持向量机,随机森林,DNN训练,混淆矩阵

文章目录 数据集介绍Python环境随机森林训练结果SVM支持向量机训练结果DNN训练结果所有代码下载 数据集介绍 下载&#xff1a;https://www.kaggle.com/datasets/tarundhamor/cicids-2019-dataset 数据个数&#xff1a; # 删除label中是WebDDoS的数据df df[df[Label] ! WebDD…

Java Web学习笔记26——Element常用组件

常见组件&#xff1a; 就是一个复制和粘贴的过程。 Table表格&#xff1a;用于展示多条结构类的数据&#xff0c;可对数据进行排序、筛选、对比或其他自定义操作。 常见组件-分页主键&#xff1a; Pagination&#xff1a;分页&#xff1a;当数据量比较多时&#xff0c;使用分…

第七届全国颗粒材料计算力学会议召开,DEMms多尺度离散模拟软件受关注

近日&#xff0c;第七届全国颗粒材料计算力学会议暨第四届计算颗粒技术国际研讨会在南京召开。会议聚焦颗粒材料的力学理论及模型、计算分析与软件开发、工程应用和相关前沿方向中的关键科学问题和难点技术问题&#xff0c;开展广泛的学术交流和讨论。 会议期间&#xff0c;积鼎…

【Python列表解锁】:掌握序列精髓,驾驭动态数据集合

文章目录 &#x1f680;一、列表&#x1f308;二、常规操作&#x1f4a5;增&#x1f4a5;删&#x1f4a5;改&#x1f4a5;查 ⭐三、补充操作 &#x1f680;一、列表 列表是一个能够存储多个同一或不同元素的序列 列表&#xff1a;list ---- [] 列表属于序列类型&#xff08;容器…

WWDC 2024前瞻:苹果如何用AI技术重塑iOS 18和Siri

苹果下周的全球开发者大会有望成为这家 iPhone 制造商历史上的关键时刻。在 WWDC 上&#xff0c;这家库比蒂诺科技巨头将展示如何选择将人工智能技术集成到其设备和软件中&#xff0c;包括通过与 OpenAI 的历史性合作伙伴关系。随着重大事件的临近&#xff0c;有关 iOS 18 及其…

高能来袭|联想拯救者携手《黑神话:悟空》玩转东方神话世界

从2020年首次发布实机演示视频以来&#xff0c;《黑神话&#xff1a;悟空》便在全球范围内获得了广泛关注&#xff0c;成为国产3A游戏的现象级爆款。6月&#xff0c;联想拯救者正式宣布成为《黑神话&#xff1a;悟空》全球官方合作伙伴&#xff0c;致力于共同革新国产游戏体验&…

老师必备!一文教你如何高效收集志愿填报信息

高考志愿填报季&#xff0c;对于每一位老师来说&#xff0c;无疑是一场信息收集与管理的硬仗。如何在众多的志愿信息中&#xff0c;高效、准确地掌握每位学生的志愿意向&#xff1f; 高考志愿填报的重要性。不仅是学生人生的一个重要转折点&#xff0c;也是老师教育生涯中的一次…

Elasticsearch 认证模拟题 - 15

一、题目 原索引 task1 的字段 title 字段包含单词 The&#xff0c;查询 the 可以查出 1200 篇文档。重建 task1 索引为 task1_new&#xff0c;重建后的索引&#xff0c; title 字段查询 the 单词&#xff0c;不能匹配到任何文档。 PUT task1 {"mappings": {"…