作业目标
- 作业一:掌握Nacos配置中心的使用,巩固强化代码编写能力(整合配置中心2、需求2)
- 作业二:掌握Feign实现远程服务调用,针对自己负责的模块能说出主动优化经验(便于后续面试)
- 作业三:掌握微服务网关的搭建、使用,巩固强化对于后端接口自测的能力(postman测试6个接口,次数预计20次左右)
工程介绍
工程技术架构图
工程目录结构图
环境准备
- 导入SpringCloud-Day2原始作业的工程:doctor-station(📎doctor-station.zip)
- 导入数据库脚本:创建数据库:doctor_station,导入脚本: 📎doctor_station.sql
- 启动nacos、doctor-station工程:
- 导入postman文件: 📎SpringCloud-Day1.json,使用postman完成下述请求
应用名称 |
访问地址 |
预期结果 |
inventory-service |
更新库存 |
{ "updateResult": true,"updateMsg": "更新成功"} |
doctor-service |
创建医嘱 |
{"createResult": true,"createMsg": "创建成功"} |
题目一
目标
能够完成微服务Nacos配置中心的使用
需求
随着需求的不断迭代更新,应用开始有越来越多的配置项,尤其部分配置项可能频繁修改。此时考虑引入一个配置中心来解决当前业务与配置耦合的问题,请结合你当前掌握的知识点,思考如下场景问题应该如何解决?
需求项 |
业务描述 |
调用库存中心服务地址优化 |
接口地址随着需求的迭代,可能会不定期产生变化,因此考虑放在配置中心 |
[耗材中心]维护时间场景优化 |
耗材中心定期做统一数据迁移,此时应用将停机维护,本次dev环境将于:2023年10月1日23:59 至 10月2日02:00进行升级维护,请完成相关配置项,并在代码中做配置项的校验:更新时间之内返回异常信息(系统维护中,请稍后重试) |
[医生站]开单次数场景优化 |
医生站作为端侧应用,为避免部分医生刷单给自己刷绩效,针对当前test环境增加一个:当天最多允许创建5单医嘱数据的配置项,请完成相关配置项,并思考:如何做到当天最多5单的逻辑,并在超过n但之后给出异常信息(今日开单次数已达上限) |
提示
- 结合今天学习内容,完成两个module的nacos配置中心依赖导入、整合
- 日志:需创建各自应用的配置文件,同时test环境的配置覆盖共享文件配置
- 维护时间:与今天课程内容基本吻合,结合今天学习内容整合即可
- 创建权限:是否考虑到了新增一个查询当天单数的SQL呢?
参考答案
1.整合nacos-config
- 依赖引入(每个微服务)
<!--nacos配置管理依赖--> <dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId> </dependency>
2.接口请求地址需求
- nacos创建配置文件:doctorservice-test.yaml、doctorservice.yaml,避免为空报错可以先写个1
因为接口地址的变更一定是从dev->test->pre->prod,因此此接口地址配置项在此场景下建议 放在每个环境配置文件,或放在共享文件且当产生变更时当前环境追加配置覆盖
- doctor-service创建配置文件:bootstrap.yml文件,并整合配置文件
spring: application: name: doctorservice # 服务名称 profiles: active: test # 当前使用test环境 cloud: nacos: server-addr: localhost:8848 config: file-extension: yaml # 文件后缀名
- 配置文件新增接口地址配置信息
- doctorservice-test.yaml,这里配置项的K-V可以自定义
inventory: updateUri: http://inventoryservice/inventory/update/
注意:配置项的换行后缩进,不要使用TAB,否则会有下述错误
- 属性读取
- 新建配置读取类: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.耗材中心维护时间
- nacos创建配置文件:inventoryservice-dev.yaml,避免为空报错可以先写个1
- inventory-service创建配置文件:bootstrap.yml文件,并整合配置文件
spring: application: name: inventoryservice # 服务名称 profiles: active: dev # 当前使用dev环境 cloud: nacos: server-addr: localhost:8848 config: file-extension: yaml # 文件后缀名
- 配置文件新增接口地址配置信息
- inventoryservice-dev.yaml,这里配置项的K-V可以自定义
inventory: stopStartTime: 2023-10-01 23:59:59 stopEndTime: 2023-10-02 02:00:00
- 属性读取,代码优化
- 新建配置读取类: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层调用关系
- 解决包路径扫描问题
- 启动类追加注解中的属性:@EnableFeignClients(clients = {InventoryClient.class})
- 重启服务并测试验证
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一样追加,参照下图