前言
从我们接触 Spring 起,就开始使用它的自动注入功能。最常用的注解有: @Autowired、@Resource
我们经常听说 @Autowired 优先按类型注入,其次再按 name 注入。而 @Resource 则正好相反,它优先按 name 注入,其次再按 type 注入。
真的是这样吗?怎么从源码的角度来进行解释?
javax.annotation.Resource
org.springframework.beans.factory.annotation.Autowired
版本约定
Spring 5.3.9 (通过 SpringBoot 2.5.3 间接引入的依赖)
正文
下面,我们就从源码的视角来观察 @Autowired、@Resource 在依赖注入时的区别
准备工作
首先,我们准备一个干净的工程。
为什么要准备一个干净的工程来研究源码?
答:请阅读之前我讲过的 阅读源码的步骤
我们只需要一段如下的代码来进行研究:
@SpringBootApplication
public class Application {
@Resource
UserService userService;
public static void main(String[] args) {
SpringApplication app = new SpringApplication(com.kvn.Application.class);
app.setBannerMode(Banner.Mode.OFF);
app.run(args);
}
}
为什么要先研究 @Resource?
答:@Resource 是 java 原生的注解,在 Spring 源码中使用较少,很容易通过 IDEA 快速检索到哪些类中使用到了这个注解。
从而通过猜测+排除的方法,快速找到 Spring 源码中对 @Resource 注入的处理代码。
正式开始
@Resource 注入规则的源码分析
我们可以找到如下的代码:
看 InjectionMetadata 的命名,应该是和自动注入有关的。
它里面有个 InjectionMetadata#inject() 方法,看起来就是做注入用的:
public void inject(Object target, @Nullable String beanName, @Nullable PropertyValues pvs) throws Throwable {
Collection<InjectedElement> checkedElements = this.checkedElements;
Collection<InjectedElement> elementsToIterate = (checkedElements != null ? checkedElements : this.injectedElements);
if (!elementsToIterate.isEmpty()) {
for (InjectedElement element : elementsToIterate) {
element.inject(target, beanName, pvs);
}
}
}
到这里,一切都只是我们的猜测。我们可以看一下 InjectedElement#inject() 里面究竟干了啥?
我们可以看到,InjectedElement 有几个子类,分别来处理不同注解的注入方式:
看到这里,我们是不是有点小兴奋了,但是,我们还是不能忘记我们的初衷:@Resource 自动注入的规则是什么?
ResourceElement 类比较简单,包含一个构造方法 和 一个 getResourceToInject() 方法:
private class ResourceElement extends LookupElement {
private final boolean lazyLookup;
public ResourceElement(Member member, AnnotatedElement ae, @Nullable PropertyDescriptor pd) {
super(member, pd);
Resource resource = ae.getAnnotation(Resource.class);
String resourceName = resource.name();
Class<?> resourceType = resource.type();
this.isDefaultName = !StringUtils.hasLength(resourceName);
if (this.isDefaultName) {
// 不指定 name 的情况下,默认取 filed 的 name
resourceName = this.member.getName();
if (this.member instanceof Method && resourceName.startsWith("set") && resourceName.length() > 3) {
resourceName = Introspector.decapitalize(resourceName.substring(3));
}
}
else if (embeddedValueResolver != null) {
// @Resource 中指定的 name 属性可以通过 占位符 来指定
resourceName = embeddedValueResolver.resolveStringValue(resourceName);
}
if (Object.class != resourceType) {
checkResourceType(resourceType);
}
else {
// No resource type specified... check field/method.
resourceType = getResourceType();
}
this.name = (resourceName != null ? resourceName : "");
this.lookupType = resourceType;
String lookupValue = resource.lookup();
this.mappedName = (StringUtils.hasLength(lookupValue) ? lookupValue : resource.mappedName());
// 对 @Lazy 的支持
Lazy lazy = ae.getAnnotation(Lazy.class);
this.lazyLookup = (lazy != null && lazy.value());
}
@Override
protected Object getResourceToInject(Object target, @Nullable String requestingBeanName) {
// 对 @Lazy 的支持
return (this.lazyLookup ? buildLazyResourceProxy(this, requestingBeanName) :
getResource(this, requestingBeanName));
}
}
构造方法里面将 @Resource 注解中的数据进行了解析。另外的 getResourceToInject() 好像是跟自动注入有关系的,但是也不太确定。
这时,断点+调试大法就上线了。我们可以在 getResourceToInject() 上打个断点,跟进去看一下。
看 @Resource 的源码,我们发现了两个小东西:
1. @Resource 中指定的 name 属性可以通过 占位符 来指定
2. 找到了 Spring 对 @Lazy 延迟注入的一个小线索
这两个发现跟我们要研究的特性没有太大的关联,在看主逻辑时不需要分心去看。
但是做为一个有技术敏感性的程序猿,细心的你可以将它记录下来,等到有时间了,也可以拿出来研究一下。^_^
果不其然,getResourceToInject() 最终会调用到 CommonAnnotationBeanPostProcessor#autowireResource():
protected Object autowireResource(BeanFactory factory, LookupElement element, String requestingBeanName) {
Object resource;
Set<String> autowiredBeanNames;
String name = element.name;
if (factory instanceof AutowireCapableBeanFactory) {
AutowireCapableBeanFactory beanFactory = (AutowireCapableBeanFactory) factory;
DependencyDescriptor descriptor = element.getDependencyDescriptor();
// @Resoure 没有指定 name(name 是默认值),并且容器中不存在这个默认 name 对应的 bean/beanDefinition,那么就使用 byType 方式进行注入
if (this.fallbackToDefaultTypeMatch && element.isDefaultName && !factory.containsBean(name)) {
autowiredBeanNames = new LinkedHashSet<>();
// 按 byType 方式解析依赖进行注入
resource = beanFactory.resolveDependency(descriptor, requestingBeanName, autowiredBeanNames, null);
if (resource == null) {
throw new NoSuchBeanDefinitionException(element.getLookupType(), "No resolvable resource object");
}
} else {
// 满足如下条件,则按 byName 的方式解析依赖进行注入:
// 1. 如果 @Resource 指定了 name,则按指定 name 解析依赖进行注入
// 2. 如果容器中存在 name 对应的 bean,则按 byName 方式进行注入
resource = beanFactory.resolveBeanByName(name, descriptor);
autowiredBeanNames = Collections.singleton(name);
}
} else {
resource = factory.getBean(name, element.lookupType);
autowiredBeanNames = Collections.singleton(name);
}
if (factory instanceof ConfigurableBeanFactory) {
ConfigurableBeanFactory beanFactory = (ConfigurableBeanFactory) factory;
for (String autowiredBeanName : autowiredBeanNames) {
if (requestingBeanName != null && beanFactory.containsBean(autowiredBeanName)) {
beanFactory.registerDependentBean(autowiredBeanName, requestingBeanName);
}
}
}
return resource;
}
@Resource 没有指定 name 时,element.isDefaultName=true
剧透一下:
@Autowired 最终也会调用beanFactory.resolveDependency()
来解析依赖进行注入
看到上面的源码后,我们疑惑基本上也就解决了: @Resource 默认是按 name 注入的,其次是按 type。
其实,准确的表达应该是:
@Resource 在不指定 name 的情况下,默认是按 field 的 name 来注入的;如果按默认的 name 找不到 bean,则会按 type 注入。
@Autowired 注入规则的源码分析
@Autowired 的分析与上面 @Resource 的分析基本上一样。所以这里就直接上结论了:
@Autowired 属性注入的处理是在 AutowiredFieldElement#inject() 方法中。
最终会调用到下面的方法,DefaultListableBeanFactory#doResolveDependency():
@Autowired 的装配顺序:
- 按 type 在上下文中查找匹配的 bean
- 如果有多个bean,则按 name 进行匹配
2.1 如果有 @Qualifier 注解,则按照 @Qualifier 指定的name进行匹配
2.2 如果没有,则按照 field 的 name 进行匹配 - 匹配不到,则报错。
如果设置 @Autowired(required=false),则注入失败时不会抛出异常
总结
-
@Resource
@Resource 在不指定 name 的情况下,默认是按 field 的 name(默认name)来注入的;如果默认的 name 找不到 bean 的话,就会按 type 注入。 -
@Autowired
@Autowired 默认按 type 进行注入,当匹配到多个 bean 时,再按照 name 来进行注入。
系统的学习源码知识,请点击视频讲解:
SpringIoC源码由浅入深 : https://edu.51cto.com/course/30243.html
点赞、收藏、关注不迷路.....⇩↓☟⬇︎⥥.....