练习

简介: 本课程围绕微服务核心组件展开,涵盖Nacos配置中心、Feign远程调用及Gateway网关搭建。通过实践掌握配置管理、服务通信优化与统一网关鉴权,提升系统可维护性与架构设计能力,强化代码整合与主动优化经验。

作业目标

  • 作业一:掌握Nacos配置中心的使用,巩固强化代码编写能力(整合配置中心2、需求2)
  • 作业二:掌握Feign实现远程服务调用,针对自己负责的模块能说出主动优化经验(便于后续面试)
  • 作业三:掌握微服务网关的搭建、使用,巩固强化对于后端接口自测的能力(postman测试6个接口,次数预计20次左右)

工程介绍

工程技术架构图

工程目录结构图


环境准备

  1. 导入SpringCloud-Day2原始作业的工程:doctor-station(📎doctor-station.zip
  2. 导入数据库脚本:创建数据库:doctor_station,导入脚本: 📎doctor_station.sql
  3. 启动nacos、doctor-station工程:
  4. 导入postman文件: 📎SpringCloud-Day1.json,使用postman完成下述请求

应用名称

访问地址

预期结果

inventory-service

更新库存

{ "updateResult": true,"updateMsg": "更新成功"}

doctor-service

创建医嘱

{"createResult": true,"createMsg": "创建成功"}


题目一

目标

能够完成微服务Nacos配置中心的使用

需求

随着需求的不断迭代更新,应用开始有越来越多的配置项,尤其部分配置项可能频繁修改。此时考虑引入一个配置中心来解决当前业务与配置耦合的问题,请结合你当前掌握的知识点,思考如下场景问题应该如何解决?

需求项

业务描述

调用库存中心服务地址优化

接口地址随着需求的迭代,可能会不定期产生变化,因此考虑放在配置中心
预期:test环境从配置中心获取请求地址(需热更新),并优化原有代码(请考虑这种配置是每个环境都存一下,还是放在共享配置中)

[耗材中心]维护时间场景优化

耗材中心定期做统一数据迁移,此时应用将停机维护,本次dev环境将于:2023年10月1日23:59 至 10月2日02:00进行升级维护,请完成相关配置项,并在代码中做配置项的校验:更新时间之内返回异常信息(系统维护中,请稍后重试)

[医生站]开单次数场景优化

医生站作为端侧应用,为避免部分医生刷单给自己刷绩效,针对当前test环境增加一个:当天最多允许创建5单医嘱数据的配置项,请完成相关配置项,并思考:如何做到当天最多5单的逻辑,并在超过n但之后给出异常信息(今日开单次数已达上限)

提示

  • 结合今天学习内容,完成两个module的nacos配置中心依赖导入、整合
  • 日志:需创建各自应用的配置文件,同时test环境的配置覆盖共享文件配置
  • 维护时间:与今天课程内容基本吻合,结合今天学习内容整合即可
  • 创建权限:是否考虑到了新增一个查询当天单数的SQL呢?

参考答案

1.整合nacos-config

  1. 依赖引入(每个微服务)
<!--nacos配置管理依赖-->
<dependency>
  <groupId>com.alibaba.cloud</groupId>
  <artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
</dependency>

2.接口请求地址需求

  1. nacos创建配置文件:doctorservice-test.yaml、doctorservice.yaml,避免为空报错可以先写个1
因为接口地址的变更一定是从dev->test->pre->prod,因此此接口地址配置项在此场景下建议
  放在每个环境配置文件,或放在共享文件且当产生变更时当前环境追加配置覆盖


  1. doctor-service创建配置文件:bootstrap.yml文件,并整合配置文件
spring:
  application:
    name: doctorservice # 服务名称
  profiles:
    active: test # 当前使用test环境
  cloud:
    nacos:
      server-addr: localhost:8848
      config:
        file-extension: yaml # 文件后缀名
  1. 配置文件新增接口地址配置信息
  • doctorservice-test.yaml,这里配置项的K-V可以自定义
inventory:
 updateUri: http://inventoryservice/inventory/update/

注意:配置项的换行后缩进,不要使用TAB,否则会有下述错误

  1. 属性读取
  • 新建配置读取类:InventoryServerConfig
package cn.itcast.config;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;
/**
 * @Description: 医生站读取耗材中心配置
 * @Date: 2023/2/26 16:42
 */
@Data
@Component
@ConfigurationProperties(prefix = "inventory")
public class InventoryServerConfig {
    private String updateUri;
}
  • DoctorService中读取配置并替换代码

  • 重启服务验证(postman调用创建医嘱,成功即可)

3.耗材中心维护时间

  1. nacos创建配置文件:inventoryservice-dev.yaml,避免为空报错可以先写个1
  2. inventory-service创建配置文件:bootstrap.yml文件,并整合配置文件
spring:
  application:
    name: inventoryservice # 服务名称
  profiles:
    active: dev # 当前使用dev环境
  cloud:
    nacos:
      server-addr: localhost:8848
      config:
        file-extension: yaml # 文件后缀名
  1. 配置文件新增接口地址配置信息
  • inventoryservice-dev.yaml,这里配置项的K-V可以自定义
inventory:
 stopStartTime: 2023-10-01 23:59:59
 stopEndTime: 2023-10-02 02:00:00
  1. 属性读取,代码优化
  • 新建配置读取类:InventoryServerConfig
package cn.itcast.config;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;
/**
 * @Description: 耗材中心配置读取
 * @Date: 2023/2/26 17:11
 */
@Data
@Component
@ConfigurationProperties(prefix = "inventory")
public class InventoryServerConfig {
    /**
     * 维护开始时间
     */
    private String stopStartTime;
    
    /**
     * 维护结束时间
     */
    private String stopEndTime;
    
}
  • InventoryService中读取配置并优化代码
package cn.itcast.service;
import cn.itcast.config.InventoryServerConfig;
import cn.itcast.entity.Inventory;
import cn.itcast.mapper.InventoryMapper;
import cn.itcast.web.response.InventoryUpdateVO;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.Objects;
/**
 * @Description: 库存核心逻辑层
 * @Date: 2023/1/30 14:57
 */
@Service
public class InventoryService {
    @Autowired
    private InventoryMapper inventoryMapper;
    @Autowired
    private InventoryServerConfig inventoryServerConfig;
    public static final String FORMAT = "yyyy-MM-dd HH:mm:ss";
    /**
     * 查询库存信息
     * @param id    库存ID
     * @return      库存信息
     */
    public Inventory queryInventory(Long id) {
        return inventoryMapper.selectById(id);
    }
    /**
     * 更新库存信息
     * @param id     更新参数:库存ID/库存数量
     * @param num    更新参数:库存ID/库存数量
     * @return          更新结果
     */
    public InventoryUpdateVO updateInventory( Long id, Long num) {
        InventoryUpdateVO result = new InventoryUpdateVO();
        // 0-合法性校验
        validUpdateTime(result);
        if (Boolean.FALSE.equals(result.getUpdateResult())) {
            return result;
        }
        // 1-查询现有库存
        Inventory inventory = queryInventory(id);
        if (Objects.isNull(inventory)) {
            result.setUpdateResult(Boolean.FALSE);
            result.setUpdateMsg("库存数据不存在,请稍后重试");
            return result;
        }
        // 2-数据合法性校验
        if (inventory.getNum() < num) {
            result.setUpdateResult(Boolean.FALSE);
            result.setUpdateMsg("库存不足,请稍后重试");
            return result;
        }
        // 3-更新
        Inventory updateEntity = new Inventory();
        updateEntity.setId(id);
        updateEntity.setNum(inventory.getNum() - num);
        int count = inventoryMapper.updateById(updateEntity);
        // 4-更新结果判断
        if (count == 0) {
            result.setUpdateResult(Boolean.FALSE);
            result.setUpdateMsg("更新失败,请稍后重试");
            return result;
        }
        result.setUpdateResult(Boolean.TRUE);
        result.setUpdateMsg("更新成功");
        return result;
    }
    /**
     * 当前时间合法性校验
     * @param result
     */
    private InventoryUpdateVO validUpdateTime(InventoryUpdateVO result) {
        // 获取当前时间
        LocalDateTime now = LocalDateTime.now();
        // 读取配置中的开始-结束时间
        LocalDateTime stopStartTime = LocalDateTime.parse(inventoryServerConfig.getStopStartTime(), DateTimeFormatter.ofPattern(FORMAT));
        LocalDateTime stopEndTime = LocalDateTime.parse(inventoryServerConfig.getStopEndTime(), DateTimeFormatter.ofPattern(FORMAT));
        // 时间窗口内,校验不通过并抛出异常
        if (now.isAfter(stopStartTime) && now.isBefore(stopEndTime)) {
            result.setUpdateResult(Boolean.FALSE);
            result.setUpdateMsg("系统维护中,请稍后重试");
            return result;
        }
        result.setUpdateResult(Boolean.TRUE);
        result.setUpdateMsg("校验通过");
        return result;
    }
}
  • 调整nacos配置,以保证规则分别命中以下两种场景
  • 成功
  • 失败

4.医生开单次数

  • 配置项增加,K-V可以自定义
doctor:
 createCount: 5
  • 增加配置类与属性
package cn.itcast.config;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;
/**
 * @Description: 医生站读取配置
 * @Date: 2023/2/26 16:42
 */
@Data
@Component
@ConfigurationProperties(prefix = "doctor")
public class DoctorServerConfig {
    private int createCount;
}
  • DoctorService 整体逻辑优化,增加查询当天创建医嘱的数量
package cn.itcast.service;
import cn.itcast.config.DoctorServerConfig;
import cn.itcast.config.InventoryServerConfig;
import cn.itcast.entity.Doctor;
import cn.itcast.mapper.DoctorMapper;
import cn.itcast.web.request.DoctorCreateParam;
import cn.itcast.web.response.DoctorCreateVO;
import cn.itcast.web.response.InventoryUpdateVO;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.web.client.RestTemplate;
import java.time.LocalDateTime;
import java.time.LocalTime;
import java.time.ZoneId;
import java.util.Date;
import java.util.Objects;
/**
 * @Description: 医生核心逻辑处理层
 * @Date: 2023/1/30 14:41
 */
@Service
public class DoctorService {
    @Autowired
    private DoctorMapper doctorMapper;
    @Autowired
    private RestTemplate restTemplate;
    @Autowired
    private InventoryServerConfig inventoryServerConfig;
    @Autowired
    private DoctorServerConfig doctorServerConfig;
    /**
     * 创建医嘱信息
     */
    public DoctorCreateVO createDoctorOrder(DoctorCreateParam param) {
        DoctorCreateVO result = new DoctorCreateVO();
        // 0-合法性校验
        validCreateCount(result);
        if (Boolean.FALSE.equals(result.getCreateResult())) {
            return result;
        }
        // 1-扣减库存(调用库存中心)
        String url = inventoryServerConfig.getUpdateUri() + param.getInventoryId() + "/" + param.getDoctorInventoryNum();
        InventoryUpdateVO inventory = restTemplate.getForObject(url, InventoryUpdateVO.class);
        // 1.1-库存存在性校验
        if (Objects.isNull(inventory)) {
            result.setCreateResult(Boolean.FALSE);
            result.setCreateMsg("创建失败,库存不存在");
            return result;
        }
        // 1.2-库存更新成功校验
        if (Boolean.FALSE.equals(inventory.getUpdateResult())) {
            result.setCreateResult(Boolean.FALSE);
            result.setCreateMsg(inventory.getUpdateMsg());
            return result;
        }
        // 2-插入医嘱信息
        Doctor doctor = new Doctor();
        doctor.setDoctorName(param.doctorName);
        doctor.setInventoryId(param.getInventoryId());
        doctor.setDoctorInventoryNum(param.getDoctorInventoryNum());
        int count = doctorMapper.insert(doctor);
        // 3-返回
        if (count == 0) {
            result.setCreateResult(Boolean.FALSE);
            result.setCreateMsg("创建失败,请稍后重试");
            return result;
        }
        result.setCreateResult(Boolean.TRUE);
        result.setCreateMsg("创建成功");
        return result;
    }
    /**
     * 校验创建次数
     * @param result
     */
    private DoctorCreateVO validCreateCount(DoctorCreateVO result) {
        QueryWrapper query = new QueryWrapper<>();
        query.ge("create_time", getStartOfDay());
        query.le("create_time", getEndOfDay());
        Integer todayCount = doctorMapper.selectCount(query);
        int createCount = doctorServerConfig.getCreateCount();
        if (todayCount >= createCount) {
            result.setCreateResult(Boolean.FALSE);
            result.setCreateMsg("今日开单次数已达上限");
            return result;
        }
        result.setCreateResult(Boolean.TRUE);
        result.setCreateMsg("校验通过");
        return result;
    }
    // 获得某天最大时间 2017-10-15 23:59:59
    public static Date getEndOfDay() {
        LocalDateTime localDateTime = LocalDateTime.now();
        LocalDateTime endOfDay = localDateTime.with(LocalTime.MAX);
        return Date.from(endOfDay.atZone(ZoneId.systemDefault()).toInstant());
    }
    // 获得某天最小时间 2017-10-15 00:00:00
    public static Date getStartOfDay() {
        LocalDateTime localDateTime = LocalDateTime.now();
        LocalDateTime startOfDay = localDateTime.with(LocalTime.MIN);
        return Date.from(startOfDay.atZone(ZoneId.systemDefault()).toInstant());
    }
}
  • 注意:以上代码为做简洁化实现,部分细节还有优化空间
  • getEndOfDay、getStartOfDay应该放在专门DateUtil中
  • sql查询应该放在专门的mapper中,不应耦合在service核心逻辑层
  • createDoctorOrder方法整体开始变得越来越长,可以将步骤1/1-1/1-2 封装成一个函数
  • postman发起测试
  • 注意:调整库存中心的nacos配置,让库存中心不在维护窗口,否则创建会失败
  • 成功
  • 失败

题目二

目标

掌握Feign实现远程服务调用,针对自己负责的模块能说出主动优化经验(便于后续面试)

需求

针对目前的工程调用,存在调用地址维护困难代码可读性差等缺点,因此考虑剥离出一个单独的API-module,本次重构优化工作具体如下:

需求项

业务描述

重构工程,引入openfeign

完成doctor-service对inventory-service调用的代码重构

调整feign日志级别为全局BASIC

考虑到生产环境不会全量日志打印,请结合目前掌握技术项完成此技术改造

完成feign连接池优化

作为开发人员,主动发现、解决落地问题是必备职业素养,请完成此模块技术改造并说清楚改造的初衷、实现过程

提示

  • 参照课程内容最佳实践,完成代码重构升级工作

参考答案


1.openfeign重构

  • doctor-service工程引入依赖
<!--feign依赖-->
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
  • 启动类增加注解:@EnableFeignClients
  • 新建:feign-api模块,完成代码迁移、删除
  • feign-api引入依赖
<dependency>
  <groupId>org.springframework.cloud</groupId>
  <artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
  • 迁移出参VO,并删除doctor-service中的文件、引入feign-api、更新对InventoryUpdateVO的import
<!--feign-api依赖-->
<dependency>
    <groupId>org.example</groupId>
    <artifactId>feign-api</artifactId>
    <version>1.0-SNAPSHOT</version>
</dependency>
  • 新建跨服务调用client:InventoryClient
package cn.itcast.feign.inventory.client;
import cn.itcast.feign.inventory.response.InventoryUpdateVO;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
/**
 * @Description: 针对耗材中心跨服务调用client
 * @Date: 2023/2/27 9:14
 */
@FeignClient("inventoryservice")
public interface InventoryClient {
    @GetMapping("/inventory/update/{id}/{num}")
    InventoryUpdateVO updateInventory(@PathVariable("id") Long id, @PathVariable("num") Long num);
}
  • 更新service层调用关系
  • 解决包路径扫描问题
  • 重启服务并测试验证

2.调整全局日志级别

配置文件新增:

feign:  
  client:
    config: 
      default: # 这里用default就是全局配置,如果是写服务名称,则是针对某个微服务的配置
        loggerLevel: BASIC #  日志级别


或创建一个类,注册日志级别(参照今天课程内容),验证效果如下:


3.优化连接池

  • 引入依赖
<!--httpClient的依赖 -->
<dependency>
    <groupId>io.github.openfeign</groupId>
    <artifactId>feign-httpclient</artifactId>
</dependency>
  • 增加配置信息
feign:
  httpclient:
    enabled: true # 开启feign对HttpClient的支持
    max-connections: 200 # 最大的连接数
    max-connections-per-route: 50 # 每个路径的最大连接数
  • 重启验证

题目三

目标

掌握微服务网关的搭建、使用

需求

针对目前的系统工程,可以明显发现技术架构上缺少一个看门神,此问题会导致所有的请求直接打到了微服务中,对于服务本身的保护基本不存在(在微服务本身不做服务保护的前提下)。当一个微服务有多个实例时(水平复制)调用方也无法做到请求的路由,一系列问题导致我们考虑引入一个网关。完成下述需求:

需求项

业务描述

完成gateway工程搭建

创建gateway工程,并能够在nacos看到即可(此需求项暂不做微服务整合,勿混淆)

完成微服务整合gateway,并能够通过网关创建医嘱、更新库存

完成通过网关访问的权限校验

校验逻辑:key=auth,账户=admin放行,其余返回异常

提示

  • 网关权限校验需实现自定义全局过滤器

参考答案


1.gateway工程搭建

  • 创建gateway模块,并引入依赖
<!--网关-->
<dependency>
  <groupId>org.springframework.cloud</groupId>
  <artifactId>spring-cloud-starter-gateway</artifactId>
</dependency>
<!--nacos服务发现依赖-->
<dependency>
  <groupId>com.alibaba.cloud</groupId>
  <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
  • 编写启动类:GateWayApplication
package cn.itcast.gateway;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
/**
 * @Description: 网关启动类
 * @Date: 2023/2/27 9:47
 */
@SpringBootApplication
public class GatewayApplication {
    public static void main(String[] args) {
        SpringApplication.run(GatewayApplication.class, args);
        System.out.println("网关启动成功");
    }
}
  • 新增配置文件:application.yml,增加启动配置。此处只启动网关即可,因此不用配置gateway路由规则
server:
  port: 10010 # 网关端口
spring:
  application:
    name: gateway # 服务名称
  cloud:
    nacos:
      server-addr: localhost:8848 # nacos地址
  • 启动后,nacos查看注册信息

2.整合gateway

  • 增加网关的路由规则配置
gateway:
      routes: # 网关路由配置
        - id: inventory-service # 路由id,自定义,只要唯一即可
          uri: lb://inventoryservice # 路由的目标地址 lb就是负载均衡,后面跟服务名称
          predicates: # 路由断言,也就是判断请求是否符合路由规则的条件
            - Path=/inventory/** # 这个是按照路径匹配,只要以/user/开头就符合要求
        - id: doctor-service # 路由id,自定义,只要唯一即可
          uri: lb://doctorservice # 路由的目标地址 lb就是负载均衡,后面跟服务名称
          predicates: # 路由断言,也就是判断请求是否符合路由规则的条件
            - Path=/doctor/** # 这个是按照路径匹配,只要以/user/开头就符合要求
  • 重启服务验证
  • 更新库存

  • 创建医嘱


3.权限校验

  • 网关增加鉴权代码
package cn.itcast.gateway.filters;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Component;
import org.springframework.util.MultiValueMap;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;
/**
 * @Description: 网关权限校验
 * @Date: 2023/2/27 10:00
 */
@Component
public class AuthorizeFilter implements GlobalFilter {
    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        // 1.获取请求参数
        MultiValueMap<String, String> params = exchange.getRequest().getQueryParams();
        // 2.获取authorization参数
        String auth = params.getFirst("auth");
        // 3.校验
        if ("admin".equals(auth)) {
            // 放行
            return chain.filter(exchange);
        }
        // 4.拦截
        // 4.1.禁止访问,设置状态码
        exchange.getResponse().setStatusCode(HttpStatus.FORBIDDEN);
        // 4.2.结束处理
        return exchange.getResponse().setComplete();
    }
}
  • postman请求测试
  • 更新库存原接口
  • 更新库存新接口
  • 创建医嘱新接口,注意:post请求不能跟GET一样追加,参照下图
