What is the principle of @Primary annotation in Spring?

1. Problem analysis

When we use Spring, we sometimes encounter the following situation.

Suppose I have two classes, A and B, and inject B into A, as follows:

@Component 
public  class  A {
     @Autowired
    B b;
}

As for B, there are multiple instances in the configuration class:

@Configuration 
@ComponentScan 
public  class  JavaConfig {
     @Bean("b1") 
    B b1 () {
         return  new  B ();
    }

    @Bean("b2") 
    B b2 () {
         return  new  B ();
    }
}

After such a project is started, the following exception will inevitably be thrown:

Of course, for such problems, I believe experienced students know how to solve them:

  1. You can use the @Resource annotation and specify the specific Bean name when using this annotation.
  2. On top of the @Autowired annotation, add an additional @Qualifier(“b1”) annotation, and use this annotation to specify the name of the Bean to be loaded.
@Component 
public  class  A {
     @Autowired 
    @Qualifier("b1")
    B b;
}
  1. On one of the multiple B objects, add the @Primary annotation to indicate which one will be used first when there are duplicate B objects.
@Configuration 
@ComponentScan 
public  class  JavaConfig {
     @Bean("b1") 
    @Primary 
    B b1 () {
         return  new  B ();
    }

    @Bean("b2") 
    B b2 () {
         return  new  B ();
    }
}

Apart from these three, are there any other ways? must have! ! ! Can the @Qualifier annotation still be used in this way in Spring? In this article, Brother Song also expanded other uses of the @Qualifier annotation. Interested friends should not miss it.

There are three methods here, among which @Resource is an annotation provided in JSR. I will not expand on it here. Brother Song will talk to you about the injection principle of @Resource annotation later. Today I mainly want to share with my friends the implementation principles of the latter two solutions.

2. Source code analysis

This article is based on the previous @Autowired. How does it inject variables? This article is spread out, so if you haven’t read the revised article yet, it is recommended to read it first, which will help you better understand this article.

2.1 doResolveDependency

How do you inject variables into @Autowired ? In section 3.3, we mentioned that when injecting B into A, the doResolveDependency method will be called. Let’s take a look at this method:

DefaultListableBeanFactory#doResolveDependency:

@Nullable 
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 ;
        }
        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);
        //... 
}

In this method, the findAutowireCandidates method is first called to find all Classes that meet the conditions. The key in the Map is the name of the Bean, and the value is a Class, which has not been instantiated yet.

If we solve the problem through the @Qualifier annotation, then the problem is solved in the findAutowireCandidates method. Can the @Qualifier annotation in the previous article Spring still be used in this way? I have already talked with my friends.

If the @Qualifier annotation does not solve the problem, it will cause the number of matchingBeans finally queried to be greater than 1, then it will enter the next if link, and further determine which Bean to use through the determineAutowireCandidate method, @Primary annotation The processing is done in this method.

2.2 determineAutowireCandidate

DefaultListableBeanFactory#determineAutowireCandidate

@Nullable 
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 ;
}

A total of three attempts have been made in this method:

  1. The first attempt is to call the determinePrimaryCandidate method to determine the best candidate Bean. This method essentially finds the best BeanName through the @Primary annotation.
  2. If the best BeanName is not found in the first step, then the determineHighestPriorityCandidate method is called to find the best Bean. This method essentially determines the priority of the Bean by looking for the @Priority annotation in JSR-330.
  3. If no suitable BeanName is found in the first two steps, then the next for loop will match the name of the Bean, that is, whether the name of the variable in class A matches the name of the target Bean. If it can match, that’s OK. . This is what we often say. The @Autowired annotation is first matched by type. If the type does not match, it will be matched by name.

The above briefly introduces the execution idea of ​​​​this method. Next, let’s take a look at the execution details.

2.2.1 determinePrimaryCandidate

@Nullable 
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();
    }
    return (getParentBeanFactory() instanceof DefaultListableBeanFactory parent &&
            parent.isPrimary(transformedBeanName, beanInstance));
}

Let’s take a look at the execution logic of this method.

All qualified BeanDefinitions are saved in the parameter candidates, the parameter key is the name of the Bean, and the Value is the corresponding BeanDefinition. Now let’s traverse the candidates. During the traversal, call the isPrimary method to determine whether the BeanDefinition contains the @Primary annotation. The logic of the isPrimary method is relatively simple, so I won’t go into details. This method involves the getMergedLocalBeanDefinition method to search in the parent container. Brother Song has also talked about these two details with everyone in his previous articles ( Spring BeanDefinition: Decryption of the father-son relationship , what is the father-son container in Spring? ).

During the search process, if there is a BeanName that meets the conditions, it is assigned to the primaryBeanName variable and then returned. If there are multiple BeanNames that meet the conditions, a NoUniqueBeanDefinitionException exception is thrown.

2.2.2 determineHighestPriorityCandidate

