Feign源码及一些理解
系类文章
1、Spring源码分析
2、Mybatis源码
文章目录
前言
这是个人技术学习总结的相关文章,是对自己的查缺补漏。
一、服务之间调用的几种方式
1、http工具
httpclient、okhttp等。RestTemplate是Spring提供的一种优雅的http调用
这种的主要是需要自己封装一下,做成通用工具来使用,没使用过的需要进行了解之后才能使用。
1.1、http协议
超文本传输协议(Hypertext Transfer Protocol,HTTP)是一个简单的请求-响应协议,它通常运行在TCP之上。它指定了客户端可能发送给服务器什么样的消息以及得到什么样的响应。即可以用在浏览器作为客户端向服务端发起请求的场景,也可以用在服务器和服务器之间一方请求另一方,运行在TCP之上在处理应用层数据的包装时需要添加一些相关的信息,可能有些信息是在服务之间调用时是无意义的。
1.2、http协议分析
http协议位于网络分层的应用从,主要负责数据的组织,可以传输如json、xml、text、以及图片的二进制数据的base64编码etc。由于设计时的应用场景是超文本的传输协议 所以设计的稍微负责,在传输效率上有域场景的原因效率上不是很高。目前http2.0协议采用二进制格式传输数据而不像1.1的文本形式,采用多路复用解决了1.1长连接时阻塞的问题。对消息头采用Hpack进行压缩传输,能够节省消息头占用的网络流量,http1.1每次请求,都会携带大量冗余的头信息,浪费了很多宽带资源。但是目前并没有普及开。
2、rpc框架
dubbo、grpc、thrift、springcloud【feign】
这种主要是适用于微服务中。
2.1、rpc协议【Remote Procedure Call Protocol】
RPC采用客户机/服务器模式。请求程序就是一个客户机,而服务提供程序就是一个服务器。首先,调用进程发送一个有进程参数的调用信息到服务进程,然后等待应答信息。
2.2、rcp协议分析
rcp协议主要是服务之间相互调用,所以设计之初就是为了解决多机情景下进程的相互调用,故协议的设计上就比较专一,当然我们也可以基于http协议来设计RPC框架。
dubbo: dubbo默认使用dubbo协议【组织数据】使用Hessian 二进制序列化【二进制】底层使用netty基于tcp协议。连接方式是长连接
适用范围:传入传出参数数据包较小(建议小于100K),消费者比提供者个数多,单一消费者无法压满提供者,尽量不要用dubbo协议传输大文件或超大字符串。
为什么要消费者比提供者个数多?
因dubbo协议采用单一长连接,假设网络为千兆网卡(1024Mbit=128MByte),根据测试经验数据每条连接最多只能压满7MByte(不同的环境可能不一样,供参考),理论上1个服务提供者需要20个服务消费者才能压满网卡为什么不能传大包?
因dubbo协议采用单一长连接,如果每次请求的数据包大小为500KByte,假设网络为千兆网卡(1024Mbit=128MByte),每条连接最大7MByte(不同的环境可能不一样,供参考),单个服务提供者的TPS(每秒处理事务数)最大为:128MByte / 500KByte = 262。单个消费者调用单个服务提供者的TPS(每秒处理事务数)最大为:7MByte / 500KByte = 14。如果能接受,可以考虑使用,否则网络将成为瓶颈。为什么采用异步单一长连接?
因为服务的现状大都是服务提供者少,通常只有几台机器,而服务的消费者多,可能整个网站都在访问该服务,比如Morgan的提供者只有6台提供者,却有上百台消费者,每天有1.5亿次调用,如果采用常规的hessian服务,服务提供者很容易就被压跨,通过单一连接,保证单一消费者不会压死提供者,长连接,减少连接握手验证等,并使用异步IO,复用线程池,防止C10K问题。接口增加方法,对客户端无影响,如果该方法不是客户端需要的,客户端不需要重新部署;
输入参数和结果集中增加属性,对客户端无影响,如果客户端并不需要新属性,不用重新
部署;输入参数和结果集属性名变化,对客户端序列化无影响,但是如果客户端不重新部署,不管输入还是输出,属性名变化的属性值是获取不到的。
总结:服务器端 和 客户端 对 领域对象 并不需要完全一致,而是按照最大匹配原则。
SpringCloud
目前最新的已经基于http2.0协议而Feign组件就是来实现RPC的,不过SpringCloud的feign组件一般是和Hystrix和Ribbon一起使用的,不过也支持去除熔断和负载来单独使用。
3、SpringCloud和Dubbo孰优孰劣
因为HTTP协议是一套广泛的应用层协议所以大家都支持,使用的时候只要组织好格式就可以,而rpc协议不是都支持的,就像是方言都讲方言才能又块又明白,有一个不懂得就聊不下去,而http就像是普通话 都能听得懂。所i传输得时候需要相同得序列化和反序列化。不过RPC协议可以更好规避掉http协议在rpc场景下得非必要参数。
二、基于当前现状的分析
当前公司主要使用的是RestTemplate方式进行系统之间的调用,这种方式也比较便捷,但是每次都需要将URL编码在参数中,这样的使用方式其实类似于httpclient客户端工具,但是这种方式个人感觉有以下弊端:
-
在哪里使用就要在哪里体现一下地址,如果服务过多,没办法知道都调用了哪些服务,以及地址都是什么,可以看一下下面的代码段
JSONArray materialTypes; try { result = restTemplate.getForEntity(url, JSONObject.class); materialTypes = result.getBody().getJSONArray("data"); } catch (Exception e) { log.error(XXXXXXXXXXXXXXXXXXXXXXXXX,URL:{}", dzssUrl, e); return; } //------------------------------------------// /** * 可以将URL统一到一个枚举中进行使用,但是依旧会有这种大量得调用代码出现, */ ResponseEntity<JSONObject> responseEntity = restTemplate.postForEntity(url, requestMap, JSONObject.class);
-
三方服务之间的调用需要记录相关的log以及耗时restTemplate 需要自己进行处理。统一处理所有三方调用的log比较麻烦
-
不是面向接口编程。
1、使用Feign的可行性分析
-
feign虽然是SpringCloud 中的一员,本身和ribbon+hystrix可以完成微服务的负载、熔断、rpc,feign在微服务中可以通过注册中心获取服务列表来进行rpc和ribbon进行负载,我们在微服务中的使用时可以指定eureka中配置的app-name,此外也可以通过指定URL的方式进行服务间的调用,这样可以脱离注册中心来进行使用。
-
feign是面向接口编程,用户在使用是自己封装一层三方接口就可以,默认底层使用的是JDK的java.net.HttpURLConnection性能上存在一定的问题,但是feign提供了相应的扩展我们可以拓展使用http-client或okhttp做为底层的默认实现。
-
feign的客户端实现类:
1) Client.Default类:默认的 feign.Client 客户端实现类,内部使用HttpURLConnnection 完成HTTP URL请求处理;
2) ApacheHttpClient 类:内部使用 Apache httpclient 开源组件完成HTTP URL请求处理的feign.Client 客户端实现类;
3) OkHttpClient类:内部使用 OkHttp3 开源组件完成HTTP URL请求处理的feign.Client 客户端实现类。
4) LoadBalancerFeignClient 类:这是一个特殊的 feign.Client 客户端实现类。内部先使用 Ribbon 负载均衡算法计算server服务器,然后使用包装的 delegate 客户端实例,去完成 HTTP URL请求处理。 -
使用服务名的方式配置需要基于注册中心提供服务列表或者在yml文件中配置多个服务节点来进行调用以及进行负载。如果我们使用URL的方式那么就可以直接进行调用。
2、在项目中使用Feign的益处
- Feign是Netflix开发的声明式、模板化的HTTP客户端, Feign可以帮助我们更快捷、优雅地调用HTTP API。
- 首先面向接口编程,像编写Controller方法一样编写三方调用接口,
- 编译三方服务的统一日志管理
- 将所有三方的调用维护在一个包或者一个接口中。
3、集成Feign组件
3.1、 POM文件引入
- 版本控制
<properties> <java.version>1.8</java.version> <!-- openfeign --> <fegin.version>2.2.6.RELEASE</fegin.version> <fegin.http.client.version>11.0</fegin.http.client.version> </properties>
- 依赖管理
<dependencyManagement> <dependencies> <!-- https://mvnrepository.com/artifact/org.springframework.cloud/spring-cloud-starter-openfeign --> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-openfeign</artifactId> <version>${fegin.version}</version> </dependency> <!-- https://mvnrepository.com/artifact/io.github.openfeign/feign-httpclient --> <dependency> <groupId>io.github.openfeign</groupId> <artifactId>feign-httpclient</artifactId> <version>${fegin.http.client.version}</version> </dependency> </dependencies> </dependencyManagement>
- 引入
<dependencies> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-openfeign</artifactId> </dependency> <dependency> <groupId>io.github.openfeign</groupId> <artifactId>feign-httpclient</artifactId> </dependency> </dependencies>
3.2、启动类代码
package com.Xxiii.xx.server; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.boot.web.embedded.tomcat.TomcatServletWebServerFactory; import org.springframework.boot.web.servlet.server.ConfigurableServletWebServerFactory; import org.springframework.cache.annotation.EnableCaching; import org.springframework.cloud.openfeign.EnableFeignClients; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.ComponentScan; import org.springframework.context.annotation.EnableAspectJAutoProxy; import org.springframework.context.annotation.PropertySource; /** * @author xy */ @EnableCaching @EnableAspectJAutoProxy //启动Feign @EnableFeignClients(basePackages = {"com.xx.xx.*"}) @SpringBootApplication @ComponentScan({"com.xx.xx.*"}) @PropertySource(ignoreResourceNotFound = true,value = {"",""}) public class XyApplication { public static void main(String[] args) { SpringApplication.run(XyApplication .class, args); } @Bean public ConfigurableServletWebServerFactory webServerFactory() { TomcatServletWebServerFactory factory = new TomcatServletWebServerFactory(); factory.addConnectorCustomizers(connector -> connector.setProperty("relaxedQueryChars", "|{}[]")); return factory; } }
3.3、yml配置
feign: httpclient: enabled: true
3.4、rmi模块代码
package com.xx.xx.rmi.service; import org.springframework.cloud.openfeign.FeignClient; import org.springframework.web.bind.annotation.*; /** * XxService * * @author xy * @version 1.0 * @description业务类 * @date 2020/12/10 16:25 */ @FeignClient( name = "xxService" ,url="${xx.url}") public interface XxService { /** * XXXXXXXXXXX * XXX工具 * @param x * @param x * @return */ @PostMapping(value = "/api/v1/x/{xx}/actions/xx") String getXX(@PathVariable("x") String x, @RequestParam("xx") String xx); }
3.5、Controller调用代码
/** * SearchService * * @author xuyang * @version 1.0 * @description 搜索业务类 * @date 2020/12/10 16:05 */ @Slf4j @Service public class SearchService { @Resource XxService xxService; }
4、原理
4.1.启用
@EnableFeignClients(basePackages = {"com.xx.xx.*"})
@Retention(RetentionPolicy.RUNTIME) @Target(ElementType.TYPE) @Documented @Import(FeignClientsRegistrar.class) public @interface EnableFeignClients {
org.springframework.cloud.openfeign.FeignClientsRegistrar
- ImportBeanDefinitionRegistrar类只能通过其他类@Import的方式来加载,通常是启动类或配置类。
- 使用@Import,如果括号中的类是ImportBeanDefinitionRegistrar的实现类,则会调用接口方法,将其中要注册的类注册成bean。
- 实现该接口的类拥有注册bean的能力。
public void registerBeanDefinitions(AnnotationMetadata metadata, BeanDefinitionRegistry registry) { registerDefaultConfiguration(metadata, registry); registerFeignClients(metadata, registry); }
org.springframework.cloud.openfeign.FeignClientsRegistrar#registerFeignClients
org.springframework.cloud.openfeign.FeignClientsRegistrar#registerFeignClientBeanDefinitionBuilder definition = BeanDefinitionBuilder .genericBeanDefinition(FeignClientFactoryBean.class);
- 这块主要是Springboot项目启动时会扫描启动类上的注解 ,让后将扫描到的接口类的名字作为key ,FeignClientFactoryBean作为value注册到应用上下文中。等到getBean的时候利用工厂Bean的getObject()方法来回去代理类。
4.2.获取对象
FeignClientFactoryBean接口实现了FactoryBean。
@Override public Object getObject() throws Exception { return getTarget(); }
通过FactoryBean来获取对象的时候,会调用org.springframework.cloud.openfeign.FeignClientFactoryBean#getTarget
Targeter targeter = get(context, Targeter.class); return (T) targeter.target(this, builder, context, new HardCodedTarget<>(type, name, url));
/** * @author Spencer Gibb */ interface Targeter { <T> T target(FeignClientFactoryBean factory, Feign.Builder feign, FeignContext context, Target.HardCodedTarget<T> target); }
org.springframework.cloud.openfeign.HystrixTargeter
org.springframework.cloud.openfeign.DefaultTargeter只有这两个类实现了targeter具体用哪个取决于如下配置
/** * @author Spencer Gibb * @author Julien Roy */ @Configuration(proxyBeanMethods = false) @ConditionalOnClass(Feign.class) @EnableConfigurationProperties({ FeignClientProperties.class, FeignHttpClientProperties.class }) @Import(DefaultGzipDecoderConfiguration.class) public class FeignAutoConfiguration { @Autowired(required = false) private List<FeignClientSpecification> configurations = new ArrayList<>(); @Bean public HasFeatures feignFeature() { return HasFeatures.namedFeature("Feign", Feign.class); } @Bean public FeignContext feignContext() { FeignContext context = new FeignContext(); context.setConfigurations(this.configurations); return context; } @Configuration(proxyBeanMethods = false) @ConditionalOnClass(name = "feign.hystrix.HystrixFeign") protected static class HystrixFeignTargeterConfiguration { @Bean @ConditionalOnMissingBean public Targeter feignTargeter() { return new HystrixTargeter(); } } @Configuration(proxyBeanMethods = false) @ConditionalOnMissingClass("feign.hystrix.HystrixFeign") protected static class DefaultFeignTargeterConfiguration { @Bean @ConditionalOnMissingBean public Targeter feignTargeter() { return new DefaultTargeter(); } } // the following configuration is for alternate feign clients if // ribbon is not on the class path. // see corresponding configurations in FeignRibbonClientAutoConfiguration // for load balanced ribbon clients. @Configuration(proxyBeanMethods = false) @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(); } } } @Configuration(proxyBeanMethods = false) @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); } } }
也就是说 feign.hystrix.HystrixFeign在classpath下能找到就会new HystrixTargeter() 否则 new DefaultTargeter();
feign.hystrix.HystrixFeign是 feign和hystrix集成的一个包内的文件。class HystrixTargeter implements Targeter { @Override public <T> T target(FeignClientFactoryBean factory, Feign.Builder feign, FeignContext context, Target.HardCodedTarget<T> target) { if (!(feign instanceof feign.hystrix.HystrixFeign.Builder)) { return feign.target(target); } feign.hystrix.HystrixFeign.Builder builder = (feign.hystrix.HystrixFeign.Builder) feign; String name = StringUtils.isEmpty(factory.getContextId()) ? factory.getName() : factory.getContextId(); SetterFactory setterFactory = getOptional(name, context, SetterFactory.class); if (setterFactory != null) { builder.setterFactory(setterFactory); } Class<?> fallback = factory.getFallback(); if (fallback != void.class) { return targetWithFallback(name, context, target, builder, fallback); } Class<?> fallbackFactory = factory.getFallbackFactory(); if (fallbackFactory != void.class) { return targetWithFallbackFactory(name, context, target, builder, fallbackFactory); } return feign.target(target); } }
DefaultTarget默认不会进行降级,HystrixTargeter的target方法会判断feign是否是feign.hystrix.HystrixFeign.Builder 如果不是 则直接调用feign的target方法。
判断当前
Feign.Builder
类型是不是HystrixFeign.builder()
因为只有当feign.hystrix.enabled=true
时。不需要降级的化默认我们直接使用feign.hystrix.enabled=false 默认就是false 。 或者排除feign-hystrix组件。
最终是通过动态代理来执行我们的httpClient的方法调用。
5、总结
Feign可以结合Hystrix进行服务降级,如果只是用来进行http调用似乎有种大材小用的感觉,不过在项目中使用Feign依旧可以提高我们rpc调用的便捷性,也便于以后的扩展。