【推荐】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);
}
}
来源:oschina
链接:https://my.oschina.net/icebergxty/blog/3142263