Spring 源码阅读 44:@Resource 注解的处理
概述
上一篇主要分析了 CommonAnnotationBeanPostProcessor 的作用、它主要实现的后处理方法,并对 JSR-250 规范做了简单介绍。Spring 主要支持了 JSR-250 规范的三个注解,即@Resource
、@PostConstruct
和@PreDestroy
。这篇文章主要分析 Spring 通过 CommonAnnotationBeanPostProcessor 后处理器对@Resource
注解支持的原理。对@Resource
注解的支持,主要通过postProcessMergedBeanDefinition
和postProcessProperties
两个方法来完成,我们按照两个方法被调用的顺序,来分别分析。
postProcessMergedBeanDefinition 方法和 @Resource 注解的解析
首先进入postProcessMergedBeanDefinition
方法。
// org.springframework.context.annotation.CommonAnnotationBeanPostProcessor#postProcessMergedBeanDefinition
@Override
public void postProcessMergedBeanDefinition(RootBeanDefinition beanDefinition, Class<?> beanType, String beanName) {
super.postProcessMergedBeanDefinition(beanDefinition, beanType, beanName);
InjectionMetadata metadata = findResourceMetadata(beanName, beanType, null);
metadata.checkConfigMembers(beanDefinition);
}
方法的第一步是调用了父类的同名方法,也就是 InitDestroyAnnotationBeanPostProcessor 类中的同名方法。不过,其父类的方法中的逻辑,与本文介绍了@``Resource
注解相关的处理逻辑没有关系,因此,先跳过这部步骤,下一篇介绍@PostConstruct
和@PreDestroy
注解的处理时,再进行分析。我们直接看下面的两行代码。
通过findResourceMetadata
获取到一个 InjectionMetadata 类型的变量metadata
,然后再调用其checkConfigMembers
方法。
我们先看findResourceMetadata
方法
获取注解元信息的 findResourceMetadata 方法
// org.springframework.context.annotation.CommonAnnotationBeanPostProcessor#findResourceMetadata
private InjectionMetadata findResourceMetadata(String beanName, final Class<?> clazz, @Nullable PropertyValues pvs) {
// Fall back to class name as cache key, for backwards compatibility with custom callers.
String cacheKey = (StringUtils.hasLength(beanName) ? beanName : clazz.getName());
// Quick check on the concurrent map first, with minimal locking.
InjectionMetadata metadata = this.injectionMetadataCache.get(cacheKey);
if (InjectionMetadata.needsRefresh(metadata, clazz)) {
synchronized (this.injectionMetadataCache) {
metadata = this.injectionMetadataCache.get(cacheKey);
if (InjectionMetadata.needsRefresh(metadata, clazz)) {
if (metadata != null) {
metadata.clear(pvs);
}
metadata = buildResourceMetadata(clazz);
this.injectionMetadataCache.put(cacheKey, metadata);
}
}
}
return metadata;
}
可以看到,方法最终返回的metadata
,是通过buildResourceMetadata
方法根据参数传入的类型信息clazz
构建的,构建一次后,会放到当前后处理器的injectionMetadataCache
中缓存,之后需要这些信息的地方,只需要从缓存中读取即可。
接下来,我们进入buildResourceMetadata
方法,看具体的构建流程。
构建注解元信息的 buildResourceMetadata 方法
代码比较长,我们先分析整体结构。省略掉中间do-while循环中的逻辑,我们可以得到以下的结构。
private InjectionMetadata buildResourceMetadata(final Class<?> clazz) {
if (!AnnotationUtils.isCandidateClass(clazz, resourceAnnotationTypes)) {
return InjectionMetadata.EMPTY;
}
List<InjectionMetadata.InjectedElement> elements = new ArrayList<>();
Class<?> targetClass = clazz;
do {
final List<InjectionMetadata.InjectedElement> currElements = new ArrayList<>();
// 省略中间的处理逻辑
elements.addAll(0, currElements);
targetClass = targetClass.getSuperclass();
}
while (targetClass != null && targetClass != Object.class);
return InjectionMetadata.forElements(elements, clazz);
}
首先,会判断当前的类型是不是候选的类型,如果不是,则直接返回空的注解元信息InjectionMetadata.EMPTY
。这里用到了一个resourceAnnotationTypes
集合,里面包含了这个方法要处理的注解类型,它的初始化在后处理器的静态代码块中。
static {
webServiceRefClass = loadAnnotationType("javax.xml.ws.WebServiceRef");
ejbClass = loadAnnotationType("javax.ejb.EJB");
resourceAnnotationTypes.add(Resource.class);
if (webServiceRefClass != null) {
resourceAnnotationTypes.add(webServiceRefClass);
}
if (ejbClass != null) {
resourceAnnotationTypes.add(ejbClass);
}
}
由此可以看到,resourceAnnotationTypes
包含了@Resource
注解,除此之外,还会根据当前的运行环境添加@``WebServiceRef
和@``EJB
注解。我们之后的分析只考虑@Resource
注解,另外领个注解的处理逻辑并没有太大差异。
再回到buildResourceMetadata
方法的结构中,参数重传入的类型信息clazz
会被赋值给变量targetClass
,另外还声明了一个elements
集合,存放 InjectedElement 类型的元素。
在do-while
循环体中,每次循环会声明一个currElements
集合变量,类型与elements
相同,在循环体的结尾,currElements
中的元素会被elements
中,并且targetClass
会指向其父类。循环会在targetClass
为空或者为 Object 类型时终止。
可以得出结论,do-while
循环会从clazz
开始,遍历其继承关系中除 Object 以外所有的类型,并从中解析出需要注入的元素,也就是 InjectedElement 集合,最后放到elements
中。
方法的最后,将elements
和类型信息封装为一个 InjectionMetadata 作为结果返回。
接下来我们看循环体中间具体的处理逻辑。
ReflectionUtils.doWithLocalFields(targetClass, field -> {
// 省略 @WebServiceRef 和 @EJB 相关的处理逻辑
else if (field.isAnnotationPresent(Resource.class)) {
if (Modifier.isStatic(field.getModifiers())) {
throw new IllegalStateException("@Resource annotation is not supported on static fields");
}
if (!this.ignoredResourceTypes.contains(field.getType().getName())) {
currElements.add(new ResourceElement(field, field, null));
}
}
});
首先是对当前类型所有属性的判断,再次要对属性判断三个条件。
- 属性需要被
@Resource
注解标记。 - 属性不能是静态的。
- 属性类型不在
ignoredResourceTypes
集合中
符合条件的属性信息会被封装成 ResourceElement 对象并添加到currElements
集合中。
接下来是对方法的处理。
ReflectionUtils.doWithLocalMethods(targetClass, method -> {
Method bridgedMethod = BridgeMethodResolver.findBridgedMethod(method);
if (!BridgeMethodResolver.isVisibilityBridgeMethodPair(method, bridgedMethod)) {
return;
}
if (method.equals(ClassUtils.getMostSpecificMethod(method, clazz))) {
// 省略 @WebServiceRef 和 @EJB 相关的处理逻辑
else if (bridgedMethod.isAnnotationPresent(Resource.class)) {
if (Modifier.isStatic(method.getModifiers())) {
throw new IllegalStateException("@Resource annotation is not supported on static methods");
}
Class<?>[] paramTypes = method.getParameterTypes();
if (paramTypes.length != 1) {
throw new IllegalStateException("@Resource annotation requires a single-arg method: " + method);
}
if (!this.ignoredResourceTypes.contains(paramTypes[0].getName())) {
PropertyDescriptor pd = BeanUtils.findPropertyForMethod(bridgedMethod, clazz);
currElements.add(new ResourceElement(method, bridgedMethod, pd));
}
}
}
});
对于当前类型中的每一个方法,首先要确保是代码中定义的方法而非编译器生成的方法,然后是对方法信息的判断条件。
- 方法被
@Resource
注解标记。 - 方法不是静态的。
- 方法的必须有且只有一个参数。
- 方法的参数类型名称不在
ignoredResourceTypes
集合中。
符合条件的方法信息,也会被封装成一个 ResourceElement 对象,并添加到currElements
集合中。
至此,buildResourceMetadata
方法就结束了。回到postProcessMergedBeanDefinition
方法中,我们看最后一行代码。
注解元信息的检查
metadata.checkConfigMembers(beanDefinition);
此处的metadata
就是刚刚buildResourceMetadata
执行的结果,我们进入checkConfigMembers
方法中查看源码。
public void checkConfigMembers(RootBeanDefinition beanDefinition) {
Set<InjectedElement> checkedElements = new LinkedHashSet<>(this.injectedElements.size());
for (InjectedElement element : this.injectedElements) {
Member member = element.getMember();
if (!beanDefinition.isExternallyManagedConfigMember(member)) {
beanDefinition.registerExternallyManagedConfigMember(member);
checkedElements.add(element);
if (logger.isTraceEnabled()) {
logger.trace("Registered injected element on class [" + this.targetClass.getName() + "]: " + element);
}
}
}
this.checkedElements = checkedElements;
}
逻辑比较清晰,遍历注入元信息中的所有成员,也就是之前处理过的属性和方法信息,对于没有注册为 BeanDefinition 的外部管理配置成员的元素进行注册,并添加到元信息的checkedElements
集合中。这一步主要是为了防止重复处理通过注解注入的元素。
小结
到这里,postProcessMergedBeanDefinition
方法的源码就分析完了,它主要完成的工作就是解析类型中被@``Resource
注解标记并且符合条件的属性和方法,将他们封装成为注解元信息,供后续的流程中使用。
接下来看postProcessProperties
方法
postProcessProperties 方法和 @Resource 注解的注入
postProcessProperties
方法在 Spring 对 Bean 实例进行属性注入之前被调用,CommonAnnotationBeanPostProcessor 中的postProcessProperties
方法的作用是完成上一步postProcessMergedBeanDefinition
方法中解析到的属性和方法的值注入。
我们进入方法的源码。
// org.springframework.context.annotation.CommonAnnotationBeanPostProcessor#postProcessProperties
@Override
public PropertyValues postProcessProperties(PropertyValues pvs, Object bean, String beanName) {
InjectionMetadata metadata = findResourceMetadata(beanName, bean.getClass(), pvs);
try {
metadata.inject(bean, beanName, pvs);
}
catch (Throwable ex) {
throw new BeanCreationException(beanName, "Injection of resource dependencies failed", ex);
}
return pvs;
}
首先调用findResourceMetadata
方法获取了注解元信息,这个方法前文中已经介绍过,这次在调用这个方法,可以直接从缓存集合中获取到注解元信息。之后就比较简单了,通过调用注解元信息的inject
方法,对参数传入的pvs
变量进行处理,也就是进行属性值的注入,最后再将处理完的pvs
返回即可。
其实这里的处理逻辑和之前的一篇文章中介绍的@``Autowired
等注解的注入逻辑基本是相同的,详细的流程可以参考之前那篇文章。
这里我们还是简单回顾一下整个流程,进入 InjectionMetadata 的inject
方法中。
// org.springframework.beans.factory.annotation.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) {
if (logger.isTraceEnabled()) {
logger.trace("Processing injected element of bean '" + beanName + "': " + element);
}
element.inject(target, beanName, pvs);
}
}
}
将checkedElements
或者injectedElements
作为要处理的元素集合,赋值给elementsToIterate
并进行遍历,然后调用每一个注入元素element
的inject
方法。这里的注入元素,就是之前步骤中找到的每一个属性或者方法的信息。
我们在进入到 InjectedElement 的inject
方法中。
// org.springframework.beans.factory.annotation.InjectionMetadata.InjectedElement#inject
protected void inject(Object target, @Nullable String requestingBeanName, @Nullable PropertyValues pvs)
throws Throwable {
if (this.isField) {
Field field = (Field) this.member;
ReflectionUtils.makeAccessible(field);
field.set(target, getResourceToInject(target, requestingBeanName));
}
else {
if (checkPropertySkipping(pvs)) {
return;
}
try {
Method method = (Method) this.member;
ReflectionUtils.makeAccessible(method);
method.invoke(target, getResourceToInject(target, requestingBeanName));
}
catch (InvocationTargetException ex) {
throw ex.getTargetException();
}
}
}
这里的逻辑就是,从 Spring 容器中找到要注入的 Bean 实例,通过给属性复制,或者使用找到的 Bean 实例作为参数调用方法的方式,进行注入操作。
此时,注入的工作就完成了。
总结
本文分析了 CommonAnnotationBeanPostProcessor 后处理器处理@Resource
等注解的解析和注入的原理。下一篇讲介绍 Spring 通过 CommonAnnotationBeanPostProcessor 对另外两个 JSR-250 注解@PostConstruct
和@PreDestroy
的支持。
转载自:https://juejin.cn/post/7153542738007293965