OpenFeign客户端是一个web声明式http远程调用工具,直接可以根据服务名称去注册中心拿到指定的服务IP集合,提供了接口和注解方式进行调用,内嵌集成了Ribbon本地负载均衡器。
1、底层都是内置了Ribbon,去调用注册中心的服务。
2、Feign是Netflix公司写的,是SpringCloud组件中的一个轻量级RESTful的HTTP服务客户端,是SpringCloud中的第一代负载均衡客户端。
3、OpenFeign是SpringCloud自己研发的,在Feign的基础上支持了Spring MVC的注解,如@RequesMapping等等。是SpringCloud中的第二代负载均衡客户端。
4、Feign本身不支持Spring MVC的注解,使用Feign的注解定义接口,调用这个接口,就可以调用服务注册中心的服务
5、OpenFeign的@FeignClient可以解析SpringMVC的@RequestMapping注解下的接口,并通过动态代理的方式产生实现类,实现类中做负载均衡并调用其他服务。
版本说明:
Spring Cloud Version:Hoxton.SR12
Spring Boot Version:2.3.12.RELEASE
不同版本源码可能会有差异
org.springframework.cloud spring-cloud-starter-openfeign
@SpringBootApplication @EnableFeignClients public class Application { public static void main(String[] args) { SpringApplication.run(Application.class, args); } }
@FeignClient(value = "kerwin-user",contextId = "userInfoClient") public interface UserInfoClient { /** 获取用户信息 */ @GetMapping("/user-info/info/{id}") String getInfo(@PathVariable("id") Long id); }
@FeignClient用于标记一个接口为Feign客户端,@FeignClient中的属性可以使用 ${feign.name} 这种方式取值
@Target(ElementType.TYPE) @Retention(RetentionPolicy.RUNTIME) @Documented @Inherited public @interface FeignClient { // name和value属性用于标注客户端名称,也可以用${propertyKey}获取配置属性 @AliasFor("name") String value() default ""; // 该类的Bean名称 PS: 这个配置很重要后续会详细介绍 String contextId() default ""; // name和value属性用于标注客户端名称,也可以用${propertyKey}获取配置属性 @AliasFor("value") String name() default ""; // 弃用 被qualifiers()替代。 @Deprecated String qualifier() default ""; // 模拟客户端的@Qualifiers值。如果qualifier()和qualifiers()都存在,我们将使用后者,除非qualifier()返回的数组为空或只包含空值或空白值,在这种情况下,我们将首先退回到qualifier(),如果也不存在,则使用default = contextId + "FeignClient"。 String[] qualifiers() default {}; // 绝对URL或可解析主机名 PS: 使用这个配置后只能定型发送,没有负载均衡能力 String url() default ""; // 是否应该解码404而不是抛出FeignExceptions boolean decode404() default false; // 用于模拟客户端的自定义配置类。可以包含组成客户端部分的覆盖@Bean定义,默认配置都在FeignClientsConfiguration类中,可以指定FeignClientsConfiguration类中所有的配置 Class>[] configuration() default {}; // 指定失败回调类 Class> fallback() default void.class; // 为指定的假客户端接口定义一个fallback工厂。fallback工厂必须生成fallback类的实例,这些实例实现了由FeignClient注释的接口。 Class> fallbackFactory() default void.class; // 所有方法级映射使用的路径前缀 String path() default ""; // 是否将虚拟代理标记为主bean。默认为true。 boolean primary() default true; }
@Retention(RetentionPolicy.RUNTIME) @Target(ElementType.TYPE) @Documented @Import(FeignClientsRegistrar.class) public @interface EnableFeignClients { // 扫描@FeignClient包地址 String[] value() default {}; // 用户扫描Feign客户端的包,也就是@FeignClient标注的类,与value同义,并且互斥 String[] basePackages() default {}; // basePackages()的类型安全替代方案,用于指定要扫描带注释的组件的包。每个指定类别的包将被扫描。 考虑在每个包中创建一个特殊的无操作标记类或接口,除了被该属性引用之外没有其他用途。 Class>[] basePackageClasses() default {}; // 为所有假客户端定制@Configuration,默认配置都在FeignClientsConfiguration中,可以自己定制 Class>[] defaultConfiguration() default {}; // 可以指定@FeignClient标注的类,如果不为空,就会禁用类路径扫描 Class>[] clients() default {}; }
我们使用的很多框架都会有自己的默认配置,尤其是和SpringBoot集成的starter包,OpenFeign集成SpringBoot的starter包同样的也会给我们做很多默认配置。
在 FeignClientsConfiguration 类中,OpenFeign为我们做了很多默认配置,这个默认配置我们都可以自定义并且覆盖。
自定义并且覆盖默认配置有两个维度,一个是使用全局配置另一个是每个FeignClient使用独立配置,这里以OpenFeign请求日志配置做介绍。
Feign远程调用时提供了日志打印功能,输出的日志级别为debug,先要把项目的输出日志设置为debug。
// 设置指定日志输出级别为debug,我这里将我自己com.kerwin包所有日志输出级别设置成了debug,根据自己需要来即可 logging: level: com: kerwin: debug
为了更加精细化控制日志输出,Feign还提供了日志内容输出的几个级别。
NONE:默认的,不显示任何日志。
BASIC:仅记录请求方法和URL以及响应状态代码和执行时间。
HEADERS:除了BASIC中定义的信息之外,还有请求和响应头的信息。
FULL:除了HEADERS中定义的信息之外,还有请求和响应的正文及元数据。
feign: client: config: # 默认配置 如果不单独配置每个服务会走默认配置 default: loggerLevel: FULL # 日志级别 NONE:不打印 BASIC:打印简单信息 HEADERS:打印头信息 FULL:打印全部信息 (默认 NONE)
feign: client: config: # 配置单独FeignClient # @FeignClient(value = "kerwin-user",contextId = "userInfoClient") # 如果FeignClient注解设置了contextId这里就使用userInfoClient如果没有设置contextId就直接使用服务名称kerwin-user userInfoClient: loggerLevel: FULL # 日志级别 NONE:默认不打印 BASIC:打印简单信息 HEADERS:打印头信息 FULL:打印全部信息(默认 NONE)
通过配置文件配置其实是最后才加载的,会将其它地方配置的信息全部顶掉有兴趣可以看看源码 FeignClientFactoryBean.configureFeign 这个方法会比其它配置加载后执行,会在这里实现替换, 替换顺序是Spring容器中的配置Bean -> 配置文件中的默认配置 -> 配置文件中的独立配置 。
PS:需要特别注意这个类上不能加@Configuration这类注解,如果被Spring扫描到了那么全局都会使用这一个配置
public class FeignCommonSpecification { @Bean @ConditionalOnMissingBean //这里如果不加@ConditionalOnMissingBean那么独立配置是无法生效的,原理在后面补充 public Logger.Level feignLoggerLevel() { return Logger.Level.FULL; } }
@EnableFeignClients(defaultConfiguration = FeignCommonSpecification.class)
要分析这个问题首先要知道Spring的 allowBeanDefinitionOverriding 参数是什么,这里简单解释一下,这个参数是控制注册 BeanDefinition 是否可以覆盖加载的,也就是说当这个参数为true时Bean的名称相同是可以覆盖的,在Spring的 DefaultListableBeanFactory 中我们可以看到这个参数默认为true,也就是说谁后加载那么在 BeanDefinitionMap 中就注册的就是谁,在 NamedContextFactory 的 createContext 方法中我们可以看到是先注册的独立配置然后在注册的缺省配置,到这里就很明显了缺省配置把独立配置顶替了,在缺省配置的方法中加上 @ConditionalOnMissingBean 可以解决这个问题,还有一点要提一下在SpringBoot中会将 allowBeanDefinitionOverriding 设置为false,但是每个 FeignClient 都会通过创建 NamedContextFactory 的 createContext 方法创建一个自己的 AnnotationConfigApplicationContext 子容器,这个子容器中 allowBeanDefinitionOverriding 默认还是true。
PS:需要特别注意这个类上不能加@Configuration这类注解,如果被Spring扫描到了那么全局都会使用这一个配置
public class UserInfoClientSpecification { @Bean public Logger.Level feignLoggerLevel() { return Logger.Level.FULL; } }
@FeignClient(value = "kerwin-user",contextId = "userInfoClient",configuration = UserInfoClientSpecification.class)
启动项目时出现这个错误信息
Description:
The bean ‘kerwin-user.FeignClientSpecification’ could not be registered. A bean with that name has already been defined and overriding is disabled.
Action:
Consider renaming one of the beans or enabling overriding by setting spring.main.allow-bean-definition-overriding=true
这个错误是因为出现了重名的Bean名称,在FeignClientsRegistrar 中注册Feign 配置信息到Spring 中时会组装一个Bean 的名称,如果在**@FeignClient** 中没有设置contextId 那么会使用value 作为组装名称的前缀,比如我们有多个**@FeignClient** 设置的value 服务名称为user 并且没有设置contextId ,那么在注册Bean 信息的时候BeanName 就会重复,Spring 原生是支持覆盖的,也就是说如果BeanName 相同那么后注册的就会将先注册的替换,但是在SpringBoot 中会将这个覆盖配置关闭,如果出现一样的BeanName 那么就会抛出上面那个异常信息,解决方法常用有两种开启第一种SpringBoot 覆盖配置、第二种在**@FeignClient** 中设置contextId ,推荐使用第二种,第一种方法别用很坑,网上很多人都是使用第一种方法解决我们公司也是使用的第一种😀。
开启Spring覆盖加载配置,Spring默认是开启的,SpringBoot默认关闭了有兴趣可以看看SpringApplication中的allowBeanDefinitionOverriding属性。
spring: main: allow-bean-definition-overriding: true
给每个@FeignClient都设置自己唯一的contextId
@FeignClient(value = "kerwin-user",contextId = "userInfoClient") public interface UserInfoClient { }
Spring Cloud OpenFeign提供了一个等价的@SpringQueryMap注释,用于将POJO或Map参数注释为查询参数Map,在参数前加上@SpringQueryMap即可。
@FeignClient(value = "demo",contextId = "demoClient") public interface DemoClient { @GetMapping(path = "/demo1") String demo1(@SpringQueryMap DemoDTO dto); }
OpenFeign超时有多种配置方式,这里介绍几种个人觉得最实用的
使用配置文件配置是最推荐的,也是在项目中使用最多的。
feign: client: config: # 默认配置 如果不单独配置每个服务会走默认配置 default: connectTimeout: 2000 # 连接超时时间 默认值:10000毫秒 readTimeout: 5000 # 读取超时时间 默认值:60000毫秒 # 配置单独FeignClient # @FeignClient(value = "kerwin-user",contextId = "userInfoClient") # 如果FeignClient注解设置了contextId这里就使用contextId=userInfoClient如果没有设置contextId就直接使用服务名称kerwin-user userInfoClient: connectTimeout: 2000 # 连接超时时间 默认值:10000毫秒 readTimeout: 5000 # 读取超时时间 默认值:60000毫秒
使用@FeignClient注解的configuration属性来指定配置类。
首先,创建一个配置类,继承自feign.Request.Options类
public class UserInfoClientSpecification extends Request.Options { public UserInfoClientSpecification() { // 设置连接超时时间为2秒,设置读取超时时间为5秒 super(2, TimeUnit.SECONDS, 5, TimeUnit.SECONDS, true); } }
然后,在使用@FeignClient注解进行声明时,使用configuration属性指定该配置类。
@FeignClient(value = "kerwin-user",contextId = "userInfoClient",configuration = UserInfoClientSpecification.class) public interface UserInfoClient{ }
在feign接口里加入Request.Options这个参数就可以单独为接口单独设置超时时间了
@GetMapping("/user-info/info/{id}") String getInfo(Request.Options options,@PathVariable("id") Long id);
调用的时候new 一下Options对象
String resp = userInfoClient.getInfo( new Request.Options(2, TimeUnit.SECONDS, 5, TimeUnit.SECONDS, true), 666L);
ribbon: ConnectTimeout: 2000 #默认 1000毫秒 ReadTimeout: 5000 #默认 1000毫秒
通过Ribbon配置文件设置超时时间只有在不做任何Feign的超时时间才有效,在LoadBalancerFeignClient.getClientConfig()方法中有一个判断,当Feign的 Request.Options = 默认的Request.Options时会使用Ribbon的超时配置。
当不做任何配置时OpenFeign默认是会在 FeignLoadBalancer.getRequestSpecificRetryHandler() 方法中获取请求重试处理器,这里可以看到第一个判断就是获取Ribbon中是否有开启 OkToRetryOnAllOperations 配置,如果Ribbon配置中开启了那么连接超时和和其它异常导致出问题都会进行重试,重试的次数也会使用Ribbon中配置的重试次数参数。
如果Ribbon没有开启 OkToRetryOnAllOperations 配置就会进行下面的判断,当请求不为 GET 请求时只会对连接超时异常进行重试,对其它异常不会重试,因为增删改请求做重试的话可能会导致同一个数据被插入两次,对查询请求做重试不会影响正常业务。
我这里调试使用的是 GET 请求,会进最下面一个方法,所有异常都会重试,这里重点看一下 **RequestSpecificRetryHandler ** 这个类,这个类是Ribbon处理重试机制的核心类,调试进入这个类的构造方法可以看到封装了重试所需要的一些参数,这些参数都是拿的Ribbon默认的参数在后面会具体说如何配置。
还有一点需要注意如果配置OpenFeign的其它配置,比如在配置文件中配置了Feign的default配置那么这里读取到的IClientConfig中的配置信息就不会是Ribbon的而是OpenFeign自己的配置,那么这个就没有Ribbon这两个重试次数的配置,结合上面几点其实不建议使用Ribbon的重试配置。
总结:也就是说OpenFeign不做任何配置,默认会对不同服务实例切换重试一次,如果不是GET请求那么只会对连接超时进行重试,如请求超时则不会重试,如果对Feign做了哪怕一个配置比如在配置文件中配置了Feign的default请求超时配置那么就无法使用Ribbon的两个重试次数的配置参数默认就是不会进行重试。
一定需要注意一点,如果对Feign做了哪怕一个配置比如在配置文件中配置了Feign的default请求超时配置那么就无法使用Ribbon的两个重试次数的配置参数,默认就是不会进行重试
ribbon: MaxAutoRetries: 1 #同一台实例最大重试次数,不包括首次调用 默认0 MaxAutoRetriesNextServer: 1 #服务实例切换重试次数 默认1 OkToRetryOnAllOperations: true #是否开启重试机制 默认关闭
在 FeignLoadBalancer.getRequestSpecificRetryHandler() 方法的入参IClientConfig中可以看到properties中的两个重试次数配置还有是否开启重试机制开关
定义一个类继承Retryer.Default
/** * 重试策略 默认 重试间隔100毫秒 最大间隔时间1秒 重试5次 */ @Slf4j public class CommonFeignRetry extends Retryer.Default { public CommonFeignRetry() { // 重试间隔是每次失败之后会等待100毫秒再次发起重试请求的间隔时间 // 最大间隔时间是每次重试的间隔时间累加起来不能超过这个最大间隔时间,如果超过了就不会在重试,哪怕还没有达到配置的重试次数 // 重试次数会受最大间隔时间和重试间隔时间影响,如果累计间隔时间超过这个最大间隔时间就不会在重试 // 重试间隔100毫秒 最大间隔时间1秒 重试5次 this(100, SECONDS.toMillis(1), 5); } public CommonFeignRetry(long period, long maxPeriod, int maxAttempts) { super(period, maxPeriod, maxAttempts); } @Override public void continueOrPropagate(RetryableException e) { log.warn("【FeignRetryAble】Message【{}】", e.getMessage()); super.continueOrPropagate(e); } @Override public Retryer clone() { return new CommonFeignRetry(); } }
通过配置文件加载重试配置类
可以做全局默认配置,也可以单独给某个FeignClient配置
feign: client: config: # 默认配置 如果不单独配置每个服务会走默认配置 default: retryer: com.kerwin.config.CommonFeignRetry # 配置单独FeignClient # @FeignClient(value = "kerwin-user",contextId = "userInfoClient") # 如果FeignClient注解设置了contextId这里就使用contextId=userInfoClient如果没有设置contextId就直接使用服务名称kerwin-user userInfoClient: retryer: com.kerwin.config.CommonFeignRetry
默认的Retryer是在FeignClientsConfiguration 配置类的feignRetryer() 方法中加载的,如果出现异常会直接抛出这个异常不会进行重试处理,在SynchronousMethodHandler 的invoke() 方法中可以看到调用的是那个Retryer,有兴趣可以看看源码
PS:除了可以通过配置文件配置,当然也可以通过配置类配置,方法和日志配置差不多。
首先要引入spring-retry包,可以不用写版本会使用SpringBoot夫包指定的版本
org.springframework.retry spring-retry
如果我们需要为特定的请求设置不同的重试策略,则可以在对应的方法上加上 @Retryable 注解,并指定对应的 Retryer 类型,如下所示:
@FeignClient(value = "kerwin-user",contextId = "userInfoClient") public interface UserInfoClient{ @GetMapping("/user-info/get-user-name/{id}") @Retryable(maxAttempts = 2) String getUserName(@PathVariable("id") Long id); }
使用上述方式,我们可以为每个请求设置不同的重试策略,从而更加灵活地处理重试问题。
PS:
要注意一个问题,如果项目引入了spring-retry包就算不配置重试GET请求也会默认服务实例切换重试1数使用的是Ribbon的配置,
spring-retry包这里使用的是RetryableFeignLoadBalancer来进行的请求调用和重试处理,在RetryableFeignLoadBalancer.execute()方法中可以看到会去封装一个RetryTemplate,这个RetryTemplate中会设置一个RetryPolicy,如果没有单独配置重试就会使用Ribbon的RibbonLoadBalancedRetryPolicy,在RibbonLoadBalancedRetryPolicy中的变量RibbonLoadBalancerContext里就能看见是如果使用Ribbon配置来进行的重试次数判断
Feign远程调用时提供了日志打印功能,输出的日志级别为debug,先要把项目的输出日志设置为debug。
// 设置指定日志输出级别为debug,我这里将我自己com.kerwin包所有日志输出级别设置成了debug,根据自己需要来即可 logging: level: com: kerwin: debug
为了更加精细化控制日志输出,Feign还提供了日志内容输出的几个级别。
NONE:默认的,不显示任何日志。
BASIC:仅记录请求方法和URL以及响应状态代码和执行时间。
HEADERS:除了BASIC中定义的信息之外,还有请求和响应头的信息。
FULL:除了HEADERS中定义的信息之外,还有请求和响应的正文及元数据。
feign: client: config: # 默认配置 如果不单独配置每个服务会走默认配置 default: loggerLevel: FULL # 日志级别 NONE:不打印 BASIC:打印简单信息 HEADERS:打印头信息 FULL:打印全部信息 (默认 NONE) # 配置单独FeignClient # @FeignClient(value = "kerwin-user",contextId = "userInfoClient") # 如果FeignClient注解设置了contextId这里就使用userInfoClient如果没有设置contextId就直接使用服务名称kerwin-user userInfoClient: loggerLevel: FULL # 日志级别 NONE:默认不打印 BASIC:打印简单信息 HEADERS:打印头信息 FULL:打印全部信息(默认 NONE)
public class UserInfoClientSpecification { @Bean public Logger.Level feignLoggerLevel() { return Logger.Level.FULL; } }
@FeignClient(value = "kerwin-user",contextId = "userInfoClient",configuration = UserInfoClientSpecification.class)
拦截器是OpenFeign可用的一种强大的工具,它可以被用来在请求和响应前后进行一些额外的处理
public class MyHeaderInterceptor implements RequestInterceptor { private static String headerName = "token"; @Override public void apply(RequestTemplate requestTemplate) { // 在这里添加额外的处理逻辑,添加请求头 RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes(); if (requestAttributes instanceof ServletRequestAttributes) { ServletRequestAttributes attributes = (ServletRequestAttributes) requestAttributes; HttpServletRequest request = attributes.getRequest(); String value = request.getHeader(headerName); requestTemplate.header(headerName, value); } } }
在配置文件中添加拦截器配置
feign: client: config: # 默认配置 如果不单独配置每个服务会走默认配置 default: request-interceptors: - com.kerwin.config.MyHeaderInterceptor # 配置单独FeignClient # @FeignClient(value = "kerwin-user",contextId = "userInfoClient") # 如果FeignClient注解设置了contextId这里就使用userInfoClient如果没有设置contextId就直接使用服务名称kerwin-user userInfoClient: request-interceptors: - com.kerwin.config.MyHeaderInterceptor
除了在配置文件中配置同时也能在配置类中配置,不过要注意加载优先级问题,推荐使用配置文件。
添加POM
io.github.openfeign feign-httpclient
添加配置
feign: httpclient: enabled: true # HttpClient的开关 max-connections: 200 # 线程池最大连接数 max-connections-per-route: 50 # 单个请求路径的最大连接数
添加POM
io.github.openfeign feign-okhttp
添加配置
feign: httpclient: max-connections: 200 # 线程池最大连接数 okhttp: enabled: true
调用的是在 FeignLoadBalancer.execute() 方法中,断点可以看到request的client为OkHttpClient,如果不替换就是HttpURLConnection
在配置文件中开启hystrix默认是关闭的
feign: # 是否开启hystrix 默认false hystrix: enabled: true
实现自己对应的FeignClient接口,重写其中的方法,该类一定要能被Spring扫描到,不然无法加载
@Component public class UserInfoClientFallback implements UserInfoClient { @Override public String getInfo(Long id) { return "服务繁忙"; } }
将fallback配置到@FeignClient中
@FeignClient(value = "kerwin-user",contextId = "userInfoClient",fallback = UserInfoClientFallback.class) public interface UserInfoClient { /** * 获取用户信息 */ @GetMapping("/user-info/info/{id}") String getInfo(@PathVariable("id") Long id); }
当调用对应方法出现异常时则会自动回调UserInfoClientFallback 中重写的方法。
使用@FeignClient中的fallbackFactory需要自己去实现FallbackFactory接口,泛型使用自己对应UserInfoClient,我们实现FallbackFactory接口的create方法可以看到入参是一个Throwable,我们可以对异常类型和异常信息进行对应的处理,该类也一定要能被Spring扫描到,不然无法加载。
PS:要实现的是feign.hystrix.FallbackFactory别实现了org.springframework.cloud.openfeign.FallbackFactor
@Component public class UserInfoClientFallbackFactory implements FallbackFactory{ @Override public UserInfoClient create(Throwable cause) { UserInfoClient userInfoClient = new UserInfoClient() { @Override public String getInfo(Long id) { cause.printStackTrace(); return "异常msg=" + cause.getMessage(); } }; return userInfoClient; } }
将我们实现的FallbackFactory配置到@FeignClient中
@FeignClient(value = "kerwin-user",contextId = "userInfoClient",fallbackFactory = UserInfoClientFallbackFactory.class) public interface UserInfoClient { /** * 获取用户信息 */ @GetMapping("/user-info/info/{id}") String getInfo(@PathVariable("id") Long id); }
通过配置文件给Ribbon配置负载均衡算法只能单独给某个服务配置,和SpringBoot集成默认的负载均衡算法为ZoneAvoidanceRule,使用Ribbon的全局配置是无效的,下面会解释为什么。
# 给kerwin-user服务单独配置随机负载均衡算法 kerwin-user: ribbon: NFLoadBalancerRuleClassName: com.netflix.loadbalancer.RandomRule # 随机算法
要探究这个问题需要看一下Ribbon和SpringBoot是如何集成的,和OpenFeign其实也类似,Ribbon会给每个服务名称都生成一个自己的Spring上下文来管理自己需要的Ribbon的一些组件Bean,比如负载均衡算法组件,Ribbon这里也会提供一个类似FeignClientsConfiguration 的缺省配置类RibbonClientConfiguration ,在RibbonClientConfiguration中的ribbonRule() 方法中可以看到如何加载的负载均衡算法组件。
跟进去propertiesFactory.isSet() 方法一直到PropertiesFactory.getClassName() 方法就能看到是如何获取的我们的配置,这里会组装一个key值去环境变量中获取对应的配置值,如果有配置则返回对应的值,如果没有配置则返回空,这个key拼接后为kerwin-user.ribbon.NFLoadBalancerRuleClassName ,正好可以和我们的单独配置对应上,如果是使用全局配置这里是读取不到的。