WhatAKitty Daily

A Programmer's Daily Record

ImportResource注解引发的血案

WhatAKitty   阅读次数loading...

背景

SpringBoot项目增加Spring拦截器,但是却发现没有生效;debug DispatchServelt,发现在查找handler的时候,由于存在多个RequestMappingHandlerMapping实例,而匹配到的handlerMaping实例内interceptors列表为空,所以无法使得拦截器生效。

追踪问题

为什么存在两个handlerMapping实例?

路由映射关系初始化探究

先看一下handlerMapping是什么时候初始化的。
DispatcherServelt类里,在刷新应用上下文(onRefresh)的时候,方法initStrategies会执行,如下代码:

1
2
3
4
5
6
7
8
9
10
11
protected void initStrategies(ApplicationContext context) {
initMultipartResolver(context);
initLocaleResolver(context);
initThemeResolver(context);
initHandlerMappings(context);
initHandlerAdapters(context);
initHandlerExceptionResolvers(context);
initRequestToViewNameTranslator(context);
initViewResolvers(context);
initFlashMapManager(context);
}

重点在于initHandlerMappings方法,在这个方法里面初始化了handlerMapping

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
private void initHandlerMappings(ApplicationContext context) {
this.handlerMappings = null;

// 是否检测所有的映射
if (this.detectAllHandlerMappings) {
// 查找上下文的所有HandlerMapping实例
Map<String, HandlerMapping> matchingBeans =
BeanFactoryUtils.beansOfTypeIncludingAncestors(context, HandlerMapping.class, true, false);
if (!matchingBeans.isEmpty()) {
this.handlerMappings = new ArrayList<HandlerMapping>(matchingBeans.values());
// 不为空的情况下进行排序
AnnotationAwareOrderComparator.sort(this.handlerMappings);
}
}
else {
// 获取主要的handlerMapping
try {
HandlerMapping hm = context.getBean(HANDLER_MAPPING_BEAN_NAME, HandlerMapping.class);
this.handlerMappings = Collections.singletonList(hm);
}
catch (NoSuchBeanDefinitionException ex) {
// Ignore, we'll add a default HandlerMapping later.
}
}

// 如果没有handlerMapping,则创建一个默认的
if (this.handlerMappings == null) {
this.handlerMappings = getDefaultStrategies(context, HandlerMapping.class);
if (logger.isDebugEnabled()) {
logger.debug("No HandlerMappings found in servlet '" + getServletName() + "': using default");
}
}
}

detectAllHandlerMappings值为true,则会从上文中查询实现了HandlerMapping接口的所有子类。找到AbstractHandlerMethodMapping抽象类,所有的控制层路由跳转(举例@RequestMapping)都通过AbstractHandlerMethodMapping实现。

可以看到在AbstractHandlerMethodMapping类内,实现了InitializingBean接口;同时在afterPropertiesSet方法内,执行了initHandlerMethods方法并初始化了handlerMapping

罪魁祸首定位

initHandlerMethods方法内通过debug断点运行应用后,发现在这里执行了两次;很明显,应用实例化了两次AbstractHandlerMethodMapping子类。

看了下AbstractHandlerMethodMapping类的所有子类,列举了下各自子类的大致用途:

子类名用途
CloudFoundryEndpointHandlerMappingPaas平台路由
EndpointHandlerMapping管理/健康检查路由
RequestMappingHandlerMapping请求映射路由
StaticRequestMappingHandlerMapping测试用静态路由

真正需要关注的是RequestMappingHandlerMapping子类,看了下真正的调用方只有WebMvcAutoConfiguration配置类内的方法,如下代码:

1
2
3
4
5
6
@Bean
@Primary
@Override
public RequestMappingHandlerMapping requestMappingHandlerMapping() {
return super.requestMappingHandlerMapping();
}

