Spring 源码阅读 44:@Resource 注解的处理

简介: 本文分析了 CommonAnnotationBeanPostProcessor 后处理器处理`@Resource`等注解的解析和注入的原理。

基于 Spring Framework v5.2.6.RELEASE

接上篇:Spring 源码阅读 43:CommonAnnotationBeanPostProcessor 分析

概述

上一篇主要分析了 CommonAnnotationBeanPostProcessor 的作用、它主要实现的后处理方法,并对 JSR-250 规范做了简单介绍。Spring 主要支持了 JSR-250 规范的三个注解,即@Resource@PostConstruct@PreDestroy。这篇文章主要分析 Spring 通过 CommonAnnotationBeanPostProcessor 后处理器对@Resource注解支持的原理。对@Resource注解的支持,主要通过postProcessMergedBeanDefinitionpostProcessProperties两个方法来完成,我们按照两个方法被调用的顺序,来分别分析。

postProcessMergedBeanDefinition 方法和 @Resource 注解的解析

首先进入postProcessMergedBeanDefinition方法。

// org.springframework.context.annotation.CommonAnnotationBeanPostProcessor#postProcessMergedBeanDefinition@OverridepublicvoidpostProcessMergedBeanDefinition(RootBeanDefinitionbeanDefinition, Class<?>beanType, StringbeanName) {
super.postProcessMergedBeanDefinition(beanDefinition, beanType, beanName);
InjectionMetadatametadata=findResourceMetadata(beanName, beanType, null);
metadata.checkConfigMembers(beanDefinition);
}

方法的第一步是调用了父类的同名方法,也就是 InitDestroyAnnotationBeanPostProcessor 类中的同名方法。不过,其父类的方法中的逻辑,与本文介绍了@Resource注解相关的处理逻辑没有关系,因此,先跳过这部步骤,下一篇介绍@PostConstruct@PreDestroy注解的处理时,再进行分析。我们直接看下面的两行代码。

通过findResourceMetadata获取到一个 InjectionMetadata 类型的变量metadata,然后再调用其checkConfigMembers方法。

我们先看findResourceMetadata方法

获取注解元信息的 findResourceMetadata 方法

// org.springframework.context.annotation.CommonAnnotationBeanPostProcessor#findResourceMetadataprivateInjectionMetadatafindResourceMetadata(StringbeanName, finalClass<?>clazz, @NullablePropertyValuespvs) {
// Fall back to class name as cache key, for backwards compatibility with custom callers.StringcacheKey= (StringUtils.hasLength(beanName) ?beanName : clazz.getName());
// Quick check on the concurrent map first, with minimal locking.InjectionMetadatametadata=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);
         }
      }
   }
returnmetadata;
}

可以看到,方法最终返回的metadata,是通过buildResourceMetadata方法根据参数传入的类型信息clazz构建的,构建一次后,会放到当前后处理器的injectionMetadataCache中缓存,之后需要这些信息的地方,只需要从缓存中读取即可。

接下来,我们进入buildResourceMetadata方法,看具体的构建流程。

构建注解元信息的 buildResourceMetadata 方法

image.png

代码比较长,我们先分析整体结构。省略掉中间do-while循环中的逻辑,我们可以得到以下的结构。

privateInjectionMetadatabuildResourceMetadata(finalClass<?>clazz) {
if (!AnnotationUtils.isCandidateClass(clazz, resourceAnnotationTypes)) {
returnInjectionMetadata.EMPTY;
   }
List<InjectionMetadata.InjectedElement>elements=newArrayList<>();
Class<?>targetClass=clazz;
do {
finalList<InjectionMetadata.InjectedElement>currElements=newArrayList<>();
// 省略中间的处理逻辑elements.addAll(0, currElements);
targetClass=targetClass.getSuperclass();
   }
while (targetClass!=null&&targetClass!=Object.class);
returnInjectionMetadata.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 相关的处理逻辑elseif (field.isAnnotationPresent(Resource.class)) {
if (Modifier.isStatic(field.getModifiers())) {
thrownewIllegalStateException("@Resource annotation is not supported on static fields");
      }
if (!this.ignoredResourceTypes.contains(field.getType().getName())) {
currElements.add(newResourceElement(field, field, null));
      }
   }
});

首先是对当前类型所有属性的判断,再次要对属性判断三个条件。

  1. 属性需要被@Resource注解标记。
  2. 属性不能是静态的。
  3. 属性类型不在ignoredResourceTypes集合中

符合条件的属性信息会被封装成 ResourceElement 对象并添加到currElements集合中。

接下来是对方法的处理。