相关文章
|
1天前
|
数据采集 人工智能 安全
|
10天前
|
云安全 监控 安全
|
2天前
|
自然语言处理 API
万相 Wan2.6 全新升级发布!人人都能当导演的时代来了
通义万相2.6全新升级,支持文生图、图生视频、文生视频,打造电影级创作体验。智能分镜、角色扮演、音画同步,让创意一键成片,大众也能轻松制作高质量短视频。
905 150
|
15天前
|
机器学习/深度学习 人工智能 自然语言处理
Z-Image:冲击体验上限的下一代图像生成模型
通义实验室推出全新文生图模型Z-Image,以6B参数实现“快、稳、轻、准”突破。Turbo版本仅需8步亚秒级生成,支持16GB显存设备,中英双语理解与文字渲染尤为出色,真实感和美学表现媲美国际顶尖模型,被誉为“最值得关注的开源生图模型之一”。
1642 8
|
6天前
|
人工智能 前端开发 文件存储
星哥带你玩飞牛NAS-12:开源笔记的进化之路,效率玩家的新选择
星哥带你玩转飞牛NAS,部署开源笔记TriliumNext!支持树状知识库、多端同步、AI摘要与代码高亮,数据自主可控,打造个人“第二大脑”。高效玩家的新选择,轻松搭建专属知识管理体系。
364 152
|
7天前
|
人工智能 自然语言处理 API
一句话生成拓扑图!AI+Draw.io 封神开源组合,工具让你的效率爆炸
一句话生成拓扑图!next-ai-draw-io 结合 AI 与 Draw.io,通过自然语言秒出架构图,支持私有部署、免费大模型接口,彻底解放生产力,绘图效率直接爆炸。
601 152
|
9天前
|
人工智能 安全 前端开发
AgentScope Java v1.0 发布,让 Java 开发者轻松构建企业级 Agentic 应用
AgentScope 重磅发布 Java 版本,拥抱企业开发主流技术栈。
562 13
|
2天前
|
编解码 人工智能 机器人
通义万相2.6,模型使用指南
智能分镜 | 多镜头叙事 | 支持15秒视频生成 | 高品质声音生成 | 多人稳定对话