更多Spring与微服务相关的教程请戳这里 Spring与微服务教程合集
1、@EnableFeignClients注解的神秘之处
在使用Feign的时候,我们都知道,需要在springboot的启动类打上@EnableFeignClients注解,才能使Feign生效,那么@EnableFeignClients这个注解到底有哪些神秘之处呢,我们往下看。
首先看一下@EnableFeignClients注解的源码(去除了注释)
package org.springframework.cloud.openfeign;
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import org.springframework.context.annotation.Import;
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@Documented
@Import(FeignClientsRegistrar.class)
public @interface EnableFeignClients {
String[] value() default {};
String[] basePackages() default {};
Class<?>[] basePackageClasses() default {};
Class<?>[] defaultConfiguration() default {};
Class<?>[] clients() default {};
}
可以很明显地看到@Import(FeignClientsRegistrar.class),接下来看看这个类的registerBeanDefinitions方法
@Override
public void registerBeanDefinitions(AnnotationMetadata metadata,
BeanDefinitionRegistry registry) {
//注册默认配置
registerDefaultConfiguration(metadata, registry);
//注册所有的FeignClient
registerFeignClients(metadata, registry);
}
这个类实际上做了两件事情,下面我分别介绍
1.1、registerDefaultConfiguration注册默认配置
点进registerDefaultConfiguration方法中,如下图
再点进上图中红框框住的方法里面去
注册默认配置到此处就结束了
1.2、registerFeignClients注册所有的FeignClient
这一节主要来看看registerFeignClients方法,顾名思义,应该是注册所有的FeignClient
public void registerFeignClients(AnnotationMetadata metadata,
BeanDefinitionRegistry registry) {
ClassPathScanningCandidateComponentProvider scanner = getScanner();
scanner.setResourceLoader(this.resourceLoader);
Set<String> basePackages;
// 获取@EnableFeignClients注解的属性
Map<String, Object> attrs = metadata
.getAnnotationAttributes(EnableFeignClients.class.getName());
// 声明一个Filter,用于找出所有打了@FeignClient主机的类
AnnotationTypeFilter annotationTypeFilter = new AnnotationTypeFilter(
FeignClient.class);
// 获取@EnableFeignClients注解的clients属性的值
final Class<?>[] clients = attrs == null ? null
: (Class<?>[]) attrs.get("clients");
// 如果没有配置clients属性,则走if
if (clients == null || clients.length == 0) {
// 将过滤器添加打扫描器上
scanner.addIncludeFilter(annotationTypeFilter);
// 如果不做额外配置,则默认的basePackages就是启动类所在的包
basePackages = getBasePackages(metadata);
}
// 如果配置了clients属性,则走else
else {
final Set<String> clientClasses = new HashSet<>();
basePackages = new HashSet<>();
for (Class<?> clazz : clients) {
basePackages.add(ClassUtils.getPackageName(clazz));
clientClasses.add(clazz.getCanonicalName());
}
AbstractClassTestingTypeFilter filter = new AbstractClassTestingTypeFilter() {
@Override
protected boolean match(ClassMetadata metadata) {
String cleaned = metadata.getClassName().replaceAll("\\$", ".");
return clientClasses.contains(cleaned);
}
};
// 将过滤器添加打扫描器上
scanner.addIncludeFilter(
new AllTypeFilter(Arrays.asList(filter, annotationTypeFilter)));
}
// 遍历所有的basePackage
for (String basePackage : basePackages) {
// 用扫描器在basePackage 下面并结合Filter找出BeanDefinition
Set<BeanDefinition> candidateComponents = scanner
.findCandidateComponents(basePackage);
// 遍历所有的BeanDefinition
for (BeanDefinition candidateComponent : candidateComponents) {
if (candidateComponent instanceof AnnotatedBeanDefinition) {
// 校验:@FeignClient注解必须作用在接口上
AnnotatedBeanDefinition beanDefinition = (AnnotatedBeanDefinition) candidateComponent;
AnnotationMetadata annotationMetadata = beanDefinition.getMetadata();
Assert.isTrue(annotationMetadata.isInterface(),
"@FeignClient can only be specified on an interface");
//获取@FeignClient注解的属性
Map<String, Object> attributes = annotationMetadata
.getAnnotationAttributes(
FeignClient.class.getCanonicalName());
// 获取FeignClient的name,具体实现细节请读者自行查看
String name = getClientName(attributes);
// 注册FeignClient的配置
registerClientConfiguration(registry, name,
attributes.get("configuration"));
// 注册FeignClient
registerFeignClient(registry, annotationMetadata, attributes);
}
}
}
}
可以看到,最后有两个方法,分别是:
- registerClientConfiguration(注册FeignClient的配置)
- registerFeignClient(注册FeignClient)
下面分别介绍这两个方法
registerClientConfiguration方法挺简单的,就是声明了一个类型为FeignClientSpecification、name为(name + "." + FeignClientSpecification.class.getSimpleName())的bean
如果你的项目中有2个FeignClient,则最终有3个FeignClientSpecification,为什么?因为@EnableFeignCliens注解也会声明一个FeignClientSpecification,这个上面已经说过了
private void registerClientConfiguration(BeanDefinitionRegistry registry, Object name,
Object configuration) {
BeanDefinitionBuilder builder = BeanDefinitionBuilder
.genericBeanDefinition(FeignClientSpecification.class);
builder.addConstructorArgValue(name);
builder.addConstructorArgValue(configuration);
registry.registerBeanDefinition(
name + "." + FeignClientSpecification.class.getSimpleName(),
builder.getBeanDefinition());
}
下面主要看registerFeignClient方法
private void registerFeignClient(BeanDefinitionRegistry registry,
AnnotationMetadata annotationMetadata, Map<String, Object> attributes) {
// @FeignClient注解所在的接口名
String className = annotationMetadata.getClassName();
// 生成一个FeignClientFactoryBean的代理类
BeanDefinitionBuilder definition = BeanDefinitionBuilder
.genericBeanDefinition(FeignClientFactoryBean.class);
// 校验fallback属性和fallbackFactory属性的值是否合格
validate(attributes);
// 给FeignClientFactoryBean的代理类添加一系列属性
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);
// bean的别名
String qualifier = getQualifier(attributes);
if (StringUtils.hasText(qualifier)) {
alias = qualifier;
}
BeanDefinitionHolder holder = new BeanDefinitionHolder(beanDefinition, className,
new String[] { alias });
// 通过该工具类创建FeignClient
BeanDefinitionReaderUtils.registerBeanDefinition(holder, registry);
}
上述代码段中,有两行关键代码是下面要讲的
- 第一个关键代码是BeanDefinitionBuilder definition = BeanDefinitionBuilder.genericBeanDefinition(FeignClientFactoryBean.class);
- 第二个关键代码是最后一行代码BeanDefinitionReaderUtils.registerBeanDefinition(holder, registry);
第二个简单点,先看第二个,点进BeanDefinitionReaderUtils.registerBeanDefinition方法里看看
public static void registerBeanDefinition(
BeanDefinitionHolder definitionHolder, BeanDefinitionRegistry registry)
throws BeanDefinitionStoreException {
// Bean的名字
String beanName = definitionHolder.getBeanName();
// 注册bean,但不表示马上初始化Bean
registry.registerBeanDefinition(beanName, definitionHolder.getBeanDefinition());
// 注册bean的别名
String[] aliases = definitionHolder.getAliases();
if (aliases != null) {
for (String alias : aliases) {
registry.registerAlias(beanName, alias);
}
}
}
这个没什么好说的,就是注册了一个Bean
接下来重点看看FeignClientFactoryBean这个类
该类的声明信息如下
class FeignClientFactoryBean
implements FactoryBean<Object>, InitializingBean, ApplicationContextAware
这里科普一下:
- Spring 中有两种类型的Bean,一种是普通Bean,另一种是工厂Bean 即 FactoryBean。FactoryBean跟普通Bean不同,其返回的对象不是指定类的一个实例,而是该FactoryBean的getObject方法所返回的对象,创建出来的对象是否属于单例由isSingleton中的返回决定。
- 而实现了InitializingBean接口为bean提供了初始化方法的方式,它只包括afterPropertiesSet方法,凡是继承该接口的类,在初始化bean的时候会执行该方法
那么先看看FeignClientFactoryBean类是如何实现afterPropertiesSet方法的
@Override
public void afterPropertiesSet() throws Exception {
Assert.hasText(this.contextId, "Context id must be set");
Assert.hasText(this.name, "Name must be set");
}
这个一看就懂,没什么好说的
接下来重要研究一下FeignClientFactoryBean类如何实现FactoryBean接口的
先来看标记1,FeignContext是在哪初始化的呢?
如下图所示,是在FeignAutoConfiguration中初始化的,这里的FeignContext是feign的全局上下文,并且@EnableConfigurationProperties注解将FeignClientProperties类给初始化了
再看标记2,点进feign方法里面
下图中的get方法,看样子好像是要根据上下文,获取一个FeignLoggerFactory实例
T instance = context.getInstance(this.contextId, type);对于这行代码,context是全局上下文,this.contextId是单个@FeignClient所声明的上下文,而type是FeignLoggerFactory类的class对象
下面红框中的代码,应该是根据@FeignClient的名称,获取由@FeignClient声明的单个上下文,并且上下文类型是AnnotationConfigApplicationContext
// 存储FeignClient的上下文
private Map<String, AnnotationConfigApplicationContext> contexts = new ConcurrentHashMap<>();
//存储配置信息
private Map<String, C> configurations = new ConcurrentHashMap<>();
// 根据FeignClient的name去查找,如果没有上下文则createContext,有则直接返回
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);
}
然后看看createContext方法,这里面有个this.defaultConfigType属性,它的类型是FeignClientsConfiguration(注意,是FeignClients而不是FeignClient,所以这是个全局配置),关于FeignClientsConfiguration我后面再讲,但是现在我很明确的告诉你这里面配置了类型为FeignLoggerFactory的bean,刚才一系列的进栈不就是为了获取它么
protected AnnotationConfigApplicationContext createContext(String name) {
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext();
// 此处的this对象是FeignContext,所以如果项目中有两个FeignClient的话,则this.configurations的数量是3
if (this.configurations.containsKey(name)) {
for (Class<?> configuration : this.configurations.get(name)
.getConfiguration()) {
将feignClient的配置信息注册到feignContext的上下文中
context.register(configuration);
}
}
// default.开头表示全局配置,将全局配置也注册到feignClient的上下文中
for (Map.Entry<String, C> entry : this.configurations.entrySet()) {
if (entry.getKey().startsWith("default.")) {
for (Class<?> configuration : entry.getValue().getConfiguration()) {
context.register(configuration);
}
}
}
// this.defaultConfigType的类型是FeignClientsConfiguration,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.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;
}
刚才经过了一系列的方法调用,接下来一路出栈,直接到下图
可总算拿到FeignLoggerFactory的实例了,接下来看下图
上图中红框中有个configureFeign方法,该方法用于对单个feignClient进行JavaConfig配置或者yml配置文件配置的
protected void configureFeign(FeignContext context, Feign.Builder builder) {
// 获取FeignClientProperties 类型的Bean
FeignClientProperties properties = this.applicationContext
.getBean(FeignClientProperties.class);
if (properties != null) {
// 如果配置文件优先,则走if
if (properties.isDefaultToProperties()) {
// JavaConfig配置
configureUsingConfiguration(context, builder);
// 以default开头的配置,即yml中的feign.client.config.default
configureUsingProperties(
properties.getConfig().get(properties.getDefaultConfig()),
builder);
// 以feignClient的name开头的配置,即yml中的feign.client.config.name
configureUsingProperties(properties.getConfig().get(this.contextId),
builder);
}
// 如果JavaConfig优先,则走else
else {
configureUsingProperties(
properties.getConfig().get(properties.getDefaultConfig()),
builder);
configureUsingProperties(properties.getConfig().get(this.contextId),
builder);
configureUsingConfiguration(context, builder);
}
}
else {
// 如果没有FeignClientProperties 这个Bean,则使用JavaConfig进行配置
configureUsingConfiguration(context, builder);
}
}
feign方法出栈后,再完整的看一下getTarget方法
<T> T getTarget() {
FeignContext context = this.applicationContext.getBean(FeignContext.class);
Feign.Builder builder = feign(context);
// 如果@FeignClient注解没有手动指定url,则走if
if (!StringUtils.hasText(this.url)) {
if (!this.name.startsWith("http")) {
this.url = "http://" + this.name;
}
else {
this.url = this.name;
}
//最终的url类似于http://service-a/service-a,两个service-a分别为name属性值和path属性值
this.url += cleanPath();
// 因为没有手动指定url,此时的请求是负载均衡的,所以用loadBalance方法套了一层,但loadBalance方法最终返回的还是targeter.target
return (T) loadBalance(builder, context,
new HardCodedTarget<>(this.type, this.name, this.url));
}
// 如果手动指定url,则走下面的代码
if (StringUtils.hasText(this.url) && !this.url.startsWith("http")) {
this.url = "http://" + this.url;
}
// 最终的string url为http://localhost:8080/service-a,localhost:8080是url属性的值,而service-a是path属性的值
String url = this.url + cleanPath();
// 获取发起http请求的客户端,默认为null
Client client = getOptional(context, Client.class);
if (client != null) {
//下面的代码虽然出现了loadBalancer的字眼,但是分别是因为ribbon和spring cloud loadBalancer在classpath上才打开它的,但是并不能提供负载均衡,因为是手动指定的url
if (client instanceof LoadBalancerFeignClient) {
client = ((LoadBalancerFeignClient) client).getDelegate();
}
if (client instanceof FeignBlockingLoadBalancerClient) {
client = ((FeignBlockingLoadBalancerClient) client).getDelegate();
}
builder.client(client);
}
//默认为HystrixTargeter
Targeter targeter = get(context, Targeter.class);
// 最终的返回值,即FeignClientFactoryBean的生产目标对象
return (T) targeter.target(this, builder, context,
new HardCodedTarget<>(this.type, this.name, url));
}
3、FeignClientsConfiguration和FeignClientProperties
这两个类分别对应全局配置和单个配置,那么何时加载的呢?
在本文中全局搜索这两个类的名字,答案就在上面。