关于@EnableWebMvc和RequestMappingHandlerMapping和RequestMappingHandlerAdapter
我们知道,在我们使用xml配置的时候,我们获取这样配置过
<!-- 注解的处理器映射器,他来解析RequestMapping,然后和处理器对应起来 --> <bean class="org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping"></bean> <!-- 注解的处理器适配器 结合上面一起协作--> <bean class="org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter"></bean>
简单的从源码里看一眼:以RequestMappingHandlerMapping为例:它间接的实现了InitializingBean接口:
@Override public void afterPropertiesSet() { this.config = new RequestMappingInfo.BuilderConfiguration(); this.config.setUrlPathHelper(getUrlPathHelper()); this.config.setPathMatcher(getPathMatcher()); this.config.setSuffixPatternMatch(this.useSuffixPatternMatch); this.config.setTrailingSlashMatch(this.useTrailingSlashMatch); this.config.setRegisteredSuffixPatternMatch(this.useRegisteredSuffixPatternMatch); this.config.setContentNegotiationManager(getContentNegotiationManager()); super.afterPropertiesSet(); } //super.afterPropertiesSet();如下 @Override public void afterPropertiesSet() { initHandlerMethods(); } // 初始化处理器 protected void initHandlerMethods() { String[] beanNames = (this.detectHandlerMethodsInAncestorContexts ? BeanFactoryUtils.beanNamesForTypeIncludingAncestors(obtainApplicationContext(), Object.class) : obtainApplicationContext().getBeanNamesForType(Object.class)); //拿到所有的beanNames for (String beanName : beanNames) { if (!beanName.startsWith(SCOPED_TARGET_NAME_PREFIX)) { Class<?> beanType = null; try { beanType = obtainApplicationContext().getType(beanName); } //这里很关键,这里表面 只处理标注了注解@Controller或者@RequestMapping的 就认为才是处理器 然后拿到里面的方法,每一个方法就是一个处理器 if (beanType != null && isHandler(beanType)) { detectHandlerMethods(beanName); } } } handlerMethodsInitialized(getHandlerMethods()); } @Override protected boolean isHandler(Class<?> beanType) { return (AnnotatedElementUtils.hasAnnotation(beanType, Controller.class) || AnnotatedElementUtils.hasAnnotation(beanType, RequestMapping.class)); }
这两个Bean很重要,在初始化的时候能吧url和Controller里的handler对应起来。但是在我们使用了Spring3.0以后,这两个Bean可以不用再显示的配置出来了,而是使用下面依据配置可以代替:
<!-- 备注:此句只写在Spring MVC的配置文件里,否则出问题 Handler映射不上 --> <mvc:annotation-driven></mvc:annotation-driven>
它的作用是启动的时候会自动注册上面两个Bean。然后这句xml的配置,效果同@EnableWebMvc
注解。同样的,这个注解只能写在Spring MVC的配置文件里,而不能写在别处(主要是要保证不能被Root容器扫描进去了~)
比如我现在的配置,就出过问题:它是个单独的配置文件,就出问题了。Controller正常加入到子容器里了,但是映射都木有了,导致请求直接404了。
跟踪源码分析原因,终于找到了原因:因为@EnableWebMvc写在单独配置文件了,而Spring根容器在初始化的时候,扫描到了这个配置类因此解析了此注解。然后类RequestMappingHandlerMapping等核心处理类就被正常加载进了Spring根容器里。
接下来初始化Spring MVC的子容器的时候,也会解析此注解。然后在创建Bean的时候,发现此Bean已经存在了,所以不会再创建了。因此最终的结果是:这两个Bean都创建了,只是它不在Spring MVC的容器了,而是在父容器了。
但是,但是,但是 【小家Spring】Spring容器(含父子容器)的启动过程源码级别分析(含web.xml启动以及全注解驱动,和ContextLoader源码分析)这篇文章里我讲到过,这种MVC的处理器如果交给父容器去管理,会直接404报错的。
只要找到了原因,就很好解决了,两种方案:
一:根容器的配置文件里排除掉就行了
@ComponentScan(value = "com.fsx", excludeFilters = { @Filter(type = FilterType.ANNOTATION, classes = {Controller.class}), //排除掉web容器的配置文件,否则会重复扫描 @Filter(type = FilterType.ASSIGNABLE_TYPE, classes = {AppConfig.class}), @Filter(type = FilterType.ASSIGNABLE_TYPE, classes = {WebMvcConfig.class}), }) @Configuration public class RootConfig { ... }
二:写在字容器里**(推荐做法)** 这样可以删除掉WebMvcConfig这个配置文件了
@ComponentScan(value = "com.fsx", useDefaultFilters = false, includeFilters = {@Filter(type = FilterType.ANNOTATION, classes = {Controller.class})} ) @Configuration @EnableWebMvc public class AppConfig implements WebMvcConfigurer { @Bean public Child child() { return new Child(); } @Override public void extendMessageConverters(List<HttpMessageConverter<?>> converters) { MappingJackson2HttpMessageConverter messageConverter = new MappingJackson2HttpMessageConverter(); messageConverter.setSupportedMediaTypes(Arrays.asList(MediaType.APPLICATION_JSON_UTF8)); converters.add(new StringHttpMessageConverter(Charset.forName("UTF-8"))); converters.add(messageConverter); } }
备注:因为Spring Boot不存在父子容器概念,因此都不存在这类似的问题
如何在Controller中获取到Spring子容器?如何获取到Controller这个Bean呢?
从上面的知识中,我们可以知道,下面这是会报错的:
@ResponseBody @GetMapping("/hello") public String helloGet() { ApplicationContext ctx1 = WebApplicationContextUtils.getWebApplicationContext(request.getSession().getServletContext()); //从controller从获取这个Bean 能获取到controller这个Bean吗 // 用跟容器或者bean 直接报错org.springframework.beans.factory.NoSuchBeanDefinitionException: No qualifying bean of type HelloController bean = ctx1.getBean(HelloController.class); System.out.println(bean); }
这个不用解释了,因为我们平时通过工具类等获取到的容器都是根容器(其实99.99%的情况下,我们也只需要获取到根容器就够了),所以找不到Controller这个Bean是正常的。
那么接下来问题虽然没大作用,但是对小伙伴是否对父子容器原理熟悉,是个考验。
问:我就要在Controller这里获取到自己(或者别的Controller),怎么办?
方案一:注入@Autowired
@Autowired private HelloController helloController; @ResponseBody @GetMapping("/hello") public String helloGet() { System.out.println(helloController == this); //true return "hello...Get"; }
方案二:实现ApplicationContextAware接口,把子容器注入进来再getBean
private ApplicationContext context; @ResponseBody @GetMapping("/hello") public String helloGet() { System.out.println(context.getParent()); //Root WebApplicationContext: startup date [Sun Feb 24 21:49:25 CST 2019]; root of context hierarchy System.out.println(context.getBean(HelloController.class)); //com.fsx.controller.HelloController@32a2d798 return "hello...Get"; }
方案三:从ServletContext上下文/request请求域里拿到子容器,然后在getBean()
@ResponseBody @GetMapping("/hello") public String helloGet() { //方法一 ApplicationContext context = (ApplicationContext) request.getAttribute(DispatcherServlet.WEB_APPLICATION_CONTEXT_ATTRIBUTE); ApplicationContext context1 = RequestContextUtils.findWebApplicationContext(request); //此处dispatcher为dispatcherServlet的默认名称 ApplicationContext context2 = (ApplicationContext) request.getServletContext().getAttribute(FrameworkServlet.SERVLET_CONTEXT_PREFIX + "dispatcher"); System.out.println(context == context1); //true System.out.println(context == context2); //true System.out.println(context.getBean(HelloController.class)); return "hello...Get"; }
至于为何能从ServletContext中获取到这个子容器,原理请参考:【小家Spring】Spring容器(含父子容器)的启动过程源码级别分析(含web.xml启动以及全注解驱动,和ContextLoader源码分析)
总结
Spring MVC父子容器的设计对隔离性非常的好,但同时也经常带来一些我们认为莫名其妙的问题,增大了使用了复杂度(这也就是为何Spring Boot使用同一个容器管理的原因吧)
只有知己知彼,从原理的层面去了解了。出现了问题才能迅速定位,从而以最快最好的方式去解决。做到心中有数,才能更容易决胜千里