ReflectionUtils.doWithLocalMethods(targetClass, method-> {
MethodbridgedMethod=BridgeMethodResolver.findBridgedMethod(method);
if (!BridgeMethodResolver.isVisibilityBridgeMethodPair(method, bridgedMethod)) {
return;
   }
if (method.equals(ClassUtils.getMostSpecificMethod(method, clazz))) {
// 省略 @WebServiceRef 和 @EJB 相关的处理逻辑elseif (bridgedMethod.isAnnotationPresent(Resource.class)) {
if (Modifier.isStatic(method.getModifiers())) {
thrownewIllegalStateException("@Resource annotation is not supported on static methods");
         }
Class<?>[] paramTypes=method.getParameterTypes();
if (paramTypes.length!=1) {
thrownewIllegalStateException("@Resource annotation requires a single-arg method: "+method);
         }
if (!this.ignoredResourceTypes.contains(paramTypes[0].getName())) {
PropertyDescriptorpd=BeanUtils.findPropertyForMethod(bridgedMethod, clazz);
currElements.add(newResourceElement(method, bridgedMethod, pd));
         }
      }
   }
});

对于当前类型中的每一个方法,首先要确保是代码中定义的方法而非编译器生成的方法,然后是对方法信息的判断条件。

  1. 方法被@Resource注解标记。
  2. 方法不是静态的。
  3. 方法的必须有且只有一个参数。
  4. 方法的参数类型名称不在ignoredResourceTypes集合中。

符合条件的方法信息,也会被封装成一个 ResourceElement 对象,并添加到currElements集合中。

至此,buildResourceMetadata方法就结束了。回到postProcessMergedBeanDefinition方法中,我们看最后一行代码。

注解元信息的检查

metadata.checkConfigMembers(beanDefinition);

此处的metadata就是刚刚buildResourceMetadata执行的结果,我们进入checkConfigMembers方法中查看源码。

