在之前的讲解中,我乐意将源码拿出来并粘贴在文章中,让大家看一下。然而,我最近意识到这样做不仅会占用很多篇幅,而且实际作用很小,因为大部分人不会花太多时间去阅读源码。
因此,从今天开始,我将采取以下几个步骤:首先,我会提前画出一张图来展示本章节要讲解的内容的调用链路,供大家参考。其次,在文章中,我只会展示最核心的代码或关键的类。剩下的内容将主要用来讲解原理。如果你真的在学习 Spring 源码,我希望你能打开你的项目,并跟着我一起深入阅读源码。现在,让我们开始吧。今天的重点是 Spring 的依赖注入。
基本使用
首先,值得注意的是,在 Spring 框架中,依赖注入是在 bean 生成后进行属性赋值的。由于我们的 bean 通常都是单例模式,所以每个类的属性都必须进行注入。在这个过程中,会涉及到代理、反射等技术的应用。如果你对这些概念不太熟悉的话,建议你提前补充一下相关的前提知识。了解这些基本概念将有助于你更好地理解和掌握 Spring 框架的依赖注入机制。
首先需要注意的是,尽管图示可能只展示了类之间的简单调用关系,但这并不代表实际的依赖注入过程就是如此简单。实际上,Spring 框架的版本和配置方式可能会导致不同的链路调用。然而,无论具体的版本差异如何,Spring 框架的依赖注入机制的基本逻辑大致是一样的。
本节课的链路调用图例地址:https://viewer.diagrams.net/index.html?tags={}&highlight=0000ff&edit=_blank&layers=1&nav=1&title=未命名绘图.drawio#Uhttps%3A%2F%2Fraw.githubusercontent.com%2FStudiousXiaoYu%2Fdraw%2Fmain%2F未命名绘图.drawio
Spring 的依赖注入有两种方式:手动注入、自动注入。下面我们详细讲解一下这两种方式。
手动注入
在手动注入中,离不开 XML 配置。有两种常见的方式可以实现手动注入:通过属性和通过构造器。手动就是我们人为控制注入的值,下面是两种配置方式:
<bean id="user" class="com.xiaoyu.service.UserService" >
<property name="orderService" ref="orderService"/>
</bean>
复制代码
上面是通过使用 set 方法进行依赖注入的方式来实现。
<bean id="user" class="com.xiaoyu.service.UserService">
<constructor-arg index="0" ref="orderService"/>
</bean>
复制代码
上面是通过使用构造方法进行依赖注入的方式来实现。
自动注入
XML 配置
XML 也有自动分配的机制,只要不是我们手动指定注入类,那就是自动注入,让我们一起了解如何进行设置。
在 XML 中,我们可以通过在定义一个 Bean 时指定自动注入模式来进行优化。这些模式包括 byType、byName、constructor、default 和 no。通过使用这些模式,我们可以更灵活地控制 Bean 的注入方式。
<bean id="user" class="com.xiaoyu.service.UserService" autowire="byType"/>
复制代码
<bean id="user" class="com.xiaoyu.service.UserService" autowire="byName"/>
复制代码
剩下的不举例了,这两种类型,都需要我们的 UserService 对象有相应的 set 方法。因为注入的点就是先找到 set 方法,然后在填充属性之前,Spring 会去解析当前类,把当前类的所有方法都解析出来。Spring 会解析每个方法,得到对应的 PropertyDescriptor 对象。PropertyDescriptor 对象中包含了几个属性:
name:获取截取后的方法名称:截取规则如下:
get 开头,则去除 get,比如“getXXX”,那么 name=XXX(首字母小写),需无参或者第一个参数为 int 类型
is 开头不并且返回值为 boolean 类型,比如“isXXX”,那么 name=XXX(首字母小写),需无参
set 开头并且有无返回值,比如“setXXX”,那么 name=XXX(首字母小写),前提是得有入参,如果无入参是解析不到 set 开头的方法的
readMethodRef:如果是 get 开头或者 is 开头的方法,都是 readMethodRef,并且存储的引用。
readMethodName:是 get 开头或者 is 开头的方法名。包含 get/is
writeMethodRef:set 开头的方法引用。
writeMethodName:set 开头的方法名,包含 set。
propertyTypeRef:如果是读方法,则获取的是返回值类型,如果是 set 写方法,则获取的是入参类型。
具体实现可自行查看源码:java.beans.Introspector#getTargetPropertyInfo()
@Autowired 注解
这个注解大家都很熟悉,我简单介绍一下它的基础用法。最后,通过查看源码,我们将依赖注入的过程完整地连起来。
属性注入
基本用法示例:
@Component
public class UserService {
@Autowired
public OrderService orderService;
}
复制代码
setter 方法注入
基本用法示例:
@Component
public class UserService {
public OrderService orderService;
@Autowired
public void setOrderService(OrderService orderService){
System.out.println(0);
this.orderService = orderService;
}
}
复制代码
构造器注入基本用法示例:
@Componentpublic class UserService {
@Component
public class UserService {
public OrderService orderService;
@Autowired
public UserService(OrderService orderService){
this.orderService = orderService;
}
}
复制代码
依赖注入关键源码解析
寻找注入点
在创建一个 Bean 的过程中,Spring 会利用 AutowiredAnnotationBeanPostProcessor 的 postProcessMergedBeanDefinition()方法来找出注入点并进行缓存。具体的找注入点的流程如下:
如果一个 Bean 的类型是 String,那么则根本不需要进行依赖注入
遍历目标类中的所有 Field 字段,field 上是否存在 @Autowired、@Value、@Inject 中的其中一个
static 字段不是注入点,不会进行自动注入
构造注入点,获取 @Autowired 中的 required 属性的值,将字段封装到 AutowiredFieldElement 对象。
遍历目标类中的所有 Method 方法。
method 上是否存在 @Autowired、@Value、@Inject 中的其中一个
static method 不是注入点,不会进行自动注入
set 方法最好有入参,没有入参或提示日志。
构造注入点,获取 @Autowired 中的 required 属性的值,将方法封装到 AutowiredMethodElement 对象。
查看是否还有父类,如果有再次循环直到没有父类。
将刚才构造好的注入点全都封装到 InjectionMetadata,作为当前 Bean 对于的注入点集合对象,并缓存。
static 字段或方法为什么不支持注入
在源码中,Spring 会判断字段或方法是否是 static 来决定是否进行注入。如果字段或方法是 static 的,Spring 不会进行注入操作。这是因为静态字段或方法是属于类的,而不是属于具体的实例。因此,在进行依赖注入时,Spring 会注入给具体的实例,而不是整个类。
我们知道 Spring 是支持创建原型 bean 的,也就是多例模式。
@Component
@Scope("prototype")
public class UserService {
@Autowired
private static OrderService orderService;
public void test() {
System.out.println("test123");
}
}
复制代码
确实,如果 OrderService 是 prototype 类型的,并且 Spring 支持注入 static 字段,那么每次注入 OrderService 到 UserService 时都会创建一个新的实例。这样做确实违背了 static 字段的本意,因为 static 字段是属于类的,而不是实例的。
注入点注入
在依赖注入的过程中,注入点的注入肯定会在 populateBean 方法中进行属性注入。在这个过程中,会调用 AutowiredAnnotationBeanPostProcessor 的 postProcessProperties()方法,该方法会直接给对象中的属性赋值。这个方法会遍历每个注入点(InjectedElement),并进行依赖注入操作。
属性字段注入
遍历所有 AutowiredFieldElement 对象。
将对应的字段封装到 DependencyDescriptor。
调用 beanFactory.resolveDependency 来获取真正需要注入的 bean。
最后将此次封装的 DependencyDescriptor 和 beanname 缓存起来,主要考虑到了原型 bean 的创建
利用反射给 filed 赋值
setter 方法注入
遍历所有 AutowiredMethodElement 对象。
调用 resolveMethodArguments 方法
遍历每个方法参数,找到匹配的 bean 对象,将方法对象封装到 DependencyDescriptor 中。
调用 beanFactory.resolveDependency 来获取真正需要注入的 bean。
最后将此次封装的 DependencyDescriptor 和 beanname 缓存起来,主要考虑到了原型 bean 的创建
利用反射给 filed 赋值
我们只需要关注 findAutowiringMetadata 方法的实现,因为大家普遍了解注入的概念。我们主要关注的是它是如何找到注入点的。
private InjectionMetadata findAutowiringMetadata(String beanName, 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 = buildAutowiringMetadata(clazz);
this.injectionMetadataCache.put(cacheKey, metadata);
}
}
}
return metadata;
}
复制代码
private InjectionMetadata buildAutowiringMetadata(final Class<?> clazz) {
// 如果一个Bean的类型是String...,那么则根本不需要进行依赖注入
if (!AnnotationUtils.isCandidateClass(clazz, this.autowiredAnnotationTypes)) {
return InjectionMetadata.EMPTY;
}
List<InjectionMetadata.InjectedElement> elements = new ArrayList<>();
Class<?> targetClass = clazz;
do {
final List<InjectionMetadata.InjectedElement> currElements = new ArrayList<>();
// 遍历targetClass中的所有Field
ReflectionUtils.doWithLocalFields(targetClass, field -> {
// field上是否存在@Autowired、@Value、@Inject中的其中一个
MergedAnnotation<?> ann = findAutowiredAnnotation(field);
if (ann != null) {
// static filed不是注入点,不会进行自动注入
if (Modifier.isStatic(field.getModifiers())) {
if (logger.isInfoEnabled()) {
logger.info("Autowired annotation is not supported on static fields: " + field);
}
return;
}
// 构造注入点
boolean required = determineRequiredStatus(ann);
currElements.add(new AutowiredFieldElement(field, required));
}
});
// 遍历targetClass中的所有Method
ReflectionUtils.doWithLocalMethods(targetClass, method -> {
Method bridgedMethod = BridgeMethodResolver.findBridgedMethod(method);
if (!BridgeMethodResolver.isVisibilityBridgeMethodPair(method, bridgedMethod)) {
return;
}
// method上是否存在@Autowired、@Value、@Inject中的其中一个
MergedAnnotation<?> ann = findAutowiredAnnotation(bridgedMethod);
if (ann != null && method.equals(ClassUtils.getMostSpecificMethod(method, clazz))) {
// static method不是注入点,不会进行自动注入
if (Modifier.isStatic(method.getModifiers())) {
if (logger.isInfoEnabled()) {
logger.info("Autowired annotation is not supported on static methods: " + method);
}
return;
}
// set方法最好有入参
if (method.getParameterCount() == 0) {
if (logger.isInfoEnabled()) {
logger.info("Autowired annotation should only be used on methods with parameters: " +
method);
}
}
boolean required = determineRequiredStatus(ann);
PropertyDescriptor pd = BeanUtils.findPropertyForMethod(bridgedMethod, clazz);
currElements.add(new AutowiredMethodElement(method, required, pd));
}
});
elements.addAll(0, currElements);
targetClass = targetClass.getSuperclass();
}
while (targetClass != null && targetClass != Object.class);
return InjectionMetadata.forElements(elements, clazz);
}
复制代码
@Resource
说到这里,可能有些小伙伴还会使用 @Resource 注解来进行依赖注入。其实,这和 @Autowired 注解的逻辑是一样的,只是调用的是其他类的相关方法。具体来说,通过 org.springframework.context.annotation.CommonAnnotationBeanPostProcessor#postProcessMergedBeanDefinition 方法来查找注入点,然后在 org.springframework.context.annotation.CommonAnnotationBeanPostProcessor#postProcessProperties 方法中进行属性填充。关于这些细节我们就不详细讨论了,如果感兴趣的话,可以查看一下源码。
@Qualifier
对于使用过 @Autowired 注解的同学来说,他们肯定也了解 @Qualifier 注解的作用。@Qualifier 主要用于解决一个接口有多个实现类的情况。为了更好地理解,我们来举一个简单的例子:
@Component
@Qualifier("userF")
public class UserF implements User{
}
复制代码
@Component
@Qualifier("userM")
public class UserM implements User{
}
复制代码
在上述内容中,简要定义了两个实现。现在我们需要使用它们。
@Component
public class UserService {
@Autowired
@Qualifier("userM")
public User user;
}
复制代码
在这种情况下,会去匹配 userM 的实体类,而不会出现多个匹配类导致异常。那么它是如何解决这个问题的呢?它是在什么时候找到 @Qualifier 注解的呢?具体的源码如下所示:
protected boolean checkQualifier(
BeanDefinitionHolder bdHolder, Annotation annotation, TypeConverter typeConverter) {
// 检查某个Qualifier注解和某个BeanDefinition是否匹配
// annotation是某个属性或某个方法参数前上所使用的Qualifier
Class<? extends Annotation> type = annotation.annotationType();
RootBeanDefinition bd = (RootBeanDefinition) bdHolder.getBeanDefinition();
// 首先判断BeanDefinition有没有指定类型的限定符
AutowireCandidateQualifier qualifier = bd.getQualifier(type.getName());
if (qualifier == null) {
qualifier = bd.getQualifier(ClassUtils.getShortName(type));
}
if (qualifier == null) {
// First, check annotation on qualified element, if any
Annotation targetAnnotation = getQualifiedElementAnnotation(bd, type);
// Then, check annotation on factory method, if applicable
if (targetAnnotation == null) {
targetAnnotation = getFactoryMethodAnnotation(bd, type);
}
if (targetAnnotation == null) {
RootBeanDefinition dbd = getResolvedDecoratedDefinition(bd);
if (dbd != null) {
targetAnnotation = getFactoryMethodAnnotation(dbd, type);
}
}
if (targetAnnotation == null) {
// Look for matching annotation on the target class
if (getBeanFactory() != null) {
try {
// 拿到某个BeanDefinition对应的类上的@Qualifier
Class<?> beanType = getBeanFactory().getType(bdHolder.getBeanName());
if (beanType != null) {
targetAnnotation = AnnotationUtils.getAnnotation(ClassUtils.getUserClass(beanType), type);
}
}
catch (NoSuchBeanDefinitionException ex) {
// Not the usual case - simply forget about the type check...
}
}
if (targetAnnotation == null && bd.hasBeanClass()) {
targetAnnotation = AnnotationUtils.getAnnotation(ClassUtils.getUserClass(bd.getBeanClass()), type);
}
}
// 注解对象的equals比较特殊,JDK层面用到了动态代理,会比较value
if (targetAnnotation != null && targetAnnotation.equals(annotation)) {
return true;
}
}
......
return true;
}
复制代码
他其实是在我们上面所说的属性注入的时候去匹配查找的。具体来说,他会调用 beanFactory.resolveDependency 方法来获取真正需要注入的 bean 时进行查找。如果想要查看相关的源码,可以去查看org.springframework.beans.factory.annotation.QualifierAnnotationAutowireCandidateResolver#isAutowireCandidate
方法。在这个方法中会有更详细的解释。
总结
今天我们主要讲解的是 Spring 依赖注入。在本文中,我们主要围绕 bean 填充属性的字段和 setter 方法展开讨论。要记住的是,在进行属性注入时,我们首先需要找到注入点并进行缓存,然后才会真正进行属性注入。需要注意的是,静态字段或方法是不会进行依赖注入的。最后,我们简单地介绍了一下关键源码,以及对 @Resource 和 @Qualifier 进行了简单的分析。如果想要学习 Spring 源码,一定要结合图例去理解,否则很容易晕头转向。
文章转载自:努力的小雨
原文链接:https://www.cnblogs.com/guoxiaoyu/p/17963111
体验地址:http://www.jnpfsoft.com/?from=001
评论