背景
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) { 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 { try { HandlerMapping hm = context.getBean(HANDLER_MAPPING_BEAN_NAME, HandlerMapping.class); this.handlerMappings = Collections.singletonList(hm); } catch (NoSuchBeanDefinitionException ex) { } }
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
类的所有子类,列举了下各自子类的大致用途:
子类名 | 用途 |
---|
CloudFoundryEndpointHandlerMapping | Paas平台路由 |
EndpointHandlerMapping | 管理/健康检查路由 |
RequestMappingHandlerMapping | 请求映射路由 |
StaticRequestMappingHandlerMapping | 测试用静态路由 |
真正需要关注的是RequestMappingHandlerMapping
子类,看了下真正的调用方只有WebMvcAutoConfiguration
配置类内的方法,如下代码:
1 2 3 4 5 6
| @Bean @Primary @Override public RequestMappingHandlerMapping requestMappingHandlerMapping() { return super.requestMappingHandlerMapping(); }
|
在这里,创建了一个名为requestMappingHandlerMapping
的RequestMappingHandlerMapping
实例类,其他地方并未进行相关的创建。在想到请求的时候,存在两个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
| 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
| 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();
if (BeanDefinitionReader.class == readerClass) { if (StringUtils.endsWithIgnoreCase(resource, ".groovy")) { readerClass = GroovyBeanDefinitionReader.class; } else { readerClass = XmlBeanDefinitionReader.class; } }
BeanDefinitionReader reader = readerInstanceCache.get(readerClass); if (reader == null) { try { reader = readerClass.getConstructor(BeanDefinitionRegistry.class).newInstance(this.registry); 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.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);
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); }
configurePathMatchingProperties(handlerMappingDef, element, parserContext); readerContext.getRegistry().registerBeanDefinition(HANDLER_MAPPING_BEAN_NAME , handlerMappingDef);
return null; }
|
可以看到,在这段代码内创建了名为org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping
的bean
定义。
而WebMvcAutoConfiguration
创建的handlerMapping的bean
定义名为requestMappingHandlerMapping
。
这个导致的后果就是,IOC容器内存在两个RequestMapping
。
解决问题
有如下三种方法:
- 定义
RequestMapping
的顺序,但是两者都是系统载入,无法修改定义。 - 将
xml
的配置更改为注解配置,这样就可以避免上述的问题 - 设置
detectAllHandlerMappings
值为false
总结
在不了解内部机制的情况下,真的不能滥用一些配置,因为往往会造成一些出人意料的事情。