本篇基于Spring Cloud Hoxton.SR9
前言
如果您在看阅读的时候感觉吃力,请自行补充基础知识,该系列只是带领大家走读源码,通过读源码,弄懂背后实现的原理以及一些扩展点,以便更好的辅助日常工作的开发。
如果您在看阅读的时候感觉吃力,请自行补充基础知识,该系列只是带领大家走读源码,通过读源码,弄懂背后实现的原理以及一些扩展点,以便更好的辅助日常工作的开发。
如果您在看阅读的时候感觉吃力,请自行补充基础知识,该系列只是带领大家走读源码,通过读源码,弄懂背后实现的原理以及一些扩展点,以便更好的辅助日常工作的开发。
重要的事情说三遍!!!
由于之前的排版有问题,代码不便于阅读,所以经过了重新排版。本篇默认读者已经知道OpenFeign是做什么的,如果不知到,请自行百度。另外如果阅读完本文觉得有些吃力,请提前学习SpringBoot自动装配基础知识。废话不多说,直接上干货。
基本用法
- 引入依赖
org.springframework.cloud
spring-cloud-starter-openfeign
- 开启feign
@EnableFeignClients
- 定义接口
@FeignClient("nacos-discovery-provider-sample") // 指向服务提供者应用
public interface EchoService {
@GetMapping("/echo/{message}")
String echo(@PathVariable("message") String message);
}
源码分析
配置解析阶段
@EnableFeignClients
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@Documented
@Import(FeignClientsRegistrar.class)
public @interface EnableFeignClients {
}
通过import注解导入了FeignClientsRegistrar
配置类,那么进入这个类中,一看详情。
FeignClientsRegistrar
这个类实现了ImportBeanDefinitionRegistrar
接口,那么就需要重写registerBeanDefinitions
方法。如下:
@Override
public void registerBeanDefinitions(AnnotationMetadata metadata,
BeanDefinitionRegistry registry) {
registerDefaultConfiguration(metadata, registry);
registerFeignClients(metadata, registry);
}
这个方法做了2两件事情:
- 注册全局配置,如果EnableFeignClients注解上配置了defaultConfiguration属性
private void registerDefaultConfiguration(AnnotationMetadata metadata,
BeanDefinitionRegistry registry) {
Map defaultAttrs = metadata
.getAnnotationAttributes(EnableFeignClients.class.getName(), true);
if (defaultAttrs != null && defaultAttrs.containsKey("defaultConfiguration")) {
String name;
if (metadata.hasEnclosingClass()) {
name = "default." + metadata.getEnclosingClassName();
}
else {
name = "default." + metadata.getClassName();
}
registerClientConfiguration(registry, name,
defaultAttrs.get("defaultConfiguration"));
}
}
- 注册FeignClients
public void registerFeignClients(AnnotationMetadata metadata,
BeanDefinitionRegistry registry) {
ClassPathScanningCandidateComponentProvider scanner = getScanner();
scanner.setResourceLoader(this.resourceLoader);
Set basePackages;
Map attrs = metadata
.getAnnotationAttributes(EnableFeignClients.class.getName());
AnnotationTypeFilter annotationTypeFilter = new AnnotationTypeFilter(
FeignClient.class);
//省略部分代码
......
for (String basePackage : basePackages) {
Set candidateComponents = scanner
.findCandidateComponents(basePackage);
for (BeanDefinition candidateComponent : candidateComponents) {
if (candidateComponent instanceof AnnotatedBeanDefinition) {
// verify annotated class is an interface
AnnotatedBeanDefinition beanDefinition = (AnnotatedBeanDefinition) candidateComponent;
AnnotationMetadata annotationMetadata = beanDefinition.getMetadata();
Assert.isTrue(annotationMetadata.isInterface(),
"@FeignClient can only be specified on an interface");
Map attributes = annotationMetadata
.getAnnotationAttributes(
FeignClient.class.getCanonicalName());
String name = getClientName(attributes);
registerClientConfiguration(registry, name,
attributes.get("configuration"));
registerFeignClient(registry, annotationMetadata, attributes);
}
}
}
}
这个方法会扫描类路径上标记@FeignClient
注解的接口,根据@EnableFeignClients
注解上的配置,并循环注册FeignClient。
进入registerFeignClient
方法
private void registerFeignClient(BeanDefinitionRegistry registry,
AnnotationMetadata annotationMetadata, Map attributes) {
String className = annotationMetadata.getClassName();
BeanDefinitionBuilder definition = BeanDefinitionBuilder
.genericBeanDefinition(FeignClientFactoryBean.class);
validate(attributes);
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();
beanDefinition.setAttribute(FactoryBean.OBJECT_TYPE_ATTRIBUTE, className);
// has a default, won't be null
boolean primary = (Boolean) attributes.get("primary");
beanDefinition.setPrimary(primary);
String qualifier = getQualifier(attributes);
if (StringUtils.hasText(qualifier)) {
alias = qualifier;
}
BeanDefinitionHolder holder = new BeanDefinitionHolder(beanDefinition, className,
new String[] { alias });
BeanDefinitionReaderUtils.registerBeanDefinition(holder, registry);
}
可以看到,这个方法就是将FeignClient注解上的属性信息,封装到BeanDefinition中,并注册到Spring容器中。但是在这个方法中,有一个关键信息,就是真实注册的是FeignClientFactoryBean
,它实现了FactoryBean
接口,表明这是一个工厂bean,用于创建代理Bean,真正执行的逻辑是FactoryBean
的getObject
方法。至此,FeignClient的配置解析阶段就完成了。下面进入FeignClientFactoryBean
,看看在这个类中都做了什么。
运行阶段
FeignClientFactoryBean
- 核心方法getObject
@Override
public Object getObject() throws Exception {
return getTarget();
}
当我们在业务代码中通过@Autowire
依赖注入或者通过getBean
依赖查找时,此方法会被调用。内部会调用getTarget
方法,那么进入这个方法一探究竟。
- getTarget
T getTarget() {
FeignContext context = applicationContext.getBean(FeignContext.class);
Feign.Builder builder = feign(context);
if (!StringUtils.hasText(url)) {
if (!name.startsWith("http")) {
url = "http://" + name;
}
else {
url = name;
}
url += cleanPath();
return (T) loadBalance(builder, context,
new HardCodedTarget<>(type, name, url));
}
if (StringUtils.hasText(url) && !url.startsWith("http")) {
url = "http://" + url;
}
String url = this.url + cleanPath();
Client client = getOptional(context, Client.class);
if (client != null) {
if (client instanceof LoadBalancerFeignClient) {
// not load balancing because we have a url,
// but ribbon is on the classpath, so unwrap
client = ((LoadBalancerFeignClient) client).getDelegate();
}
if (client instanceof FeignBlockingLoadBalancerClient) {
// not load balancing because we have a url,
// but Spring Cloud LoadBalancer is on the classpath, so unwrap
client = ((FeignBlockingLoadBalancerClient) client).getDelegate();
}
builder.client(client);
}
Targeter targeter = get(context, Targeter.class);
return (T) targeter.target(this, builder, context,
new HardCodedTarget<>(type, name, url));
}
首先从Spring上下文中,获取FeignContext这个Bean,这个bean是在哪里注册的呢?是在FeignAutoConfiguration
中注册的。
然后判断url属性是否为空,如果不为空,则生成默认的代理类;如果为空,则走负载均衡,生成带有负载均衡的代理类。那么重点关注loadBalance
方法。
- loadBalance
protected T loadBalance(Feign.Builder builder, FeignContext context,
HardCodedTarget target) {
Client client = getOptional(context, Client.class);
if (client != null) {
builder.client(client);
Targeter targeter = get(context, Targeter.class);
return targeter.target(this, builder, context, target);
}
throw new IllegalStateException(
"No Feign Client for loadBalancing defined. Did you forget to include spring-cloud-starter-netflix-ribbon?");
}
首先调用getOptional
方法,这个方法就是根据contextId
,获取一个子上下文,然后从这个子上下文中查找Client bean,SpringCloud会为每一个feignClient创建一个子上下文,然后存入以contextId为key的map中,详见NamedContextFactory
的getContext
方法。此处会返回LoadBalancerFeignClient
这个Client。详见:FeignRibbonClientAutoConfiguration
会导入相关配置类。
然后会从子上下文中,查找Targeter
bean,默认返回的是DefaultTargeter
,
最后调用target
方法。
- DefaultTargeter
class DefaultTargeter implements Targeter {
@Override
public T target(FeignClientFactoryBean factory, Feign.Builder feign,
FeignContext context, Target.HardCodedTarget target) {
return feign.target(target);
}
}
最终底层调用Feign.Builder
的target
方法。进入Feign
class中,看看到底做了什么事情
- Feign
public T target(Target target) {
return this.build().newInstance(target);
}
public Feign build() {
//省略部分代码
......
return new ReflectiveFeign(handlersByName, this.invocationHandlerFactory, this.queryMapEncoder);
}
可以看到最终是通过创建ReflectiveFeign
对象,然后调用newInstance
方法返回了一个代理对象,通过名字可以发现,底层使用的是java反射创建的。
那么看看ReflectiveFeign
的newInstance
方法到底做了什么。
- ReflectiveFeign
public T newInstance(Target target) {
//根据接口类和Contract协议解析方式,解析接口类上的方法和注解,转换成内部的MethodHandler处理方式
Map nameToHandler = this.targetToHandlersByName.apply(target);
Map methodToHandler = new LinkedHashMap();
List defaultMethodHandlers = new LinkedList();
Method[] var5 = target.type().getMethods();
int var6 = var5.length;
for(int var7 = 0; var7 < var6; ++var7) {
Method method = var5[var7];
if (method.getDeclaringClass() != Object.class) {
if (Util.isDefault(method)) {
DefaultMethodHandler handler = new DefaultMethodHandler(method);
defaultMethodHandlers.add(handler);
methodToHandler.put(method, handler);
} else {
methodToHandler.put(method, nameToHandler.get(Feign.configKey(target.type(), method)));
}
}
}
//基于Proxy.newProxyInstance 为接口类创建动态实现,将所有的请求转换给InvocationHandler 处理。
InvocationHandler handler = this.factory.create(target, methodToHandler);
T proxy = Proxy.newProxyInstance(target.type().getClassLoader(), new Class[]{target.type()}, handler);
Iterator var12 = defaultMethodHandlers.iterator();
while(var12.hasNext()) {
DefaultMethodHandler defaultMethodHandler = (DefaultMethodHandler)var12.next();
defaultMethodHandler.bindTo(proxy);
}
return proxy;
}
见注释。此处nvocationHandler handler = this.factory.create(target, methodToHandler);
真实返回的是FeignInvocationHandler
,当在自己的业务类中调用feign接口方法时,会调用FeignInvocationHandler
的invoke
方法。
- ReflectiveFeign.FeignInvocationHandler
@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);
}
在invoke
方法中,会调用 this.dispatch.get(method)).invoke(args)
。this.dispatch.get(method)
会返回一个SynchronousMethodHandler,
进行拦截处理。这个方法会根据参数生成完成的RequestTemplate
对象,这个对象是Http请求的模版,代码如下。
看SynchronousMethodHandler
中的invoke
类
- SynchronousMethodHandler
@Override
public Object invoke(Object[] argv) throws Throwable {
RequestTemplate template = buildTemplateFromArgs.create(argv);
Options options = findOptions(argv);
Retryer retryer = this.retryer.clone();
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()
方法,该方法通过RequestTemplate
生成Request
请求对象,然后利用Http Client(默认)
获取response
,来获取响应信息
Object executeAndDecode(RequestTemplate template, Options options) throws Throwable {
Request request = targetRequest(template);
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();
} catch (IOException e) {
if (logLevel != Logger.Level.NONE) {
logger.logIOException(metadata.configKey(), logLevel, e, elapsedTime(start));
}
throw errorExecuting(request, e);
}
//省略部分代码
......
}
client.execute(request, options);
默认使用HttpURLConnection
发起远程调用,这里的client为LoadBalancerFeignClient
。那么看看他的execute
方法。
- LoadBalancerFeignClient
@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);
FeignLoadBalancer.RibbonRequest ribbonRequest = new FeignLoadBalancer.RibbonRequest(
this.delegate, request, uriWithoutHost);
IClientConfig requestConfig = getClientConfig(options, clientName);
return lbClient(clientName)
.executeWithLoadBalancer(ribbonRequest, requestConfig).toResponse();
}
catch (ClientException e) {
IOException io = findIOException(e);
if (io != null) {
throw io;
}
throw new RuntimeException(e);
}
}
最终通过Ribbon负载均衡器发起远程调用,具体分析见另一篇关于Ribbon的源码分析。
总结
通过源码我们了解了Spring Cloud OpenFeign
的加载配置创建流程。通过注解@FeignClient
和@EnableFeignClients
注解实现了client的配置声明注册,再通过FeignRibbonClientAutoConfiguration
和FeignAutoConfiguration
类进行自动装配
本文仅对feign源码的主线进行分析,还有很多细节并未介绍,如果读者感兴趣,可以参考本文,自行阅读源码。
补充
Feign的组成
接口 | 作用 | 默认值 |
---|---|---|
Feign.Builder | Feign的入口 | Feign.Builder |
Client | Feign底层用什么去请求 | 和Ribbon配合时:LoadBalancerFeignClient不和Ribbon配合时:Fgien.Client.Default |
Contract | 契约,注解支持 | SpringMVCContract |
Encoder | 编码器 | SpringEncoder |
Decoder | 解码器 | ResponseEntityDecoder |
Logger | 日志管理器 | Slf4jLogger |
RequestInterceptor | 用于为每个请求添加通用逻辑(拦截器,例子:比如想给每个请求都带上heared) | 无 |
Feign的日志级别
日志级别 | 打印内容 |
---|---|
NONE(默认) | 不记录任何日志 |
BASIC | 仅记录请求方法,URL,响应状态代码以及执行时间(适合生产环境) |
HEADERS | 记录BASIC级别的基础上,记录请求和响应的header |
FULL | 记录请求和响应header,body和元数据 |
如何给Feign添加日志级别
局部配置
方式一:代码实现
- 编写配置类
public class FeignConfig {
@Bean
public Logger.Level Logger() {
return Logger.Level.FULL;
}
}
添加Feign配置类,可以添加在主类下,但是不用添加@Configuration。如果添加了@Configuration而且又放在了主类之下,那么就会所有Feign客户端实例共享,同Ribbon配置类一样父子上下文加载冲突;如果一定添加@Configuration,就放在主类加载之外的包。(建议还是不用加@Configuration)
- 配置@FeignClient
@FeignClient(name = "alibaba-nacos-discovery-server",configuration = FeignConfig.class)
public interface NacosDiscoveryClientFeign {
@GetMapping("/hello")
String hello(@RequestParam(name = "name") String name);
}
方式二:配置文件实现
feign:
client:
config:
#要调用的微服务名称
clientName:
loggerLevel: FULL
全局配置
方式一:代码实现
@EnableFeignClients(defaultConfiguration = FeignConfig.class)
方式二:配置文件实现
feign:
client:
config:
#将调用的微服务名称改成default就配置成全局的了
default:
loggerLevel: FULL
Feign支持的配置项
代码方式支持配置项
配置项 | 作用 |
---|---|
Logger.Level | 指定日志级别 |
Retryer | 指定重试策略 |
ErrorDecoder | 指定错误解码器 |
Request.Options | 超时时间 |
Collection |
拦截器 |
SetterFactory | 用于设置Hystrix的配置属性,Fgien整合Hystrix才会用 |
详见FeignClientsConfiguration
中配置
配置文件属性支持配置项
feign:
client:
config:
feignName:
connectTimeout: 5000 # 相当于Request.Optionsn 连接超时时间
readTimeout: 5000 # 相当于Request.Options 读取超时时间
loggerLevel: full # 配置Feign的日志级别,相当于代码配置方式中的Logger
errorDecoder: com.example.SimpleErrorDecoder # Feign的错误解码器,相当于代码配置方式中的ErrorDecoder
retryer: com.example.SimpleRetryer # 配置重试,相当于代码配置方式中的Retryer
requestInterceptors: # 配置拦截器,相当于代码配置方式中的RequestInterceptor
- com.example.FooRequestInterceptor
- com.example.BarRequestInterceptor
# 是否对404错误解码
decode404: false
encode: com.example.SimpleEncoder
decoder: com.example.SimpleDecoder
contract: com.example.SimpleContract
Feign还支持对请求和响应进行GZIP压缩,以提高通信效率,
仅支持Apache HttpClient,详见FeignContentGzipEncodingAutoConfiguration
配置方式如下:
# 配置请求GZIP压缩
feign.compression.request.enabled=true
# 配置响应GZIP压缩
feign.compression.response.enabled=true
# 配置压缩支持的MIME TYPE
feign.compression.request.mime-types=text/xml,application/xml,application/json
# 配置压缩数据大小的下限
feign.compression.request.min-request-size=2048
Feign默认使用HttpUrlConnection进行远程调用,可以通过配置开启HttpClient或OkHttp3,具体详见FeignRibbonClientAutoConfiguration
,配置如下:
feign.httpclient.enabled=true
//或
feign.okhttp.enabled=true
并添加相应的依赖即可
io.github.openfeign
feign-okhttp
//或
io.github.openfeign
feign-httpclient
关于Spring Cloud OpenFeign的配置有很多,本文只是列出了部分配置,更多配置请自行阅读源码。
欢迎关注我的公众号:程序员L札记