莫要嘲笑看源码的朋友,现在面试仅仅是八股文是靠不住了,更多是问项目问题以及源码及问题。我也是逼不得已,不然谁想造轮子,很累且枯燥!
个人觉得看源码的前提是得会用,用熟了可以猜猜别人是怎么实现的,如果有相关官方文档那就在看看官方文档。
不过,可惜的是很多官方文档写得很烂,让你看了会那种云里雾里的。
最近我在研究openfeign
源码的时候,发现在源码中有个关键注解:@Import
。
项目启动类:
/**
* @author tianwc 公众号:java后端技术全栈、面试专栏
* @version 1.0.0
* @date 2023年07月07日 16:47
* 在线刷题 1200+题和1000+篇干货文章:博客地址
*/
@EnableFeignClients(basePackages = {"com.tian.feign"})
@SpringBootApplication()
public class MqApplication {
public static void main(String[] args) {
SpringApplication.run(MqApplication.class, args);
}
}
登录后复制
然后,就是我们的feignclient
接口:
/**
* @author tianwc 公众号:java后端技术全栈、面试专栏
* @version 1.0.0
* @date 2023年07月07日 16:47
* 在线刷题 1200+题和1000+篇干货文章:博客地址
*/
@FeignClient(contextId = "userFeignClient", value = "charge-user-service")
public interface UserFeignClient {
/**
* 邀请成功增加收益
*
* @param invitedDto 邀请增加收益
* @return 邀请成功
*/
@PostMapping("/user/invited/register")
CommonResult invitedRegister(@RequestBody InvitedDto invitedDto);
}
登录后复制
使用案例:
/**
* @author tianwc 公众号:java后端技术全栈、面试专栏
* @version 1.0.0
* @date 2023年07月07日 16:47
* 在线刷题 1200+题和1000+篇干货文章:博客地址
*/
@RestController
@RequestMapping("/user")
public class UserController {
@Resource
UserFeignClient userFeignClient;
@PostMapping("/invited")
public CommonResult invitedRegister(){
//省略不想搞的代码
return userFeignClient.invitedRegister(invitedDto);
}
}
登录后复制
从上面的代码中,我们可以看出openfeign
关键代码有:
@EnableFeignClients(basePackages = {"com.tian.feign"})
@FeignClient(contextId = "userFeignClient", value = "charge-user-service")
userFeignClient.invitedRegister(invitedDto);
@EnableFeignClients
@EnableFeignClients
这个注解在启动类上,我们肯定要重点关注。
小技巧:凡是以
@Enable
开头的各种注解基本上都是开启xxxx
。比如:@EnableFeignClients
表示开启feign客户端。
我们进入@EnableFeignClients
中
/**
* @author tianwc 公众号:java后端技术全栈、面试专栏
* @version 1.0.0
* @date 2023年07月07日 16:47
* 在线刷题 1200+题和1000+篇干货文章:博客地址
*/
@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
,表示我们需要扫描的包目录。
如果既没有指定
basePackages
,也没有指定basePackageClasses
,则采用启动类所在的目录作为包扫描路径。默认是这种情况。
本文重点来了,在这个注解@EnableFeignClients
上有个注解@Import(FeignClientsRegistrar.class)
,这里到底是有什么作用?
@Import()
@Import()
注解是在spring 3.0版本中引入的,字面意义就是导入.
@Import
注解的全类名是org.springframework.context.annotation.Import
。其只有一个默认的value属性,该属性类型为Class[]
,表示可以传入一个或多个Class对象。
通过注释可以看出,该注解有如下作用:
可以导入一个或多个组件类(通常是@Configuration配置类)该注解的功能与Spring XML中的元素相同。可以导入
@Configuration
配置类、ImportSelect
和ImportBeanDefinitionRegistrar
的实现类。
从spring 4.2版本开始,还可以引用常规组件类(普通类),该功能类似于AnnotationConfigApplicationContext.register()
方法。
该注解可以在类中声明,也可以在元注解中声明。如果需要导入XML或其他非@Configuration
定义的资源,可以使用@ImportResource
注释。
通常有三种使用方式:
@Import
一个普通类 spring会将该类加载到spring容器中@Import
一个类,该类实现了ImportBeanDefinitionRegistrar
接口,在重写的registerBeanDefinitions
方法里面,能拿到BeanDefinitionRegistry
的注册器,能手工往beanDefinitionMap
中注册beanDefinition
@Import
一个类 该类实现了ImportSelector
重写selectImports
方法该方法返回了String[]数组的对象,数组里面的类都会注入到spring容器当中。
下面我们来聊聊@Import
在openfeign的这里是起到什么作用。
openfeign
中作用
回答上面的代码里
@Import(FeignClientsRegistrar.class)
登录后复制
这里导入的是FeignClientsRegistrar
类,我们再来看看他的类关系图:
从类关系图来看,FeignClientsRegistrar
实现了ImportBeanDefinitionRegistrar
接口。再结合@Import
的三种使用方式中的第二种方式,能手工往beanDefinitionMap
中注册 beanDefinition
。
/**
* @author tianwc 公众号:java后端技术全栈、面试专栏
* @version 1.0.0
* @date 2023年07月07日 16:47
* 在线刷题 1200+题和1000+篇干货文章:博客地址
*/
@Override
public void registerBeanDefinitions(AnnotationMetadata metadata,
BeanDefinitionRegistry registry) {
registerDefaultConfiguration(metadata, registry);
registerFeignClients(metadata, registry);
}
登录后复制
这个方法registerBeanDefinitions()
是feign的核心入口方法,其中会做两件事:
注册默认的配置和注册所有的FeignClient。
registerDefaultConfiguration(metadata, registry)
这个方法是负责注册OpenFeign
的默认配置 ,逻辑相对简单:
private void registerDefaultConfiguration(AnnotationMetadata metadata,
BeanDefinitionRegistry registry) {
//获取@EnableFeignClients的全部属性
//@EnableFeignClients(basePackages = {"com.tian.feign"})
//这里的basePackages就是我们指定的熟悉
Map 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"));
}
}
登录后复制
defaultAttrs
中内容如下:
但是这里我们只关注defaultConfiguration,我们并有对其进行设置,所以我们可以忽略他。重点是下面这个方法。
registerFeignClients(metadata, registry)
这里就是项目启动时和openfeign相关的核心代码,这也是@EnableFeignClients
和@FeignClient
两个注解关联起来的地方。
我们进入源码中:
public void registerFeignClients(AnnotationMetadata metadata,
BeanDefinitionRegistry registry) {
ClassPathScanningCandidateComponentProvider scanner = getScanner();
scanner.setResourceLoader(this.resourceLoader);
Set basePackages;
Map attrs = metadata
.getAnnotationAttributes(EnableFeignClients.class.getName());
AnnotationTypeFilter annotationTypeFilter = new AnnotationTypeFilter(
FeignClient.class);
final Class[] clients = attrs == null ? null
: (Class[]) attrs.get("clients");
if (clients == null || clients.length == 0) {
scanner.addIncludeFilter(annotationTypeFilter);
basePackages = getBasePackages(metadata);
}
else {
final Set 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)));
}
for (String basePackage : basePackages) {
Set candidateComponents = scanner
.findCandidateComponents(basePackage);
for (BeanDefinition candidateComponent : candidateComponents) {
if (candidateComponent instanceof AnnotatedBeanDefinition) {
// verify annotated class is an interface
AnnotatedBeanDefinition beanDefinition = (AnnotatedBeanDefinition) candidateComponent;
AnnotationMetadata annotationMetadata = beanDefinition.getMetadata();
Assert.isTrue(annotationMetadata.isInterface(),
"@FeignClient can only be specified on an interface");
Map attributes = annotationMetadata
.getAnnotationAttributes(
FeignClient.class.getCanonicalName());
String name = getClientName(attributes);
registerClientConfiguration(registry, name,
attributes.get("configuration"));
registerFeignClient(registry, annotationMetadata, attributes);
}
}
}
}
登录后复制
代码一行一行看是不是觉得很累,我给你总结好了。
上面的方法分为以下七个步骤:
- 首先获取
@EnableFeignClients
注解的所有属性,主要为了拿到扫描包路径(basePackages
); - 因为一般不会在
@EnableFeignClients
注解中配置clients属性,所以会进入到clients属性为空时的逻辑; - 然后通过
getScanner()
方法获取扫描器:ClassPathScanningCandidateComponentProvider
,并将上下文AnnotationConfigServletWebServerApplicationContext
作为扫描器的ResourceLoader
; - 接着给扫描器
ClassPathScanningCandidateComponentProvider
添加一个注解过滤器(AnnotationTypeFilter),只过滤出包含@FeignClient
注解的BeanDefinition
; - 再通过
getBasePackages(metadata)
方法获取@EnableFeingClients
注解中的指定的包扫描路径 或 扫描类;如果没有获取到,则默认扫描启动类所在的包路径; - 然后进入到核心逻辑:通过
scanner.findCandidateComponents(basePackage)
方法从包路径下扫描出所有标注了@FeignClient
注解并符合条件装配的接口; - 最后将
FeignClientConfiguration
在BeanDefinitionRegistry
中注册一下,再对FeignClient
做真正的注册操作。
总结
在openfeign
源码中的@Import注解在这里的作用就是将扫描到带有FeignClient
注解的全部接口类以bean的形式注册到spring IOC容器中。
再来强调一下@Import注解使用方式:
@Import
一个普通类 spring会将该类加载到spring容器中@Import
一个类,该类实现了ImportBeanDefinitionRegistrar
接口,在重写的registerBeanDefinitions
方法里面,能拿到BeanDefinitionRegistry
的注册器,能手工往beanDefinitionMap
中注册beanDefinition
@Import
一个类 该类实现了ImportSelector
重写selectImports
方法该方法返回了String[]数组的对象,数组里面的类都会注入到spring容器当中。
好了,今天就分享这么多。这个openfeign里还有很多很有意思的地方,我们下次再分享吧!