SpringCloud【OpenFeign】
相关官方文档:Spring Cloud OpenFeign Features :: Spring Cloud Openfeign
1,远程调用
1.1,声明式实现--调用自己的服务
注解驱动:
开启远程调用:@EnableFeignClients
指定远程地址:@FeignClient
指定请求方式:@GetMapping、@PostMapping、@DeleteMapping...
指定携带数据:@RequestHeader、@RequestParam、@RequestBody...
指定结果返回:响应模式
导入依赖:services模块下导入
<dependency><groupId>org.springframework.cloud</groupId><artifactId>spring-cloud-starter-openfeign</artifactId></dependency>
开启Feign:启动类加注解
package org.example.order;import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
import org.springframework.cloud.openfeign.EnableFeignClients;@EnableFeignClients//开启feign远程调用功能
@EnableDiscoveryClient
@SpringBootApplication
public class OrderMainApplication {public static void main(String[] args) {SpringApplication.run(OrderMainApplication.class, args);}
}
编写远程调用客户端:新建feign接口org.example.order.feign.ProductFeignClient
package org.example.order.feign;import org.example.producet.domain.Product;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;//需要调用的服务的名字
@FeignClient(value = "services-product")
public interface ProductFeignClient {/*** mvc注解的两个使用逻辑* @GetMapping如果是在controller注解下,代表接受对应get请求,@PathVariable代表接受对应参数,接受请求时,把{id}拿过来封装给id* @GetMapping如果是在FeignClient注解下,代表发送对应get请求,@PathVariable代表发送对应参数,谁调用这个方法谁传递id进来,放到{id}** 返回Product 把services-product返回的json字符串转Product*/@GetMapping("/getProductById/{id}")Product getProductById(@PathVariable("id") Long id);}
修改调用接口:orderServiceImlp
@Slf4j
@Service
public class OrderServiceImpl implements OrderService { @AutowiredProductFeignClient productFeignClient;@Overridepublic Order createOrder(Long userId, Long productId) {
// Product product = getProductByRemoteWithLoadBalancerByAnnotatin(productId);Product product = productFeignClient.getProductById(productId);Order order = new Order();order.setId(1l);//todo 远程调用order.setTotalAmount(product.getPrice().multiply(new BigDecimal(product.getNum())));order.setUserId(userId);order.setNickName("爱学习");order.setAddress("北京");//todo 远程调用order.setProductList(Arrays.asList(product));return order;}
}
测试发现openFeign自带负载均衡 ,如果开启了多个product服务,会负载均衡调用
1.2,第三方API--调用第三方服务
编写远程调用
/*** 测试直接远程调用三方api* value :此时没有注册中心,就随意命名* url:三方api地址*/
@FeignClient(value = "test",url = "https://www.baidu.com")
public interface EduTestFeignClient {//根据三方api接口文档,的地址和参数编写@PostMapping("/getInfo")String pageQueryInfo(@RequestParam("key")String key,@RequestParam("signature")String signature,@RequestBody RequestBodyVo requestBodyVo);
}
实现调用
@RestController
@RequestMapping("/testFeign")
public class TestClient {@Autowiredprivate testFeignClient testFeignClient;@GetMapping("/getInfo")public String getInfo(){RequestBodyVo requestBodyVo = new RequestBodyVo();requestBodyVo.setOrgId("123");String str = testFeignClient.pageQueryInfo("eduZHJ17J0ki9d4rXWb","srUAnLFEw2YP+RnB3PBVNsveJr35BuncUISX5x2rxZk=",requestBodyVo);System.out.println(str);return str;}
}
思考:(面试题)
客户端负载均衡和服务端负载均衡的区别?
客户端负载均衡:发起调用方,根据自己的负载均衡算法,选择一个对方服务,发起调用
客户端发起请求---注册中心获取所有地址---客户端选择一个--发起调用
服务端负载均衡:客户端只管发,服务端固定接口收到请求后,负载均衡分给自己的服务
客户端发起请求---服务端开启多个,但只对外暴露一个固定接口---请求过来之后,自己选择分给哪个服务--发起调用
2,进阶用法
2.1,日志
1,添加日志级别:yml添加
#日志级别
logging:level:org.example.order.feign: debug
2,添加日志全记录组件
package org.example.order.config;import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import feign.Logger;@Configuration
public class OrderConfig {/*** 日志全记录组件* @return*/@BeanLogger.Level feignLoggerLevel(){return Logger.Level.FULL;}
}
3,测试日志
3.1,TestController
@AutowiredApiFeignClient apiFeignClient;@GetMapping("/getApiInfo")public String getApiInfo(){return apiFeignClient.organization("cxxxxxxxxx");}
3.2,ApiFeignClient
package org.example.order.feign;import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;//需要调用的服务的名字
@FeignClient(value = "test", url = "https://devops.aliyun.com")
public interface ApiFeignClient {@GetMapping("/organization/{OrganizationId}")String organization(@PathVariable("OrganizationId") String OrganizationId);}
3.3,日志显示
2.2,超时配置
场景:A通过OpenFeign调用B,B宕机或者响应非常非常慢,则会导致A一直连接不上B或者连接上了一个读取不到数据,如果A一直等待B 的返回就会出现服务雪崩问题,(如果有服务调用A也会一直无法得到返回,相当于整条链路都卡死)
解决:限时等待,如果为超时,就正常返回,如果超时,就中断调用,此时有两种方式,一种就是直接返回错误信息,另一种就是返回兜底数据,兜底数据设置见后续笔记
超时时间
连接超时:connectTimeout(默认10s)
读取超时:readTimeout(默认60s)
此时我们只需要在商品服务加一个休眠时间,订单服务超过默认时间就会报错返回
@Service
public class ProduecServiceImpl implements ProductService {@Overridepublic Product getProductById(Long productId) {Product product = new Product();product.setId(productId);product.setPrice(new BigDecimal(10));product.setProducetName("苹果"+productId);product.setNum(3);try {TimeUnit.SECONDS.sleep(100);} catch (InterruptedException e) {throw new RuntimeException(e);}return product;}
}
如何设置超时时间
可以在原application,yml中添加配置,也可以新建yml,引入原application.yml,此处新建
application-feign.yml
spring:cloud:openfeign:client:config:#默认配置,如果有多个服务的时候,没有特别配置都使用默认配置default:#日志记录logger-level: full#连接超时时间 1sconnect-timeout: 1000#读取时间 2sread-timeout: 2000#客户端名字 ,没有像这样单独指定的,都用上面默认配置services-product:#日志记录logger-level: full#连接超时时间 3sconnect-timeout: 3000#读取时间 5sread-timeout: 5000
application,yml中引入 application-feign.yml
spring:profiles:include: feign
2.3,重试机制
场景:远程调用失败,还可以尝试多次调用,如果某次成功则返回成功,如果多次调用依然失败,则结束调用,返回错误
重试策略
间隔时间,最大间隔时间,最大尝试次数
比如间隔时间100ms,最大间隔时间1s,最大尝试次数5
第一次重复:100ms之后再次调用
第二次重复:100 x 1.5 ms之后调用
第三次重复:100 x 1.5 x 1.5 ms之后调用
以此类推..........要么达到最大间隔时间1s之后,就不再x1.5,而是直接按最大间隔时间 ,每秒重复一次,要么达到最大次数5次之后不再调用
此时可在orderConfig类里面添加配置
@Configuration
public class OrderConfig {/*** 重试器*/@BeanRetryer retryer(){return new Retryer.Default();}/*** Retryer.Default()默认参数如下:间隔100ms,最大间隔1s,最大重复5次* public Default() {* this(100L, TimeUnit.SECONDS.toMillis(1L), 5);* }* 如果需要自定义参数值:可以传参* @Bean* Retryer retryer(){* return new Retryer.Default(200,2,5);* }*/}
2.4,拦截器
请求拦截器:给远程服务发送请求之前,请求拦截器做最后一次拦截,可以对请求做定制化修改(比如给请求投添加业务字段),然后把请求发给远程请求
响应拦截器:远程服务处理完请求之后,把响应数据交给openFeign,openFeign使用响应拦截器,对响应数据进行预处理,处理完之后,把结果返回给调用服务
请求拦截器测试用例:
编写拦截器interceptor.XTokenRequestInterceptor.java
package org.example.order.interceptor;import feign.RequestInterceptor;
import feign.RequestTemplate;
import org.springframework.stereotype.Component;import java.util.UUID;//方式2,在拦截器类上加注解
@Component
public class XTokenRequestInterceptor implements RequestInterceptor {/*** 请求拦截器* @param requestTemplate 请求模板*/@Overridepublic void apply(RequestTemplate requestTemplate) {System.out.println("XTokenRequestInterceptor");//给请求头上加一个XTokenrequestTemplate.header("X-Token", UUID.randomUUID().toString());}
}
方式1,在application-feign.yml里面加配置,这种配置,仅对这个客户端有效
spring:cloud:openfeign:client:config:services-product:######添加拦截器配置,request-interceptors:- org.example.order.interceptor.XTokenRequestInterceptor
方法2,在拦截器类上加注解,openfeign官网有详细,会在容器中找对应一些组件,其中就有请求拦截器,就会自动应用
//方式2,在拦截器类上加注解
@Component
public class XTokenRequestInterceptor implements RequestInterceptor {/*** 请求拦截器* @param requestTemplate 请求模板*/@Overridepublic void apply(RequestTemplate requestTemplate) {System.out.println("XTokenRequestInterceptor");//给请求头上加一个XTokenrequestTemplate.header("X-Token", UUID.randomUUID().toString());}
}
拦截器写好之后,也生效之后,测试获取请求头内容
在商品的controller中添加获取
package org.example.product.controller;@RestController
public class ProductController {@AutowiredProductService productService;@GetMapping("/getProductById/{productId}")public Product getProductById(@PathVariable("productId") Long productId, HttpServletRequest request) {String header = request.getHeader("X-Token");System.out.println("product==="+header);Product product = productService.getProductById(productId);return product;}
}
请求调用之后,即可在商品的控制台查看header
2.4,兜底返回(fallback)
场景:远程调用时,调用超时或者调用错误,会返回错误信息,当不需要返回错误时,加一个返回兜底数据(默认数据,或者假数据,不影响后续业务),不报错,增加用户体验
首先编写一个兜底返回org.example.order.feign.fallback.ProductFeignClientFallback
实现ProductFeignClient 接口
package org.example.order.feign.fallback;
import java.math.BigDecimal;import org.example.order.feign.ProductFeignClient;
import org.example.producet.domain.Product;
import org.springframework.stereotype.Component;@Component
public class ProductFeignClientFallback implements ProductFeignClient {@Overridepublic Product getProductById(Long id) {Product product = new Product();product.setId(id);product.setPrice(new BigDecimal("0"));product.setProducetName("兜底回调的数据");product.setNum(0);return product;}
}
在ProductFeignClient 接口中添加fallback = ProductFeignClientFallback.class,这时,在远程调用成功则不管,如果调用失败,则会调用fallback,返回ProductFeignClientFallback里面编写的默认数据
package org.example.order.feign;import org.example.order.feign.fallback.ProductFeignClientFallback;
import org.example.producet.domain.Product;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;//需要调用的服务的名字
@FeignClient(value = "services-product",fallback = ProductFeignClientFallback.class)
public interface ProductFeignClient {@GetMapping("/getProductById/{id}")Product getProductById(@PathVariable("id") Long id);}
此功能需要配合sentinel使用,所以此时需要导入sentinel的包,order--pom.xml
<dependency><groupId>com.alibaba.cloud</groupId><artifactId>spring-cloud-starter-alibaba-sentinel</artifactId></dependency>
开启配置,在application-feign.yml配置文件添加,开启sentinel熔断功能
feign:sentinel:enabled: true
然后重启服务器,先测试连通,再关闭商品服务,测试无法连通商品服务的情况下,就会返回默认兜底数据