# spring-cloud-study **Repository Path**: xccit/spring-cloud-study ## Basic Information - **Project Name**: spring-cloud-study - **Description**: SpringCloud+SpringCloud Alibaba学习仓库 - **Primary Language**: Unknown - **License**: MulanPSL-2.0 - **Default Branch**: master - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 0 - **Forks**: 1 - **Created**: 2025-05-17 - **Last Updated**: 2025-10-20 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README ## SpringCloud+SpringCloud Alibaba学习笔记 ### 搭建初始工程 #### 工程结构 > cloud-demo > > ​ model > > ​ services > > ​ service-product > > ​ service-order #### 坐标引入 `cloud-demo` ```xml 4.0.0 org.springframework.boot spring-boot-starter-parent 3.3.4 io.xccit cloud-demo 1.0-SNAPSHOT pom services model 17 17 UTF-8 2023.0.3 2023.0.3.2 org.springframework.cloud spring-cloud-dependencies ${spring.cloud.version} import pom com.alibaba.cloud spring-cloud-alibaba-dependencies ${spring.cloud.alibaba.version} import pom ``` `services` ```xml 4.0.0 io.xccit cloud-demo 1.0-SNAPSHOT services pom service-order service-product 17 17 UTF-8 org.springframework.boot spring-boot-starter-web org.springframework.boot spring-boot-starter-test test org.projectlombok lombok io.xccit model 1.0-SNAPSHOT ``` `model` ```xml 4.0.0 io.xccit cloud-demo 1.0-SNAPSHOT model 17 17 UTF-8 org.projectlombok lombok ``` ### 创建启动类 `以service-product为例` ```java @SpringBootApplication public class ProductApplication { public static void main(String[] args) { SpringApplication.run(ProductApplication.class, args); } } ``` ### 创建配置文件 `以service-product为例` ```yaml server: port: 8082 ``` ### 模拟微服务集群 `将启动类的配置多复制几份` ![image-20250517203244471](https://fastly.jsdelivr.net/gh/xccit/blog_images/images_resource/image-20250517203244471.png) `修改复制后的启动类配置,为其配置启动参数` ![image-20250517203346320](https://fastly.jsdelivr.net/gh/xccit/blog_images/images_resource/image-20250517203346320.png) ## 服务发现 ### 搭建Nacos环境 `官网:`[Nacos 概览 | Nacos 官网](https://nacos.io/docs/latest/overview/) #### docker ```shell docker pull nacos/nacos-server:v2.5.1 docker run --name nacos \ -e MODE=standalone \ -p 8848:8848 \ -p 9848:9848 \ -p 9849:9849 \ -p 7848:7848 \ -d nacos/nacos-server:v2.5.1 ``` ### 坐标 `services` ```xml com.alibaba.cloud spring-cloud-starter-alibaba-nacos-discovery ``` ### 启动类注解 ```java @EnableDiscoveryClient ``` ### 配置 ```yaml spring: application: name: service-product cloud: nacos: discovery: server-addr: 127.0.0.1:8848 ``` ### 测试 访问:`localhost:8848/nacos`,点击服务管理-->服务列表 ![image-20250517203939108](https://fastly.jsdelivr.net/gh/xccit/blog_images/images_resource/image-20250517203939108.png) ## RestTemplate远程调用 ### 环境搭建 创建商品接口,根据商品ID获取商品信息,然后订单接口,创建订单,需要远程调用获取商品接口来封装订单信息,配置RestTemplate来进行远程调用,`参考model模块,service-product以及service-order` `配置RestTemplate` ```java package io.xccit.order.config; import org.springframework.cloud.client.loadbalancer.LoadBalanced; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.web.client.RestTemplate; /** * @author CH_ywx * @version 1.0 * @create 2025/5/17 *

远程调用配置