To understand the determineHighestPriorityCandidate method, you must first understand the usage of the @Priority annotation. Considering that some friends may not be familiar with the @Priority annotation, I will also tell you a few words here.

The @Priority annotation is somewhat similar to @Order and can be used to specify the priority of a Bean. This is an annotation provided in JSR, so if you want to use this annotation, you need to add dependencies first:

< dependency > 
    < groupId > jakarta.annotation </ groupId > 
    < artifactId > jakarta.annotation-api </ artifactId > 
    < version > 2.1.1 </ version > 
</ dependency >

Then add the annotation on the class, like this:

public  interface  IBService {
}
@Component 
@Priority(100) 
public  class  BServiceImpl1  implements  IBService {
}
@Component 
@Priority(101) 
public  class  BServiceImpl2  implements  IBService {
}

The number in the @Priority annotation indicates the priority. The larger the number, the smaller the priority. When IBService is injected into A in the future, beans with high priority will be searched first. Although the @Priority annotation can be added to a class or a method, in actual practice, this annotation will not take effect if it is added to a method and can only be added to a class. As for the reason, you will understand after reading the following source code analysis.

Now let’s look at the determineHighestPriorityCandidate method:

@Nullable 
protected String determineHighestPriorityCandidate (Map<String, Object> candidates, Class<?> requiredType) {
     String  highestPriorityBeanName  =  null ;
     Integer  highestPriority  =  null ;
     for (Map.Entry<String, Object> entry : candidates.entrySet()) {
         String  candidateBeanName  = entry.getKey();
         Object  beanInstance  = entry.getValue();
         if (beanInstance != null ) {
             Integer  candidatePriority  = getPriority(beanInstance);
             if (candidatePriority != null ) {
                 if (highestPriorityBeanName != null ) {
                     if (candidatePriority.equals(highestPriority)) {
                         throw  new  NoUniqueBeanDefinitionException (requiredType, candidates.size(),
                                 "Multiple beans found with the same priority ('" + highestPriority +
                                 "') among candidates: " + candidates.keySet()) ;
                    }
                    else  if (candidatePriority < highestPriority) {
                        highestPriorityBeanName = candidateBeanName;
                        highestPriority = candidatePriority;
                    }
                }
                else {
                    highestPriorityBeanName = candidateBeanName;
                    highestPriority = candidatePriority;
                }
            }
        }
    }
    return highestPriorityBeanName;
}

The overall processing idea of ​​the determineHighestPriorityCandidate method is very similar to the determinePrimaryCandidate method. The difference is that the determinePrimaryCandidate method processes the @Primary annotation, while the determineHighestPriorityCandidate method processes the @Priority annotation.

The determineHighestPriorityCandidate method also traverses candidates, and then calls the getPriority method to obtain the specific priority value. Then select an appropriate beanName based on this specific number and return it. If there are multiple beans with the same priority, a NoUniqueBeanDefinitionException exception will be thrown.

Finally, let’s look at the getPriority method. After several twists and turns, this method will call the AnnotationAwareOrderComparator#getPriority method:

@Override 
@Nullable 
public Integer getPriority (Object obj) {
     if (obj instanceof Class<?> clazz) {
         return OrderUtils.getPriority(clazz);
    }
    Integer  priority  = OrderUtils.getPriority(obj.getClass());
     if (priority == null   && obj instanceof DecoratingProxy decoratingProxy) {
         return getPriority(decoratingProxy.getDecoratedClass());
    }
    return priority;
}

As you can see, the final step here is to call the OrderUtils.getPriority method to find the @Priority annotation on the parameter clazz, and find the corresponding value on the annotation and return it. When OrderUtils.getPriority is executed, the parameter is clazz, that is, only the annotations on clazz will be searched, and the annotations on the method will not be searched. Therefore, I said earlier that the @Priority annotation must be added to the class to be effective.

2.2.3 Match by name

Finally, let’s look at the logic of matching by name:

// 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;
    }
}
protected  boolean  matchesBeanName (String beanName, @Nullable String candidateName) {
     return (candidateName != null &&
            (candidateName.equals(beanName) || ObjectUtils.containsElement(getAliases(beanName), candidateName)));
}

As you can see, here we also traverse the candidates collection, and then call the matchesBeanName method. In this method, we will judge descriptor.getDependencyName()whether the candidate BeanName and the variable name to be injected ( ) are equal. If they are equal, just return it directly. That is, the following code can be run without additional annotations without reporting an error:

@Component 
public  class  AService {
     @Autowired
    B b1;

}
@Configuration 
@ComponentScan 
public  class  JavaConfig {

    @Bean 
    public B b1 () {
         return  new  B ();
    }

    @Bean 
    B b2 () {
         return  new  B ();
    }

}

3. Summary

Okay, after the above analysis, friends now understand the complete processing logic of @Primary annotation~ How does this article inject variables into @Autowired? Can it still be used like this with the @Qualifier annotation in Spring? The effect is better when eaten together!