1、什么是OpenFeign?
1)feign 是Spring Cloud 组件中一个轻量的RestFul的HTTP服务客户端;Feign内置了 Ribbon,用来做客户端负载均衡,远程的调用服务服务中的服务。
2)OpenFeign 是spring cloud 对feign进行了封装,使其支持SpringMVC注解;OpenFeign
的注解 @FeignClient 可以解析@RequestMapping 注解下的接口,并通过动态代理的方式生成实现类,在实现类中做负载均衡并远程调用其他服务
2、OpenFeign 使用步骤:
2.1、引入依赖
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
2.2、在启动类上添加注解 @EnableFeignClients 来开启OpenFeign远程调用功能
@EnableFeignClients //开启远程调用
@EnableDiscoveryClient //开启服务注册,向服务注册中心Nacos注册当前服务
@SpringBootApplication
public class ProductApplication {
public static void main(String[] args) {
SpringApplication.run(ProductApplication.class, args);
}
}
2.3、定义feign远程调用接口
因为 openfeign 是声明式的,所以需要编写一个接口告诉springcloud 需要调用的目
标服务。
远程调用接口类上需要设置注解 @FeignClient,并指定远程调用的目标服务
定义用于远程调用具体功能的方法,@RequestMapping 注解设置目标服务的具体功
能的请求地址;接口方法参数是传入目标功能方法的参数,接口返回值用于接收目标
功能方法的返回值; Feign接口方法签名一般定义成 与 目标功能方法 一致
代码如下:
/**
* Feign远程调用服务,远程调用coupon
*/
@FeignClient(value = "gulimall-coupon",configuration = {FeignConfig.class})
public interface CouponFeignService {
@PostMapping("coupon/spubounds/save")
public R saveSpuBounds(@RequestBody SpuBoundsTo spuBounds);
@PostMapping("coupon/skufullreduction/saveinfo")
public R saveSkuReduction(@RequestBody SkuReductionTo skuReductionTo);
}
2.3)在业务层引入 接口 CouponFeignService ,直接调用CouponFeignService 中的方法
就可以完成远程调用,如下图所示:
public class Test{
@Autowired
private CouponFeignService couponFeignService;
public void FeignTest(){
SpuBoundsTo spuBounds = new SpuBoundsTo();
R result = couponFeignService.saveSpuBounds(spuBounds);
if(result.getCode() != 0){
log.error("远程调用失败");
}
}
}
2.4、feign 网络超时、重试次数和日志配置
服务之间网络调用肯定会有调用失败、超时的情况,feign默认是不打印日志的,
针对这些情况我们需要自己设置配置,下面定义一个配置类来配置打印日志、
失败重试次数、超时时间等
代码如下:
@Configuration
public class FeignConfig {
/**
* 配置请求重试次数
* @return
*/
@Bean
public Retryer feignRetryer(){
/**
* period:间隔时间
* maxPeriod:最大间隔时间
* maxAttempts:重试次数
*/
return new Retryer.Default(200, SECONDS.toMillis(2), 10);
}
/**
* 设置请求超时时间
*默认
* public Options() {
* this(10 * 1000, 60 * 1000);
* }
*
* connectTimeout:连接超时,防止由于服务器处理时间长而阻塞调用者。
* readTimeout:读超时,从连接建立时开始应用,在返回响应时间过长时触发。
*
*/
@Bean
Request.Options feignOptions() {
return new Request.Options(60 * 1000, 60 * 1000);
}
/**
* 打印请求日志
* feign日志4种级别:
* NONE: 默认不打印任何日志
* BASIC: 基础日志,仅打印请求url、请求方法、请求耗时、请求状态码
* HEADERS: 除了打印BASIC规定的日志外,还打印请求头和响应头
* FULL: 打印请求/应答所有信息,包括请求url、请求方法、耗时、请求状态码、请求消息(包括请求头和请求消息体)
* 应答消息(响应消息体和消息头)
*
* feign日志生效的前提是在工程配置文件application.yml中开启日志打印,如下所示:
* #开启日志打印
* logging:
* level:
* com.gulimall.product: debug #配置日志级别,表示 com.gulimall.product 包下的代码运行时都打印debug日志
*
* @return
*/
@Bean
public feign.Logger.Level multipartLoggerLevel() {
return feign.Logger.Level.FULL;
}
}
3、@FeignClient注解
@FeignClient 注解常用属性属性如下:
(1)name 、serviceId、value:
目标服务的名称,用于服务发现;
注意:若配置了url属性,则只表示一个名称,并不能用于服务发现
(2)url:
手动指定远程调用服务地址;若配置了url,则请求会不通过Ribbon直接请求
这个服务;一般用于测试阶段
(3)contextId:
表示当前接口生成的Bean在spring中的唯一标识
当目标服务(如:gulimall-coupon)的多个接口不想放到同一个FeignClient
接口中,但每个FeignClient接口设置的目标服务都是gulimall-coupon,如下;
//接口1
@FeignClient(value="gulimall-coupon")
public interface CouponFeignService {
@PostMapping("coupon/spubounds/save")
public R saveSpuBounds(@RequestBody SpuBoundsTo spuBounds);
}
//接口2
@FeignClient(value = "gulimall-coupon")
public interface CouponFeignService2 {
@PostMapping("coupon/skufullreduction/saveinfo")
public R saveSkuReduction(@RequestBody SkuReductionTo skuReductionTo);
}
这时服务启动会报FeignClient接口生成的Bean注入失败的错误,
The bean 'gulimall-coupon.FeignClientSpecification' could not be registered. A bean with that name has already been defined and overriding is disabled.
解决方式:
(3.1)在配置文件中加上下边的配置,表示允许spring 中允许名称一样的Bea
spring.main.allow-bean-definition-overriding=true
(3.2)在@FeignClient 注解中配置 contextId,如下所示:
//接口1
@FeignClient(value="gulimall-coupon",contextId = "coupon1")
public interface CouponFeignService {
@PostMapping("coupon/spubounds/save")
public R saveSpuBounds(@RequestBody SpuBoundsTo spuBounds);
}
//接口2
@FeignClient(value = "gulimall-coupon",contextId = "coupon2")
public interface CouponFeignService2 {
@PostMapping("coupon/skufullreduction/saveinfo")
public R saveSkuReduction(@RequestBody SkuReductionTo skuReductionTo);
}
环境启动后,FeignClient接口在spring 中的表示
注意:
在解析注解@EnableFeignClients 注册FeignClient接口过程中设置conxtextId的值
时,若没配置conxtextId 值,他会拿serviceId、name 、value 中的一个作为
contextId的值,代码如下:
//注册FeignClient
private void registerFeignClient(BeanDefinitionRegistry registry, AnnotationMetadata annotationMetadata, Map<String, Object> attributes) {
String className = annotationMetadata.getClassName();
BeanDefinitionBuilder definition = BeanDefinitionBuilder.genericBeanDefinition(FeignClientFactoryBean.class);
this.validate(attributes);
definition.addPropertyValue("url", this.getUrl(attributes));
definition.addPropertyValue("path", this.getPath(attributes));
String name = this.getName(attributes);
definition.addPropertyValue("name", name);
String contextId = this.getContextId(attributes);
definition.addPropertyValue("contextId", contextId);
definition.addPropertyValue("type", className);
definition.addPropertyValue("decode404", attributes.get("decode404"));
definition.addPropertyValue("fallback", attributes.get("fallback"));
definition.addPropertyValue("fallbackFactory", attributes.get("fallbackFactory"));
definition.setAutowireMode(2);
//拿contextId的值作为别名alias的一部分
String alias = contextId + "FeignClient";
AbstractBeanDefinition beanDefinition = definition.getBeanDefinition();
boolean primary = (Boolean)attributes.get("primary");
beanDefinition.setPrimary(primary);
//若配置了qualifier,则以qualifier 作为别名
String qualifier = this.getQualifier(attributes);
if (StringUtils.hasText(qualifier)) {
alias = qualifier;
}
BeanDefinitionHolder holder = new BeanDefinitionHolder(beanDefinition, className, new String[]{alias});
//注册bean
BeanDefinitionReaderUtils.registerBeanDefinition(holder, registry);
}
//
private String getContextId(Map<String, Object> attributes) {
String contextId = (String)attributes.get("contextId");
if (!StringUtils.hasText(contextId)) {
return this.getName(attributes);
} else {
contextId = this.resolve(contextId);
return getName(contextId);
}
}
//
String getName(Map<String, Object> attributes) {
String name = (String)attributes.get("serviceId");
if (!StringUtils.hasText(name)) {
name = (String)attributes.get("name");
}
if (!StringUtils.hasText(name)) {
name = (String)attributes.get("value");
}
name = this.resolve(name);
return getName(name);
}
(4)fallback:
远程调用执行异常的回调类,该异常回调类必须实现 FeignClient接口,
如下所示:
@FeignClient(value = "gulimall-coupon",contextId = "coupon2",fallback = ConponFeignFallBack.class)
public interface CouponFeignService2 {
@PostMapping("coupon/skufullreduction/saveinfo")
public R saveSkuReduction(@RequestBody SkuReductionTo skuReductionTo);
}
@Component
public class ConponFeignFallBack implements CouponFeignService2 {
@Override
public R saveSkuReduction(SkuReductionTo skuReductionTo) {
return R.error("远程调用失败");
}
}
(5)fallbackFactory:
工厂类,用于生成fallback类实例,通过这个属性我们可以实现每个接口
通用的容错逻辑,但不需要实现feignclient接口,如下:
@FeignClient(value="gulimall-coupon",contextId = "coupon1",fallbackFactory = ConponFeignFallBackFactory.class)
public interface CouponFeignService {
@PostMapping("coupon/spubounds/save")
public R saveSpuBounds(@RequestBody SpuBoundsTo spuBounds);
}
//泛型表示我们要处理那个FeignClient 接口的异常
@Component
public class ConponFeignFallBackFactory implements FallbackFactory<CouponFeignService> {
@Override
public CouponFeignService create(Throwable throwable) {
return new CouponFeignService() {
@Override
public R saveSpuBounds(SpuBoundsTo spuBounds) {
return R.error("远程回调异常");
}
};
}
}
(6)path:
定义 远程调用的统一前缀
(7)configuration:
指定Feign的配置文件,可以指定多个,如:configuration = {FeignConfig.class}
四、补充
1、Feign远程调用时,请求不包含页面Http请求的一些头数据的问题?
可以通过 feign.RequestInterceptor(拦截器) 的来解决这个问题,通过
RequestInterceptor 中的 apply 方法将 页面Http请求的请求头数据同步到 Feign 请求中
注意:若当前环境中存在 feign.RequestInterceptor 的实例,Feign 请求会自动调用
示例代码如下:
@Configuration
public class FeignConfig {
@Bean
public RequestInterceptor requestInterceptor(){
return new RequestInterceptor() {
/**
*
* @param requestTemplate Feign 请求
*/
@Override
public void apply(RequestTemplate requestTemplate) {
//1、获取当前HttpRequest 请求的请求头数据
//获取所有的请求属性
ServletRequestAttributes requestAttributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
//获取HttpRequest,HttpRequest
HttpServletRequest request = requestAttributes.getRequest();
//根据key获取http请求中的数据,如:Cookie
String cookie = request.getHeader("Cookie");
//2、将http请求的请求头数据同步到Feign请求中
//将cookie 添加到Feign请求中
requestTemplate.header("Cookie",cookie);
}
};
}
}