在这里,创建了一个名为requestMappingHandlerMappingRequestMappingHandlerMapping实例类,其他地方并未进行相关的创建。在想到请求的时候,存在两个RequestMappingHandlerMapping实例,那么肯定有其他入口能够创建新的实例。

经过N小时的折腾查询(过程不堪回首),终于发现是应用内的ImportResource引入的。

1
2
3
4
5
6
7
8
9
@SpringBootApplication(scanBasePackages = {"com.xxx"})
@ImportResource("classpath*:spring/applicationContext.xml")
public class BootstrapApp {

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

}

上述代码,在应用启动类上加了@ImportResource("classpath*:spring/applicationContext.xml")。这个就是罪魁祸首。

原因探究

为什么@ImportResource在注入的过程中会注册一个RequestMappingHandlerMapping实例,且为啥这个实例没有被@Primary覆盖?

定位到ConfigurationClassPostProcessor类,这个类是对所有配置类文件做解析。在方法processConfigBeanDefinitions内使用ConfigurationClassParser类具体执行解析操作。具体可以看这篇文章【Spring-Bean解析分析过程

在方法doProcessConfigurationClass

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 处理任意@ImportResource注解
if (sourceClass.getMetadata().isAnnotated(ImportResource.class.getName())) {
AnnotationAttributes importResource =
AnnotationConfigUtils.attributesFor(sourceClass.getMetadata(), ImportResource.class);
// 获取注解上的资源位置
String[] resources = importResource.getStringArray("locations");
// 获取资源的解释器
Class<? extends BeanDefinitionReader> readerClass = importResource.getClass("reader");
for (String resource : resources) {
String resolvedResource = this.environment.resolveRequiredPlaceholders(resource);
// 将资源位置和对应的解释器放入配置文件实例内
configClass.addImportedResource(resolvedResource, readerClass);
}
}

解析了@ImportResource,并把属性注入到配置文件类内,在这里只是一个标记,并没真正处理。

解析完成后,ConfigurationClassBeanDefinitionReader类会读取配置类文件,并加载所有的beanDefinition

1
2
3
4
5
6
7
// 读取资源内容并且根据内容创建bean定义
if (this.reader == null) {
this.reader = new ConfigurationClassBeanDefinitionReader(
registry, this.sourceExtractor, this.resourceLoader, this.environment,
this.importBeanNameGenerator, parser.getImportRegistry());
}
this.reader.loadBeanDefinitions(configClasses);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
private void loadBeanDefinitionsForConfigurationClass(ConfigurationClass configClass,
TrackedConditionEvaluator trackedConditionEvaluator) {

if (trackedConditionEvaluator.shouldSkip(configClass)) {
String beanName = configClass.getBeanName();
if (StringUtils.hasLength(beanName) && this.registry.containsBeanDefinition(beanName)) {
this.registry.removeBeanDefinition(beanName);
}
this.importRegistry.removeImportingClass(configClass.getMetadata().getClassName());
return;
}

if (configClass.isImported()) {
registerBeanDefinitionForImportedConfigurationClass(configClass);
}
for (BeanMethod beanMethod : configClass.getBeanMethods()) {
loadBeanDefinitionsForBeanMethod(beanMethod);
}
loadBeanDefinitionsFromImportedResources(configClass.getImportedResources());
loadBeanDefinitionsFromRegistrars(configClass.getImportBeanDefinitionRegistrars());
}

