2024-12-18  阅读(77)
版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。 本文链接:https://www.skjava.com/mianshi/baodian/detail/1681732751

回答

如果我们想将某个 Bean 设置为默认 Bean,一般推荐使用 @Primary。当应用中存在多个候选 Bean 的时候,Spring 会优先选择标记了 @Primary 的那个作为注入的默认 Bean。

详解

举个例子:

@Component
public class SkServiceA implements SkService {
    // Implementation of MyService
}

@Component
@Primary
public class SkServiceB implements SkService {
    // Implementation of MyService
}

在这里,如果某个地方需要注入 SkService ,Spring 会优先选择 @Primary 标注的 SkServiceB。但是,如果我们使用了 @Qualifier 指定了具体的 Bean 名称,那么即使有 @Primary,Spring 也会注入指定的 Bean。

多候选 Bean 问题

Spring 容器在初始化时,如果某个类型有多个 Bean 的候选对象,如果我们不做任何处理,则系统会抛出如下异常:

NoUniqueBeanDefinitionException: No qualifying bean of type 'xxx' available: expected single matching bean but found x: [beanA, beanB]

为了避免这种情况的发生,Spring 提供了两种方式处理:

  1. @Primary:标记一个默认 Bean,当多个候选存在时优先选择。
  2. @Qualifier:明确指定使用某个特定的 Bean。

在目标 Bean 明确的情况下,我们使用 @Qualifier 比较好。

在项目开发过程中,如果我们有一个实现类是主实现类,那么大明哥推荐将该实现类添加 @Primary。同时,对于特殊的实现,使用 @Qualifier 来显示指定。在一些场景中,尤其是需要替换第三方库的时候,我们有时候需要覆盖默认实现,则我们就可以在自定义实现上添加 Primary 满足要求。

在实际使用中,我们需要根据业务需求搭配使用 @Primary@Qualifier,以确保依赖注入的明确性和灵活性。

关于 @Qualifier 请阅读面试题:Spring 中的 @Qualifier 注解的作用?

@Primary 的实现原理

我们直接看源码,DefaultListableBeanFactory#doResolveDependency()

  public Object doResolveDependency(DependencyDescriptor descriptor, @Nullable String beanName,
      @Nullable Set<String> autowiredBeanNames, @Nullable TypeConverter typeConverter) throws BeansException {

      // 省略部分代码...

      Map<String, Object> matchingBeans = findAutowireCandidates(beanName, type, descriptor);
      if (matchingBeans.isEmpty()) {
        if (isRequired(descriptor)) {
          raiseNoMatchingBeanFound(type, descriptor.getResolvableType(), descriptor);
        }
        return null;
      }

      String autowiredBeanName;
      Object instanceCandidate;

      if (matchingBeans.size() > 1) {
        autowiredBeanName = determineAutowireCandidate(matchingBeans, descriptor);
        if (autowiredBeanName == null) {
          if (isRequired(descriptor) || !indicatesMultipleBeans(type)) {
            return descriptor.resolveNotUnique(descriptor.getResolvableType(), matchingBeans);
          }
          else {
            // In case of an optional Collection/Map, silently ignore a non-unique case:
            // possibly it was meant to be an empty collection of multiple regular beans
            // (before 4.3 in particular when we didn't even look for collection beans).
            return null;
          }
        }
        instanceCandidate = matchingBeans.get(autowiredBeanName);
      }
      // 省略部分代码...
  }

调用 findAutowireCandidates() 找到所有满足条件的 class,如果 matchingBeans.size() > 1 ,则调用 determineAutowireCandidate()

  protected String determineAutowireCandidate(Map<String, Object> candidates, DependencyDescriptor descriptor) {
    Class<?> requiredType = descriptor.getDependencyType();
    String primaryCandidate = determinePrimaryCandidate(candidates, requiredType);
    if (primaryCandidate != null) {
      return primaryCandidate;
    }
    String priorityCandidate = determineHighestPriorityCandidate(candidates, requiredType);
    if (priorityCandidate != null) {
      return priorityCandidate;
    }
    // Fallback
    for (Map.Entry<String, Object> entry : candidates.entrySet()) {
      String candidateName = entry.getKey();
      Object beanInstance = entry.getValue();
      if ((beanInstance != null && this.resolvableDependencies.containsValue(beanInstance)) ||
          matchesBeanName(candidateName, descriptor.getDependencyName())) {
        return candidateName;
      }
    }
    return null;
  }