*/ @Configuration public class RemoteConfig { @Bean public RestTemplate restTemplate(){ return new RestTemplate(); } } ``` ### 1.DiscoveryClient ```java package io.xccit.order.service.impl; import java.math.BigDecimal; import java.util.Arrays; import java.util.List; import io.xccit.model.order.domain.Order; import io.xccit.model.product.domain.Product; import io.xccit.order.service.OrderService; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.cloud.client.ServiceInstance; import org.springframework.cloud.client.discovery.DiscoveryClient; import org.springframework.cloud.client.loadbalancer.LoadBalancerClient; import org.springframework.stereotype.Service; import org.springframework.web.client.RestTemplate; /** * @author CH_ywx * @version 1.0 * @create 2025/5/17 *

*/ @Slf4j @Service public class OrderServiceImpl implements OrderService { @Autowired private DiscoveryClient discoveryClient; @Autowired private RestTemplate restTemplate; @Override public Order createOrder(Long userId, Long productId) { Product productByRemote = getProductByRemote(productId); Order order = new Order(); order.setId(1L); order.setTotalAmount(new BigDecimal(String.valueOf(productByRemote.getPrice().multiply(new BigDecimal(productByRemote.getNum()))))); order.setUserId(userId); order.setAddress("甘肃省兰州市城关区雁北路400号"); order.setProductList(Arrays.asList(productByRemote)); return order; } /** * 远程调用获取商品信息 * @param productId * @return */ public Product getProductByRemote(Long productId) { List instances = discoveryClient.getInstances("service-product"); ServiceInstance serviceInstance = instances.get(0); String url = "http://" + serviceInstance.getHost() + ":" + serviceInstance.getPort() + "/product/" + productId; log.info("调用商品服务获取商品信息,url:{}", url); Product product = restTemplate.getForObject(url, Product.class); return product; } } ``` ### 2.LoadBalancerClient(负载均衡) ```xml org.springframework.cloud spring-cloud-starter-loadbalancer ``` ```java package io.xccit.order.service.impl; import java.math.BigDecimal; import java.util.Arrays; import java.util.List; import io.xccit.model.order.domain.Order; import io.xccit.model.product.domain.Product; import io.xccit.order.service.OrderService; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.cloud.client.ServiceInstance; import org.springframework.cloud.client.discovery.DiscoveryClient; import org.springframework.cloud.client.loadbalancer.LoadBalancerClient; import org.springframework.stereotype.Service; import org.springframework.web.client.RestTemplate; /** * @author CH_ywx * @version 1.0 * @create 2025/5/17 *

*/ @Slf4j @Service public class OrderServiceImpl implements OrderService { @Autowired private DiscoveryClient discoveryClient; @Autowired private LoadBalancerClient loadBalancerClient; @Autowired private RestTemplate restTemplate; @Override public Order createOrder(Long userId, Long productId) { Product productByRemote = getProductByRemoteWithLoadBalancer(productId); Order order = new Order(); order.setId(1L); order.setTotalAmount(new BigDecimal(String.valueOf(productByRemote.getPrice().multiply(new BigDecimal(productByRemote.getNum()))))); order.setUserId(userId); order.setAddress("甘肃省兰州市城关区雁北路400号"); order.setProductList(Arrays.asList(productByRemote)); return order; } /** * 远程调用获取商品信息 * @param productId * @return */ public Product getProductByRemote(Long productId) { List instances = discoveryClient.getInstances("service-product"); ServiceInstance serviceInstance = instances.get(0); String url = "http://" + serviceInstance.getHost() + ":" + serviceInstance.getPort() + "/product/" + productId; log.info("调用商品服务获取商品信息,url:{}", url); Product product = restTemplate.getForObject(url, Product.class); return product; } /** * 负载均衡远程调用获取商品信息 * @param productId * @return */ public Product getProductByRemoteWithLoadBalancer(Long productId) { //轮询负载均衡算法(每个实例都换着来一次) ServiceInstance serviceInstance = loadBalancerClient.choose("service-product"); String url = "http://" + serviceInstance.getHost() + ":" + serviceInstance.getPort() + "/product/" + productId; log.info("调用商品服务获取商品信息,url:{}", url); Product product = restTemplate.getForObject(url, Product.class); return product; } } ``` ### 3.基于注解式的负载均衡 ```java package io.xccit.order.config; import org.springframework.cloud.client.loadbalancer.LoadBalanced; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.web.client.RestTemplate; /** * @author CH_ywx * @version 1.0 * @create 2025/5/17 *

远程调用配置

*/ @Configuration public class RemoteConfig { @LoadBalanced //注解式负载均衡,不加则没有负载均衡功能 @Bean public RestTemplate restTemplate(){ return new RestTemplate(); } } ``` ```java package io.xccit.order.service.impl; import java.math.BigDecimal; import java.util.Arrays; import java.util.List; import io.xccit.model.order.domain.Order; import io.xccit.model.product.domain.Product; import io.xccit.order.service.OrderService; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.cloud.client.ServiceInstance; import org.springframework.cloud.client.discovery.DiscoveryClient; import org.springframework.cloud.client.loadbalancer.LoadBalancerClient; import org.springframework.stereotype.Service; import org.springframework.web.client.RestTemplate; /** * @author CH_ywx * @version 1.0 * @create 2025/5/17 *

*/ @Slf4j @Service public class OrderServiceImpl implements OrderService { @Autowired private DiscoveryClient discoveryClient; @Autowired private LoadBalancerClient loadBalancerClient; @Autowired private RestTemplate restTemplate; @Override public Order createOrder(Long userId, Long productId) { Product productByRemote = getProductByRemoteWithLoadBalancerAnnotation(productId); Order order = new Order(); order.setId(1L); order.setTotalAmount(new BigDecimal(String.valueOf(productByRemote.getPrice().multiply(new BigDecimal(productByRemote.getNum()))))); order.setUserId(userId); order.setAddress("甘肃省兰州市城关区雁北路400号"); order.setProductList(Arrays.asList(productByRemote)); return order; } /** * 远程调用获取商品信息 * @param productId * @return */ public Product getProductByRemote(Long productId) { List instances = discoveryClient.getInstances("service-product"); ServiceInstance serviceInstance = instances.get(0); String url = "http://" + serviceInstance.getHost() + ":" + serviceInstance.getPort() + "/product/" + productId; log.info("调用商品服务获取商品信息,url:{}", url); Product product = restTemplate.getForObject(url, Product.class); return product; } /** * 负载均衡远程调用获取商品信息 * @param productId * @return */ public Product getProductByRemoteWithLoadBalancer(Long productId) { //轮询负载均衡算法(每个实例都换着来一次) ServiceInstance serviceInstance = loadBalancerClient.choose("service-product"); String url = "http://" + serviceInstance.getHost() + ":" + serviceInstance.getPort() + "/product/" + productId; log.info("调用商品服务获取商品信息,url:{}", url); Product product = restTemplate.getForObject(url, Product.class); return product; } /** * 注解式负载均衡远程调用获取商品信息 * @param productId * @return * @see io.xccit.order.config.RemoteConfig#restTemplate() */ public Product getProductByRemoteWithLoadBalancerAnnotation(Long productId) { String url = "http://service-product" + "/product/" + productId; log.info("调用商品服务获取商品信息,url:{}", url); Product product = restTemplate.getForObject(url, Product.class); return product; } } ``` ### 测试时注意事项 `基于注解式的负载均衡`需要在product模块的接口方法中写一个打印,方便控制台日志查看是从哪个端口的实例中获取到的商品信息,`其他的负载均衡`测试只需要在远程调用的方法中打印该URL即可 ## 注册中心 ### 坐标 ```xml com.alibaba.cloud spring-cloud-starter-alibaba-nacos-config ``` ### 导入配置 ```yaml spring: config: import: nacos:service-order.properties ``` ### Nacos配置 `官网文档:`[Nacos 融合 Spring Cloud,成为注册配置中心 | Nacos 官网](https://nacos.io/docs/v2/ecology/use-nacos-with-spring-cloud/) ![image-20250519170212377](https://fastly.jsdelivr.net/gh/xccit/blog_images/images_resource/image-20250519170212377.png) ### 读取配置 ```java @Slf4j @RestController @RequestMapping("/order") public class OrderController { @Autowired private OrderService orderService; @Value("${order.time-out}") private String orderTimeOut; @GetMapping("/create") public Order createOrder(Long userId, Long productId) { log.info("Nacos配置订单超时:{}", orderTimeOut); return orderService.createOrder(userId, productId); } } ``` ![image-20250519171301840](https://fastly.jsdelivr.net/gh/xccit/blog_images/images_resource/image-20250519171301840.png) `请求接口查看到的日志信息` ### 激活配置属性的自动刷新 `@RefreshScope`注解 ```java @Slf4j @RefreshScope @RestController @RequestMapping("/order") public class OrderController { @Autowired private OrderService orderService; @Value("${order.time-out}") private String orderTimeOut; @GetMapping("/create") public Order createOrder(Long userId, Long productId) { log.info("Nacos配置订单超时:{}", orderTimeOut); return orderService.createOrder(userId, productId); } } ``` 这样,当属性在Nacos中修改掉时,项目可以自动刷新该配置的值,重启项目,修改Nacos配置,重新测试 ![image-20250519171947272](https://fastly.jsdelivr.net/gh/xccit/blog_images/images_resource/image-20250519171947272.png) ![image-20250519172024194](./README.assets/image-20250519172024194-1747646427962-1.png) ### 暂时不需要配置中心但是有配置中心依赖的模块 `启动报错` ![image-20250519172426999](https://fastly.jsdelivr.net/gh/xccit/blog_images/images_resource/image-20250519172426999.png) `解决方式` ```yaml spring: cloud: config: import-check: enabled: false ``` ![image-20250519172630787](https://fastly.jsdelivr.net/gh/xccit/blog_images/images_resource/image-20250519172630787.png) ### 配置中心动态无感刷新 `@RefreshScope`+`@Value`的方式太过繁琐,使用SpringBoot提供的`ConfigurationProperties`会更方便 #### 新建一个配置类 ```java /** * @author CH_ywx * @version 1.0 * @create 2025/5/19 *

Order模块使用Nacos配置中心的属性

*/ @Component @Data @ConfigurationProperties(prefix = "order") public class OrderNacosProperties { private String timeOut; } ``` `注意:配置中心的-或者_写法在JavaBean中使用驼峰命名规则即可` #### Controller改造 ```java @Slf4j @RestController @RequestMapping("/order") public class OrderController { @Autowired private OrderService orderService; @Autowired private OrderNacosProperties orderNacosProperties; @GetMapping("/create") public Order createOrder(Long userId, Long productId) { log.info("Nacos配置订单超时:{}", orderNacosProperties.getTimeOut()); return orderService.createOrder(userId, productId); } } ``` #### 重启测试 ![image-20250519173713948](https://fastly.jsdelivr.net/gh/xccit/blog_images/images_resource/image-20250519173713948.png) ### 监听配置 `ApplicationRunner`配合`NacosConfigManager组件` ```java @SpringBootApplication @EnableDiscoveryClient public class OrderApplication { public static void main(String[] args) { SpringApplication.run(OrderApplication.class, args); } /** * 启动成功后执行的一次性任务 * @param nacosConfigManager 自动从容器中拿出的配置管理器(原本没有参数,加上之后会自动从容器中拿) * @return */ @Bean ApplicationRunner applicationRunner(NacosConfigManager nacosConfigManager){ return args -> { System.out.println("Order服务启动成功"); ConfigService configService = nacosConfigManager.getConfigService(); //参数值参照Nacos配置文件的属性来填写 配置文件ID 配置文件所属组 configService.addListener("service-order.properties", "DEFAULT_GROUP", new Listener() { @Override public Executor getExecutor() { //返回一个固定4个线程的线程池(随意,不一定是4) return Executors.newFixedThreadPool(4); } @Override public void receiveConfigInfo(String s) { System.out.println("监听到配置文件更新: " + s); } }); }; } } ``` `重启测试,项目重启后修改Nacos配置` image-20250519174755209 ![image-20250519174722022](https://fastly.jsdelivr.net/gh/xccit/blog_images/images_resource/image-20250519174722022.png) ### 配置优先级 `先导入优先`--->`外部优先` 先导入优先:导入多个配置后,按照声明的优先级生效,相同配置的值以先导入的为主 外部优先:外部配置比本地配置文件的优先级更高 `外部配置+本地配置`合并后,生成一个配置列表,程序启动后,配置列表加载到项目的环境变量中去,以后要获取的配置按照项目的环境变量中的值为准 ### 数据隔离 微服务开发中,模块多,配置也多,如果配置都在一起,容易混淆且不好管理,这个时候就需要配置`数据隔离` #### 1.Naocs命名空间 `默认只有一个public` ![image-20250520222657581](https://fastly.jsdelivr.net/gh/xccit/blog_images/images_resource/image-20250520222657581.png) `创建多个` ![image-20250520222840001](https://fastly.jsdelivr.net/gh/xccit/blog_images/images_resource/image-20250520222840001.png) #### 2.为服务创建多个配置 `分别点击不同的命名空间,创建配置文件` ![image-20250520223001081](https://fastly.jsdelivr.net/gh/xccit/blog_images/images_resource/image-20250520223001081.png) `通用配置common.yaml` ```yaml order: time-out: 30min ``` `数据源datasource.yaml` ```yaml order: jdbc-driver-class: com.mysql.jdbc.cj.Driver url: jdbc:mysql://localhost:3306/service-order?useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai username: root password: root ``` `其他命名空间中,可以使用克隆来快速复制配置数据` ![image-20250520223619744](https://fastly.jsdelivr.net/gh/xccit/blog_images/images_resource/image-20250520223619744.png) ![image-20250520223654877](https://fastly.jsdelivr.net/gh/xccit/blog_images/images_resource/image-20250520223654877.png) > `Tips:`克隆后记得修改配置,以免测试时无法区分 #### 3.按需加载 `order模块的application.yml` ```yaml server: port: 8081 spring: application: name: service-order cloud: nacos: discovery: server-addr: 127.0.0.1:8848 config: # 对应的命名空间 namespace: dev config: import: # ?group=order --> nacos配置的分组 - nacos:common.yaml?group=order - nacos:datasource.yaml?group=order ``` `OrderNacosProperties.java` ```java @Component @Data @ConfigurationProperties(prefix = "order") public class OrderNacosProperties { private String timeOut; private String jdbcDriverClass; private String url; private String username; private String password; } ``` `OrderController.java` ```java @RestController @RequestMapping("/order") public class OrderController { @Autowired private OrderService orderService; @Autowired private OrderNacosProperties orderNacosProperties; /** * 创建订单 * @param userId * @param productId * @return */ @GetMapping("/create") public Order createOrder(Long userId, Long productId) { return orderService.createOrder(userId, productId); } /** * 获取nacos配置 * @return */ @GetMapping("/nacos-config") public String getNacosConfig() { return orderNacosProperties.toString(); } } ``` `重启测试` ![image-20250520225100210](https://fastly.jsdelivr.net/gh/xccit/blog_images/images_resource/image-20250520225100210.png) `日志中也有信息,后面success表示前面的配置加载成功` ![image-20250520225210986](https://fastly.jsdelivr.net/gh/xccit/blog_images/images_resource/image-20250520225210986.png) #### 4.不同环境动态加载 使用`SpringBoot的多环境激活配置` `application.yml` ```yml server: port: 8081 spring: application: name: service-order profiles: # 激活的环境配置 active: dev cloud: nacos: discovery: server-addr: 127.0.0.1:8848 ``` `application-dev.yml` ```yml spring: cloud: nacos: config: # 对应的命名空间 namespace: dev config: import: # ?group=order --> nacos配置的分组 - nacos:common.yaml?group=order - nacos:datasource.yaml?group=order ``` `application-test.yml` ```yml spring: cloud: nacos: config: # 对应的命名空间 namespace: test config: import: # ?group=order --> nacos配置的分组 - nacos:common.yaml?group=order - nacos:datasource.yaml?group=order ``` `application-prod.yml` ```yml spring: cloud: nacos: config: # 对应的命名空间 namespace: prod config: import: # ?group=order --> nacos配置的分组 - nacos:common.yaml?group=order - nacos:datasource.yaml?group=order ``` `改变application.yml的active后面的值,测试多环境获取到的配置值是否一致` `dev:` ![image-20250520230300070](http://fastly.jsdelivr.net/gh/xccit/blog_images/images_resource/image-20250520230300070.png) `test:` ![image-20250520230516551](http://fastly.jsdelivr.net/gh/xccit/blog_images/images_resource/image-20250520230516551.png) `prod:` ![image-20250520230554213](http://fastly.jsdelivr.net/gh/xccit/blog_images/images_resource/image-20250520230554213.png) ## OpenFeign远程调用 ### 坐标 ```xml org.springframework.cloud spring-cloud-starter-openfeign ``` ### 开启远程调用 `@EnableFeignClients注解` ```java @EnableFeignClients @SpringBootApplication @EnableDiscoveryClient public class OrderApplication { public static void main(String[] args) { SpringApplication.run(OrderApplication.class, args); } /** * 启动成功后执行的一次性任务 * @param nacosConfigManager 自动从容器中拿出的配置管理器(原本没有参数,加上之后会自动从容器中拿) * @return */ @Bean ApplicationRunner applicationRunner(NacosConfigManager nacosConfigManager){ return args -> { System.out.println("Order服务启动成功"); ConfigService configService = nacosConfigManager.getConfigService(); //参数值参照Nacos配置文件的属性来填写 配置文件ID 配置文件所属组 configService.addListener("service-order.properties", "DEFAULT_GROUP", new Listener() { @Override public Executor getExecutor() { //返回一个固定4个线程的线程池(随意,不一定是4) return Executors.newFixedThreadPool(4); } @Override public void receiveConfigInfo(String s) { System.out.println("监听到配置文件更新: " + s); } }); }; } } ``` ### 定义远程调用接口 ```java @FeignClient("service-product") public interface ProductFeignClient { /** * 根据id查询商品信息 * @param id * @return */ @GetMapping("/product/{id}") Product getProductById(@PathVariable("id") Long id); } ``` ### 使用远程调用 ```java @Autowired private ProductFeignClient productFeignClient; @Override public Order createOrder(Long userId, Long productId) { // Product productByRemote = getProductByRemoteWithLoadBalancerAnnotation(productId); Product productByRemote = productFeignClient.getProductById(productId); Order order = new Order(); order.setId(1L); order.setTotalAmount(new BigDecimal(String.valueOf(productByRemote.getPrice().multiply(new BigDecimal(productByRemote.getNum()))))); order.setUserId(userId); order.setAddress("甘肃省兰州市城关区雁北路400号"); order.setProductList(Arrays.asList(productByRemote)); return order; } ``` ### 测试 ![image-20250524200755309](https://fastly.jsdelivr.net/gh/xccit/blog_images/images_resource/image-20250524200755309.png) ## OpenFeign调用三方接口 ### 三方接口 [[聚美智数\]股票实时行情查询_API专区_云市场-阿里云](https://market.aliyun.com/apimarket/detail/cmapi00065924?spm=5176.28663977.J_tds7u7INVs2Clozcb3f5f.44.5f07e69fHHwy11&innerSource=search#sku=yuncode5992400001) 调用示例:[[聚美智数\]股票实时行情查询_API专区_云市场-阿里云](https://market.aliyun.com/apimarket/detail/cmapi00065924?spm=5176.28663977.J_tds7u7INVs2Clozcb3f5f.44.5f07e69fHHwy11&innerSource=search#sku=yuncode5992400001) ### 定义请求参数 ```java @Data public class HkStocksKline implements Serializable { private String symbol; private String type; private String limit; } ``` ### 定义远程调用接口 ```java @FeignClient(name = "hk-finance",url="https://jmqqgphqcx.market.alicloudapi.com") public interface HkFinanceFeignClient { @PostMapping(value = "/finance/hk-stocks-kline", consumes = MediaType.APPLICATION_FORM_URLENCODED_VALUE) String getHkFinanceData(@RequestHeader("Authorization") String appcode, @RequestBody HkStocksKline hkStocksKline); } ``` ### 定义测试的Controller ```java @RestController @RequestMapping("/hk-finance") public class HkFinanceController { @Autowired private HkFinanceFeignClient hkFinanceFeignClient; @PostMapping("/get-finance-data") public String getFinanceData(@RequestParam("appcode") String appcode, @RequestBody HkStocksKline hkStocksKline) { System.out.println(appcode); String auth = "APPCODE " + appcode; System.out.println(auth); return hkFinanceFeignClient.getHkFinanceData(auth, hkStocksKline); } } ``` ### 测试 使用`PostMan`等工具 ![image-20250524210159995](https://fastly.jsdelivr.net/gh/xccit/blog_images/images_resource/image-20250524210159995.png) ## OpenFeign 日志 ### 配置日志级别 ```yaml # openfeign的日志级别 logging: level: io.xccit.order.feign: debug ``` ### 将Feign的Logger注入到容器 ```java /** * feign日志级别 * @return */ @Bean public Logger.Level feignLoggerLevel(){ return Logger.Level.FULL; } ``` ### 测试 ![image-20250525234313065](https://fastly.jsdelivr.net/gh/xccit/blog_images/images_resource/image-20250525234313065.png) `这样,只有配置了日志的包下的远程调用会打印日志` ## OpenFeign 超时控制 ### 默认等待时间 `连接超时:`10s `请求超时:`60s ### 修改商品业务代码 ```java @Service public class ProductServiceImpl implements ProductService { @Override public Product getProductById(Long id) { Product product = new Product(); product.setId(id); product.setPrice(new BigDecimal("88.88")); product.setProductName("《JavaWEB响应式编程实战》"); product.setNum(100); //模拟超时 try { TimeUnit.SECONDS.sleep(100); } catch (InterruptedException e) { throw new RuntimeException(e); } return product; } } ``` ### 请求订单创建接口 ![image-20250526000411936](https://fastly.jsdelivr.net/gh/xccit/blog_images/images_resource/image-20250526000411936.png) `请求超时` ### 修改超时等待时间 [Spring Cloud OpenFeign Features :: Spring Cloud Openfeign](https://docs.spring.io/spring-cloud-openfeign/reference/spring-cloud-openfeign.html#spring-cloud-feign-overriding-defaults) `文档示例` ```yaml spring: cloud: openfeign: client: config: feignName: url: http://remote-service.com connectTimeout: 5000 readTimeout: 5000 loggerLevel: full errorDecoder: com.example.SimpleErrorDecoder retryer: com.example.SimpleRetryer defaultQueryParameters: query: queryValue defaultRequestHeaders: header: headerValue requestInterceptors: - com.example.FooRequestInterceptor - com.example.BarRequestInterceptor responseInterceptor: com.example.BazResponseInterceptor dismiss404: false encoder: com.example.SimpleEncoder decoder: com.example.SimpleDecoder contract: com.example.SimpleContract capabilities: - com.example.FooCapability - com.example.BarCapability queryMapEncoder: com.example.SimpleQueryMapEncoder micrometer.enabled: false ``` `application-feign.yaml` ```yaml spring: cloud: openfeign: client: config: # 默认配置,未配置的远程调用客户端使用此默认配置 default: logger-level: full connect-timeout: 1000 read-timeout: 5000 service-product: logger-level: full # 单位: ms connect-timeout: 3000 read-timeout: 5000 ``` ### 再次启动测试 ![image-20250526002358961](https://fastly.jsdelivr.net/gh/xccit/blog_images/images_resource/image-20250526002358961.png) `这次仅用5秒就返回结果` ## OpenFeign重试机制 ### 核心配置 ```java /** * feign重试机制 * @return */ @Bean public Retryer retryer(){ return new Retryer.Default(); } ``` > 将以上代码配置到远程调用的配置类中,默认的重试器就好了,重启项目测试即可。 ![image-20250602223826144](https://fastly.jsdelivr.net/gh/xccit/blog_images/images_resource/image-20250602223826144.png) 这样,如果没有调用成功,便会自动进行重试。 ![image-20250602224043081](https://fastly.jsdelivr.net/gh/xccit/blog_images/images_resource/image-20250602224043081.png) > 查看Retryer.Default的源码看到,请求间隔100ms,最大间隔1s,最多重试5次,可以在new Retryer.Default时可以传值。 ## OpenFeign请求拦截器 ### 配置请求拦截器 #### 全局有效 ```java @Component public class XTokenRequestInterceptor implements RequestInterceptor { @Override public void apply(RequestTemplate requestTemplate) { // 设置X-Token请求头,值为随机生成的UUID字符串 requestTemplate.header("X-Token", UUID.randomUUID().toString()); System.out.println("X-TokenRequestInterceptor"); } } ``` #### 仅对某个服务有效 > 将拦截器不要注入到容器,取消`@Component`注解 ```yaml service-product: logger-level: full # 单位: ms connect-timeout: 3000 read-timeout: 5000 # 自定义拦截器,多个用逗号分隔,仅对当前服务有效 request-interceptors: - io.xccit.order.interceptor.XTokenRequestInterceptor ``` ### 重启测试 ![image-20250602225711477](https://fastly.jsdelivr.net/gh/xccit/blog_images/images_resource/image-20250602225711477.png) ![image-20250602225657697](https://fastly.jsdelivr.net/gh/xccit/blog_images/images_resource/image-20250602225657697.png) > 请求拦截器对多种业务场景是非常有效的,例如登录后下单,远程调用商品信息时,登录的Token会丢失,使用请求拦截器,每次远程调用时总是带上Token,这样就能请求到需要验证Token的接口信息。 ### 验证请求时携带的Token与远程接口接收到的Token ```java @Component public class XTokenRequestInterceptor implements RequestInterceptor { @Override public void apply(RequestTemplate requestTemplate) { // 设置X-Token请求头,值为随机生成的UUID字符串 String xtoken = UUID.randomUUID().toString(); requestTemplate.header("X-Token", xtoken); System.out.println("X-TokenRequestInterceptor:" + xtoken); } } ``` ```java @GetMapping("/{id}") public Product getProductById(@PathVariable Long id, HttpServletRequest request) { String header = request.getHeader("X-Token"); System.out.println("X-Token:"+header); return productService.getProductById(id); } ``` ![image-20250602230354214](https://fastly.jsdelivr.net/gh/xccit/blog_images/images_resource/image-20250602230354214.png) ![image-20250602230422431](https://fastly.jsdelivr.net/gh/xccit/blog_images/images_resource/image-20250602230422431.png) > 接收到的XToken是一致的 ## OpenFeign兜底返回 > 兜底返回是指远程调用失败后兜底的数据,数据可以从缓存中拿,也可以直接使用一些有特征的假数据,告诉用户目前这个服务可能有问题而不是直接报错。 ### 配置商品服务兜底返回 ```java @Component public class ProductClientFallBack implements ProductFeignClient { /** * 根据id查询商品信息 * * @param id * @return */ @Override public Product getProductById(Long id) { Product product = new Product(); product.setId(id); product.setPrice(new BigDecimal("0")); product.setProductName(""); product.setNum(0); return product; } } ``` ### 测试 #### 1.关闭重试器 ```java /** * feign重试机制 * @return */ // @Bean // public Retryer retryer(){ // return new Retryer.Default(); // } } ``` #### 2.在远程客户端接口指定兜底返回的类 ```java @FeignClient(name = "service-product",fallback = ProductClientFallBack.class) public interface ProductFeignClient { /** * 根据id查询商品信息 * @param id * @return */ @GetMapping("/product/{id}") Product getProductById(@PathVariable("id") Long id); } ``` #### 3.重启 `正常调用` ![image-20250602232323848](https://fastly.jsdelivr.net/gh/xccit/blog_images/images_resource/image-20250602232323848.png) `关闭商品服务` ![image-20250602232356808](https://fastly.jsdelivr.net/gh/xccit/blog_images/images_resource/image-20250602232356808.png) ![image-20250602232444606](https://fastly.jsdelivr.net/gh/xccit/blog_images/images_resource/image-20250602232444606.png) > 这里报错的原因是需要配合`Sentinel框架`来实现,只需要知道兜底返回的用法即可。 ## 使用Sentinel完成兜底返回 ### 整合Sentinel ```xml com.alibaba.cloud spring-cloud-starter-alibaba-sentinel ``` ### 配置熔断功能开启 ```yaml # 开启Sentinel对Feign的支持 feign: sentinel: enabled: true ``` ### 重启测试 ![image-20250602233110675](https://fastly.jsdelivr.net/gh/xccit/blog_images/images_resource/image-20250602233110675.png) > 返回了兜底返回配置的数据 ## Sentinel框架 ### Sentinel简介 官方文档:[introduction | Sentinel](https://sentinelguard.io/zh-cn/docs/introduction.html) #### 资源 资源是 Sentinel 的关键概念。它可以是 Java 应用程序中的任何内容,例如,由应用程序提供的服务,或由应用程序调用的其它应用提供的服务,甚至可以是一段代码。在接下来的文档中,我们都会用资源来描述代码块。 只要通过 Sentinel API 定义的代码,就是资源,能够被 Sentinel 保护起来。大部分情况下,可以使用方法签名,URL,甚至服务名称作为资源名来标示资源。 #### 规则 围绕资源的实时状态设定的规则,可以包括流量控制规则、熔断降级规则以及系统保护规则。所有规则可以动态实时调整。 ### 安装Sentinel `使用Docker` ```shell docker pull bladex/sentinel-dashboard docker run --name sentinel -d -p 8858:8858 bladex/sentinel-dashboard ``` > 官方文档有windows版的jar文件,也可以使用,默认8080端口,默认登录用户名密码:`sentinel/sentinel` > > [Releases · alibaba/Sentinel](https://github.com/alibaba/Sentinel/releases) ### 整合Sentinel 在`services(微服务模块的父级模块)`中导入Sentinel依赖 ```xml com.alibaba.cloud spring-cloud-starter-alibaba-sentinel ``` 为每个服务模块的`配置文件`中配置Sentinel控制台 ```yaml spring: cloud: sentinel: transport: dashboard: localhost:8858 enabled: true # 开启懒加载 eager: true ``` `浏览器`访问控制台URL:`localhost:8858`,登录 ![image-20250603231542459](https://fastly.jsdelivr.net/gh/xccit/blog_images/images_resource/image-20250603231542459.png) > 这样就完成了初步的整合 ### 测试接口流控 #### 1.定义资源 ```java @SentinelResource(value = "createOrder") @Override public Order createOrder(Long userId, Long productId) { // Product productByRemote = getProductByRemoteWithLoadBalancerAnnotation(productId); log.info("调用商品服务获取商品信息时间{}", System.currentTimeMillis()); Product productByRemote = productFeignClient.getProductById(productId); Order order = new Order(); order.setId(1L); order.setTotalAmount(new BigDecimal(String.valueOf(productByRemote.getPrice().multiply(new BigDecimal(productByRemote.getNum()))))); order.setUserId(userId); order.setAddress("甘肃省兰州市城关区雁北路400号"); order.setProductList(Arrays.asList(productByRemote)); log.info("调用商品服务获取商品信息后时间{}", System.currentTimeMillis()); return order; } ``` #### 2.重启并访问该接口 image-20250603232511712 #### 3.查看资源 ![image-20250603232605043](https://fastly.jsdelivr.net/gh/xccit/blog_images/images_resource/image-20250603232605043.png) #### 4.设置流控 ![image-20250603232716971](https://fastly.jsdelivr.net/gh/xccit/blog_images/images_resource/image-20250603232716971.png) ![image-20250603232752566](https://fastly.jsdelivr.net/gh/xccit/blog_images/images_resource/image-20250603232752566.png) > `QPS:`单机每秒访问量 > > `并发线程数:`单机每秒线程处理数量 这里设置为`单机每秒`只能处理`1`个请求 ![image-20250603233010362](https://fastly.jsdelivr.net/gh/xccit/blog_images/images_resource/image-20250603233010362.png) #### 5.重启测试 ![image-20250603234030853](https://fastly.jsdelivr.net/gh/xccit/blog_images/images_resource/image-20250603234030853.png) `当你快速刷新时,便会报错,需要注意:如果电脑处理快,可以把每秒处理的请求放多一点,比如设置为5,疯狂刷新浏览器,超过5次就会报错` ### 处理web异常 #### 1.定义统一资源返回对象 `放置在model模块下即可` ```java @Data @Builder @NoArgsConstructor @AllArgsConstructor @ToString public class AjaxResult { private Integer code; private String message; private T data; public static AjaxResult success() { return new AjaxResult<>(200, "操作成功", null); } public static AjaxResult success(T data) { if (data != null) { return new AjaxResult<>(200, "操作成功", data); } return new AjaxResult<>(200, "操作成功", null); } public static AjaxResult error(String message) { return new AjaxResult<>(500, message, null); } public static AjaxResult error(Integer code, String message) { return new AjaxResult<>(code, message, null); } } ``` #### 2.自定义BlockException ![image-20250604185651195](https://fastly.jsdelivr.net/gh/xccit/blog_images/images_resource/image-20250604185651195.png) 资源被限流后,报出的异常就是这个类抛出的,而捕获该异常的类为`BlockExceptionHandler`,只需自定义一个子类并加入容器即可生效。 ```java @Component public class CustomBlockExceptionHandler implements BlockExceptionHandler { @Override public void handle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, String resourceName, BlockException e) throws Exception { httpServletResponse.setContentType("application/json;charset=UTF-8"); PrintWriter writer = httpServletResponse.getWriter(); ObjectMapper objectMapper = new ObjectMapper(); AjaxResult result = AjaxResult.error("资源:"+ resourceName +"已被限流,原因:" + e.getMessage()); String json = objectMapper.writeValueAsString(result); writer.write(json); writer.flush(); writer.close(); } } ``` ### 3.重启测试 #### 3.1重新定义规则 > 由于限流规则默认保存在内存中,重启后会导致规则丢失,需要重新定义规则 #### 3.2测试 ![image-20250604191541859](https://fastly.jsdelivr.net/gh/xccit/blog_images/images_resource/image-20250604191541859.png) `这里没有捕获到限流原因,我们重新编写捕获逻辑` ```java @Component public class CustomBlockExceptionHandler implements BlockExceptionHandler { @Override public void handle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, String resourceName, BlockException e) throws Exception { httpServletResponse.setContentType("application/json;charset=UTF-8"); PrintWriter writer = httpServletResponse.getWriter(); ObjectMapper objectMapper = new ObjectMapper(); AjaxResult result = AjaxResult.error("资源:"+ resourceName +"已被限流,原因:" + e.getClass()); String json = objectMapper.writeValueAsString(result); writer.write(json); writer.flush(); writer.close(); } } ``` ![image-20250604192224862](https://fastly.jsdelivr.net/gh/xccit/blog_images/images_resource/image-20250604192224862.png) ### 处理业务方法异常 Sentinel默认的异常处理机制,如果业务方法没有指定异常处理,就会抛给SpringBoot使用默认异常处理机制,现在删掉`/order/create`接口的流控,给`OrderServiceImpl`的`createOrder`方法设置流控规则,看一下异常会怎么处理: ![image-20250612223241730](https://fastly.jsdelivr.net/gh/xccit/blog_images/images_resource/image-20250612223241730.png) ![image-20250612223344575](https://fastly.jsdelivr.net/gh/xccit/blog_images/images_resource/image-20250612223344575.png) 进入了SpringBoot默认的白页。 #### 1.指定异常处理 ```java @SentinelResource(value = "createOrder",blockHandler = "createOrderBlockHandler") @Override public Order createOrder(Long userId, Long productId) { ... } ``` 使用`blockHandler`指定一个与本方法签名一致的方法来兜底返回 ```java /** * 兜底方法 * @param userId * @param productId * @param e * @return */ public Order createOrderBlockHandler(Long userId, Long productId, BlockException e) { Order order = new Order(); order.setId(0L); order.setTotalAmount(new BigDecimal("0")); order.setUserId(0L); order.setAddress("异常信息:" + e.getMessage()); return order; } ``` ![image-20250612224441608](https://fastly.jsdelivr.net/gh/xccit/blog_images/images_resource/image-20250612224441608.png) ### 处理远程调用异常 ![image-20250612225204343](https://fastly.jsdelivr.net/gh/xccit/blog_images/images_resource/image-20250612225204343.png) 之前处理web异常时给远程调用客户端指定的fallback,如果触发异常就会执行,如果没有指定则继续使用SpringBoot的白页异常返回。