Feign是什么
feign是在SpringCloud微服务框架下,实现微服务间相互调用的开发利器,官网介绍为:https://docs.spring.io/spring-cloud-openfeign/docs/2.2.6.RELEASE/reference/html/ 在这个地址下可以看到详细的feign的介绍,基本上可以跟着这个教程走一遍。
feign是什么?作为英语单词,他的意思是假装、佯装,他假装自己是一个客户端,而底层是通过http请求进行通信的。这与传统的客户端(带驱动程序的如mysql-jdbc-connector)是不一样的,但概念上类似,只是通信方式上有些差别。
代码地址
本次演示的代码放到gitee上了。 feign演示代码地址,可以直接检出测试,也可以fork后自己随意修改。
由于feign是用于微服务间的调用的,所以在这里,需要有一个注册中心,我选择的是nacos注册中心,nacos选用的服务端版本是2.1.0,具体可以从这个地方下载:https://github.com/alibaba/nacos/releases/tag/2.1.0 如果打不开的话多刷新几次,如果还是打不开,可以来我的云盘下载:链接: https://pan.baidu.com/s/11BDue7WvzAIiswlIch9bcw 提取码: 8j2s。
启动nacos
nacos的官网地址:https://nacos.io/zh-cn/,详细的用法暂且不表,启动方式为:
在 nacos-server-2.1.0\nacos\bin 目录下,打开cmd,执行 startup.cmd -m standalone,启动nacos服务,界面显示如下表示启动成功:
![](https://i-blog.csdnimg.cn/blog_migrate/29407567b744df23b9315889203cb84c.png)
启动后,访问nacos地址:
http://localhost:8848/nacos,登录用户名密码为: nacos/nacos,可以看到如下界面:
![](https://i-blog.csdnimg.cn/blog_migrate/d5f54fa3f9b5911d15934ef9ed068d44.png)
启动服务提供者provider
服务提供者需要添加依赖:
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
其版本号由依赖管理控制:
<dependencyManagement>
<dependencies>
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-alibaba-dependencies</artifactId>
<version>2.1.0.RELEASE</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
稍后我们会分析其代码及原理。
服务启动类:
@SpringBootApplication
public class ProviderApplication {
public static void main(String[] args) {
SpringApplication.run(ProviderApplication.class, args);
}
}
服务只加了一个controller,用于简单测试:
@RestController
public class HelloController {
@RequestMapping("/provider/hello")
public String hello(@RequestParam("name") String name){
return "from provider:"+name;
}
}
服务配置参数文件:
server:
port: 8081
spring:
cloud:
nacos:
discovery:
server-addr: 127.0.0.1:8848
application.name: provider
直接运行ProviderApplication启动服务,启动后,在浏览器访问:http://localhost:8081/provider/hello?name=1 可以看到如下输出:
![](https://i-blog.csdnimg.cn/blog_migrate/b3e96acd7e5b89932780825ee7207a14.png)
启动服务消费者consumer
消费者服务,添加nacos依赖和feign依赖:
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
(当然spring-boot-starter-web依赖是必须加的,具体可参考资料地址里的gitee仓库)。
启动类:
@SpringBootApplication
@EnableFeignClients("demo.consumer")
public class ConsumerApplication {
public static void main(String[] args) {
SpringApplication.run(ConsumerApplication.class, args);
}
}
消费者的controller:
@RestController
public class ConsumerController {
@Autowired
private ProviderFeignClient providerFeignClient;
@RequestMapping(path="/consumer/hello")
public String consumer(@RequestParam("name") String name){
return "from consumer:"+name;
}
@RequestMapping("/consumer/provider")
public String consumerProvider(@RequestParam("name") String name){
String hello = providerFeignClient.hello(name);
return "from consumer,"+hello;
}
}
feign接口定义:
@FeignClient("provider")
public interface ProviderFeignClient {
@RequestMapping("/provider/hello")
public String hello(@RequestParam("name") String name);
}
服务配置文件:
server:
port: 8082
spring:
cloud:
nacos:
discovery:
server-addr: 127.0.0.1:8848
application.name: consumer
直接运行ConsumerApplication启动消费者服务。
consumer服务启动后,测试controller自身接口,访问地址 http://localhost:8082/consumer/hello?name=1 可以看到如下输出。
![](https://i-blog.csdnimg.cn/blog_migrate/e89918d4a46bed44dc9178fc70806d24.png)
查看服务状态
provider服务和consumer服务都启动后,可以在nacos上查看服务注册情况。
可以清楚看到两个服务都注册上了,后面即可以进行调用测试了。
![](https://i-blog.csdnimg.cn/blog_migrate/c9ca7d6e055eda6012a2280022493f17.png)
测试feign接口
注意上面在consumer服务里的ConsumerController里面有一个方法:
@RequestMapping("/consumer/provider")
public String consumerProvider(@RequestParam("name") String name){
String hello = providerFeignClient.hello(name);
return "from consumer,"+hello;
}
这个方法就用到了我们本次要说的重点:feign接口,他注入了feign接口,对provider服务进行调用,下面测试这个接口。
在浏览器中访问地址:http://localhost:8082/consumer/provider?name=1 可以看到如下返回:
![](https://i-blog.csdnimg.cn/blog_migrate/89cd5161e8b302dce1ea4c32626692b4.png)
可以看出,成功实现了consumer服务队provider服务的调用。下面我们进行深入分析。
分析调用流程
分析调用,有一个技巧,那就是:断点调试,包括任何的问题、任何的框架,归根结底就是调试来学习的。包括自己在工作中编写的代码,也通过调试来发现问题,分析流程。
那么在哪里加断点呢?这很重要,这里也是有技巧的,一般就是加在调用入口处,或者说,就是在用户编写的代码里找入口断点(当然,在对框架比较熟悉之后,可以在任何自己关心的地方加断点,但思路不变,仍然是对未知区域的入口处加断点)。
加入口断点
说了这么多,那么feign调用,应该在哪里加断点呢?ProviderFeignClient本身只是个接口,没法加断点,但我们就是要调试这个接口,所以我们是对ConsumerController加断点。
@RequestMapping("/consumer/provider")
public String consumerProvider(@RequestParam("name") String name){
String hello = providerFeignClient.hello(name); // 在这个地方加断点
return "from consumer,"+hello;
}
仍然访问地址 http://localhost:8082/consumer/provider?name=1 进入设置的断点:
![](https://i-blog.csdnimg.cn/blog_migrate/4f7add7f2aa4e6284d07145b92483338.png)
然后按F5进入方法内(IDEA下为F7单步,后面无特别说明,按照Eclipse的默认快捷键来操作)。
![](https://i-blog.csdnimg.cn/blog_migrate/8b461a5e4aba34b2a20944c191a9c5da.png)
进入Proxy代理回调类
可以看到是进入到了FeignInvocationHandler#invoke()方法,ConsumerController调用的providerFeignClient是一个jdk动态代理$Proxy对象,看FeignInvocationHandler也确实实现了Proxy里的重要接口类:java.lang.reflect.InvocationHandler。下面看下FeignInvocationHandler的invoke方法:
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
if ("equals".equals(method.getName())) {
try {
Object otherHandler =
args.length > 0 && args[0] != null ? Proxy.getInvocationHandler(args[0]) : null;
return equals(otherHandler);
} catch (IllegalArgumentException e) {
return false;
}
} else if ("hashCode".equals(method.getName())) {
return hashCode();
} else if ("toString".equals(method.getName())) {
return toString();
}
return dispatch.get(method).invoke(args); // 在这里放下一个断点
}
这里很好理解,一般我们自己给接口编写Proxy代理也是这样的思路,根据方法名进行相应的处理,这里先排除掉Object的常用方法,然后从dispatch这个map中获取method对应的处理MethodHandler,进行调用。至于这里dispatch是怎么来的,暂且不管。
在dispatch.get.invoke()的地方单步进入,进入如下所示调用栈:
![](https://i-blog.csdnimg.cn/blog_migrate/3852d6abd49c39703a1b2521cbf8d1c0.png)
可以看到这里进入的是SynchronousMethodHandler.invoke()方法,他实现了MethodHandler接口。
调用MethodHandler
MethodHandler简单理解,就是在ProviderFeign里每一个接口方法对应的处理类,为什么一个方法一个?因为每个方法都有差异,可能是GET POST,url不同,请求参数不同,返回类型不同等等。
下面分析下SynchronousMethodHandler.invoke()
@Override
public Object invoke(Object[] argv) throws Throwable {
RequestTemplate template = buildTemplateFromArgs.create(argv); // 创建一个template对象
Options options = findOptions(argv);
Retryer retryer = this.retryer.clone(); // 本次feign调用的重试器
while (true) {
try {
return executeAndDecode(template, options); // 核心方法,下个断点加在这个地方
} catch (RetryableException e) {
try {
retryer.continueOrPropagate(e); // 判断是否重试,一般默认不重试
} catch (RetryableException th) {
Throwable cause = th.getCause();
if (propagationPolicy == UNWRAP && cause != null) {
throw cause;
} else {
throw th;
}
}
if (logLevel != Logger.Level.NONE) {
logger.logRetry(metadata.configKey(), logLevel);
}
continue;
}
}
}
可以看出,executeAndDecode()方法是核心方法,进入这个方法:
SynchronousMethodHandler.executeAndDecode()这个代码有点长,不过并不复杂,走完这个方法后,feign调用也就结束了。
★调用executeAndDecode
这是处理的主方法,是阅读代码时应该关注的重点。
SynchronousMethodHandler#executeAndDecode()
Object executeAndDecode(RequestTemplate template, Options options) throws Throwable {
Request request = targetRequest(template); // 应用RequestInterceptor拦截器,通过Feign.Target.apply()得到Request对象。
if (logLevel != Logger.Level.NONE) {
logger.logRequest(metadata.configKey(), logLevel, request);
}
Response response;
long start = System.nanoTime();
try {
response = client.execute(request, options); // 这是核心方法,即将执行调用了
// ensure the request is set. TODO: remove in Feign 12
response = response.toBuilder()
.request(request)
.requestTemplate(template)
.build(); // 给响应对象关联上对应的请求对象和requestTemplate,后续可以使用这俩属性
} catch (IOException e) {
if (logLevel != Logger.Level.NONE) {
logger.logIOException(metadata.configKey(), logLevel, e, elapsedTime(start));
}
throw errorExecuting(request, e);
}
long elapsedTime = TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - start);
boolean shouldClose = true;
try {
if (logLevel != Logger.Level.NONE) {
response =
logger.logAndRebufferResponse(metadata.configKey(), logLevel, response, elapsedTime);
}
if (Response.class == metadata.returnType()) { // 如果用户的feign接口方法返回类型恰好就是Feign.Response的话,此时不管response.status()是成功还是失败,直接返回
if (response.body() == null) {
return response;
}
if (response.body().length() == null ||
response.body().length() > MAX_RESPONSE_BUFFER_SIZE) {
shouldClose = false;
return response;
}
// Ensure the response body is disconnected
byte[] bodyData = Util.toByteArray(response.body().asInputStream());
return response.toBuilder().body(bodyData).build(); // 设置用户最关心的返回数据并返回,不过注意,正常情况下用户feign接口返回类型不会是Response
}
if (response.status() >= 200 && response.status() < 300) { // status状态码2xx表示成功
if (void.class == metadata.returnType()) { // 空返回值,直接返回
return null;
} else {
Object result = decode(response); // 解码,这里就是调用feign.codec.Decoder的实现类,典型的如ResponseEntityDecoder
shouldClose = closeAfterDecode;
return result; // 一般到这就结束了
}
} else if (decode404 && response.status() == 404 && void.class != metadata.returnType()) { // 如果404不认为是报错,在这里正常decode
Object result = decode(response);
shouldClose = closeAfterDecode;
return result;
} else { // 响应码失败的情况,进入feign的错误处理.一般这里自定义一个ErrorDecoder,读取response对象转义为业务异常,自定义ErrorDecoder里可以使用ResponseEntityDecoder解析response为Map,
throw errorDecoder.decode(metadata.configKey(), response);
}
} catch (IOException e) {
if (logLevel != Logger.Level.NONE) {
logger.logIOException(metadata.configKey(), logLevel, e, elapsedTime);
}
throw errorReading(request, response, e);
} finally {
if (shouldClose) {
ensureClosed(response.body());
}
}
}
解释一下,targetRequest()之所以能通过RequestTemplate返回Request对象,是因为RequestTemplate就是为产生Request对象而服务的,可以看到RequestTemplate.request()方法,就是用RequestTemplate(请求模板)里的各个参数(请求方法、url、headers、requestBody)等来构造出Request的。
具体调用的细节说明见上方代码注释。下面看一下本方法里的核心方法:
response = client.execute(request, options);
调试可以看到,这里的client是 LoadBalancerFeignClient 实例。
调用feign.Client#execute
调用LoadBalancerFeignClient#execute()方法。
![](https://i-blog.csdnimg.cn/blog_migrate/13cf1888db05e73e5b3a98989afef331.png)
看下这段代码:LoadBalancerFeignClient#execute()
@Override
public Response execute(Request request, Request.Options options) throws IOException {
try {
URI asUri = URI.create(request.url());
String clientName = asUri.getHost(); // 这里取到的就是服务名了
URI uriWithoutHost = cleanUrl(request.url(), clientName); // 去掉http://provider前缀
FeignLoadBalancer.RibbonRequest ribbonRequest = new FeignLoadBalancer.RibbonRequest(
this.delegate, request, uriWithoutHost); // delegate是feign.Client$Default默认实现
IClientConfig requestConfig = getClientConfig(options, clientName);
return lbClient(clientName) // 获取FeignLoadBalancer对象
.executeWithLoadBalancer(ribbonRequest, requestConfig).toResponse(); // 动态选择服务实例并发起调用
}
catch (ClientException e) {
IOException io = findIOException(e);
if (io != null) {
throw io;
}
throw new RuntimeException(e);
}
}
这里lbClient()方法返回的对象类型是FeignLoadBalancer,这个是具有一个ribbon的loadbalancer属性的,就是 ILoadBalancer lb,他就是借助ribbon的软负载功能实现按服务名负载均衡的。
lbClient(clientName).executeWithLoadBalancer()这个方法,其实就是看FeignLoadBalancer对象。
FeignLoadBalancer <- AbstractLoadBalancerAwareClient <- LoadBalancerContext
这里调用的是executeWithLoadBalancer()方法,这个方法是在AbstractLoadBalancerAwareClient父类的。
AbstractLoadBalancerAwareClient#executeWithLoadBalancer()
public T executeWithLoadBalancer(final S request, final IClientConfig requestConfig) throws ClientException {
LoadBalancerCommand<T> command = buildLoadBalancerCommand(request, requestConfig); //构建了一个Command对象,这个对象是在ribbon-loadbalancer包下的
try {
return command.submit( // 提交一个任务,我们关心的逻辑就在这里面,至于command本身怎么处理的,不用深究
new ServerOperation<T>() {
@Override
public Observable<T> call(Server server) {
URI finalUri = reconstructURIWithServer(server, request.getUri());
S requestForServer = (S) request.replaceUri(finalUri);
try {
// 这个地方注意下,是真正执行网络请求的地方
return Observable.just(AbstractLoadBalancerAwareClient.this.execute(requestForServer, requestConfig));
}
catch (Exception e) {
return Observable.error(e);
}
}
})
.toBlocking() // 同步等待结果
.single(); // 获取返回值
} catch (Exception e) {
Throwable t = e.getCause();
if (t instanceof ClientException) {
throw (ClientException) t;
} else {
throw new ClientException(e);
}
}
}
LoadBalancerCommand
不用对LoadBalancerCommand有什么疑惑,这就是个普通的对象,他是ribbon基于rxjava做的负载均衡处理关键类,里面用大量的rxjava的api实现了服务负载均衡。只是如果不用rxjava会更好懂一点而已。
LoadBalancerCommand#submit()
public Observable<T> submit(final ServerOperation<T> operation) {
final ExecutionInfoContext context = new ExecutionInfoContext();
if (listenerInvoker != null) {
try {
listenerInvoker.onExecutionStart();
} catch (AbortExecutionException e) {
return Observable.error(e);
}
}
// 重试次数是在retryHandler中统一配置的,有兴趣可以自行追溯一下在哪配置
final int maxRetrysSame = retryHandler.getMaxRetriesOnSameServer();
final int maxRetrysNext = retryHandler.getMaxRetriesOnNextServer();
// Use the load balancer
// 构造Observable对象,先选出server作为Observable的对象流,然后转换为新的Observable
Observable<T> o = // 这块用到了rxjava的Observable,具体可不用深究
(server == null ? selectServer() : Observable.just(server))
.concatMap(new Func1<Server, Observable<T>>() {
@Override
// Called for each server being selected
// 就是在这里转化了一个新的Observable,因为网络调用是耗时的,相当于进行异步化
public Observable<T> call(Server server) {
context.setServer(server);
final ServerStats stats = loadBalancerContext.getServerStats(server);
// Called for each attempt and retry
// 这里会进行自动重试,针对maxRetrysSame同一服务实例重试次数
Observable<T> o = Observable
.just(server)
.concatMap(new Func1<Server, Observable<T>>() {
@Override
public Observable<T> call(final Server server) {
context.incAttemptCount();
loadBalancerContext.noteOpenConnection(stats);
if (listenerInvoker != null) {
try {
listenerInvoker.onStartWithServer(context.toExecutionInfo());
} catch (AbortExecutionException e) {
return Observable.error(e);
}
}
final Stopwatch tracer = loadBalancerContext.getExecuteTracer().start();
// 注意看这里回调了operation,这个operation就是上一个代码的command.submit()提交的
return operation.call(server).doOnEach(new Observer<T>() {
// 这块省略N行代码,这块代码主要就是
});
}
});
// 这里的重试针对的是同一个server
if (maxRetrysSame > 0)
o = o.retry(retryPolicy(maxRetrysSame, true));
return o;
}
});
// 这里会进行重试,重试下一个服务实例
if (maxRetrysNext > 0 && server == null)
o = o.retry(retryPolicy(maxRetrysNext, false));
return o.onErrorResumeNext(new Func1<Throwable, Observable<T>>() {
@Override
public Observable<T> call(Throwable e) {
if (context.getAttemptCount() > 0) {
if (maxRetrysNext > 0 && context.getServerAttemptCount() == (maxRetrysNext + 1)) {
e = new ClientException(ClientException.ErrorType.NUMBEROF_RETRIES_NEXTSERVER_EXCEEDED,
"Number of retries on next server exceeded max " + maxRetrysNext
+ " retries, while making a call for: " + context.getServer(), e);
}
else if (maxRetrysSame > 0 && context.getAttemptCount() == (maxRetrysSame + 1)) {
e = new ClientException(ClientException.ErrorType.NUMBEROF_RETRIES_EXEEDED,
"Number of retries exceeded max " + maxRetrysSame
+ " retries, while making a call for: " + context.getServer(), e);
}
}
if (listenerInvoker != null) {
listenerInvoker.onExecutionFailed(e, context.toFinalExecutionInfo());
}
return Observable.error(e);
}
});
}
这块代码可以不用深究,因为他用到了大量rxjava的api,比较难以阅读。
对于我们来说,主要关注两个地方:
selectServer()方法选取server实例(ribbon软负载)
AbstractLoadBalancerAwareClient.this.execute()方法执行网络请求
feign不可能直接用服务名进行网络调用,一定是先根据服务名按照某种选取策略,选取一个可用的服务实例后,确定本次请求的实际地址,也就是ip:port,在这个场景下就是provider服务的地址 localhost:8081.然后用传统的网络调用方法进行http调用,并将返回结果处理为接口方法返回值类型。
LoadBalancerCommand#submit()提交的任务,需要再重点说一下:
new ServerOperation<T>() {
@Override
public Observable<T> call(Server server) {
// 使用server重建url,也就是把http://provider替换为http://localhost:8081
URI finalUri = reconstructURIWithServer(server, request.getUri());
// 用替换后的url替换request里的url,这里的request就是RibbonRequest对象
S requestForServer = (S) request.replaceUri(finalUri);
try { // 替换后执行网络调用
return Observable.just(AbstractLoadBalancerAwareClient.this.execute(requestForServer, requestConfig));
}
catch (Exception e) {
return Observable.error(e);
}
}
})
AbstractLoadBalancerAwareClient.this.execute()==FeignLoadBalancer#execute()
执行FeignLoadBalancer#execute
FeignLoadBalancer#execute()
@Override
public RibbonResponse execute(RibbonRequest request, IClientConfig configOverride)
throws IOException {
// 得到请求的options参数,就是截时间,如连接超时事件、读取超时时间等,作为调用的参数传递
Request.Options options;
if (configOverride != null) {
RibbonProperties override = RibbonProperties.from(configOverride);
options = new Request.Options(override.connectTimeout(this.connectTimeout),
override.readTimeout(this.readTimeout));
}
else {
options = new Request.Options(this.connectTimeout, this.readTimeout);
}
// 一切准备就绪,调用request.client()执行网络请求,这里的client()就是在new RibbonRequest()的时候传入的delegate,在这里其实就是个Client$Default实例
Response response = request.client().execute(request.toRequest(), options);
return new RibbonResponse(request.getUri(), response);
}
可以看出最终其实是feign.Client$Default进行的请求,这就来到了咱们最后一个代码分析了。
调用feign.Client$Default
feign.Client$Default
@Override
public Response execute(Request request, Options options) throws IOException {
// 这个方法的具体代码就在下面
HttpURLConnection connection = convertAndSend(request, options);
return convertResponse(connection, request);
}
HttpURLConnection convertAndSend(Request request, Options options) throws IOException {
final URL url = new URL(request.url());
// 终于露出真面目了,使用的HttpURLConnection进行http通信
final HttpURLConnection connection = this.getConnection(url);
if (connection instanceof HttpsURLConnection) {
HttpsURLConnection sslCon = (HttpsURLConnection) connection;
if (sslContextFactory != null) {
sslCon.setSSLSocketFactory(sslContextFactory);
}
if (hostnameVerifier != null) {
sslCon.setHostnameVerifier(hostnameVerifier);
}
}
// 应用上面说到的options
connection.setConnectTimeout(options.connectTimeoutMillis());
connection.setReadTimeout(options.readTimeoutMillis());
connection.setAllowUserInteraction(false);
connection.setInstanceFollowRedirects(options.isFollowRedirects());
connection.setRequestMethod(request.httpMethod().name());
Collection<String> contentEncodingValues = request.headers().get(CONTENT_ENCODING);
boolean gzipEncodedRequest =
contentEncodingValues != null && contentEncodingValues.contains(ENCODING_GZIP);
boolean deflateEncodedRequest =
contentEncodingValues != null && contentEncodingValues.contains(ENCODING_DEFLATE);
boolean hasAcceptHeader = false;
Integer contentLength = null;
// 添加http请求头
for (String field : request.headers().keySet()) {
if (field.equalsIgnoreCase("Accept")) {
hasAcceptHeader = true;
}
for (String value : request.headers().get(field)) {
if (field.equals(CONTENT_LENGTH)) {
if (!gzipEncodedRequest && !deflateEncodedRequest) {
contentLength = Integer.valueOf(value);
connection.addRequestProperty(field, value);
}
} else {
connection.addRequestProperty(field, value);
}
}
}
// Some servers choke on the default accept string.
if (!hasAcceptHeader) {
connection.addRequestProperty("Accept", "*/*");
}
// 有请求体时的处理(如POST方式)
if (request.requestBody().asBytes() != null) {
if (contentLength != null) {
connection.setFixedLengthStreamingMode(contentLength);
} else {
connection.setChunkedStreamingMode(8196);
}
connection.setDoOutput(true);
OutputStream out = connection.getOutputStream();
if (gzipEncodedRequest) {
out = new GZIPOutputStream(out);
} else if (deflateEncodedRequest) {
out = new DeflaterOutputStream(out);
}
try {
// 将请求体写入http输出流
out.write(request.requestBody().asBytes());
} finally {
try {
out.close();
} catch (IOException suppressed) { // NOPMD
}
}
}
// 此时connection已经通信完成,后续解析响应信息
return connection;
}
}
Client$Default#convertResponse()
Response convertResponse(HttpURLConnection connection, Request request) throws IOException {
// 获取响应码
int status = connection.getResponseCode();
String reason = connection.getResponseMessage();
if (status < 0) {
throw new IOException(format("Invalid status(%s) executing %s %s", status,
connection.getRequestMethod(), connection.getURL()));
}
// 获取响应头
Map<String, Collection<String>> headers = new LinkedHashMap<>();
for (Map.Entry<String, List<String>> field : connection.getHeaderFields().entrySet()) {
// response message
if (field.getKey() != null) {
headers.put(field.getKey(), field.getValue());
}
}
// 获取响应体大小
Integer length = connection.getContentLength();
if (length == -1) {
length = null;
}
// 获取响应流,但并未读取流
InputStream stream;
if (status >= 400) {
stream = connection.getErrorStream();
} else {
stream = connection.getInputStream();
}
// 构建Response对象
return Response.builder()
.status(status)
.reason(reason)
.headers(headers)
.request(request)
.body(stream, length)
.build();
}
到这里就已经服务调用结束了,后面又会回到SynchronousMethodHandler.executeAndDecode()方法的decode环节,就是调用decoder.decode(response, metadata.returnType()),如果返回的是对象就采用json反序列化,如果是普通的string,就直接读取string了。(这里的转换器跟springmvc里的其实是一个东西)。
到这里就是彻底执行完了,从ProviderFeign接口方法调用返回了,最后返回到consumer服务,在浏览器显示。
feign框架的意义
可以看出的是,通过引入feign,他让我们可以用普通接口的方式来进行跨服务的网络调用,这对于开发人员来说能屏蔽通信细节,用起来跟本地接口方法并无差别,不用操心底层如何通信,只要按照feign的要求编写接口、添加注解即可。
为了更好的理解feign,我们来想一下,如果不用feign,让我们自己完全手动实现跨服务调用的话,我们会怎么做?
确定服务名,比如确定要调用的服务叫provider
根据服务名,使用注册中心的api获取服务地址列表。(比如注入DiscoveryClient来获取)
根据某种实例选择策略,选取某个服务实例,作为本次请求的服务地址(本例只起了一个实例)
确定要调用的服务端url,在这里就是provider服务的url:/provider/hello
用确定的服务地址,拼接上url,使用喜欢的http通信工具进行通信,比如HttpURLConnection,http-client,okhttp等。
对http通信后的响应结果,首先判断响应码,如果不是2xx就认为失败,报错处理
如果响应码是2xx,就读取结果,比如读取到一个json串
将读取到的结果反序列化为自定义的实体对象,进行后续使用,或就转成一个Map进行使用。
可以看到,除了确定服务名、url地址、返回数据类型之外,其他操作都是重复的,而框架就是为了解决这些重复问题而出现的。下面进行上方步骤与feign的对应。
1 对应于ProviderFeign接口中的@FeignClient("provider")
2和3对应于LoadBalancerCommand#submit()中的selectServer()方法
4对应于ProviderFeign中的@RequestMapping("/provider/hello")
5对应于LoadBalancerCommand#submit()中的reconstructURIWithServer()方法
6对应于SynchronousMethodHandler.executeAndDecode()方法中response.status()的判断
7和8对应于SynchronousMethodHandler.executeAndDecode()中的decode()方法
另外,异常处理feign也考虑到了,我们只需要定义一个ErrorDecoder就可以实现异常捕捉处理。
相信通过这么比较的看,就更能理解feign框架做了什么,更好理解feign的处理调用流程了。
站在实际使用的角度,feign是如何处理我们的请求的(归纳调用流程)?
我们调用了ProviderFeign.hello()方法
我们调用的其实是通过FeignClientFactoryBean对接口代理后的动态代理类,在这里就是ReflectiveFeign。
ReflectiveFeign里有一个list对象:dispatch,这里面保存了对ProviderFeign的解析结果,可以理解为url到处理handler的映射集合。
根据url找到对应的handler,这里是SynchronousMethodHandler(意为同步方法调用处理器),调用他的invoke()方法,实际调用executeAndDecode().
开始构建请求RequestTemplate对象,确定请求控制选项,重试处理方式等
从RequestTemplate得到Request对象,通过feign.Client对象执行服务调用,这里的feign.Client实现为LoadBalancerFeignClient,从名字中可以看出,这是具有负载均衡能力的client,另外注意到他有一个delegate代理对象就是Client$Default,是用于确定服务地址后使用的真实通信类。
把原Request请求转换为RibbonRequest对象,把上面说的delegate对象也设置给他
获取FeignLoadBalancer对象,这个对象里设置了ILoadBalancer对象,这其实是ribbon里的核心对象,用于负载均衡的。
调用FeignLoadBalancer的executeWithLoadBalancer()方法进行服务调用
构建LoadBalancerCommand对象进行rxjava风格的调用,rxjava不用太多理解,重点是看这个command他submit的任务是什么,就是替换url地址,进行调用。
在上面说的command.submit()的任务执行中,会调用FeignLoadBalancer重载的方法execute(),这个意思就是说服务地址已经确定了,按原计划调用。最后还是用了HttpURLConnection的方式完成了调用。
调用栈回到第4步SynchronousMethodHandler的executeAndDecode方法,进行Response响应结果的处理,简单说来就是判断http状态码,将响应结果转为ProviderFeign接口方法定义的返回类型,在这里是String,那么就是用StringHttpMessageConverter进行转换。
feign调用结束并返回,用户调用的ProviderFeign#hello()方法返回结果,继续后面的正常代码。
Feign如何进行的接口动态代理
这就要说到@EnableFeignClients注解了。点进去看下,可以看到他@Import导入了配置类FeignClientsRegistrar,这个类再看下。(这里仅展示部分关键代码)
// 向spring容器注册bean定义
private void registerFeignClient(BeanDefinitionRegistry registry,
AnnotationMetadata annotationMetadata, Map<String, Object> attributes) {
String className = annotationMetadata.getClassName();
// 采用工厂bean的方式,具体为FeignClientFactoryBean这个FactoryBean,所以关注getObject()方法
BeanDefinitionBuilder definition = BeanDefinitionBuilder
.genericBeanDefinition(FeignClientFactoryBean.class);
validate(attributes);
// 这里添加的一系列参数,都是feign构造代理实例需要的,如服务名、接口类型等
definition.addPropertyValue("url", getUrl(attributes));
definition.addPropertyValue("path", getPath(attributes));
String name = getName(attributes);
definition.addPropertyValue("name", name);
String contextId = 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(AbstractBeanDefinition.AUTOWIRE_BY_TYPE);
// 设置的别名
String alias = contextId + "FeignClient";
AbstractBeanDefinition beanDefinition = definition.getBeanDefinition();
boolean primary = (Boolean) attributes.get("primary"); // has a default, won't be
// null
beanDefinition.setPrimary(primary);
String qualifier = getQualifier(attributes);
if (StringUtils.hasText(qualifier)) {
alias = qualifier;
}
// 注册beanDefinition
BeanDefinitionHolder holder = new BeanDefinitionHolder(beanDefinition, className,
new String[] { alias });
BeanDefinitionReaderUtils.registerBeanDefinition(holder, registry);
}
因为ProviderFeign只是个接口,所以如果想从spring容器中注入一个ProviderFeign类型的bean,就得注册一个对应类型的bean,这个bean的实现类一般就是个FactoryBean,在getObject()方法里进行接口的动态代理,返回代理后的实例。
如果想深入了解,可以去看下FeignClientFactoryBean#getObject()方法,这就是ProviderFeign的代理类生成的地方。
FeignAutoConfiguration自动配置
这个自动配置类里,注入FeignClientSpecification,可以理解为得到用户在@FeignClient中通过configuration属性设置的FeignClient配置信息,在这里配置的bean作用域为本FeignClient。比如还有个OrderFeign,StorageFeign,对应订单服务、仓库服务,完全可以执行不同的configuration类.
同时FeignAutoConfiguration定义了最重要的对象:FeignContext,就是这里要说到的。他是spring-context的NamedContextFactory实例,至于命名上下文是什么,可以再查阅相关资料,简单理解就是根据名字获取独立上下文,这对于feign来说很重要,因为我们很可能对不同的feign有不同的配置,而这些配置很多情况下就是个标准的bean,如果都放在一个spring容器中,会存在相互污染、定义混乱的情况,所以使用了命名上下文。(对标ribbon里的SpringClientFactory)
FeignAutoConfiguration还定义了一些默认实现,比如feign.Client对象,当client独立上下文中没定义对应的bean时,可以用父容器的(也就是我们当前的spring容器)。
Feign与Ribbon的自动整合
feign和ribbon如何结合的,可以参考在feign框架的意义里说的,第8点,“获取FeignLoadBalancer对象,这个对象里设置了ILoadBalancer对象,这其实是ribbon里的核心对象,用于负载均衡的。”
feign通过对ILoadBalancer对象的使用,实现了负载均衡功能,所有对ribbon的配置仍然可以用,因为feign是站在比较高的层次来用ribbon的api的,对于feign而言,他更关心的也不是确定服务实例地址,而是如何进行url映射、请求参数包装request、响应结果解码,以及异常处理机制等。
本篇到此终于结束,希望对大家有所帮助,能够对大家的调试、理解框架有一点启发。