开始之前
@EnableFeignClients和@FeignClient
熟悉SpringBoot的小伙伴对@EnableFeignClients
和@FeignClient
这两个注解不会陌生,前者使用在启动类上标志开启Feign,后者使用在接口上,最终实现rpc调用。
feign就是基于SpringBoot的rpc框架。接下来具体说明这两个注解的基础源码和作用
@EnableFeignClients
其源码如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
@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 {};
}
|
- 当我们配置了
basePackages
属性时,会扫描该数组中路径的@FeignClient
,创建发送http的代理对象,不配置该属性默认扫描注解的类所在的包路径下,通常即启动类所在路径。
- 因此我们配置了
basePackages
属性可以降低扫描范围,减少项目启动时间
@EnableFeignClients
注解上有一个核心注解@Import({FeignClientsRegistrar.class})
,导入了一个FeignClientsRegistrar
类,该类实现了ImportBeanDefinitionRegistrar
接口
(不了解该接口也没关系,接着往下看)
@FeignClient
其源码如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
|
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface FeignClient {
@AliasFor("name")
String value() default "";
@AliasFor("value")
String name() default "";
String url() default "";
//......
}
|
- 省略了大部分属性,主要关注
name
和url
,value
属性为name
的别名。
- 通常我们设置
name
未指定url
时,会根据name
的值去注册中心获取一个url(通常是eureka,feign作为一个独立的rpc框架,未与eureka强绑定),当我们设置了url
后,feign就会使用我们设置的url。
自定义rpc注解
接下来我们实现上述两个注解的功能。
@Client
自定义注解,用于替换@FeignClient
注解,其代码如下:
1
2
3
4
|
public @interface Client {
String url() default "";
String name() default "";
}
|
@EnableClients
自定义注解,用于替换@EnableFeignClients
注解,表示开启扫描@Client
。开发源码如下:
1
2
3
4
5
6
7
8
9
|
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE})
@Documented
@Import({ClientsRegistrar.class})
public @interface EnableClients {
String[] basePackages() default {};
}
|
- 我们自定义的这个注解与
@EnableFeignClients
非常相似,也导入了一个ClientsRegistrar.class
,后续我们也会为该类实现ImportBeanDefinitionRegistrar
接口
- 该注解只有一个
basePackages
属性(我们只是实现一个简化版的feign)
ClientsRegistrar.class
的部分代码实现如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
public class ClientsRegistrar implements ResourceLoaderAware, EnvironmentAware {
private ResourceLoader resourceLoader;
private Environment environment;
@Override
public void setResourceLoader(ResourceLoader resourceLoader) {
this.resourceLoader = resourceLoader;
}
@Override
public void setEnvironment(Environment environment) {
this.environment = environment;
}
}
|
- 首先创建两个私有成员变量
resourceLoader
和environment
,并实现ResourceLoaderAware
和EnvironmentAware
接口未它们赋值,后面会用到
接着我们实现ImportBeanDefinitionRegistrar
接口,实现对应方法的代码如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
|
@Override
public void registerBeanDefinitions(AnnotationMetadata metadata, BeanDefinitionRegistry registry) {
try {
Map<String, Object> attrs = metadata.getAnnotationAttributes(EnableClients.class.getName());
//获取要扫描的包路径
String[] basePackages = (String[])attrs.get("basePackages");
Set<String> basePackageSet = new HashSet<>(Arrays.asList(basePackages));
//如果未配置basePackages,默认启动类所在目录下
if (basePackageSet.isEmpty()) basePackageSet.add(ClassUtils.getPackageName(metadata.getClassName()));
//扫描包路径下的@Client
Set<BeanDefinition> candidateComponents = findClient(basePackageSet);
//注册扫描到的@Client
register(registry,candidateComponents);
}catch (Exception e) {
e.printStackTrace();
}
}
|
- 首先获取
@EnableClients
注解的basePackages
属性值,是一个字符串数组。
- Set用于去重,如果为空,则添加一个注解所在类的路径(通常是启动类)
findClient
方法是查找路径下所有的@Client
注解
register
方法注册每个@Client
注解的类为一个BeanDefinition
实例,我们会获取注解的name
和url
以及所在类的className
,用ClientFactoryBean.class
包装
- 完整的
ClientsRegistrar
类代码如下,可以直接复制到自己的项目中测试:
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
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
|
import org.springframework.beans.factory.annotation.AnnotatedBeanDefinition;
import org.springframework.beans.factory.config.BeanDefinition;
import org.springframework.beans.factory.config.BeanDefinitionHolder;
import org.springframework.beans.factory.support.AbstractBeanDefinition;
import org.springframework.beans.factory.support.BeanDefinitionBuilder;
import org.springframework.beans.factory.support.BeanDefinitionReaderUtils;
import org.springframework.beans.factory.support.BeanDefinitionRegistry;
import org.springframework.context.EnvironmentAware;
import org.springframework.context.ResourceLoaderAware;
import org.springframework.context.annotation.ClassPathScanningCandidateComponentProvider;
import org.springframework.context.annotation.ImportBeanDefinitionRegistrar;
import org.springframework.core.env.Environment;
import org.springframework.core.io.ResourceLoader;
import org.springframework.core.type.AnnotationMetadata;
import org.springframework.core.type.filter.AnnotationTypeFilter;
import org.springframework.util.ClassUtils;
import org.springframework.util.MultiValueMap;
import java.util.Arrays;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
public class ClientsRegistrar implements ImportBeanDefinitionRegistrar, ResourceLoaderAware, EnvironmentAware {
private ResourceLoader resourceLoader;
private Environment environment;
@Override
public void registerBeanDefinitions(AnnotationMetadata metadata, BeanDefinitionRegistry registry) {
try {
Map<String, Object> attrs = metadata.getAnnotationAttributes(EnableClients.class.getName());
//获取要扫描的包路径
String[] basePackages = (String[])attrs.get("basePackages");
Set<String> basePackageSet = new HashSet<>(Arrays.asList(basePackages));
//如果未配置basePackages,默认启动类所在目录下
if (basePackageSet.isEmpty()) basePackageSet.add(ClassUtils.getPackageName(metadata.getClassName()));
//扫描包路径下的@Client
Set<BeanDefinition> candidateComponents = findClient(basePackageSet);
//注册扫描到的@Client
register(registry,candidateComponents);
}catch (Exception e) {
e.printStackTrace();
}
}
private void register(BeanDefinitionRegistry registry,Set<BeanDefinition> candidateComponents) {
for (BeanDefinition candidateComponent : candidateComponents) {
AnnotatedBeanDefinition beanDefinition = (AnnotatedBeanDefinition)candidateComponent;
AnnotationMetadata annotationMetadata = beanDefinition.getMetadata();
String className = annotationMetadata.getClassName();
MultiValueMap<String, Object> attributes = annotationMetadata.getAllAnnotationAttributes(Client.class.getCanonicalName());
String url = (String) attributes.getFirst("url");
String name = (String) attributes.getFirst("name");
BeanDefinitionBuilder definition = BeanDefinitionBuilder.genericBeanDefinition(ClientFactoryBean.class);
definition.addPropertyValue("url",url);
definition.addPropertyValue("name",name);
definition.addPropertyValue("type",className);
definition.setAutowireMode(AbstractBeanDefinition.AUTOWIRE_BY_TYPE);
AbstractBeanDefinition bd = definition.getBeanDefinition();
bd.setPrimary(true);
BeanDefinitionHolder holder = new BeanDefinitionHolder(bd, className, null);
BeanDefinitionReaderUtils.registerBeanDefinition(holder, registry);
}
}
private Set<BeanDefinition> findClient(Set<String> basePackageSet) {
Set<BeanDefinition> candidates = new HashSet<>();
ClassPathScanningCandidateComponentProvider scanner = getScanner();
for (String basePackage : basePackageSet) {
Set<BeanDefinition> candidateComponents = scanner.findCandidateComponents(basePackage);
candidates.addAll(candidateComponents);
}
return candidates;
}
private ClassPathScanningCandidateComponentProvider getScanner() {
ClassPathScanningCandidateComponentProvider scanner = new ClassPathScanningCandidateComponentProvider(false, this.environment) {
protected boolean isCandidateComponent(AnnotatedBeanDefinition beanDefinition) {
boolean isCandidate = false;
if (beanDefinition.getMetadata().isIndependent() && !beanDefinition.getMetadata().isAnnotation()) {
isCandidate = true;
}
return isCandidate;
}
};
scanner.setResourceLoader(this.resourceLoader);
AnnotationTypeFilter annotationTypeFilter = new AnnotationTypeFilter(Client.class);
scanner.addIncludeFilter(annotationTypeFilter);
return scanner;
}
@Override
public void setResourceLoader(ResourceLoader resourceLoader) {
this.resourceLoader = resourceLoader;
}
@Override
public void setEnvironment(Environment environment) {
this.environment = environment;
}
}
|
ClientFactoryBean.class
的核心在于实现FactoryBean
接口,其代码如下:
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
|
import cn.hutool.http.HttpUtil;
import org.springframework.beans.factory.FactoryBean;
import org.springframework.core.annotation.AnnotatedElementUtils;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RequestParam;
import java.lang.annotation.Annotation;
import java.lang.reflect.Proxy;
import java.util.HashMap;
import java.util.Map;
public class ClientFactoryBean implements FactoryBean<Object> {
private Class<?> type;
private String url;
private String name;
public Class<?> getType() {
return type;
}
public void setType(Class<?> type) {
this.type = type;
}
public String getUrl() {
return url;
}
public void setUrl(String url) {
this.url = url;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
@Override
public Object getObject() throws Exception {
Object proxy = Proxy.newProxyInstance(ClientFactoryBean.class.getClassLoader(), new Class[]{type}, (o, method, objects) -> {
RequestMapping methodMapping = AnnotatedElementUtils.findMergedAnnotation(method, RequestMapping.class);
RequestMethod[] methods = methodMapping.method();
if (methods.length == 0) {
methods = new RequestMethod[] { RequestMethod.GET };
}
String name = methods[0].name().toLowerCase();
String path = methodMapping.path()[0];
Map<String, Object> queryMap = new HashMap<>();
Annotation[][] parameterAnnotations = method.getParameterAnnotations();
for (int i = 0; i < parameterAnnotations.length; i++) {
RequestParam requestParam = RequestParam.class.cast(parameterAnnotations[i][0]);
queryMap.put(requestParam.value(),objects[i]);
}
return request(name, url, path, queryMap);
});
return proxy;
}
private String request(String name, String url, String path, Map<String, Object> queryMap) {
if ("get".equals(name)) {
return HttpUtil.get(url + path, queryMap);
}else if ("post".equals(name)) {
return HttpUtil.post(url + path, queryMap);
}else {
throw new RuntimeException("Not Supported...");
}
}
@Override
public Class<?> getObjectType() {
return this.type;
}
@Override
public boolean isSingleton() {
return true;
}
}
|
- 代码比较简单,核心在于
getObject
方法返回一个代理对象
- 该类的属性为最开始注册
BeanDefinition
实例设置的值,创建对象时会通过get
和set
方法赋值
- 简单说明一下
FactoryBean
接口的作用,@Autowired
执行对应的逻辑时会找到对应的BeanDefinition
实例,
我们在最开始注册的className为@Client
所在类路径,值为ClientFactoryBean
的BeanDefinition
实例,会判断是否是FactoryBean
的实现类,
如果是则执行它的getObject
方法获取实例(一般像@Component
注解的类就不是FactoryBean
的实现类,因此直接实例化BeanDefinition
实例包装的类),
这也就是这里使用@Autowired
没有直接实例化ClientFactoryBean
的原因,而是实例化后调用getObject
方法
- 我们的jdk动态代理实现中,简单地处理了
@RequestMapping
和@RequestParam
注解,其他暂不处理。
- 最终通过
request
方法发送了一个http请求,网上随便找一个http工具类,实际这里我们使用的是hutool
的HttpUtils,对应依赖为:
1
2
3
4
5
|
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>5.7.19</version>
</dependency>
|
- 实际上feign框架中解析
@RequestMapping
、@RequestBody
等等注解都是使用springmvc的方法
测试
上面我们已经完成了所有代码开发,下面进行测试。
- 启动类上添加
@EnableClients
注解,代码如下:
1
2
3
4
5
6
7
|
@SpringBootApplication
@EnableClients
public class DemoApplication {
public static void main(String[] args) {
SpringApplication.run(DemoApplication.class,args);
}
}
|
- 创建对应远程服务接口并添加
@Client
注解,代码如下:
1
2
3
4
5
6
7
8
9
|
@Client(url = "127.0.0.1:8080", name = "notify")
public interface NotifyService {
@RequestMapping("/hello")
void sayHello();
@RequestMapping("/hi")
String sayHi(@RequestParam("name") String name, @RequestParam("age") Integer age);
}
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
|
@RestController
public class TestController {
@Autowired
private NotifyService notifyService;
@RequestMapping("/test")
public void test() {
notifyService.sayHello();
String result = notifyService.sayHi("张三", 15);
System.out.println("result: "+result);
}
@RequestMapping("/hello")
public void hello() {
System.out.println("hello hello hello");
}
@RequestMapping("/hi")
public String hi(@RequestParam("name") String name, @RequestParam("age") Integer age) {
System.out.println("hi hi hi "+name+" "+age);
return "请求成功";
}
}
|
共创建了三个接口,/test
接口模拟客服端,/hi
、/hello
接口模拟服务器端(虽然它们都在同一项目中),启动项目访问/test
接口控制台打印如下:
1
2
3
|
hello hello hello
hi hi hi 张三 15
result: 请求成功
|
检查打印信息,符合我们的预期,通过自定义注解实现了feign的基本功能。
小结