(可以略过的废话)前段时间一个朋友在做spring security相关配置时遇到一些问题,请我帮忙解决一些问题,由于我自身并没有使用过security,所以花了一段时间去学习spring security的使用和解析了一下spring security源码。
时至今日,终于可以再次重新开始学习spring cloud相关的内容了~
扫描@FeignClient
注解的包路径,扫描这些包下的所有类,获取被@FeignClient
注解的接口,生成代理对象。
这个其实跟上面的很像,就是做一些标记类,然后这些标记类所在的包及其子包里的类都会被扫描。
这个是对所有feign 客户端进行配置设定使用的。
可以覆写feign client的相关组件定义
feign.codec.Decoder
: 解码器,也就是feign远程调用其他接口获取结果后进行解码成对应的对象
feign.codec.Encoder
: 编码器,feign在远程调用之前,需要先将请求参数进行编码成对应的表单或者json之类的。
feign.Contract
:注解解释器,也就是用来解读@FeignClient
标注的interface上的所有注解信息,转化为对应的url和参数。因为web框架比较多。每种注解不一致。feign是Netflix内部使用的,所以他们并没有对springmvc进行支持。后来openFeign中才添加对spring mvc的支持(也就是SpringMvcContract
)。
会基于这几个类和类所在的包进行扫描的。
FeignClientsRegistrar
实现了ImportBeanDefinitionRegistrar
接口。
而对于spring来说,实现了ImportBeanDefinitionRegistrar
的类,会在org.springframework.context.annotation.ConfigurationClassBeanDefinitionReader#loadBeanDefinitionsFromRegistrars
方法中会进行registerBeanDefinitions
方法的调用。
所在在FeignClientRegistrar
中,我们只需要关注registerBeanDefinitions
方法即可。
@Override
public void registerBeanDefinitions(AnnotationMetadata metadata,
BeanDefinitionRegistry registry) {
// 默认配置注册
registerDefaultConfiguration(metadata, registry);
// 注册FeignClient
registerFeignClients(metadata, registry);
}
private void registerDefaultConfiguration(AnnotationMetadata metadata,
BeanDefinitionRegistry registry) {
// 获取EnableFeignClients注解里面的参数信息
Map<String, Object> 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"));
}
}
上面就是从注解中获取配置信息,然后将配置信息注入到spring容器中。
具体如何注入的呢?
首先通过BeanDefinitionBuilder
构建一个FeignClientSpecification
对象,然后注入上面生成的name和配置信息。
最后通过BeanDefinitionRegistry
将由BeanDefinitionBuilder
构建出的FeignClientSpecification
注入到spring容器中去。
从上面我们知道,如果我们要自定义编码器Encoder
,解码器Decoder
,注解解释器Contract
时,实际上配置是交给FeignClientSpecification
管理的。
其实这里可以深究一下FeignClientSpecification
这个东西在哪用了,毕竟我们如果对组件进行了修改或者自定义,那么如何融合到feign的流程中,也是很有比较有探究价值的。
从上图我们可以看到,总共只有6处使用到这个类。其中两处需要关注的地方我已经标注出来。
第一个使用地:
从这个类的名字可以看出,这是个自动装配类,还是Feign直接相关的自动装配。那么这个类所在的包的spring.factories
就是我们后面探索的关键所在了。
这里可以看到,FeignClientSpecification
作为bean被Autowired到一个List集合中去了。那么后续的使用只需要关注configurations了。
这里就先不深入了,在后面探索spring.factories
时,再深入研究。
FeignContext
,见名知意,就是Feign的上下文。而上下文是跟作为泛型的FeignClientSpecification
有关的。
显然FeignContext
也是有关于FeignClientSpecification
的操作的。
2.基于@EnableFeignClient的clients配置
final Set<String> clientClasses = new HashSet<>();
basePackages = new HashSet<>();
for (Class<?> clazz : clients) {
// 获取clients里面配置的class的包名,并加入到包扫描集合中
basePackages.add(ClassUtils.getPackageName(clazz));
// FeignClient类
clientClasses.add(clazz.getCanonicalName());
}
// 测试使用的
AbstractClassTestingTypeFilter filter = new AbstractClassTestingTypeFilter() {
@Override
protected boolean match(ClassMetadata metadata) {
String cleaned = metadata.getClassName().replaceAll("\\$", ".");
return clientClasses.contains(cleaned);
}
};
// 扫描器添加过滤器,一个是测试使用的,一个是基于FeignClient注解的过滤器
scanner.addIncludeFilter(
new AllTypeFilter(Arrays.asList(filter, annotationTypeFilter)));
所以上面其实就是干了一个事情,就是将clients配置的类所在的包路径提取出来,然后再走包扫描。
3.包扫描
for (String basePackage : basePackages) {
// 扫描出包含@FeignClient注解的类
Set<BeanDefinition> candidateComponents = scanner
.findCandidateComponents(basePackage);
for (BeanDefinition candidateComponent : candidateComponents) {
if (candidateComponent instanceof AnnotatedBeanDefinition) {
// 只要注解类bean声明
// verify annotated class is an interface
AnnotatedBeanDefinition beanDefinition = (AnnotatedBeanDefinition) candidateComponent;
AnnotationMetadata annotationMetadata = beanDefinition.getMetadata();
// 判断这个@FeignClient注解的这个class是不是interface
Assert.isTrue(annotationMetadata.isInterface(),
"@FeignClient can only be specified on an interface");
// 获取注解配置的属性信息
Map<String, Object> attributes = annotationMetadata
.getAnnotationAttributes(
FeignClient.class.getCanonicalName());
// 这里是获取客户端名称,用于注册到spring中去
String name = getClientName(attributes);
// 在EnableFeignClient中可以配置全局FeignClient的配置
// 在FeignClient中可以针对单个进行配置
// 这也就是为啥我们在FeignAutoConfiguration中看到FeignClientSpecification
// 是一个集合的原因了。
registerClientConfiguration(registry, name,
attributes.get("configuration"));
// 注册FeignClientFactoryBean到spring容器中
registerFeignClient(registry, annotationMetadata, attributes);
}
}
}
关于ClassPathScanningCandidateComponentProvider
如何扫描到@FeignClient类:
这里插入一个spring的知识点,ClassPathScanningCandidateComponentProvider
这个类就是用来基于路径来扫描合适的组件的。
这其中有个很重要的东西,就是我们如何通过类路径扫描类并获取到自己想要的组件呢?
就是通过ClassPathScanningCandidateComponentProvider
的isCandidateComponent
方法来进行判断的。
从上面我们可以看到,我们创建了一个注解类型过滤器,并注入到了ClassPathScanningCandidateComponentProvider
的includeFilters
中,然后在扫描的过程中通过通过AnnotationTypeFilter去匹配扫描出来的每个类。
至此,我们就知道如何获取@FeignClient
注解的类了。
4.registerFeignClient 注册FeignClient到spring中
其实这里说将FeignClient
注册到spring中其实,不是很正确。其实是注册了一个FeignClientFactoryBean
到spring容器中去了。
private void registerFeignClient(BeanDefinitionRegistry registry,
AnnotationMetadata annotationMetadata, Map<String, Object> attributes) {
// 1.获取被@FeignClient注解的Interface的名称
String className = annotationMetadata.getClassName();
// 2.创建一个FeignClientFactoryBean的spring bean构建器
BeanDefinitionBuilder definition = BeanDefinitionBuilder
.genericBeanDefinition(FeignClientFactoryBean.class);
// 3.参数校验:主要是关于失败回调的配置
validate(attributes);
// 4.设置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);
// 远程调用404是否解码
definition.addPropertyValue("decode404", attributes.get("decode404"));
// 失败回调
definition.addPropertyValue("fallback", attributes.get("fallback"));
// 失败回调工厂
definition.addPropertyValue("fallbackFactory", attributes.get("fallbackFactory"));
// 设置注入类型,通过类型进行注册(也就是className)
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;
}
// 通过holder,可以通过className,也可以通过别名来获取这个FeignClient
BeanDefinitionHolder holder = new BeanDefinitionHolder(beanDefinition, className,
new String[] { alias });
// 5.注册FeignClientFactoryBean到spring容器中
BeanDefinitionReaderUtils.registerBeanDefinition(holder, registry);
}
从上面我们可以看到,我们可以看到所有关于FeignClient的信息都被封装到了FeignClientFactoryBean
中,其实这个时候并没有创建FeignClient对象。
真正的FeignClient对象是在FeignClientFactoryBean
里通过getObject()
方法创建的。
这里其实又涉及到Spring的一个知识点:
关于FactoryBean
对象,在spring初始化bean过程中会通过org.springframework.beans.factory.support.FactoryBeanRegistrySupport#getObjectFromFactoryBean
方法来对实现了FactoryBean
的bean进行getObject()
方法调用。通过这个getObject
就能获取这个工厂bean创建的对象。
5.FeignClientFactoryBean#getObject获取真正的FeignClient
@Override
public Object getObject() throws Exception {
return getTarget();
}
<T> T getTarget() {
FeignContext context = applicationContext.getBean(FeignContext.class);
// 获取feign构建器
Feign.Builder builder = feign(context);
// 一般我们都是通过注册中心+feign来使用,所以基本都是进入下面这个判断
// 然后就返回了。
// 也就是说url我们都不会填的
if (!StringUtils.hasText(this.url)) {
// 这里就是判断是不是基于注册中心进行远程调用
if (!this.name.startsWith("http")) {
url = "http://" + this.name;
}
else {
url = this.name;
}
url += cleanPath();
// 将feign与负载均衡器和限流器进行结合,生成一个对象进行返回
// 这里层层包裹
// Targeter: 有两个实现DefaultTargeter,HystrixTargeter
// 默认为HystrixTargeter,这个其实针对的是HystrixFeign来进行配置的
// 最终这里会创建出一个this.type的代理对象,
// 这个对象持有LoadBalanceFeignClient
// 以及LoadBalancerFeignClient
// 这个里面的东西比较复杂,稍后单独开一篇解析一下
return (T) loadBalance(builder, context, new HardCodedTarget<>(this.type,
this.name, url));
}
// 下面的可以先不看了,一般走不到。
if (StringUtils.hasText(this.url) && !this.url.startsWith("http")) {
this.url = "http://" + this.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
// 指定了url,这个时候就不需要负载均衡了,所以要从负载均衡器中取出发送http请求的
// client
client = ((LoadBalancerFeignClient)client).getDelegate();
}
builder.client(client);
}
Targeter targeter = get(context, Targeter.class);
return (T) targeter.target(this, builder, context, new HardCodedTarget<>(
this.type, this.name, url));
}
基于建造者模式构建Feign
protected Feign.Builder feign(FeignContext context) {
// feign客户端日志工厂
FeignLoggerFactory loggerFactory = get(context, FeignLoggerFactory.class);
Logger logger = loggerFactory.create(this.type);
// @formatter:off
Feign.Builder builder = get(context, Feign.Builder.class)
// required values
.logger(logger) // feign独立日志配置
.encoder(get(context, Encoder.class)) // 编码器
.decoder(get(context, Decoder.class)) // 解码器
.contract(get(context, Contract.class)); // 注解解释器
// @formatter:on
// 设置一下配置
// 1.连接超时时间
// 2.重试次数
// 3.异常解码器
// 4.请求拦截器
// 5.404解码器是否开启设置
// 6. 自定义编码器
// 7. 自定义解码器
// 8. 自定义注解解码器(就是解析注解为url)
configureFeign(context, builder);
return builder;
}
单从@EnableFeignClient
注解引入,即可看到很多信息。其中最主要的就是Encoder
,Decoder
,Contract
这三个组件。
还有FeignClientFactoryBean
如何将Feign构建器
,FeignContext
,HysterixFeign
,LoadBalancerFeignClient
一起整合到带有@FeignClient
注解的接口的代理类中。
举例说明:
@FeignClient
@RequestMapping("/user")
public interface ServiceAController{
@RequestMapping("/hello")
void sayHello();
}
流程:
1.在启动阶段首先会创建一个FeignClientFactoryBean
,它包含ServiceAController的所有信息,其中包含@FeignClient注解的参数,类名等信息
2.FeignClientFactoryBean
会创建一个Feign.Builder
,也就是Feign的构建器,并往里面添加各种配置和组件
3.获取LoadBalancerFeignClient
,添加到Feign.Builder
中
4.获取HystrixTargeter
,如果feign开启了Hystrix,则进行相关失败回调设置。
5.通过Feign.Builder.build()
创建一个ReflectiveFeign
对象。此时ReflectiveFeign
对象中包含LoadBalancerFeignClient
,FeignClientFactoryBean
,hystrix
配置等信息。
6.通过ReflectiveFeign.newInstance
方法创建一个ServiceAController的代理对象,其中InvocationHandler
为ReflectiveFeign
的内部类,它在创建时会带入ReflectiveFeign
所携带的内容。