在这里可以看到对解析的做了处理,重点关注loadBeanDefinitionsFromImportedResources方法,这个方法对引入的资源做了解析并注册为beanDefinition

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
private void loadBeanDefinitionsFromImportedResources(
Map<String, Class<? extends BeanDefinitionReader>> importedResources) {

Map<Class<?>, BeanDefinitionReader> readerInstanceCache = new HashMap<Class<?>, BeanDefinitionReader>();

for (Map.Entry<String, Class<? extends BeanDefinitionReader>> entry : importedResources.entrySet()) {
String resource = entry.getKey();
Class<? extends BeanDefinitionReader> readerClass = entry.getValue();

// Default reader selection necessary?
if (BeanDefinitionReader.class == readerClass) {
if (StringUtils.endsWithIgnoreCase(resource, ".groovy")) {
// .groovy脚本文件解析
readerClass = GroovyBeanDefinitionReader.class;
}
else {
// 主要对于.xml文件的解析,也可以解析其他文件
readerClass = XmlBeanDefinitionReader.class;
}
}

// 通过readerClass获取具体的reader实例
BeanDefinitionReader reader = readerInstanceCache.get(readerClass);
if (reader == null) {
try {
// 如果不存在则实例化reader
reader = readerClass.getConstructor(BeanDefinitionRegistry.class).newInstance(this.registry);
// Delegate the current ResourceLoader to it if possible
if (reader instanceof AbstractBeanDefinitionReader) {
AbstractBeanDefinitionReader abdr = ((AbstractBeanDefinitionReader) reader);
abdr.setResourceLoader(this.resourceLoader);
abdr.setEnvironment(this.environment);
}
readerInstanceCache.put(readerClass, reader);
}
catch (Throwable ex) {
throw new IllegalStateException(
"Could not instantiate BeanDefinitionReader class [" + readerClass.getName() + "]");
}
}

// 通过reader读取bean定义
reader.loadBeanDefinitions(resource);
}
}

在这个类里面,reader根据导入资源文件格式实例化后,通过reader来读取bean定义。像我们这里导入的是xml资源文件,则会生成XmlBeanDefinitionReader实例。

在这个实例对于元素的具体解析里,可以看到答案:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
@Override
public BeanDefinition parse(Element element, ParserContext parserContext) {
Object source = parserContext.extractSource(element);
XmlReaderContext readerContext = parserContext.getReaderContext();

CompositeComponentDefinition compDefinition = new CompositeComponentDefinition(element.getTagName(), source);
parserContext.pushContainingComponent(compDefinition);

RuntimeBeanReference contentNegotiationManager = getContentNegotiationManager(element, source, parserContext);

// 创建了一个HanderMapping的bean定义
RootBeanDefinition handlerMappingDef = new RootBeanDefinition(RequestMappingHandlerMapping.class);
handlerMappingDef.setSource(source);
handlerMappingDef.setRole(BeanDefinition.ROLE_INFRASTRUCTURE);
handlerMappingDef.getPropertyValues().add("order", 0);
handlerMappingDef.getPropertyValues().add("contentNegotiationManager", contentNegotiationManager);

if (element.hasAttribute("enable-matrix-variables")) {
Boolean enableMatrixVariables = Boolean.valueOf(element.getAttribute("enable-matrix-variables"));
handlerMappingDef.getPropertyValues().add("removeSemicolonContent", !enableMatrixVariables);
}
else if (element.hasAttribute("enableMatrixVariables")) {
Boolean enableMatrixVariables = Boolean.valueOf(element.getAttribute("enableMatrixVariables"));
handlerMappingDef.getPropertyValues().add("removeSemicolonContent", !enableMatrixVariables);
}

// 在这里注册handerMapping的bean定义
configurePathMatchingProperties(handlerMappingDef, element, parserContext);
readerContext.getRegistry().registerBeanDefinition(HANDLER_MAPPING_BEAN_NAME , handlerMappingDef);

// ....其他代码省略

return null;
}

可以看到,在这段代码内创建了名为org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMappingbean定义。

WebMvcAutoConfiguration创建的handlerMapping的bean定义名为requestMappingHandlerMapping

这个导致的后果就是,IOC容器内存在两个RequestMapping

解决问题

有如下三种方法:

  1. 定义RequestMapping的顺序,但是两者都是系统载入,无法修改定义。
  2. xml的配置更改为注解配置,这样就可以避免上述的问题
  3. 设置detectAllHandlerMappings值为false

总结

在不了解内部机制的情况下,真的不能滥用一些配置,因为往往会造成一些出人意料的事情。