publicvoidcheckConfigMembers(RootBeanDefinitionbeanDefinition) {
Set<InjectedElement>checkedElements=newLinkedHashSet<>(this.injectedElements.size());
for (InjectedElementelement : this.injectedElements) {
Membermember=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@OverridepublicPropertyValuespostProcessProperties(PropertyValuespvs, Objectbean, StringbeanName) {
InjectionMetadatametadata=findResourceMetadata(beanName, bean.getClass(), pvs);
try {
metadata.inject(bean, beanName, pvs);
   }
catch (Throwableex) {
thrownewBeanCreationException(beanName, "Injection of resource dependencies failed", ex);
   }
returnpvs;
}

首先调用findResourceMetadata方法获取了注解元信息,这个方法前文中已经介绍过,这次在调用这个方法,可以直接从缓存集合中获取到注解元信息。之后就比较简单了,通过调用注解元信息的inject方法,对参数传入的pvs变量进行处理,也就是进行属性值的注入,最后再将处理完的pvs返回即可。

其实这里的处理逻辑和之前的一篇文章中介绍的@Autowired等注解的注入逻辑基本是相同的,详细的流程可以参考之前那篇文章。

传送门:Spring 源码阅读 42:AutowiredAnnotationBeanPostProcessor 分析(3)

这里我们还是简单回顾一下整个流程,进入 InjectionMetadata 的inject方法中。

// org.springframework.beans.factory.annotation.InjectionMetadata#injectpublicvoidinject(Objecttarget, @NullableStringbeanName, @NullablePropertyValuespvs) throwsThrowable {
Collection<InjectedElement>checkedElements=this.checkedElements;
Collection<InjectedElement>elementsToIterate=         (checkedElements!=null?checkedElements : this.injectedElements);
if (!elementsToIterate.isEmpty()) {
for (InjectedElementelement : elementsToIterate) {
if (logger.isTraceEnabled()) {
logger.trace("Processing injected element of bean '"+beanName+"': "+element);
         }
element.inject(target, beanName, pvs);
      }
   }
}

checkedElements或者injectedElements作为要处理的元素集合,赋值给elementsToIterate并进行遍历,然后调用每一个注入元素elementinject方法。这里的注入元素,就是之前步骤中找到的每一个属性或者方法的信息。

我们在进入到 InjectedElement 的inject方法中。

// org.springframework.beans.factory.annotation.InjectionMetadata.InjectedElement#injectprotectedvoidinject(Objecttarget, @NullableStringrequestingBeanName, @NullablePropertyValuespvs)
throwsThrowable {
if (this.isField) {
Fieldfield= (Field) this.member;
ReflectionUtils.makeAccessible(field);
field.set(target, getResourceToInject(target, requestingBeanName));
   }
else {
if (checkPropertySkipping(pvs)) {
return;
      }
try {
Methodmethod= (Method) this.member;
ReflectionUtils.makeAccessible(method);
method.invoke(target, getResourceToInject(target, requestingBeanName));
      }
catch (InvocationTargetExceptionex) {
throwex.getTargetException();
      }
   }
}

这里的逻辑就是,从 Spring 容器中找到要注入的 Bean 实例,通过给属性复制,或者使用找到的 Bean 实例作为参数调用方法的方式,进行注入操作。

此时,注入的工作就完成了。

总结

本文分析了 CommonAnnotationBeanPostProcessor 后处理器处理@Resource等注解的解析和注入的原理。下一篇讲介绍 Spring 通过 CommonAnnotationBeanPostProcessor 对另外两个 JSR-250 注解@PostConstruct@PreDestroy的支持。

目录
相关文章
|
7天前
|
运维 Java 程序员
Spring5深入浅出篇:基于注解实现的AOP
# Spring5 AOP 深入理解:注解实现 本文介绍了基于注解的AOP编程步骤,包括原始对象、额外功能、切点和组装切面。步骤1-3旨在构建切面,与传统AOP相似。示例代码展示了如何使用`@Around`定义切面和执行逻辑。配置中,通过`@Aspect`和`@Around`注解定义切点,并在Spring配置中启用AOP自动代理。 进一步讨论了切点复用,避免重复代码以提高代码维护性。通过`@Pointcut`定义通用切点表达式,然后在多个通知中引用。此外,解释了AOP底层实现的两种动态代理方式:JDK动态代理和Cglib字节码增强,默认使用JDK,可通过配置切换到Cglib
|
1天前
|
监控 Java 应用服务中间件
Spring Boot 源码面试知识点
【5月更文挑战第12天】Spring Boot 是一个强大且广泛使用的框架,旨在简化 Spring 应用程序的开发过程。深入了解 Spring Boot 的源码,有助于开发者更好地使用和定制这个框架。以下是一些关键的知识点:
13 6
|
2天前
|
Java 应用服务中间件 测试技术
深入探索Spring Boot Web应用源码及实战应用
【5月更文挑战第11天】本文将详细解析Spring Boot Web应用的源码架构,并通过一个实际案例,展示如何构建一个基于Spring Boot的Web应用。本文旨在帮助读者更好地理解Spring Boot的内部工作机制,以及如何利用这些机制优化自己的Web应用开发。
12 3
|
3天前
|
JSON 前端开发 Java
【JAVA进阶篇教学】第七篇:Spring中常用注解
【JAVA进阶篇教学】第七篇:Spring中常用注解
|
5天前
|
存储 前端开发 Java
Spring Boot自动装配的源码学习
【4月更文挑战第8天】Spring Boot自动装配是其核心机制之一,其设计目标是在应用程序启动时,自动配置所需的各种组件,使得应用程序的开发和部署变得更加简单和高效。下面是关于Spring Boot自动装配的源码学习知识点及实战。
13 1
|
6天前
|
JavaScript Java 开发者
Spring Boot中的@Lazy注解:概念及实战应用
【4月更文挑战第7天】在Spring Framework中,@Lazy注解是一个非常有用的特性,它允许开发者控制Spring容器的bean初始化时机。本文将详细介绍@Lazy注解的概念,并通过一个实际的例子展示如何在Spring Boot应用中使用它。
18 2
|
6天前
|
传感器 人工智能 前端开发
JAVA语言VUE2+Spring boot+MySQL开发的智慧校园系统源码(电子班牌可人脸识别)Saas 模式
智慧校园电子班牌,坐落于班级的门口,适合于各类型学校的场景应用,班级学校日常内容更新可由班级自行管理,也可由学校统一管理。让我们一起看看,电子班牌有哪些功能呢?
47 4
JAVA语言VUE2+Spring boot+MySQL开发的智慧校园系统源码(电子班牌可人脸识别)Saas 模式
|
Java Spring
通过Spring Resource接口获取资源(6)
通过Spring Resource接口获取资源
1031 0
|
Java Spring 数据格式
Spring Resource接口获取资源
1.1.1. Resource简介  在Spring内部实现机制,针对于资源文件(配置的xml文件)有一个统一的接口Resource。   1.1.1.1. 接口定义的方法 1.exists():判断资源文件是否存在。
1532 0
|
Java Spring 数据格式
通过Spring Resource接口获取资源
通过Spring Resource接口获取资源 目录 1       Resource简介 2       通过ResourceLoader获取资源 3       在bean中获取Resource的方式   1       Resource简介        在Spring内部,针对于资源文件有一个统一的接口Resource表示。
871 0