四、RESTful客户端 - OpenFeign
OpenFeign 是 Spring Cloud 在原有 Netflix 的 Feign 的基础之上改造成自己的远程调用组件。
OpenFeign 是个声明式 RESTful 网络请求客户端,使得编写 Web 服务客户端更加方便和快捷,只需要使用 OpenFeign 提供的注解修饰定义网络请求的接口类,就可以使用该接口的实例发送 RESTful 风格的网络请求 。OpenFeign 还可以集成 Ribbon 和 Hystrix 来提供负载均衡和网络断路器的功能。
RESTful 网络请求是指 RESTful 风格的网络请求,其中 REST 是 Resource Representational State Transfer 的缩写,直接翻译即“资源表现层状态转移”。Resource 代表互联网资源。所谓“资源”是网络上的一个实体,或者说网上的一个具体信息。 它可以是一段文本、一首歌曲、一种服务,可以使用一个 URI 指向它,每种“资源”对应一个 URI。Representational 是“表现层”意思。 “资源”是一种消息实体,它可以有多种外在的表现形式,我们把“资源”具体呈现出来的形式叫作它的“表现层”。 比如说文本可以用 TXT 格式进行表现,也可以使用 XML 格式、 JSON 格式和二进制格式;视频可以用 MP4 格式表 现,也可以用 AVI 格式表现。 URI 只代表资源的实体,不代表它的形式 。它的具体表现形 式,应该由 HTTP 请求的头信息 Accept 和 Content-Type 字段指定,这两个字段是对“表现层”的描述。 State Transfer 是指“状态转移”。 客户端访问服务的过程中必然涉及数据和状态的转化。 如果客户端想要操作服务端资源,必须通过某种手段,让服务器端资源发生“状态转移”。 而这种转化是建立在表现层之上的,所以被称为 “表现层状态转移”。 客户端通过使 HTTP 协议中的四个动词来实现上述操作,它们分别是:获取资源的 GET、 新建或更新资源的 POST 、更新资源的 PUT 和删除资源的 DELETE。
4.1、OpenFeign简介
首先先贴上 OpenFeign 的GitHub地址。
OpenFeign 是一个声明式、模板化的 RESTful 网络请求客户端。 OpenFeign 会根据带有注解的函数信息构建出网络请求的模板,在发送网络请求之前,OpenFeign 会将函数的参数值设置到这些请求模板中。
在 Spring Cloud 中,使用 OpenFeign 非常简单——创建一个接口,并在接口上添加一些注解即可。除了使用 SpringMVC 的注解之外,OpenFeign 也支持多种自带的注解。
使用 OpenFeign 的 Spring 应用架构一般分为三个部分,分别为服务注中心、服务提供者和服务消费者。服务提供者向服务注册中心注册自己,然后服务消费者通过 OpenFeign 发送请求时, OpenFeign 会向服务注册中心获取关于服务提供者的信息,然后再向服务提供者发送网络请求。
4.2、核心
在阅读源码的时候,可以考虑以下两点:
- 被 @FeignClient 注解修饰的接口类如何创建,也就是其Bean实例如何被创建到容器中。
- 在调用 @FeignClient 对象的网络请求相关函数时,OpenFeign 是如何发送网络请求的。
4.3、源码分析
4.3.1、注解:@EnableFeignClients
@EnableFeignClients 类似 @EnableEurekaServer 注解,其作用就像是 OpenFeign 的开关一样,一切 OpenFeign 的相关操作都是从它开始的。@EnableFeignClients 有几个作用,一个是引入了 FeignClientsRegistrar;二是指定扫描了 FeignClient 的包信息,就是指定了 FeignClient 接口类所在的包名;三是指定了 FeignClient 接口类的自定义配置类。
@EnableFeignClients 注解的定义如下:
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@Documented
@Import(FeignClientsRegistrar.class)
public @interface EnableFeignClients {
//下面三个函数都是为了指定需要扫描的包
String[] value() default {};
String[] basePackages() default {};
Class<?>[] basePackageClasses() default {};
//指定自定义feignclient的自定义配置,可以配置Decoder、Encoder、Contract等组件,FeignClientsConfiguration是默认的配置
Class<?>[] defaultConfiguration() default {};
Class<?>[] clients() default {};
}
在上述注解定义中,引入了 FeignClientsRegistrar 组件,而该类是 ImportBeanDefinitionRegistrar 的子类,用于动态注册 BeanDefinition。所以 OpenFeign 通过 FeignClientsRegistrar 来处理 @FeignClient 注解修饰的 FeignClient 接口类,将这些接口类的 BeanDefinition 注册到 Spring 容器中,这样就可以使用 @Autowired 等方式来自动装载这些 FeignClient 接口类的 Bean 实例。
FeignClientsRegistrar 的部分代码如下:
class FeignClientsRegistrar
implements ImportBeanDefinitionRegistrar, ResourceLoaderAware, EnvironmentAware {
//other...
@Override
public void registerBeanDefinitions(AnnotationMetadata metadata,
BeanDefinitionRegistry registry) {
//从@EnableFeignClients注解的属性值来构建Feign的自定义Configuration进行注册
registerDefaultConfiguration(metadata, registry);
//扫描package,注册被@FeignClient注解修饰的接口类的bean信息
registerFeignClients(metadata, registry);
}
}
如上述代码所示,FeignClientsRegistrar 的 registerBeanDefinitions
方法主要做了两个事情, 一是注册 @EnableFeignClients 提供的自定义配置类 中的相关Bean实例, 二是根据 @EnableFeignClients 提供的包信息扫描 @FeignClient 注解修饰的 FeignClient 接口类,然后进行 Bean 实例注册。
4.3.1.1、注册默认配置
//FeignClientsRegistrar#registerDefaultConfiguration
private void registerDefaultConfiguration(AnnotationMetadata metadata,
BeanDefinitionRegistry registry) {
//获取@EnableFeignClients注解的属性键值对
Map<String, Object> defaultAttrs = metadata
.getAnnotationAttributes(EnableFeignClients.class.getName(), true);
//如果配置了@EnableFeignClients的defaultConfiguration属性,则进行注册该属性对应的Configuration
//后期使用default {}
if (defaultAttrs != null && defaultAttrs.containsKey("defaultConfiguration")) {
String name;
//判断是否是内部类
if (metadata.hasEnclosingClass()) {
name = "default." + metadata.getEnclosingClassName();
}
else {
//name="default" + "." + @EnableFeignClients修饰的类路径
name = "default." + metadata.getClassName();
}
//注册defaultConfiguration指定的Configuration配置类
registerClientConfiguration(registry, name,
defaultAttrs.get("defaultConfiguration"));
}
}
如上述代码所示, registerDefaultConfiguration
方法会判断 @EnableFeignClients 注解是否设置了 defaultConfiguration
, 如果有,则将调用 registerClientConfiguration
方法,进行 BeanDefinitionRegistry 的注册。
//FeignClientsRegistrar#registerClientConfiguration
private void registerClientConfiguration(BeanDefinitionRegistry registry,
Object name, Object configuration) {
//使用BeanDefinitionBuilder来生成BeanDefinition,并注册到registry中
BeanDefinitionBuilder builder = BeanDefinitionBuilder
.genericBeanDefinition(FeignClientSpecification.class);
//添加构造函数的参数
builder.addConstructorArgValue(name);
builder.addConstructorArgValue(configuration);
registry.registerBeanDefinition(
name + "." + FeignClientSpecification.class.getSimpleName(),
builder.getBeanDefinition());
}
BeanDefinitionRegistry 是 Spring 框架中用于动态注册 BeanDefinition 信息的接口,调用其registerBeanDefinition
方法可以将 BeanDefinition 注册到 Spring 容器中,其中 name
属性就是注册 BeanDefinition 的名称,类似:default.xxx.FeignClientSpecification
,这里的xxx
一般为使用 @EnableFeignClients 修饰的类的全路径。
重点:FeignClientSpecification 类实现了 NamedContextFactory.Specification 接口,Spring Cloud 框架使用 NamedContextFactory 来创建一系列的运行上下文(ApplicationContext),来让对应的 Specification 在这些上下文中创建实例对象,这样使得各个子上下文中的实例对象相互独立,互不影响,可以方便地通过子上下文管理一系列不同的实例对象。
public abstract class NamedContextFactory<C extends NamedContextFactory.Specification>
implements DisposableBean, ApplicationContextAware {
private final String propertySourceName;
private final String propertyName;
//每个name对应一个ApplicationContext
private Map<String, AnnotationConfigApplicationContext> contexts = new ConcurrentHashMap<>();
//每个name对应一个Specification
private Map<String, C> configurations = new ConcurrentHashMap<>();
//父ApplicationContext
private ApplicationContext parent;
private Class<?> defaultConfigType;
public NamedContextFactory(Class<?> defaultConfigType, String propertySourceName,
String propertyName) {
this.defaultConfigType = defaultConfigType;
this.propertySourceName = propertySourceName;
this.propertyName = propertyName;
}
@Override
public void setApplicationContext(ApplicationContext parent) throws BeansException {
this.parent = parent;
}
//在FeignAutoConfiguration#feignContext方法会调用
public void setConfigurations(List<C> configurations) {
for (C client : configurations) {
this.configurations.put(client.getName(), client);
}
}
public Set<String> getContextNames() {
return new HashSet<>(this.contexts.keySet());
}
//实现了DisposableBean接口,在对象消亡时会调用destroy方法来销毁bean实例
@Override
public void destroy() {
Collection<AnnotationConfigApplicationContext> values = this.contexts.values();
for (AnnotationConfigApplicationContext context : values) {
// This can fail, but it never throws an exception (you see stack traces
// logged as WARN).
context.close();
}
this.contexts.clear();
}
//根据name获取ApplicationContext,如果不存在,则先创建一个AnnotationConfigApplicationContext上下文对象,并存储保存,否则直接返回name对应的AnnotationConfigApplicationContext上下文对象
protected AnnotationConfigApplicationContext getContext(String name) {
if (!this.contexts.containsKey(name)) {
synchronized (this.contexts) {
if (!this.contexts.containsKey(name)) {
this.contexts.put(name, createContext(name));
}
}
}
return this.contexts.get(name);
}
//创建与name对应的AnnotationConfigApplicationContext子上下文中,并在其注册对应的配置等
protected AnnotationConfigApplicationContext createContext(String name) {
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext();
if (this.configurations.containsKey(name)) {
for (Class<?> configuration : this.configurations.get(name)
.getConfiguration()) {
//在子上下文中注册与name关联的Configuration(独立)
context.register(configuration);
}
}
for (Map.Entry<String, C> entry : this.configurations.entrySet()) {
if (entry.getKey().startsWith("default.")) {
//在子上下文中注册默认的Configuration(共用)
for (Class<?> configuration : entry.getValue().getConfiguration()) {
context.register(configuration);
}
}
}
//注册PropertyPlaceholderAutoConfiguration和FeignClientsConfiguration配置类
context.register(PropertyPlaceholderAutoConfiguration.class,
this.defaultConfigType);
context.getEnvironment().getPropertySources().addFirst(new MapPropertySource(
this.propertySourceName,
Collections.<String, Object>singletonMap(this.propertyName, name)));
if (this.parent != null) {
// Uses Environment from parent as well as beans
//所有的context的parent相同,这样一些相同的bean可以通过parent的context来获取
context.setParent(this.parent);
// jdk11 issue
// https://github.com/spring-cloud/spring-cloud-netflix/issues/3101
context.setClassLoader(this.parent.getClassLoader());
}
context.setDisplayName(generateDisplayName(name));
context.refresh();
return context;
}
protected String generateDisplayName(String name) {
return this.getClass().getSimpleName() + "-" + name;
}
//从name对应的AnnotationConfigApplicationContext获取单个Bean实例信息
public <T> T getInstance(String name, Class<T> type) {
AnnotationConfigApplicationContext context = getContext(name);
if (BeanFactoryUtils.beanNamesForTypeIncludingAncestors(context,
type).length > 0) {
return context.getBean(type);
}
return null;
}
public <T> ObjectProvider<T> getLazyProvider(String name, Class<T> type) {
return new ClientFactoryObjectProvider<>(this, name, type);
}
public <T> ObjectProvider<T> getProvider(String name, Class<T> type) {
AnnotationConfigApplicationContext context = getContext(name);
return context.getBeanProvider(type);
}
//
public <T> T getInstance(String name, Class<?> clazz, Class<?>... generics) {
ResolvableType type = ResolvableType.forClassWithGenerics(clazz, generics);
return getInstance(name, type);
}
@SuppressWarnings("unchecked")
public <T> T getInstance(String name, ResolvableType type) {
AnnotationConfigApplicationContext context = getContext(name);
String[] beanNames = BeanFactoryUtils.beanNamesForTypeIncludingAncestors(context,
type);
if (beanNames.length > 0) {
for (String beanName : beanNames) {
if (context.isTypeMatch(beanName, type)) {
return (T) context.getBean(beanName);
}
}
}
return null;
}
//从name对应的AnnotationConfigApplicationContext中获取T类型的Bean实例,由BeanName与Bean组成的K-V对象
public <T> Map<String, T> getInstances(String name, Class<T> type) {
AnnotationConfigApplicationContext context = getContext(name);
if (BeanFactoryUtils.beanNamesForTypeIncludingAncestors(context,
type).length > 0) {
return BeanFactoryUtils.beansOfTypeIncludingAncestors(context, type);
}
return null;
}
/**
* Specification with name and configuration.
*/
public interface Specification {
String getName();
Class<?>[] getConfiguration();
}
}
NamedContextFactory 有几个功能:
- 创建 AnnotationConfigApplicationContext 子上下文(#createContext方法);
- 在子上下文中创建并获取 Bean 实例(#createContext方);
- 当子上下文消亡时清除其中的 Bean 实例(#destroy方法):由于 NameContextFactory 实现了DisposableBean 接口 ,当 NamedContextFactory 实例消亡时, Spring 框架会调用其
destroy
方法,清除掉自己创建的所有子上下文和自身包含的所有组件实例。
在 OpenFeign 中, FeignContext 继承了 NamedContextFactory ,用于存储各类 OpenFeign 的组件实例。
public class FeignContext extends NamedContextFactory<FeignClientSpecification> {
public FeignContext() {
super(FeignClientsConfiguration.class, "feign", "feign.client.name");
}
}
FeignContext 的构造方法调用父类的构造方法,传入的默认配置类defaultConfigType
为 FeignClientsConfiguration,propertySourceName
为Feign
,propertyName
为feign.client.name
。
@Configuration(proxyBeanMethods = false)
public class FeignClientsConfiguration {
@Autowired
private ObjectFactory<HttpMessageConverters> messageConverters;
@Autowired(required = false)
private List<AnnotatedParameterProcessor> parameterProcessors = new ArrayList<>();
@Autowired(required = false)
private List<FeignFormatterRegistrar> feignFormatterRegistrars = new ArrayList<>();
@Autowired(required = false)
private Logger logger;
@Autowired(required = false)
private SpringDataWebProperties springDataWebProperties;
@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() {
PageableSpringEncoder encoder = new PageableSpringEncoder(
new SpringEncoder(this.messageConverters));
if (springDataWebProperties != null) {
encoder.setPageParameter(
springDataWebProperties.getPageable().getPageParameter());
encoder.setSizeParameter(
springDataWebProperties.getPageable().getSizeParameter());
encoder.setSortParameter(
springDataWebProperties.getSort().getSortParameter());
}
return encoder;
}
@Bean
@ConditionalOnMissingBean
public Contract feignContract(ConversionService feignConversionService) {
return new SpringMvcContract(this.parameterProcessors, feignConversionService);
}
@Bean
public FormattingConversionService feignConversionService() {
FormattingConversionService conversionService = new DefaultFormattingConversionService();
for (FeignFormatterRegistrar feignFormatterRegistrar : this.feignFormatterRegistrars) {
feignFormatterRegistrar.registerFormatters(conversionService);
}
return conversionService;
}
@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);
}
@Bean
@ConditionalOnClass(name = "org.springframework.data.domain.Page")
public Module pageJacksonModule() {
return new PageJacksonModule();
}
@Configuration(proxyBeanMethods = false)
@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();
}
}
}
在讲解 @EnableFeignClients 注解的时候介绍到, FeignClientsConfiguration 是 OpenFeign 默认的配置类,它会创建一些默认的 Bean 对象,比如 Encoder、Decoder、Contract、Retryer 等。后面再介绍,在此先过。
总结:NamedContextFactory 会创建出 AnnotationConfigApplicationContext 实例,并以 name 作为标识,然后每个 AnnotationConfigApplicationContext 实例都会注册部分配置类, 从而可以给出一系列基于配置类生成的组件实例,这样就可以基于 name 来管理一系列的组件实例,为不同的 FeignClient 准备不同配置组件实例, 比如说 Decoder、 Encoder等。