Spring Boot crater record (@ SpringBootApplication conflicts with @ ComponentScan)

Keywords: SpringBoot Spring Java Lombok

I. Introduction

Let's take a look at the phenomenon. You can go first Here Download the code.

A brief introduction to the inner class

package com.iceberg.springboot.web;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
//@ComponentScan("com.iceberg.springboot.biz")
//@ComponentScan("com.iceberg.springboot.manager")
public class WebApplication {

    public static void main(String[] args) {
        SpringApplication.run(WebApplication.class, args);
    }
}

Application startup class: that's not enough. The comments above are the key to this step.

package com.iceberg.springboot.web.controller;

import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.context.event.ApplicationReadyEvent;
import org.springframework.context.ApplicationListener;
import org.springframework.web.bind.annotation.RestController;

@Slf4j
@RestController
public class TestController implements ApplicationListener<ApplicationReadyEvent> {

    @Override
    public void onApplicationEvent(ApplicationReadyEvent event) {
        log.warn("--------------------------TestController Loaded-----------------------------");
    }
}

TestController: the ApplicationListener is implemented here. After the Spring container is started, this method will be called. Of course, the precondition is that TestController must be loaded into the Spring container, so we can judge whether this class is loaded by Spring through this log.

As we all know, @ SpringBootApplication annotation will automatically scan the subpackages of the same package as it, so TestController will be loaded by Spring.

Let's start the application and see the results:

It's easy to understand that the log can be printed. If it's not printed, it's a ghost.

Then we uncomment one of @ ComponentScan and run it again

package com.iceberg.springboot.web;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
@ComponentScan("com.iceberg.springboot.biz")
//@ComponentScan("com.iceberg.springboot.manager")
public class WebApplication {

    public static void main(String[] args) {
        SpringApplication.run(WebApplication.class, args);
    }
}

The log is missing! What the fuck???

Don't panic. Let's open the second comment to see the result

package com.iceberg.springboot.web;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
@ComponentScan("com.iceberg.springboot.biz")
@ComponentScan("com.iceberg.springboot.manager")
public class WebApplication {

    public static void main(String[] args) {
        SpringApplication.run(WebApplication.class, args);
    }
}

The log appears again!

I don't know how you are feeling, but my mood is probably the same as the two expression packs above. I even doubted that Spring has BUG for a while, but finally I found the cause of the problem. As for whether it's BUG or not, please judge by yourself.

II. Analysis of Spring Boot startup process

Complete process analysis can be seen Here , we only analyze the parts used here, and read @ ComponentScans directly

//ConfigurationClassParser.java
//258th elements
protected final SourceClass doProcessConfigurationClass(ConfigurationClass configClass, SourceClass sourceClass) throws IOException {
    //Omit some codes
    
    Set<AnnotationAttributes> componentScans = AnnotationConfigUtils.attributesForRepeatable(
				sourceClass.getMetadata(), ComponentScans.class, ComponentScan.class);
    
    //Omit some codes
}

//AnnotationConfigUtils.java
//288th elements
static Set<AnnotationAttributes> attributesForRepeatable(AnnotationMetadata metadata,
                                                         Class<?> containerClass, Class<?> annotationClass) {

    return attributesForRepeatable(metadata, containerClass.getName(), annotationClass.getName());
}

//AnnotationConfigUtils.java
//295th elements
//containerClassName: org.springframework.context.annotation.ComponentScans
//annotationClassName: org.springframework.context.annotation.ComponentScan
static Set<AnnotationAttributes> attributesForRepeatable(
    AnnotationMetadata metadata, String containerClassName, String annotationClassName) {

    Set<AnnotationAttributes> result = new LinkedHashSet<>();

    //Find @ ComponentScan annotation
    addAttributesIfNotNull(result, metadata.getAnnotationAttributes(annotationClassName, false));

    //Find @ ComponentScans annotation
    Map<String, Object> container = metadata.getAnnotationAttributes(containerClassName, false);
    if (container != null && container.containsKey("value")) {
        for (Map<String, Object> containedAttributes : (Map<String, Object>[]) container.get("value")) {
            addAttributesIfNotNull(result, containedAttributes);
        }
    }

    //Merge results
    return Collections.unmodifiableSet(result);
}

The upper part is the code of the overall process, and the problem part is the lower two methods. Let's look at them one by one

metadata.getAnnotationAttributes(annotationClassName, false))
metadata.getAnnotationAttributes(containerClassName, false)

First look at the logic of metadata.getAnnotationAttributes(annotationClassName, false))

I'll omit the middle process. Anyway, if you go down all the way, you'll get to the bottom

//AnnotatedElementUtils.java
//903rd elements
//element: class com.iceberg.springboot.web.WebApplication
//annotationName: org.springframework.context.annotation.ComponentScan
private static <T> T searchWithGetSemantics(AnnotatedElement element,
                                            Set<Class<? extends Annotation>> annotationTypes, @Nullable String annotationName,
                                            @Nullable Class<? extends Annotation> containerType, Processor<T> processor,
                                            Set<AnnotatedElement> visited, int metaDepth) {
    if (visited.add(element)) {
        try {
            //Get all comments on WebApplication class
            //Here you will get @ SpringBootApplication and @ ComponentScan uncommented
            List<Annotation> declaredAnnotations = Arrays.asList(AnnotationUtils.getDeclaredAnnotations(element));
            T result = searchWithGetSemanticsInAnnotations(element, declaredAnnotations,
                                                           annotationTypes, annotationName, containerType, processor, visited, metaDepth);
            if (result != null) {
                return result;
            }

            //Omit some codes
        }
        catch (Throwable ex) {
            AnnotationUtils.handleIntrospectionFailure(element, ex);
        }
    }

    return null;
}

