1. 简介
feign是一个声明式的HTTP客户端,spring-cloud-openfeign将feign集成到spring boot中,在接口上通过注解声明Rest协议,将http调用转换为接口方法的调用,使得客户端调用http服务更加简单。
当前spring cloud最新稳定版本是Edgware,feign在其集成的spring-cloud-netflix 1.4.0.RELEASE版本中。
spring cloud下一个版本是Finchley,将会单独集成spring-cloud-openfeign
2. demo
我们来看个简单的例子。源代码链接:https://github.com/along101/spring-boot-test/tree/master/feign-test
2.1 服务端代码
使用spring boot编写一个简单的Rest服务
1 |
|
接口代码:1
2
3
4
5"/test") (
public interface HelloService {
"/hello1", method = RequestMethod.GET) (value =
String hello(@RequestParam("name") String name);
}
代码很简单,通过springMVC注解在接口HelloService上声明Rest服务,HelloController被@RestController注解声明为一个Rest服务。
启动spring boot 就可通过浏览器访问http://localhost:8080/test/hello1?name=ppdai得到返回Hello ppdai。
2.2 客户端代码
客户端pom中需要加入spring-cloud-starter-feign的依赖
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18<dependencies>
...
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-feign</artifactId>
</dependency>
</dependencies>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>Camden.SR7</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
在客户端中新建一个HelloClient接口继承服务端HelloService接口:1
2
3
4
5//在spring boot配置文件中配置remote.hello.service.host=http://localhost:8080
"HELLO-SERVICE", url = "${remote.hello.service.host}") (value =
public interface HelloClient extends HelloService {
}
HelloClient接口上注解@FeignClient,声明为Feign的客户端,参数url指定服务端地址。
在spring boot启动类上增加注解@EnableFeignClients
1 |
|
注意,HelloClient接口需要在启动类package或者子package之下。
编写测试类测试:1
2
3
4
5
6
7
8
9
10
11
12 (SpringJUnit4ClassRunner.class)
(classes = FeignClientApplication.class)
public class HelloClientTest {
private HelloClient helloClient;
public void testClient() throws Exception {
String result = helloClient.hello("ppdai");
System.out.println(result);
}
}
启动服务端后,运行该测试类,在控制台会打印出Hello ppdai
3. 原理分析
看到客户端测试类中,我们只用了一行代码,就能完成对远程Rest服务的调用,相当的简单。为什么这么神奇,这几段代码是如何做到的呢?
3.1 @EnableFeignClients 注解声明客户端接口
入口是启动类上的注解@EnableFeignClients,源代码:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16 (RetentionPolicy.RUNTIME)
(ElementType.TYPE)
(FeignClientsRegistrar.class)
public EnableFeignClients {
//basePackages的别名
String[] value() default {};
//声明基础包,spring boot启动后,会扫描该包下被@FeignClient注解的接口
String[] basePackages() default {};
//声明基础包的类,通过该类声明基础包
Class<?>[] basePackageClasses() default {};
//默认配置类
Class<?>[] defaultConfiguration() default {};
//直接声明的客户端接口类
Class<?>[] clients() default {};
}
@EnableFeignClients的参数声明客户端接口的位置和默认的配置类。
3.1.1 @FeignClient注解,将接口声明为Feign客户端
1 | (ElementType.TYPE) |
3.2 FeignClientsRegistrar 注册客户端
“FeignClientsRegistrar 注册客户端”)FeignClientsRegistrar 注册客户端
@EnableFeignClients注解上被注解了@Import(FeignClientsRegistrar.class),@Import注解的作用是将指定的类作为Bean注入到Spring Context中,我们再来看被引入的FeignClientsRegistrar1
2
3
4
5
6
7
8
9
10
11
12class FeignClientsRegistrar implements ImportBeanDefinitionRegistrar,
ResourceLoaderAware, BeanClassLoaderAware {
。。。
public void registerBeanDefinitions(AnnotationMetadata metadata,
BeanDefinitionRegistry registry) {
registerDefaultConfiguration(metadata, registry);
registerFeignClients(metadata, registry);
}
。。。
}
FeignClientsRegistrar类实现了3个接口:
- 接口ResourceLoaderAware用于注入ResourceLoader
- 接口BeanClassLoaderAware用于注入ClassLoader
- 接口ImportBeanDefinitionRegistrar用于动态向Spring Context中注册bean
ImportBeanDefinitionRegistrar接口方法registerBeanDefinitions有两个参数
- AnnotationMetadata 包含被@Import注解类的信息
这里 @Import注解在@EnableFeignClients上,@EnableFeignClients注解在spring boot启动类上,AnnotationMetadata拿到的是spring boot启动类的相关信息
- BeanDefinitionRegistry bean定义注册中心
3.3 registerDefaultConfiguration方法,注册默认配置
registerDefaultConfiguration方法代码:
1 | private void registerDefaultConfiguration(AnnotationMetadata metadata, |
取出@EnableFeignClients注解参数defaultConfiguration,注册到spring Context中。registerClientConfiguration方法代码如下:
1 |
|
这里使用spring 动态注册bean的方式,注册了一个FeignClientSpecification的bean。
3.4 FeignClientSpecification 客户端定义
一个简单的pojo,继承了NamedContextFactory.Specification,两个属性String name 和 Class<?>[] configuration,用于FeignContext命名空间独立配置,后面会用到。1
2
3
4
5
6
7
8
9
10
class FeignClientSpecification implements NamedContextFactory.Specification {
private String name;
private Class<?>[] configuration;
}
3.5 registerFeignClients方法,注册feign客户端
1 | public void registerFeignClients(AnnotationMetadata metadata, |
这个方法主要逻辑是扫描注解声明的客户端,调用registerFeignClient方法注册到registry中。这里是一个典型的spring动态注册bean的例子,可以参考这段代码在spring中轻松的实现类路径下class扫描,动态注册bean到spring中。想了解spring类的扫描机制,可以断点到ClassPathScanningCandidateComponentProvider.findCandidateComponents方法中,一步步调试。
3.6 registerFeignClient方法,注册单个客户feign端
1 | private void registerFeignClient(BeanDefinitionRegistry registry, |
registerFeignClient方法主要是将FeignClientFactoryBean工厂Bean注册到registry中,spring初始化后,会调用FeignClientFactoryBean的getObject方法创建bean注册到spring context中。
3.7 FeignClientFactoryBean 创建feign客户端的工厂
1 |
|
FeignClientFactoryBean实现了FactoryBean接口,是一个工厂bean
3.7.1 FeignClientFactoryBean.getObject方法
1 |
|
这段代码有个比较重要的逻辑,如果在@FeignClient注解中设置了url参数,就不走Ribbon,直接url调用,否则通过Ribbon调用,实现客户端负载均衡。
可以看到,生成Feign客户端所需要的各种配置对象,都是通过FeignContex中获取的。
3.7.2 FeignContext 隔离配置
在@FeignClient注解参数configuration,指定的类是Spring的Configuration Bean,里面方法上加@Bean注解实现Bean的注入,可以指定feign客户端的各种配置,包括Encoder/Decoder/Contract/Feign.Builder等。不同的客户端指定不同配置类,就需要对配置类进行隔离,FeignContext就是用于隔离配置的。
1 | public class FeignContext extends NamedContextFactory<FeignClientSpecification> { |
FeignContext继承NamedContextFactory,空参数构造函数指定FeignClientsConfiguration类为默认配置。
NamedContextFactory实现接口ApplicationContextAware,注入ApplicationContextAware作为parent:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87public abstract class NamedContextFactory<C extends NamedContextFactory.Specification>
implements DisposableBean, ApplicationContextAware {
//命名空间对应的Spring Context
private Map<String, AnnotationConfigApplicationContext> contexts = new ConcurrentHashMap<>();
//不同命名空间的定义
private Map<String, C> configurations = new ConcurrentHashMap<>();
//父ApplicationContext,通过ApplicationContextAware接口注入
private ApplicationContext parent;
//默认配置类
private Class<?> defaultConfigType;
private final String propertySourceName;
private final String propertyName;
。。。
//设置配置,在FeignAutoConfiguration中将Spring Context中的所有FeignClientSpecification设置进来,如果@EnableFeignClients有设置参数defaultConfiguration也会加进来,前面已经分析在registerDefaultConfiguration方法中注册的FeignClientSpecification Bean
public void setConfigurations(List<C> configurations) {
for (C client : configurations) {
this.configurations.put(client.getName(), client);
}
}
//获取指定命名空间的ApplicationContext,先从缓存中获取,没有就创建
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);
}
//创建ApplicationContext
protected AnnotationConfigApplicationContext createContext(String name) {
//新建AnnotationConfigApplicationContext
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext();
//根据name在configurations找到所有的配置类,注册到context总
if (this.configurations.containsKey(name)) {
for (Class<?> configuration : this.configurations.get(name)
.getConfiguration()) {
context.register(configuration);
}
}
//将default.开头的默认默认也注册到Context中
for (Map.Entry<String, C> entry : this.configurations.entrySet()) {
if (entry.getKey().startsWith("default.")) {
for (Class<?> configuration : entry.getValue().getConfiguration()) {
context.register(configuration);
}
}
}
//注册一些需要的bean
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) {
// 设置parent
context.setParent(this.parent);
}
//刷新,完成bean生成
context.refresh();
return context;
}
//从命名空间中获取指定类型的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;
}
//从命名空间中获取指定类型的Bean
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;
}
}
关键的方法是createContext,为每个命名空间独立创建ApplicationContext,设置parent为外部传入的Context,这样就可以共用外部的Context中的Bean,又有各种独立的配置Bean,熟悉springMVC的同学应该知道,springMVC中创建的WebApplicatonContext里面也有个parent,原理跟这个类似。
从FeignContext中获取Bean,需要传入命名空间,根据命名空间找到缓存中的ApplicationContext,先从自己注册的Bean中获取bean,没有获取到再从到parent中获取。
3.7.3 创建Feign.Builder
了解了FeignContext的原理,我们再来看feign最重要的构建类创建过程
1 | protected Feign.Builder feign(FeignContext context) { |
这里设置了Feign.Builder所必须的参数Encoder/Decoder/Contract,其他参数都是可选的。这三个必须的参数从哪里来的呢?答案是在FeignContext的构造器中,传入了默认的配置FeignClientsConfiguration,这个配置类里面初始化了这三个参数。
3.7.4 FeignClientsConfiguration 客户端默认配置
1 |
|
可以看到,feign需要的decoder/enoder通过适配器共用springMVC中的HttpMessageConverters引入。
feign有自己的注解体系,这里通过SpringMvcContract适配了springMVC的注解体系。
3.7.5 SpringMvcContract 适配feign注解体系
SpringMvcContract继承了feign的类Contract.BaseContract,作用是解析接口方法上的注解和方法参数,生成MethodMetadata用于接口方法调用过程中组装http请求。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27public class SpringMvcContract extends Contract.BaseContract
implements ResourceLoaderAware {
。。。
//处理Class上的注解
protected void processAnnotationOnClass(MethodMetadata data, Class<?> clz) {
。。。
}
//处理方法
public MethodMetadata parseAndValidateMetadata(Class<?> targetType, Method method) {
。。。
}
//处理方法上的注解
protected void processAnnotationOnMethod(MethodMetadata data,
Annotation methodAnnotation, Method method) {
。。。
}
//处理参数上的注解
protected boolean processAnnotationsOnParameter(MethodMetadata data,
Annotation[] annotations, int paramIndex) {
。。。
}
}
几个覆盖方法分别是处理类上的注解,处理方法,处理方法上的注解,处理方法参数注解,最终生成完整的MethodMetadata。feign自己提供的Contract和扩展javax.ws.rx的Contract原理都是类似的。
3.7.6 Targeter 生成接口动态代理
Feign.Builder生成后,就要用Target生成feign客户端的动态代理,这里FeignClientFactoryBean中使用Targeter,Targeter有两个实现类,分别是HystrixTargeter和DefaultTargeter,DefaultTargeter很简单,直接调用HardCodedTarget生成动态代理,HystrixTargeter源码如下:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24class HystrixTargeter implements Targeter {
public <T> T target(FeignClientFactoryBean factory, Feign.Builder feign, FeignContext context,
Target.HardCodedTarget<T> target) {
//如果不是HystrixFeign.Builder,直接调用target生成代理
if (!(feign instanceof feign.hystrix.HystrixFeign.Builder)) {
return feign.target(target);
}
//找到fallback或者fallbackFactory,设置到hystrix中
feign.hystrix.HystrixFeign.Builder builder = (feign.hystrix.HystrixFeign.Builder) feign;
Class<?> fallback = factory.getFallback();
if (fallback != void.class) {
return targetWithFallback(factory.getName(), context, target, builder, fallback);
}
Class<?> fallbackFactory = factory.getFallbackFactory();
if (fallbackFactory != void.class) {
return targetWithFallbackFactory(factory.getName(), context, target, builder, fallbackFactory);
}
return feign.target(target);
}
。。。
}
到这里,接口的动态代理就生成了,然后回到FeignClientFactoryBean工厂bean中,会将动态代理注入到SpringContext,在使用的地方,就可以通过@Autowire方式注入了。
3.8 loadBalance方法,客户端负载均衡
如果@FeignClient注解中没有配置url参数,将会通过loadBalance方法生成Ribbon的动态代理:1
2
3
4
5
6
7
8
9
10
11 protected <T> T loadBalance(Feign.Builder builder, FeignContext context,
HardCodedTarget<T> target) {
//这里获取到的Client是LoadBalancerFeignClient
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);
}
。。。
}
LoadBalancerFeignClient在FeignRibbonClientAutoConfiguration中自动配置的Bean
3.8.1 LoadBalancerFeignClient 负载均衡客户端
1 | public class LoadBalancerFeignClient implements Client { |
代码逻辑也比较简单,就是是配到Ribbon客户端上调用。Ribbon的相关使用和原理就不在本文中描述。
4. 总结
feign本身是一款优秀的开源组件,spring cloud feign又非常巧妙的将feign集成到spring boot中。
本文通过对spring cloud feign源代码的解读,详细的分析了feign集成到spring boot中的原理,使我们更加全面的了解到feign的使用。
spring cloud feign也是一个很好的学习spring boot的例子,从中我们可以学习到:
- spring boot注解声明注入bean
- spring类扫描机制
- spring接口动态注册bean
- spring命名空间隔离ApplicationContext
本文转载:拍拍贷基础框架团队博客