在这个方法里面,我们可以看到是一次调用三个方法 determinePrimaryCandidate()determineHighestPriorityCandidate()matchesBeanName() 来确认,不为空就返回:

  • determinePrimaryCandidate()
protected String determinePrimaryCandidate(Map<String, Object> candidates, Class<?> requiredType) {
    String primaryBeanName = null;
    for (Map.Entry<String, Object> entry : candidates.entrySet()) {
      String candidateBeanName = entry.getKey();
      Object beanInstance = entry.getValue();
      if (isPrimary(candidateBeanName, beanInstance)) {
        if (primaryBeanName != null) {
          boolean candidateLocal = containsBeanDefinition(candidateBeanName);
          boolean primaryLocal = containsBeanDefinition(primaryBeanName);
          if (candidateLocal && primaryLocal) {
            throw new NoUniqueBeanDefinitionException(requiredType, candidates.size(),
                "more than one 'primary' bean found among candidates: " + candidates.keySet());
          }
          else if (candidateLocal) {
            primaryBeanName = candidateBeanName;
          }
        }
        else {
          primaryBeanName = candidateBeanName;
        }
      }
    }
    return primaryBeanName;
  }
  
protected boolean isPrimary(String beanName, Object beanInstance) {
    String transformedBeanName = transformedBeanName(beanName);
    if (containsBeanDefinition(transformedBeanName)) {
      return getMergedLocalBeanDefinition(transformedBeanName).isPrimary();
    }
    BeanFactory parent = getParentBeanFactory();
    return (parent instanceof DefaultListableBeanFactory &&
        ((DefaultListableBeanFactory) parent).isPrimary(transformedBeanName, beanInstance));
  }

这个方法的逻辑比较简单,迭代调用 isPrimary() 来判断这个 BeanDefinition 上面是否含有 @Primary

  • determineHighestPriorityCandidate():该方法的本质是通过查找 JSR-330 中的 @Priority ,来确定 Bean 的优先级。
  • matchesBeanName():该方法则是通过 BeanName 来进行匹配的。

这两个方法不是我们本篇面试题的重点,就不多介绍了。


Java 面试宝典是大明哥全力打造的 Java 精品面试题,它是一份靠谱、强大、详细、经典的 Java 后端面试宝典。它不仅仅只是一道道面试题,而是一套完整的 Java 知识体系,一套你 Java 知识点的扫盲贴。

它的内容包括:

  • 大厂真题:Java 面试宝典里面的题目都是最近几年的高频的大厂面试真题。
  • 原创内容:Java 面试宝典内容全部都是大明哥原创,内容全面且通俗易懂,回答部分可以直接作为面试回答内容。
  • 持续更新:一次购买,永久有效。大明哥会持续更新 3+ 年,累计更新 1000+,宝典会不断迭代更新,保证最新、最全面。
  • 覆盖全面:本宝典累计更新 1000+,从 Java 入门到 Java 架构的高频面试题,实现 360° 全覆盖。
  • 不止面试:内容包含面试题解析、内容详解、知识扩展,它不仅仅只是一份面试题,更是一套完整的 Java 知识体系。
  • 宝典详情:https://www.yuque.com/chenssy/sike-java/xvlo920axlp7sf4k
  • 宝典总览:https://www.yuque.com/chenssy/sike-java/yogsehzntzgp4ly1
  • 宝典进展:https://www.yuque.com/chenssy/sike-java/en9ned7loo47z5aw

目前 Java 面试宝典累计更新 400+ 道,总字数 42w+。大明哥还在持续更新中,下图是大明哥在 2024-12 月份的更新情况:

想了解详情的小伙伴,扫描下面二维码加大明哥微信【daming091】咨询

同时,大明哥也整理一套目前市面最常见的热点面试题。微信搜[大明哥聊 Java]或扫描下方二维码关注大明哥的原创公众号[大明哥聊 Java] ,回复【面试题】 即可免费领取。

阅读全文