@ Qualifier Advanced Applications - Bulk Dependency Injection by Category

Keywords: Java Spring Attribute xml Mybatis

Each sentence

Ross: Top pick may be aquatic, but MVP definitely doesn't.

Preface

stay Last article (on @LoadBalanced load balancing) At the end of this article, I put forward a very important question, and suggested that the small partners think deeply about it for themselves. This article mainly aims at this question and gives a unified answer and explanation.
Because I think this knowledge point belongs to one of the core contents of Spring Framework and is very important, I hope it will be helpful to you.

Background case

When it comes to the @Qualifier annotation, it's not unfamiliar: it's used for "precise matching" beans, and it's generally used for case s with multiple instances of the same type of beans, which can be used to identify and match.
I thought the @Qualifier annotation would be enough to identify attributes and classes, until I saw this application in LoadBalancer AutoConfiguration:

@LoadBalanced
@Autowired(required = false)
private List<RestTemplate> restTemplates = Collections.emptyList();    

It can inject all RestTemplate types in the container and beans annotated with @LoadBalanced.
This usage surprises me very much. It provides me with an additional way of thinking and adds a new way of playing to my framework. In order to integrate it and use it as far as possible to avoid not mining pits, we can only uncover it and understand its usage from the bottom principle.

Qualifier Annotation Autowire Candidate Resolver

It is an implementation class that relies on the injected candidate processor interface Autowire Candidate Resolver, inherited from GenericType Aware Autowire Candidate Resolver, so this class is the most powerful processor (except Context Annotation Autowire Candidate Resolver). Spring defaults to using it for candidate processing.

It can almost be called the "implementation class" of the @Qualifier annotation, and is dedicated to parsing the annotation.
With the above questions, the principle analysis is as follows:

// @since 2.5
public class QualifierAnnotationAutowireCandidateResolver extends GenericTypeAwareAutowireCandidateResolver {
    // It's a List, and you can see that it doesn't just support org. spring framework. beans. factory. annotation. Qualifier
    private final Set<Class<? extends Annotation>> qualifierTypes = new LinkedHashSet<>(2);
    private Class<? extends Annotation> valueAnnotationType = Value.class;


    // Empty Construction: The default support is @Qualifier and @Qualifier of the JSR330 standard
    public QualifierAnnotationAutowireCandidateResolver() {
        this.qualifierTypes.add(Qualifier.class);
        try {
            this.qualifierTypes.add((Class<? extends Annotation>) ClassUtils.forName("javax.inject.Qualifier", QualifierAnnotationAutowireCandidateResolver.class.getClassLoader()));
        } catch (ClassNotFoundException ex) {
            // JSR-330 API not available - simply skip.
        }
    }
    
    // Non-empty constructs: You can specify additional annotation types yourself
    // Note: If qualifierType is specified by the constructor, the above two are not supported, so it is not recommended to use them.
    // It is recommended to use addQualifierType() provided by it to add~~~
    public QualifierAnnotationAutowireCandidateResolver(Class<? extends Annotation> qualifierType) {
    ... // Omitting add/set method    

    // This is the most important interface method to determine whether the provided Bean - > Bean Definition Holder is a candidate
    // (Return true to indicate that the Bean is eligible)
    @Override
    public boolean isAutowireCandidate(BeanDefinitionHolder bdHolder, DependencyDescriptor descriptor) {
        // 1. Look at the parent class first: ean defines whether dependency injection is allowed, whether generic types match
        boolean match = super.isAutowireCandidate(bdHolder, descriptor);
        // 2. Continue to judge the @Qualifier annotation if all are satisfied~~~~
        if (match) {
            // 3. See if the annotated @Qualifier annotation and candidate beans match ~ (the core logic here)
            // descriptor generally encapsulates the parameters of the attribute writing method, that is, the annotations on the method parameters.
            match = checkQualifiers(bdHolder, descriptor.getAnnotations());
            // 4. If the Field/Method parameters match, we will continue to look at the Method method where the parameters are located.
            // If the constructor / returns void. Further check whether the @Qualifier qualifier labeled on the constructor/method matches
        if (match) {
                MethodParameter methodParam = descriptor.getMethodParameter();
                // If it's Field, the method Param is null, so it needs to be null here.
                if (methodParam != null) {
                    Method method = methodParam.getMethod();
                    // Method = null denotes the constructor void.class denotes that the method returns void
                    if (method == null || void.class == method.getReturnType()) {
                        // Note that the methodParam.getMethodAnnotations() method may return empty
                        // After all, it's not necessarily necessary to annotate @Qualifier and other annotations on the constructor/common method.~~~~
                        // At the same time, we are warned that the @Qualifier annotation on the method should not be labeled arbitrarily.
                        match = checkQualifiers(bdHolder, methodParam.getMethodAnnotations());
                    }
                }
            }
        }
        return match;
    }
    ...
}

Where the source code comment is located, I step by step mark out an execution step logic that matches it. Attention should be paid to the following points:

  • Qualifier Types support the caller's own designation (by default only @Qualifier type is supported)
  • Only when type matching, Bean definition matching, generic matching are all Ok, can @Qualifier be used to match more accurately.
  • The logic of descriptor.getAnnotations() is:

        - If `Dependency Descriptor'describes a field (`Field'), go to the field and get the annotations.
        - If the description is a method parameter (`MethodParameter'), then the annotation of the method parameter is returned.
  • Step 3 match = true indicates that the qualifier on the Field / method parameter is matched~

Note: If you can walk into the isAutowire Candidate () method, it must be marked with the @Autowire annotation (which can be processed by the Autowire Annotation Bean PostProcessor), so the length of the array returned by descriptor.getAnnotations() is at least 1.

checkQualifiers() method:
QualifierAnnotationAutowireCandidateResolver: 

    // Match the given qualifier annotation with the candidate bean definition. In naming, you find that this is a negative number, which means that multiple annotations match together.
    // The qualifier referred to here is obviously only the @Qualifier annotation by default
    protected boolean checkQualifiers(BeanDefinitionHolder bdHolder, Annotation[] annotationsToSearch) {
        // Many people wonder why not annotate the return or true?
        // Please refer to my explanation above: methodParam.getMethodAnnotations() method may return empty, so... you can understand it.
        if (ObjectUtils.isEmpty(annotationsToSearch)) {
            return true;
        }
        SimpleTypeConverter typeConverter = new SimpleTypeConverter();

        // Traverse through each annotation (typically two @Autowired+@Qualifier annotations)
        // Two annotations for this example: @Autowired+@LoadBalanced two annotations ~(@LoadBalanced is marked with @Qualifier)
        for (Annotation annotation : annotationsToSearch) {
            Class<? extends Annotation> type = annotation.annotationType();
            boolean checkMeta = true; // Whether to check meta-annotations
            boolean fallbackToMeta = false;

            // The logic of the isQualifier method is as follows: Is it a qualified annotation (default / develop your own specified)
            // This article's org. spring framework. cloud. client. loadbalancer. LoadBalanced returns true
            if (isQualifier(type)) {
                // checkQualifier: Check whether the current annotation qualifier matches
                if (!checkQualifier(bdHolder, annotation, typeConverter)) {
                    fallbackToMeta = true; // No match. So fallback to Meta
                } else {
                    checkMeta = false; // When matched, there is no need to verify metadata.~~~
                }
            }

            // Start checking metadata (if the above matches, you don't need to check metadata)
            // For example, @Autowired annotations / other custom annotations (which are not matched anyway) will come in and check metadata one by one.
            // When will it come to checkMeta? For example, @A is marked with @Qualifier. @ B is marked @A. When the qualifier is @B, fallback will come.
            if (checkMeta) {
                boolean foundMeta = false;
                // type.getAnnotations() results in metaannotations: @Documented, @Retention, @Target, etc.
                for (Annotation metaAnn : type.getAnnotations()) {
                    Class<? extends Annotation> metaType = metaAnn.annotationType();
                    if (isQualifier(metaType)) {
                        foundMeta = true; // The annotation is found as soon as it comes in, marked true to indicate that it was found from the meta-annotation
                        // Only accept fallback match if @Qualifier annotation has a value...
                        // Otherwise it is just a marker for a custom qualifier annotation.
                        // Fallback = true (qualifier but not true) but no valeu value
                        // Or there's no match at all. That's embarrassing. Just return false.~
                        if ((fallbackToMeta && StringUtils.isEmpty(AnnotationUtils.getValue(metaAnn))) || !checkQualifier(bdHolder, metaAnn, typeConverter)) {
                            return false;
                        }
                    }
                }
                // fallbackToMeta =true. If you don't find a match, return to false
                if (fallbackToMeta && !foundMeta) {
                    return false;
                }
            }
        }
        // Equivalent: Only if all comments return false will the Bean be considered legitimate~~~
        return true;
    }

    // Determine whether a type is qualifier type: a qualifier that represents all my support
    // The key to this article is the following statement: Type is the type of qualifier or @Qualifier annotated on this annotation (is Annotation Present)
    protected boolean isQualifier(Class<? extends Annotation> annotationType) {
        for (Class<? extends Annotation> qualifierType : this.qualifierTypes) {
            // Type is the type of qualifier or @Qualifier annotated on this annotation (is AnnotationPresent)
            if (annotationType.equals(qualifierType) || annotationType.isAnnotationPresent(qualifierType)) {
                return true;
            }
        }
        return false;
    }

The checkQualifiers() method checks all annotations of the annotation (iterating through checks one by one), as follows:

  • If the qualifier annotation (itself @Qualifier or is AnnotationPresent) matches, go ahead and look at the next annotation.

        - In other words, the annotations labeled `@Qualifier'are qualifiers (`isQualifier ()= true `)
  • If the qualifier annotation does not match, then fallback. Continue to see if the qualifier annotation tagged on it (if any) matches, and if it matches, it does.
  • If it's not a qualifier annotation, it's fallback logic.
  • In short: if it is not for qualifier annotations to be ignored directly. If multiple qualifier annotations are valid, all matches must be made before final matches can be counted.

Tips: The effect that qualifiers do not take effect is not necessarily injection failure, but injection success if it is single. It's just that if multiple beans appear, they can't distinguish, so injection fails.~

Its fallback strategy can only find one more level up (no more). For example, using the @B annotation in the previous example can also have the effect of @Qualifier, but if we add an @C level, the qualifier will not work.

Note: Class. is Annotation Present (Class <? Extends Annotation > annotation Class) indicates whether annotation Class is marked on this type (this type can be any Class type).
This method is not transitive: for example, annotation A is marked with @Qualifier, annotation B is marked with @A annotation, so you can use this method to determine whether @Qualifier on @B is returned to false (even if the @Inherited annotation is written, because it has nothing to do with it)

In fact, this does not explain why @LoadBalanced participated in dependency injection in this article, and we have to continue to look at the essence of checkQualifier() method (method name is singular, meaning exact checking of a single annotation):

QualifierAnnotationAutowireCandidateResolver: 

    // Check whether a comment qualifier matches the current Bean
    protected boolean checkQualifier(BeanDefinitionHolder bdHolder, Annotation annotation, TypeConverter typeConverter) {
        // Type: Annotation type bd: RotBean Definition of the current Bean 
        Class<? extends Annotation> type = annotation.annotationType();        
        RootBeanDefinition bd = (RootBeanDefinition) bdHolder.getBeanDefinition();
    
        // ======== Here are the key steps for matching=========
        // 1. The qualifiers fields of Bean definition information are generally worthless (except for configuration in the XML era)
        // Long names can't be tried with short names. Obviously here qualifier is null
        AutowireCandidateQualifier qualifier = bd.getQualifier(type.getName());
        if (qualifier == null) {
            qualifier = bd.getQualifier(ClassUtils.getShortName(type));
        }
        
        //Here's what's really interesting. Please look at the steps carefully.
        if (qualifier == null) {
            // First, check annotation on qualified element, if any
            // 1. The word method is to take this type of annotation declaration from the bd tag, where the target Annotation is null in the non-XML configuration era.
            Annotation targetAnnotation = getQualifiedElementAnnotation(bd, type);
            // Then, check annotation on factory method, if applicable
            // 2. If null. Take this type of commentary in the factory method. Two annotations @Bean and @LoadBalanced are marked in this method, so the targetAnnotation is no longer null.~~
            if (targetAnnotation == null) {
                targetAnnotation = getFactoryMethodAnnotation(bd, type);
            }

            // If this kind of wood exists, it will go to the father to find it.
            if (targetAnnotation == null) {
                RootBeanDefinition dbd = getResolvedDecoratedDefinition(bd);
                if (dbd != null) {
                    targetAnnotation = getFactoryMethodAnnotation(dbd, type);
                }
            }


            // If xml, factory method, father have not found this method. That good fellow, go back to the class itself.
            // That is to say, if @LoadBalanced is tagged on RestTemplate, it is also broad-minded.
            if (targetAnnotation == null) {
                // Look for matching annotation on the target class
                ...
            }
        
            // Find it, and if and only if it's the comment, return true.~
            // Tips: equals is used here, so even if both the target and the Bean are marked with the @Qualifier attribute, the value value is the same.~~~~
            // Simply put: only if the value is the same, will it be selected. Otherwise, this Bean is not eligible.
            if (targetAnnotation != null && targetAnnotation.equals(annotation)) {
                return true;
            }
        }

        // Fabulous. If target Annotation is not found, it is not matched. Still don't give up, get all the annotation attributes of the current annotation and continue to try matching
        Map<String, Object> attributes = AnnotationUtils.getAnnotationAttributes(annotation);
        if (attributes.isEmpty() && qualifier == null) {
            return false;
        }
        ... // Details are not described. That's why our @Qualifier annotation works on a class because it's very compatible here.~
    }

// ================= Its two most important judgments=================
if (targetAnnotation != null && targetAnnotation.equals(annotation));

// Fall back on bean name (or alias) match
if (actualValue == null && attributeName.equals(AutowireCandidateQualifier.VALUE_KEY) &&
                    expectedValue instanceof String && bdHolder.matchesName((String) expectedValue));

The implementation of the checkQualifier() method is sufficient to see that Spring is a good framework for case comprehensiveness, compatibility and flexibility. Spring's powerful support and flexible extension give Spring Boot and Spring Cloud more possibilities in framework level design.~

@ Advanced use of Qualifier

@ Autowired is automatically assembled according to type. When there are more than one Bean of the same type in the Spring container, it needs to be used together with @Qualifier.

Example 1:
@Configuration
public class WebMvcConfiguration {

    @Qualifier("person1")
    @Autowired
    public Person person;

    @Bean
    public Person person1() {
        return new Person("fsx01", 16);
    }
    @Bean
    public Person person2() {
        return new Person("fsx02", 18);
    }
}

The single test code is as follows (the same below):

public static void main(String[] args) {
    ApplicationContext context = new AnnotationConfigApplicationContext(WebMvcConfiguration.class);
    WebMvcConfiguration bean = context.getBean(WebMvcConfiguration.class);
    // Print field values
    System.out.println(bean.person);

}

Print Person(name=fsx01, age=16) after running, fully in line with expectations. This is also our most common and simplest use of the @Qualifier annotation.

Example two:

If you are careful, you may notice that the @Qualifier annotation allows inheritance (@Inherited), can be tagged on fields, methods, method parameters, classes, annotations.
So it can also be used as follows:

@Configuration
public class WebMvcConfiguration {

    @MyAnno // All beans marked with this annotation will be included in the bag, please install List (because there will be more than one)
    @Autowired
    public List<Person> person;
    
    @MyAnno
    @Bean
    public Person person1() {
        return new Person("fsx01", 16);
    }
    @MyAnno
    @Bean
    public Person person2() {
        return new Person("fsx02", 18);
    }

    // Custom Annotations: The @Qualifier Annotation is marked above
    @Target({FIELD, METHOD})
    @Retention(RetentionPolicy.RUNTIME)
    @Qualifier
    @interface MyAnno {
    }
}

Run the single test and print [Person(name=fsx01, age=16), Person(name=fsx02, age=18)], which meets expectations.

Example three:

If you don't want to customize annotations, it's also possible to use @Qualifier annotation category injection directly, as in the following cases:

@Configuration
public class WebMvcConfiguration {
    @Qualifier("person2")
    @Autowired
    public List<Person> person;

    @Qualifier("person2")
    @Bean
    public Person person1() {
        return new Person("fsx01", 16);
    }

    @Qualifier
    @Bean
    public Person person2() {
        return new Person("fsx02", 18);
    }
    @Qualifier
    @Bean
    public Person person3() {
        return new Person("fsx03", 20);
    }
}

The final result of the operation is:

[Person(name=fsx01, age=16), Person(name=fsx02, age=18)]

It injects either the same value specified by @Qualifier or the same bean Name (or alias). This part of the matching code is:

The checkQualifier method:

1. The annotations on the header are completely equals (type and value are the same, counting as matching success)
    targetAnnotation != null && targetAnnotation.equals(annotation)
    
2. Fall back on bean name (or alias) match. If @Qualifier does not match, it falls back to BeanName matching. The rule is:
   The `value'attribute annotated on the header (which must be present) will eventually be matched if the bean Name/alias matches the last name.
   
    actualValue == null && attributeName.equals(AutowireCandidateQualifier.VALUE_KEY) &&
    expectedValue instanceof String && bdHolder.matchesName((String) expectedValue)

Note: The use of classes and parameters is relatively simple, so there is no demonstration here.
As you can see from the details of the @Qualifier design, the value attribute of annotations is not necessary, so it can be used well in joint annotation scenarios.

Details of dependency injection and the use of @Qualifier should also be noted as follows:

  1. @ Autowired should not be injected into fields of Object type, because more than N errors can be found in the container. But List < Object > is possible (equivalent to taking all beans over)
  2. The ability to use @Qualifier as a high-level feature to implement dependency injection on demand and by category (not type) is highly appreciated, providing more possibilities for framework redevelopment designers.

If the specified value is qualified / matched by key, annotation matching like @LoadBalanced understands that the achievement is classified and qualified by category, and the degree of freedom is higher.

Recommended reading

Why does a @LoadBalanced annotation enable RestTemplate to have load balancing capabilities? [Enjoy Spring Cloud]

summary

This article introduces @Qualifier advanced application scenarios and cases. By combining the use of @LoadBalanced annotation, it should be said that it can open up a new perspective for you to see @Qualifier, and even Spring's dependency injection. It is quite meaningful for subsequent understanding, custom extension/use.

== If you are interested in Spring, Spring Boot, MyBatis and other source code analysis, you can add me wx: fsx641385712, invite you to join the group and fly together manually.==
== If you are interested in Spring, Spring Boot, MyBatis and other source code analysis, you can add me wx: fsx641385712, invite you to join the group and fly together manually.==

Posted by raguskra on Mon, 16 Sep 2019 21:13:00 -0700