Spring Boot踩坑记录(@SpringBootApplication与@ComponentScan存在冲突)

孤者浪人 提交于 2019-12-12 17:01:16

【推荐】2019 Java 开发者跳槽指南.pdf(吐血整理) >>>

一、简介

我们先来看一下现象,各位可以先去这里下载代码。

简单介绍一下里边的类

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);
    }
}

应用启动类:这个就不多说了,上边的注解就是这次踩坑的关键。

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已加载-----------------------------");
    }
}

TestController:这里实现了ApplicationListener,当Spring容器启动完成后,会调用这个方法,当然前提是TestController必须被加载到Spring容器中,所以我们可以通过这条日志判断这个类是否被Spring加载。

众所周知,@SpringBootApplication这个注解会自动扫描跟它同一个包的子包,所以TestController会被Spring加载。

我们启动一下应用看一下结果:

可以日志被打印了,这个很好理解,如果没打印那才是见鬼了。

然后我们取消其中一个@ComponentScan的注释,再运行一下

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);
    }
}

日志不见了!What the fuck???

先不要慌,我们把第二个注释也打开看一下结果

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);
    }
}

日志又出现了!

不知道各位看官什么心情,但我的心情大概就跟上边两个表情包一样,甚至一度怀疑Spring有BUG,但是最终我还是找到了问题的原因,至于是不是BUG......各位自己评判。

二、Spring Boot启动流程分析

完整的流程分析可以看这里,我们这里只分析用到的部分,直接看读取@ComponentScans

//ConfigurationClassParser.java
//第258行
protected final SourceClass doProcessConfigurationClass(ConfigurationClass configClass, SourceClass sourceClass) throws IOException {
    //省略部分代码
    
    Set<AnnotationAttributes> componentScans = AnnotationConfigUtils.attributesForRepeatable(
				sourceClass.getMetadata(), ComponentScans.class, ComponentScan.class);
    
    //省略部分代码
}

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

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

//AnnotationConfigUtils.java
//第295行
//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<>();

    //查找@ComponentScan注解
    addAttributesIfNotNull(result, metadata.getAnnotationAttributes(annotationClassName, false));

    //查找@ComponentScans注解
    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);
        }
    }

    //将结果合并
    return Collections.unmodifiableSet(result);
}

上边就是整体流程的代码,出问题的部分就是下边这两个方法,我们一个一个看

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

先看metadata.getAnnotationAttributes(annotationClassName, false))的逻辑

中间过程我就省略了,反正你一路往下走,会走到下边的方法

//AnnotatedElementUtils.java
//第903行
//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 {
            //获取WebApplication类上的所有注解
            //这里会取到@SpringBootApplication和取消注释的@ComponentScan
            List<Annotation> declaredAnnotations = Arrays.asList(AnnotationUtils.getDeclaredAnnotations(element));
            T result = searchWithGetSemanticsInAnnotations(element, declaredAnnotations,
                                                           annotationTypes, annotationName, containerType, processor, visited, metaDepth);
            if (result != null) {
                return result;
            }

            //省略部分代码
        }
        catch (Throwable ex) {
            AnnotationUtils.handleIntrospectionFailure(element, ex);
        }
    }

    return null;
}

上边的代码获取了应用启动类上所有的注解,然后调用了searchWithGetSemanticsInAnnotations方法来进行实际的判断,这里有三种情况

  • 只存在@SpringBootApplication注解
  • 存在@SpringBootApplication和一个@ComponentScan注解
  • 存在@SpringBootApplication和多个@ComponentScan注解

下面我们来逐个的分析:

(1)只存在@SpringBootApplication注解

//AnnotatedElementUtils.java
//第967行
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循环作用是检测是否有@ComponentScan注解
    //显然第一种情况下是没有这个注解的,所以直接省略掉第一个循环的代码
    
    //省略部分代码
    
    //第二个循环,递归寻找注解中是否存在@ComponentScan和@ComponentScans注解
    //这里就会找到@SpringBootApplication中的@ComponentScan然后返回
    //如果还想继续深入下到底是怎么找的,各位可以自己看下代码
    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)存在@SpringBootApplication和一个@ComponentScan注解

//AnnotatedElementUtils.java
//第967行
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)) {
            //判断是否是@ComponentScan注解
            if (annotationTypes.contains(currentAnnotationType) ||
                currentAnnotationType.getName().equals(annotationName) ||
                processor.alwaysProcesses()) {
                T result = processor.process(element, annotation, metaDepth);
                if (result != null) {
                    //这个判断的作用是啥咱也不知道
                    //有大佬知道的话请赐教~
                    //但是这里的判断是false,所以会直接返回结果
                    if (processor.aggregates() && metaDepth == 0) {
                        processor.getAggregatedResults().add(result);
                    }
                    else {
                        return result;
                    }
                }
            }
            
            //省略部分代码
            
        }
    }
    
    //省略第二个循环
}

可以看到当有一个@ComponentScan注解的时候,它会直接返回这个注解,不会再去解析@SpringBootApplication中的配置,所以TestController就没有被加载到Spring容器中。

(3)存在@SpringBootApplication和多个@ComponentScan注解

如果一个@ComponentScan注解会覆盖掉@SpringBootApplication中的对应配置,那为什么多个@ComponentScan注解却又可以了呢?

嘿嘿,这里先说明一下,我们之前分析的源码都是位于spring-core这个包下的,所以即使你不用Spring Boot,它的逻辑也是这样子,那我们平常使用多个@ComponentScan的时候也没出现问题呀?

这里就要介绍一个新的注解——@Repeatable,我们来看下@ComponentScan的代码

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

可以看到上边有个@Repeatable注解,这是JDK8新增的注解,作用就是当有多个@ComponentScan注解时,将它们转成数组作为@ComponentScans的value。

所以第三种情况就是@SpringBootApplication和一个@ComponentScans注解,注意这里是带s的,所以@SpringBootApplication中的包扫描依然会生效。

@ComponentScans注解的解析的逻辑在metadata.getAnnotationAttributes(containerClassName, false)方法里,具体的逻辑就不分析了,毕竟我们已经找到了问题的根源。

三、总结

然后我们总结下三种现象的原因:

(1)只有@SpringBootApplication,日志正常打印

@SpringBootApplication默认会扫描同包及子包,所以TestController被扫描,打印日志

(2)存在@SpringBootApplication和一个@ComponentScan注解,不打印日志

@ComponentScan注解会先被处理,然后返回,使得@SpringBootApplication中的配置没有生效

(3)存在@SpringBootApplication和多个@ComponentScan注解,日志正常打印

多个@ComponentScan注解会被整合成一个@ComponentScans注解,不影响@SpringBootApplication中配置的正确读取

解决方法:

使用@ComponentScans注解,而不是直接使用@ComponentScan注解

这是最完美方案,既不会影响SpringBoot本身的配置,你也可以随意自定义自己的配置

@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);
    }
}
易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!