The above code gets all annotations on the application startup class, and then calls the searchWithGetSemanticsInAnnotations method for practical judgement. Here are three cases.

  • Only @ SpringBootApplication annotation exists
  • There are @ SpringBootApplication and a @ ComponentScan annotation
  • There are @ SpringBootApplication and multiple @ ComponentScan annotations

Let's analyze one by one:

(1) only @ SpringBootApplication annotation exists

//AnnotatedElementUtils.java
//967th elements
private static <T> T searchWithGetSemanticsInAnnotations(@Nullable AnnotatedElement element,
                                                         List<Annotation> annotations, Set<Class<? extends Annotation>> annotationTypes,
                                                         @Nullable String annotationName, @Nullable Class<? extends Annotation> containerType,
                                                         Processor<T> processor, Set<AnnotatedElement> visited, int metaDepth) {
    //The first for loop is to check whether there is @ ComponentScan annotation
    //Obviously, there is no such annotation in the first case, so the code of the first loop is omitted directly
    
    //Omit some codes
    
    //In the second loop, recursively look for @ ComponentScan and @ ComponentScan annotations
    //Here you will find @ ComponentScan in @ SpringBootApplication and return
    //If you want to go further and find out how, you can look at the code yourself
    for (Annotation annotation : annotations) {
        Class<? extends Annotation> currentAnnotationType = annotation.annotationType();
        if (!AnnotationUtils.hasPlainJavaAnnotationsOnly(currentAnnotationType)) {
            T result = searchWithGetSemantics(currentAnnotationType, annotationTypes,
                                              annotationName, containerType, processor, visited, metaDepth + 1);
            if (result != null) {
                processor.postProcess(element, annotation, result);
                if (processor.aggregates() && metaDepth == 0) {
                    processor.getAggregatedResults().add(result);
                }
                else {
                    return result;
                }
            }
        }
    }
}

(2) there are @ SpringBootApplication and a @ ComponentScan annotation

//AnnotatedElementUtils.java
//967th elements
private static <T> T searchWithGetSemanticsInAnnotations(@Nullable AnnotatedElement element,
                                                         List<Annotation> annotations, Set<Class<? extends Annotation>> annotationTypes,
                                                         @Nullable String annotationName, @Nullable Class<? extends Annotation> containerType,
                                                         Processor<T> processor, Set<AnnotatedElement> visited, int metaDepth) {
    for (Annotation annotation : annotations) {
        Class<? extends Annotation> currentAnnotationType = annotation.annotationType();
        if (!AnnotationUtils.isInJavaLangAnnotationPackage(currentAnnotationType)) {
            //Determine whether it is @ ComponentScan annotation
            if (annotationTypes.contains(currentAnnotationType) ||
                currentAnnotationType.getName().equals(annotationName) ||
                processor.alwaysProcesses()) {
                T result = processor.process(element, annotation, metaDepth);
                if (result != null) {
                    //What is the function of this judgment? We don't know
                    //If you have a big man, please give me some advice~
                    //But the judgment here is false, so the result will be returned directly
                    if (processor.aggregates() && metaDepth == 0) {
                        processor.getAggregatedResults().add(result);
                    }
                    else {
                        return result;
                    }
                }
            }
            
            //Omit some codes
            
        }
    }
    
    //Omit the second cycle
}

It can be seen that when there is an @ ComponentScan annotation, it will directly return this annotation and will not parse the configuration in @ SpringBootApplication, so TestController has not been loaded into the Spring container.

(3) there are @ SpringBootApplication and multiple @ ComponentScan annotations

If a @ ComponentScan annotation will overwrite the corresponding configuration in @ SpringBootApplication, why can multiple @ ComponentScan annotations be used again?

Hehe, let's first explain that the source code we analyzed before is under the spring core package, so even if you don't use Spring Boot, its logic is the same, so we usually use multiple @ ComponentScan without any problems?

Here is a new annotation - @ Repeatable. Let's take a look at @ ComponentScan's code

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@Documented
@Repeatable(ComponentScans.class)
public @interface ComponentScan {
}

You can see that there is a @ Repeatable annotation on the top, which is a new annotation of JDK8. Its purpose is to convert multiple @ ComponentScan annotations into an array as the value of @ ComponentScan.

So the third case is @ SpringBootApplication and a @ ComponentScans annotation. Note that s is included here, so package scanning in @ SpringBootApplication will still take effect.

@In metadata.getAnnotationAttributes(containerClassName, false) method, the specific logic will not be analyzed. After all, we have found the root cause of the problem.

Three, summary

Then we summarize the reasons for the following three phenomena:

(1) only @ SpringBootApplication can print logs normally

@SpringBootApplication will scan the same package and sub package by default, so TestController will be scanned and print the log

(2) there are @ SpringBootApplication and a @ ComponentScan annotation, and the log will not be printed

@The ComponentScan annotation will be processed first and then returned, so that the configuration in @ SpringBootApplication does not take effect

(3) there are @ SpringBootApplication and multiple @ ComponentScan annotations, and the log is printed normally

Multiple @ ComponentScan annotations will be integrated into one @ ComponentScan annotation, which will not affect the correct reading of the configuration in @ SpringBootApplication

resolvent:

Use @ ComponentScan annotation instead of @ ComponentScan annotation directly

This is the perfect solution, which will not affect the configuration of SpringBoot itself, or you can customize your own configuration at will

@SpringBootApplication
@ComponentScans({
        @ComponentScan("com.iceberg.springboot.biz"),
        @ComponentScan("com.iceberg.springboot.manager")  
})
public class WebApplication {

    public static void main(String[] args) {
        SpringApplication.run(WebApplication.class, args);
    }
}

Posted by tllewellyn on Thu, 12 Dec 2019 00:57:45 -0800