今日练习

简介: 本课程作业聚焦微服务核心组件实践,涵盖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一样追加,参照下图

相关文章
|
12天前
|
数据采集 人工智能 安全
|
7天前
|
机器学习/深度学习 人工智能 前端开发
构建AI智能体:七十、小树成林,聚沙成塔:随机森林与大模型的协同进化
随机森林是一种基于决策树的集成学习算法,通过构建多棵决策树并结合它们的预测结果来提高准确性和稳定性。其核心思想包括两个随机性:Bootstrap采样(每棵树使用不同的训练子集)和特征随机选择(每棵树分裂时只考虑部分特征)。这种方法能有效处理大规模高维数据,避免过拟合,并评估特征重要性。随机森林的超参数如树的数量、最大深度等可通过网格搜索优化。该算法兼具强大预测能力和工程化优势,是机器学习中的常用基础模型。
344 164
|
6天前
|
机器学习/深度学习 自然语言处理 机器人
阿里云百炼大模型赋能|打造企业级电话智能体与智能呼叫中心完整方案
畅信达基于阿里云百炼大模型推出MVB2000V5智能呼叫中心方案,融合LLM与MRCP+WebSocket技术,实现语音识别率超95%、低延迟交互。通过电话智能体与座席助手协同,自动化处理80%咨询,降本增效显著,适配金融、电商、医疗等多行业场景。
345 155
|
7天前
|
编解码 人工智能 自然语言处理
⚽阿里云百炼通义万相 2.6 视频生成玩法手册
通义万相Wan 2.6是全球首个支持角色扮演的AI视频生成模型,可基于参考视频形象与音色生成多角色合拍、多镜头叙事的15秒长视频,实现声画同步、智能分镜,适用于影视创作、营销展示等场景。
575 4
|
15天前
|
SQL 自然语言处理 调度
Agent Skills 的一次工程实践
**本文采用 Agent Skills 实现整体智能体**,开发框架采用 AgentScope,模型使用 **qwen3-max**。Agent Skills 是 Anthropic 新推出的一种有别于mcp server的一种开发方式,用于为 AI **引入可共享的专业技能**。经验封装到**可发现、可复用的能力单元**中,每个技能以文件夹形式存在,包含特定任务的指导性说明(SKILL.md 文件)、脚本代码和资源等 。大模型可以根据需要动态加载这些技能,从而扩展自身的功能。目前不少国内外的一些框架也开始支持此种的开发方式,详细介绍如下。
1013 7