前言
本文我们一起看看SpringCloud中的OpenFeign组件,探讨下其整体结构和带给我的启发。内容涵盖启用Feign,创建代理对象,到最终响应处理的整个过程。
一、啥是OpenFeign
OpenFeign是是一个基于Http协议的RPC组件,简化在基于SpringCloud微服务环境下完成服务间调用的开发。那跟Feign有啥区别呢? 早期SpringCloud版本下的RPC组件是Feign,后来停止更新后退出了OpenFeign。此外相比Feign,OpenFeign更加open,支持处理SpringMVC中的@RequestMapping注解。目前项目,大家一般都用的大多是OpenFeign。
二、启用FeignClient
1. Maven依赖
别问我为啥不贴Gradle,就是不会用,因为Maven用顺手了也就没再折腾。
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
<version>3.1.3</version>
</dependency>
2. 代码声明
关键代码就是这个 @EnableFeignClients(basePackages = {“com.test.spi”}), 到这里就启用了OpenFeign。要问到底是咋启用的,说明小伙你很不错,别着急往后看。
@Slf4j
@EnableFeignClients(basePackages = {"com.test.spi"})
@SpringBootApplication
public class TestApplication {
public static void main(String[] args) {
SpringApplication.run(TestApplication.class, args);
}
}
3. 背后的秘密
首先看@EnableFeignClients到底是咋定义的?
其中最关键的是@Import。如果你看过前面写的Spring系列,应该知道有个叫ConfigurationClassParser的类,其中processImports方法完成了对@Import注解的处理,创建一个ImportBeanDefinitionRegistrar接口类的实例。
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@Documented
@Import(FeignClientsRegistrar.class)
public @interface EnableFeignClients {
}
ImportBeanDefinitionRegistrar接口内部提供了register方法来完成BeanDefinition的注册。因此FeignClientsRegistrar也实现了该接口;
class FeignClientsRegistrar implements ImportBeanDefinitionRegistrar, ResourceLoaderAware, EnvironmentAware {
@Override
public void registerBeanDefinitions(AnnotationMetadata metadata, BeanDefinitionRegistry registry) {
registerDefaultConfiguration(metadata, registry);
registerFeignClients(metadata, registry);
}
}
这里我们可以看到其中注册了DefaultConfiguration和FeginClient对应的BeanDefinition。咱们顺着这条线继续。
private void registerFeignClient(BeanDefinitionRegistry registry, AnnotationMetadata annotationMetadata,
Map<String, Object> attributes) {
String className = annotationMetadata.getClassName();
Class clazz = ClassUtils.resolveClassName(className, null);
ConfigurableBeanFactory beanFactory = registry instanceof ConfigurableBeanFactory
? (ConfigurableBeanFactory) registry : null;
String contextId = getContextId(beanFactory, attributes);
String name = getName(attributes);
FeignClientFactoryBean factoryBean = new FeignClientFactoryBean();
factoryBean.setBeanFactory(beanFactory);
factoryBean.setName(name);
factoryBean.setContextId(contextId);
factoryBean.setType(clazz);
factoryBean.setRefreshableClient(isClientRefreshEnabled());
BeanDefinitionBuilder definition = BeanDefinitionBuilder.genericBeanDefinition(clazz, () -> {
factoryBean.setUrl(getUrl(beanFactory, attributes));
factoryBean.setPath(getPath(beanFactory, attributes));
factoryBean.setDecode404(Boolean.parseBoolean(String.valueOf(attributes.get("decode404"))));
Object fallback = attributes.get("fallback");
if (fallback != null) {
factoryBean.setFallback(fallback instanceof Class ? (Class<?>) fallback
: ClassUtils.resolveClassName(fallback.toString(), null));
}
Object fallbackFactory = attributes.get("fallbackFactory");
if (fallbackFactory != null) {
factoryBean.setFallbackFactory(fallbackFactory instanceof Class ? (Class<?>) fallbackFactory
: ClassUtils.resolveClassName(fallbackFactory.toString(), null));
}
return factoryBean.getObject();
});
definition.setAutowireMode(AbstractBeanDefinition.AUTOWIRE_BY_TYPE);
definition.setLazyInit(true);
validate(attributes);
AbstractBeanDefinition beanDefinition = definition.getBeanDefinition();
beanDefinition.setAttribute(FactoryBean.OBJECT_TYPE_ATTRIBUTE, className);
beanDefinition.setAttribute("feignClientsRegistrarFactoryBean", factoryBean);
boolean primary = (Boolean) attributes.get("primary");
beanDefinition.setPrimary(primary);
String[] qualifiers = getQualifiers(attributes);
if (ObjectUtils.isEmpty(qualifiers)) {
qualifiers = new String[] { contextId + "FeignClient" };
}
BeanDefinitionHolder holder = new BeanDefinitionHolder(beanDefinition, className, qualifiers);
BeanDefinitionReaderUtils.registerBeanDefinition(holder, registry);
registerOptionsBeanDefinition(registry, contextId);
}
到这里我们看到了熟悉的手法,那就是FactoryBean,FeignClientFactoryBean,其中设置了各种配置,获取实例的方法在factory.getObject()。在下一节,咱们详细看看getObject方法。
三、创建代理对象
1. 从Client开始
@Override
public Object getObject() {
return getTarget();
}
<T> T getTarget() {
FeignContext context = beanFactory != null ? beanFactory.getBean(FeignContext.class)
: applicationContext.getBean(FeignContext.class);
Feign.Builder builder = feign(context);
if (!StringUtils.hasText(url)) {
if (LOG.isInfoEnabled()) {
LOG.info("For '" + name + "' URL not provided. Will try picking an instance via load-balancing.");
}
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 FeignBlockingLoadBalancerClient) {
client = ((FeignBlockingLoadBalancerClient) client).getDelegate();
}
if (client instanceof RetryableFeignBlockingLoadBalancerClient) {
client = ((RetryableFeignBlockingLoadBalancerClient) client).getDelegate();
}
builder.client(client);
}
applyBuildCustomizers(context, builder);
Targeter targeter = get(context, Targeter.class);
return (T) targeter.target(this, builder, context, new HardCodedTarget<>(type, name, url));
}
从代码里可以看到,没有配置url则进入FeignBlockingLoadBalancerClient,此处的URL最终需要从注册中心里获取。如果配置了url,一般来说是网关的url,负载均衡由网关完成。 此外,如果自己在applicationContext中注册了FeignBlockingLoadBalancerClient或者RetryableFeignBlockingLoadBalancerClient,框架也会自动识别并使用已提供的client。作为一个框架, 不能强制要求用户用什么(面向抽象),还得考虑用户可能用什么(支持上下文感知),支持替换(松耦合)。至此,我明白自己写不来框架是有原因的。
2. 最爱的Customizer
再说这个 applyBuildCustomizers, 允许整一堆的customizer,框架层面专门定义了一个接口FeignBuilderCustomer,灵活性不要不要的。
private void applyBuildCustomizers(FeignContext context, Feign.Builder builder) {
Map<String, FeignBuilderCustomizer> customizerMap = context.getInstances(contextId,
FeignBuilderCustomizer.class);
if (customizerMap != null) {
customizerMap.values().stream().sorted(AnnotationAwareOrderComparator.INSTANCE)
.forEach(feignBuilderCustomizer -> feignBuilderCustomizer.customize(builder));
}
additionalCustomizers.forEach(customizer -> customizer.customize(builder));
}
3. Targeter接着浪
Targeter targeter = get(context, Targeter.class);
虽然只有1行代码,但从逻辑上说完成了上下文感知,也就是从上下文中获取Targeter对象。代码中直接可以找到的有2种,DefaultTargeter 和 FeignCircuitBreakerTargeter。后者从名字可以看出是一个支持熔断的targeter。但是Targeter从哪里来呢?
这个问题严格说之前没写过,算是个盲区。首先我们找到了创建实例的地方。结尾的几个字母说明了问题AutoConfiguration。这就得说说SpringBoot的AutoConfiguration机制了。 所以这玩意是在某个spring.factories中配置了,进而导致该Configuration中的Bean被自动装配到了容器中。
@Configuration(proxyBeanMethods = false)
@ConditionalOnClass(Feign.class)
@EnableConfigurationProperties({ FeignClientProperties.class, FeignHttpClientProperties.class,
FeignEncoderProperties.class })
public class FeignAutoConfiguration {
}
有了这个思路,直接去找jar中的META-INF文件夹。最终在 spring-cloud-openfeign-core-3.1.3.jar中的META-INF/spring.factories中找到如下结果
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
org.springframework.cloud.openfeign.hateoas.FeignHalAutoConfiguration,\
org.springframework.cloud.openfeign.FeignAutoConfiguration,\
org.springframework.cloud.openfeign.encoding.FeignAcceptGzipEncodingAutoConfiguration,\
org.springframework.cloud.openfeign.encoding.FeignContentGzipEncodingAutoConfiguration,\
org.springframework.cloud.openfeign.loadbalancer.FeignLoadBalancerAutoConfiguration
到这里,Targeter对象算是找到了。注意到这里,我们仅仅是流程上知道了什么对象在哪里创建,具体的创建细节我们没有讨论。毕竟一开始不能太深,否则容易沉沦于细节迷失方向。今天咱们的重点是,全局视角,流程打通。
4. 最终实例化
接下来以DefaultTargeter为例看看咱们可用的Feign对象具体是咋创建出来的。
DefaultTargeter 从这里可以看到实际调用的是FeignBuilder对象的target方法
class DefaultTargeter implements Targeter {
@Override
public <T> T target(FeignClientFactoryBean factory, Feign.Builder feign, FeignContext context,
Target.HardCodedTarget<T> target) {
return feign.target(target);
}
}
Feign.Builder
public <T> T target(Target<T> target) {
return build().newInstance(target);
}
public Feign build() {
Client client = Capability.enrich(this.client, capabilities);
Retryer retryer = Capability.enrich(this.retryer, capabilities);
List<RequestInterceptor> requestInterceptors = this.requestInterceptors.stream()
.map(ri -> Capability.enrich(ri, capabilities))
.collect(Collectors.toList());
Logger logger = Capability.enrich(this.logger, capabilities);
Contract contract = Capability.enrich(this.contract, capabilities);
Options options = Capability.enrich(this.options, capabilities);
Encoder encoder = Capability.enrich(this.encoder, capabilities);
Decoder decoder = Capability.enrich(this.decoder, capabilities);
InvocationHandlerFactory invocationHandlerFactory =
Capability.enrich(this.invocationHandlerFactory, capabilities);
QueryMapEncoder queryMapEncoder = Capability.enrich(this.queryMapEncoder, capabilities);
SynchronousMethodHandler.Factory synchronousMethodHandlerFactory =
new SynchronousMethodHandler.Factory(client, retryer, requestInterceptors, logger,
logLevel, decode404, closeAfterDecode, propagationPolicy, forceDecoding);
ParseHandlersByName handlersByName =
new ParseHandlersByName(contract, options, encoder, decoder, queryMapEncoder,
errorDecoder, synchronousMethodHandlerFactory);
return new ReflectiveFeign(handlersByName, invocationHandlerFactory, queryMapEncoder);
}
到这里可以看到最终需要进入ReflectiveFeign#newInstance()
ReflectiveFeign
public <T> T newInstance(Target<T> target) {
Map<String, MethodHandler> nameToHandler = targetToHandlersByName.apply(target);
Map<Method, MethodHandler> methodToHandler = new LinkedHashMap<Method, MethodHandler>();
List<DefaultMethodHandler> defaultMethodHandlers = new LinkedList<DefaultMethodHandler>();
for (Method method : target.type().getMethods()) {
if (method.getDeclaringClass() == Object.class) {
continue;
} else 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)));
}
}
InvocationHandler handler = factory.create(target, methodToHandler);
T proxy = (T) Proxy.newProxyInstance(target.type().getClassLoader(),
new Class<?>[] {target.type()}, handler);
for (DefaultMethodHandler defaultMethodHandler : defaultMethodHandlers) {
defaultMethodHandler.bindTo(proxy);
}
return proxy;
}
到这里就完成了Feign的实例化。整个过程涉及的关键对象有Client,Customizer和Targeter。有个FeignClient的代理对象,终于可以进入实际的RPC环节了。
四、RPC过程
在开始之前先聊几句背景知识,基于Feign的RPC实际上是一次基于Http协议的网络通信。从工程实现的角度,底层必然依赖成熟的HTTP通信框架(Apache HttpClien、okhttp)等。因此,此处在架构要分层设计,OpenFeign 应该做http协议和底层通信之外的其他事情。这就包括定义和实现方法和参数注解标准,最终将调用转换为一次网络请求,并将通信层提供的响应报文转换为目标类型的返回值。在OpenFeign中,这些都是由MethodHandler完成。
1. 构造MethodHandler
OpenFeign通过Contract来获取每个FeignClient中方法的MethodMetadata,并基于此创建对应RequestTemplate.Factory,进而创建MethodHandler。最终将method.configKey和MethodHandler作为键值对存储在Map中。细节代码如下:
ReflectiveFeign.ParseHandlersByName
static final class ParseHandlersByName {
private final Contract contract;
private final Options options;
private final Encoder encoder;
private final Decoder decoder;
private final ErrorDecoder errorDecoder;
private final QueryMapEncoder queryMapEncoder;
private final SynchronousMethodHandler.Factory factory;
ParseHandlersByName(
Contract contract,
Options options,
Encoder encoder,
Decoder decoder,
QueryMapEncoder queryMapEncoder,
ErrorDecoder errorDecoder,
SynchronousMethodHandler.Factory factory) {
this.contract = contract;
this.options = options;
this.factory = factory;
this.errorDecoder = errorDecoder;
this.queryMapEncoder = queryMapEncoder;
this.encoder = checkNotNull(encoder, "encoder");
this.decoder = checkNotNull(decoder, "decoder");
}
public Map<String, MethodHandler> apply(Target target) {
List<MethodMetadata> metadata = contract.parseAndValidateMetadata(target.type());
Map<String, MethodHandler> result = new LinkedHashMap<String, MethodHandler>();
for (MethodMetadata md : metadata) {
BuildTemplateByResolvingArgs buildTemplate;
if (!md.formParams().isEmpty() && md.template().bodyTemplate() == null) {
buildTemplate =
new BuildFormEncodedTemplateFromArgs(md, encoder, queryMapEncoder, target);
} else if (md.bodyIndex() != null || md.alwaysEncodeBody()) {
buildTemplate = new BuildEncodedTemplateFromArgs(md, encoder, queryMapEncoder, target);
} else {
buildTemplate = new BuildTemplateByResolvingArgs(md, queryMapEncoder, target);
}
if (md.isIgnored()) {
result.put(md.configKey(), args -> {
throw new IllegalStateException(md.configKey() + " is not a method handled by feign");
});
} else {
result.put(md.configKey(),
factory.create(target, md, buildTemplate, options, decoder, errorDecoder));
}
}
return result;
}
}
2. 执行RPC
显然首先得知道需要哪个MethodHandler,这个dispatch过程在InvocationHandler中完成,为了不与前面实例构造过程重复,该细节在此处体现。下面代码为创建Proxy对象时,使用的FeignInvocationHandler定义。
FeignInvocationHandler
static class FeignInvocationHandler implements InvocationHandler {
private final Target target;
private final Map<Method, MethodHandler> dispatch;
FeignInvocationHandler(Target target, Map<Method, MethodHandler> dispatch) {
this.target = checkNotNull(target, "target");
this.dispatch = checkNotNull(dispatch, "dispatch for %s", target);
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
return dispatch.get(method).invoke(args);
}
}
接下来以默认的MethodHandler实现类SynchronousMethodHandler进行介绍。
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;
}
}
}
以上为整个调用过程,先构造RequestTemplate,然后执行executeAndDecode,接下来根据Retriery的定义和响应结果确定重试逻辑。返回的Response其body要么是原始字节,要么是结构化的字符串(如JSON或者XML)通过Decoder对象转换为我们需要的对象。
至此,一个超精简版的RPC过程结束,后续会尝试对OpenFeign提供的扩展进行更进一步的分享。
总结
以上就是今天要讲的内容,本文介绍了OpenFeign在SpringBoot环境下如何启用@FeignClient注解,对应的BeanDefinition是如何注册的,BeanInstance是如何生成的,并简单分析了基于Feign的RPC调用过程。从内容上来看,基本覆盖了FeignClient的骨架内容。实际上,每个步骤在横向环节上,也有诸多值得注意的细节,姑且留给后续分享,希望本文能帮助你对OpenFeign有一个初步的了解,对RPC框架的内部有些参考。
|