springCloud-openFeign使用原理 本文讲解spring-cloud
环境下的openFeign
的用法,探究spring-cloud
是如何让openfeign
开箱即用的。本文会假设读者已经熟练使用openfeign
,对openFeign
源码已有了解。
1.先让项目运行起来 新建spring-cloud
项目,添加如下依赖
1 2 3 4 <dependency > <groupId > org.springframework.cloud</groupId > <artifactId > spring-cloud-starter-openfeign</artifactId > </dependency >
下面以获取百度贴吧首页为例。
写一个接口,其中@FeignClient
注解上url属性
标识请求的域名,@GetMapping
标注请求method
和path
,@RequestParam
标注请求参数。
1 2 3 4 5 6 7 8 9 @FeignClient(name = "javaBaCLient", url = "https://tieba.baidu.com") public interface Baidu { @GetMapping(value = "/f") String getMainPage (@RequestParam("kw") String kw) ; }
启动类上添加注解@EnableFeignClients
。
将Badu.class
类注入到其某个类中,并调用其中的方法,可以看到spring-cloud
已经自动将上面的接口生成了代理类,一个开箱即用的案例就写好了。下面开始探究下它的实现原理和默认配置。
1 2 3 4 5 6 7 8 @Autowired private Baidu baidu;@PostConstruct public void test () { String java = baidu.getMainPage("java" ); System.out.println(java); }
总结 开箱即用,非常简单的就能将Demo跑起来。如果在已经存在的SpringBoot
项目上添加spring-cloud-openfeign
,要注意spring-cloud
的版本必须匹配。这里有一个网址是spring-cloud
项目的版本对应关系
https://start.spring.io/actuator/info
2.自动装配 这里可能你会很好去,spring-cloud
是怎么做的,他到底为我们做了什么?
首先从@EnableFeignClients
注解上找到org.springframework.cloud.openfeign.FeignClientsRegistrar
类
查看它的registerBeanDefinitions
方法,它做了两件事情。
找到@EnableFeignClients
注解,将其属性defaultConfiguration
对应的类注册为属性。
扫描带有@FeignClien
注解的类,将其注册为FeignClientFactoryBean
1 2 3 4 5 6 public void registerBeanDefinitions (AnnotationMetadata metadata, BeanDefinitionRegistry registry) { registerDefaultConfiguration(metadata, registry); registerFeignClients(metadata, registry); }
这里关键在FeignClientFactoryBean
类,这是一个工厂类
,它的getObject()
方法里面是真正生成Feign接口代理类的。细节请看注释。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 @Override public Object getObject () throws Exception { return getTarget(); } <T> T getTarget () { FeignContext context = this .applicationContext.getBean(FeignContext.class); Feign.Builder builder = feign(context); if (!StringUtils.hasText(this .url)) { if (!this .name.startsWith("http" )) { this .url = "http://" + this .name; } else { this .url = this .name; } this .url += cleanPath(); return (T) loadBalance(builder, context, new HardCodedTarget <>(this .type, this .name, this .url)); } if (StringUtils.hasText(this .url) && !this .url.startsWith("http" )) { this .url = "http://" + this .url; } String url = this .url + cleanPath(); Client client = getOptional(context, Client.class); if (client != null ) { if (client instanceof LoadBalancerFeignClient) { client = ((LoadBalancerFeignClient) client).getDelegate(); } builder.client(client); } Targeter targeter = get(context, Targeter.class); return (T) targeter.target(this , builder, context, new HardCodedTarget <>(this .type, this .name, url)); } protected Feign.Builder feign (FeignContext context) { FeignLoggerFactory loggerFactory = get(context, FeignLoggerFactory.class); Logger logger = loggerFactory.create(this .type); Feign.Builder builder = get(context, Feign.Builder.class) .logger(logger) .encoder(get(context, Encoder.class)) .decoder(get(context, Decoder.class)) .contract(get(context, Contract.class)); configureFeign(context, builder); return builder; }
总结 通过@EnableFeignClients
来引入一个ImportBeanDefinitionRegistrar
。这个类里面注册了默认配置,扫描了所有的@FeignClient
接口,将其注册为FactoryBean
,真实使用时将会调用这个FactoryBean
的getObject
方法,此时才会产生代理类,并且生成代理类需要的各种组件都是从容器里获取的。
3.默认配置 根据@EnableFeignClient
注解上的注释,找到了下面这个类org.springframework.cloud.openfeign.FeignClientsConfiguration
,这是一个配置类,会被自动扫描到,它里面注入了以下组件。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 @Bean @ConditionalOnMissingBean public Decoder feignDecoder () { return new OptionalDecoder ( new ResponseEntityDecoder (new SpringDecoder (this .messageConverters))); }@Bean @ConditionalOnMissingBean @ConditionalOnMissingClass("org.springframework.data.domain.Pageable") public Encoder feignEncoder () { return new SpringEncoder (this .messageConverters); }@Bean @ConditionalOnClass(name = "org.springframework.data.domain.Pageable") @ConditionalOnMissingBean public Encoder feignEncoderPageable () { return new PageableSpringEncoder (new SpringEncoder (this .messageConverters)); }@Bean @ConditionalOnMissingBean public Contract feignContract (ConversionService feignConversionService) { return new SpringMvcContract (this .parameterProcessors, feignConversionService); }@Bean @ConditionalOnMissingBean public Retryer feignRetryer () { return Retryer.NEVER_RETRY; }@Bean @Scope("prototype") @ConditionalOnMissingBean public Feign.Builder feignBuilder (Retryer retryer) { return Feign.builder().retryer(retryer); }@Bean @ConditionalOnMissingBean(FeignLoggerFactory.class) public FeignLoggerFactory feignLoggerFactory () { return new DefaultFeignLoggerFactory (this .logger); }@Configuration @ConditionalOnClass({ HystrixCommand.class, HystrixFeign.class }) protected static class HystrixFeignConfiguration { @Bean @Scope("prototype") @ConditionalOnMissingBean @ConditionalOnProperty(name = "feign.hystrix.enabled") public Feign.Builder feignHystrixBuilder () { return HystrixFeign.builder(); } }
总结 上面就是spring-cloud
对Feign
的默认配置了,他们都是以bean的形式放在容器里,我们可以自己提供上面任何组件取而代之,这样我们替换其中任何组件都是非常简单的。
4.默认客户端 Feign默认使用的是Java原生的客户端,我们实际使用时一般要用Apache Httpclient
或者okhttp
代替,spring-cloud
为我们默认提供了这两种框架的自动配置,且是自动选择的。
配置代码在org.springframework.cloud.openfeign.FeignAutoConfiguration
里面,看名字就知道这是一个自动装配的配置类。
如果存在HttpClient
和 feign.ApacheHttpClient
,则HttpClient
就会生效
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 @Configuration @ConditionalOnClass(ApacheHttpClient.class) @ConditionalOnMissingClass("com.netflix.loadbalancer.ILoadBalancer") @ConditionalOnMissingBean(CloseableHttpClient.class) @ConditionalOnProperty(value = "feign.httpclient.enabled", matchIfMissing = true) protected static class HttpClientFeignConfiguration { private final Timer connectionManagerTimer = new Timer ( "FeignApacheHttpClientConfiguration.connectionManagerTimer" , true ); @Autowired(required = false) private RegistryBuilder registryBuilder; private CloseableHttpClient httpClient; @Bean @ConditionalOnMissingBean(HttpClientConnectionManager.class) public HttpClientConnectionManager connectionManager ( ApacheHttpClientConnectionManagerFactory connectionManagerFactory, FeignHttpClientProperties httpClientProperties) { final HttpClientConnectionManager connectionManager = connectionManagerFactory .newConnectionManager(httpClientProperties.isDisableSslValidation(), httpClientProperties.getMaxConnections(), httpClientProperties.getMaxConnectionsPerRoute(), httpClientProperties.getTimeToLive(), httpClientProperties.getTimeToLiveUnit(), this .registryBuilder); this .connectionManagerTimer.schedule(new TimerTask () { @Override public void run () { connectionManager.closeExpiredConnections(); } }, 30000 , httpClientProperties.getConnectionTimerRepeat()); return connectionManager; } @Bean public CloseableHttpClient httpClient (ApacheHttpClientFactory httpClientFactory, HttpClientConnectionManager httpClientConnectionManager, FeignHttpClientProperties httpClientProperties) { RequestConfig defaultRequestConfig = RequestConfig.custom() .setConnectTimeout(httpClientProperties.getConnectionTimeout()) .setRedirectsEnabled(httpClientProperties.isFollowRedirects()) .build(); this .httpClient = httpClientFactory.createBuilder() .setConnectionManager(httpClientConnectionManager) .setDefaultRequestConfig(defaultRequestConfig).build(); return this .httpClient; } @Bean @ConditionalOnMissingBean(Client.class) public Client feignClient (HttpClient httpClient) { return new ApacheHttpClient (httpClient); } @PreDestroy public void destroy () throws Exception { this .connectionManagerTimer.cancel(); if (this .httpClient != null ) { this .httpClient.close(); } } }
如果存在okhttp
,则会使用下面的配置,这里就不做解释了。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 @Configuration @ConditionalOnClass(OkHttpClient.class) @ConditionalOnMissingClass("com.netflix.loadbalancer.ILoadBalancer") @ConditionalOnMissingBean(okhttp3.OkHttpClient.class) @ConditionalOnProperty("feign.okhttp.enabled") protected static class OkHttpFeignConfiguration { private okhttp3.OkHttpClient okHttpClient; @Bean @ConditionalOnMissingBean(ConnectionPool.class) public ConnectionPool httpClientConnectionPool ( FeignHttpClientProperties httpClientProperties, OkHttpClientConnectionPoolFactory connectionPoolFactory) { Integer maxTotalConnections = httpClientProperties.getMaxConnections(); Long timeToLive = httpClientProperties.getTimeToLive(); TimeUnit ttlUnit = httpClientProperties.getTimeToLiveUnit(); return connectionPoolFactory.create(maxTotalConnections, timeToLive, ttlUnit); } @Bean public okhttp3.OkHttpClient client (OkHttpClientFactory httpClientFactory, ConnectionPool connectionPool, FeignHttpClientProperties httpClientProperties) { Boolean followRedirects = httpClientProperties.isFollowRedirects(); Integer connectTimeout = httpClientProperties.getConnectionTimeout(); Boolean disableSslValidation = httpClientProperties.isDisableSslValidation(); this .okHttpClient = httpClientFactory.createBuilder(disableSslValidation) .connectTimeout(connectTimeout, TimeUnit.MILLISECONDS) .followRedirects(followRedirects).connectionPool(connectionPool) .build(); return this .okHttpClient; } @PreDestroy public void destroy () { if (this .okHttpClient != null ) { this .okHttpClient.dispatcher().executorService().shutdown(); this .okHttpClient.connectionPool().evictAll(); } } @Bean @ConditionalOnMissingBean(Client.class) public Client feignClient (okhttp3.OkHttpClient client) { return new OkHttpClient (client); } }
需要添加如下依赖,Apache Httpclient
才会自动配置,注意要和Feign版本一致。
1 2 3 4 5 <dependency > <groupId > io.github.openfeign</groupId > <artifactId > feign-httpclient</artifactId > <version > 10.4.0</version > </dependency >
总结 spring-cloud
底层是可以根据项目存在的依赖选择使用apache httpclient
或者 okhttp
来作为底层实现,而且默认配置都比较合理 ,几乎不用改动。
需要注意的一点是,项目内只是存在Apache Httpclient
的依赖是不够的,还需要添加feign-httpclient
的依赖做适配包才可以。okhttp
也是同理,都需要一个适配包。
5.其他配置 还有一个配置类也会被自动扫描到,上面缺少的client相关的bean会从这里获取org.springframework.cloud.commons.httpclient.HttpClientConfiguration
,以Apache Httpclient
为例。他的源码是这样的,他的方法都带有@ConditionalOnMissingBean
注解,所以优先级最低。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 @Configuration @ConditionalOnProperty(name = "spring.cloud.httpclientfactories.apache.enabled", matchIfMissing = true) @ConditionalOnClass(HttpClient.class) static class ApacheHttpClientConfiguration { @Bean @ConditionalOnMissingBean public ApacheHttpClientConnectionManagerFactory connManFactory () { return new DefaultApacheHttpClientConnectionManagerFactory (); } @Bean @ConditionalOnMissingBean public HttpClientBuilder apacheHttpClientBuilder () { return HttpClientBuilder.create(); } @Bean @ConditionalOnMissingBean public ApacheHttpClientFactory apacheHttpClientFactory ( HttpClientBuilder builder) { return new DefaultApacheHttpClientFactory (builder); } } .... .... ....
控制日志等级 配置loggerlevel bean
。FeignFactory
创建时,会查找Logger.Level
的实例。
1 2 3 4 5 @Bean public feign.Logger.Level level () { return feign.Logger.Level.FULL; }
FeignClient接口方法上的注解解析过程 代码里我们看到spring-cloud-openfeign
没有使用feign提供的Contract
,而是自己写了一个SpringMvcContract
,所以对接口的解析已经不是原来的思路了,使用的注解也不一样。下面做简单介绍。
首先是解析类上的注解,这里会解析类上RequestMapping
的value值作为url属性。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 @Override protected void processAnnotationOnClass (MethodMetadata data, Class<?> clz) { if (clz.getInterfaces().length == 0 ) { RequestMapping classAnnotation = findMergedAnnotation(clz, RequestMapping.class); if (classAnnotation != null ) { if (classAnnotation.value().length > 0 ) { String pathValue = emptyToNull(classAnnotation.value()[0 ]); pathValue = resolve(pathValue); if (!pathValue.startsWith("/" )) { pathValue = "/" + pathValue; } data.template().uri(pathValue); } } } }
然后是解析方法上的注解
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 @Override protected void processAnnotationOnMethod (MethodMetadata data, Annotation methodAnnotation, Method method) { if (!RequestMapping.class.isInstance(methodAnnotation) && !methodAnnotation .annotationType().isAnnotationPresent(RequestMapping.class)) { return ; } RequestMapping methodMapping = findMergedAnnotation(method, RequestMapping.class); RequestMethod[] methods = methodMapping.method(); if (methods.length == 0 ) { methods = new RequestMethod [] { RequestMethod.GET }; } checkOne(method, methods, "method" ); data.template().method(Request.HttpMethod.valueOf(methods[0 ].name())); if (methodMapping.value().length > 0 ) { String pathValue = emptyToNull(methodMapping.value()[0 ]); if (pathValue != null ) { pathValue = resolve(pathValue); if (!pathValue.startsWith("/" ) && !data.template().path().endsWith("/" )) { pathValue = "/" + pathValue; } data.template().uri(pathValue, true ); } } parseProduces(data, method, methodMapping); parseConsumes(data, method, methodMapping); parseHeaders(data, method, methodMapping); data.indexToExpander(new LinkedHashMap <Integer, Param.Expander>()); }
最后是解析参数上的注解
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 @Override protected boolean processAnnotationsOnParameter (MethodMetadata data, Annotation[] annotations, int paramIndex) { boolean isHttpAnnotation = false ; AnnotatedParameterProcessor.AnnotatedParameterContext context = new SimpleAnnotatedParameterContext ( data, paramIndex); Method method = this .processedMethods.get(data.configKey()); for (Annotation parameterAnnotation : annotations) { AnnotatedParameterProcessor processor = this .annotatedArgumentProcessors .get(parameterAnnotation.annotationType()); if (processor != null ) { Annotation processParameterAnnotation; processParameterAnnotation = synthesizeWithMethodParameterNameAsFallbackValue( parameterAnnotation, method, paramIndex); isHttpAnnotation |= processor.processArgument(context, processParameterAnnotation, method); } } if (isHttpAnnotation && data.indexToExpander().get(paramIndex) == null ) { TypeDescriptor typeDescriptor = createTypeDescriptor(method, paramIndex); if (this .conversionService.canConvert(typeDescriptor, STRING_TYPE_DESCRIPTOR)) { Param.Expander expander = this .convertingExpanderFactory .getExpander(typeDescriptor); if (expander != null ) { data.indexToExpander().put(paramIndex, expander); } } } return isHttpAnnotation; }
参数上的解析不是在此类里做的,而是委托给annotatedArgumentProcessors
,来做,默认有下面四个解析器,分别可以解析@PathVariable
, @RequestParam
,@RequestHeader,
@SpringQueryMap
注解。这里不展开将这四个解析器的源码了。
1 2 3 4 annotatedArgumentResolvers.add(new PathVariableParameterProcessor ()); annotatedArgumentResolvers.add(new RequestParamParameterProcessor ()); annotatedArgumentResolvers.add(new RequestHeaderParameterProcessor ()); annotatedArgumentResolvers.add(new QueryMapParameterProcessor ());
总结 上面就是SpringMvcContract
解析接口的过程,spring-cloud-openfeign
抛弃了原来feign的注解,而是复用了springmvc
里面的部分注解,降低了理解成本还是挺好的。
总结
以上就是spring-cloud
配置openfeign
的过程,读懂源码使用起来才能得心应手。从源码看出我们只要提供好底层依赖,依靠spring-cloud
的默认配置就能的到一个不错效果。
其实spring-cloud
只使用了openFeign
少部分自带功能,很多都是自己定制的如encoder decoder contract
。还有一些组件是直接选择了固定的值如retry选择了no_retry,log选择了slf4j-feign 等,主要是使用了Feign的思想而不是他的代码。
下篇文章我们讲一讲spring-cloud-Feign
负载均衡相关的源码,和spring-cloud-Feign组合断路器
相关的源码。