前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >【小家Spring】如何证明Spring是存在父子容器的?顺便解决Spring MVC访问一直404问题(配置文件没问题)

【小家Spring】如何证明Spring是存在父子容器的?顺便解决Spring MVC访问一直404问题(配置文件没问题)

作者头像
YourBatman
发布2019-09-03 16:42:08
1.2K0
发布2019-09-03 16:42:08
举报
文章被收录于专栏:BAT的乌托邦BAT的乌托邦
前言

各位老铁们是否遇曾经遇到过这样的疑惑:同样是Spring容器里的Bean,为何能够@Autowireservice进Controller里面,但是反之注入就报错呢?报找不到bean~

但是自己从容器里明明可以拿到这个Bean啊,怎么回事呢?

同样的我们发现,容器里面的属性值,容器之间也是不互通的?

环境准备

准备一个传统的Spring环境(注意,一定不能是Spring Boot环境),为了偷懒,项目环境各位移步此处:

【小家Spring】Spring注解驱动开发—Servlet 3.0整合Spring MVC(不使用web.xml部署描述符,全注解驱动)

如何证明Spring是存在父子容器的

我们现在的结论是,在Web环境中,是分为SpringMvc管理的子容器,和Spring管理的父容器。如何证明呢?

基于上面的项目环境(请参看项目环境,因为如果项目环境不对,得到的效果可能会被误导的,总之就是保证@Controller只被扫描一次

我们分别让Controller和Service实现ApplicationContextAware,就可以证明出:

结论,虽然类型AnnotationConfigWebApplicationContext:但是显然是不一样的,从它从写的toString()方法里可以看出:

代码语言:javascript
复制
	@Override
	public String toString() {
		StringBuilder sb = new StringBuilder(getDisplayName());
		sb.append(": startup date [").append(new Date(getStartupDate()));
		sb.append("]; ");
		ApplicationContext parent = getParent();
		if (parent == null) {
			sb.append("root of context hierarchy");
		}
		else {
			sb.append("parent: ").append(parent.getDisplayName());
		}
		return sb.toString();
	}

==============备注:说一下Spring Boot环境下的:

结果我们发现(各位自己去试验哈),使用的是一模一样的(这里指的一模一样,就是一个,地址值都是一样的)AnnotationConfigEmbeddedWebApplicationContext,可以看出在boot环境中使用的是相同的容器管理的(无父子容器概念)。备注:该类在org.springframework.boot.context.embedded中这个包里面,属于Boot后来自己实现的

附上一个继承图谱:

注意:

我们的ApplicationContext以及BeanFactory都是可以直接@Autowired的,如下:

代码语言:javascript
复制
Controller注入:
    @Autowired
    private ApplicationContext applicationContext; ////WebApplicationContext for namespace 'dispatcher-servlet': startup date [Thu Mar 07 15:25:04 CST 2019]; parent: Root 
    @Autowired
    private BeanFactory beanFactory; // org.springframework.beans.factory.support.DefaultListableBeanFactory@41b6d6b3: defining beans [...

Service注入:
    @Autowired
    private ApplicationContext applicationContext; //Root WebApplicationContext: startup date [Thu Mar 07 15:25:02 CST 2019]; root of context hierarchy
    @Autowired
    private BeanFactory beanFactory; //rg.springframework.beans.factory.support.DefaultListableBeanFactory@747a2296: defining beans [...

由此可以看出,容器直接注入进来就行。但是,但是,但是如果存在父子容器的话,在不同的层,注入的对象也是不一样的,这点在了解了Spring容器的机制的情况下,是很好理解的~~~

如何证明Spring的父容器不能访问子容器的Bean

其实这个在上面的那篇博文里已经举例了。

比如,我在Web子容器的配置文件里注册一个Bean:

代码语言:javascript
复制
@ComponentScan(value = "com.fsx", useDefaultFilters = false,
        includeFilters = {@Filter(type = FilterType.ANNOTATION, classes = {Controller.class})}
)
@Configuration
public class AppConfig {

    @Bean
    public Child child() {
        return new Child();
    }
}

然后在Spring管理的Root父容器里注册一个Bean:

代码语言:javascript
复制
@ComponentScan(value = "com.fsx", excludeFilters = {
        @Filter(type = FilterType.ANNOTATION, classes = {Controller.class}),
        //排除掉web容器的配置文件,否则会重复扫描
        @Filter(type = FilterType.ASSIGNABLE_TYPE, classes = {AppConfig.class})
})
@Configuration
public class RootConfig {

    @Bean
    public Parent parent() {
        return new Parent();
    }

}

然后我们先在Controller里面注入这两个Bean,如下:

代码语言:javascript
复制
@Controller
@RequestMapping("/controller")
public class HelloController implements ApplicationContextAware {

    @Autowired
    private HelloService helloService;
    @Autowired
    private Parent parent;
    @Autowired
    private Child child;

    @ResponseBody
    @GetMapping("/hello")
    public String helloGet() {
        System.out.println(parent);
        System.out.println(child);
        System.out.println(helloService);
        System.out.println(helloService.hello());
        return "hello...Get";
    }
}

启动,访问。我们发现一切正常,并且都有值。

现在我在Service里注入这两个类如下:

代码语言:javascript
复制
@Service
public class HelloServiceImpl implements HelloService, ApplicationContextAware {

    @Autowired
    private Parent parent;
    @Autowired
    private Child child;

    @Override
    public Object hello() {
        System.out.println(parent);
        System.out.println(child);
        return "service hello";
    }
}

启动项目,我们就发现报错了,找不到Child这和Bean.

从上面这个例子,就可以看出。子容器是可以访问父容器里的Bean的,但是父容器不能访问子容器内的Bean。所以很显然,直接向Service里面@Autowire一个Controller,启动时候也是会报错的~

另外可以说一点,父子容器的初始化顺序为:先父容器,再子容器。所以web组件一般都是最后被初始化的(当然还存在循环嵌套的情况,另当别论了)。因为若使用web.xml配置Spring容器,是先执行ContextLoaderListener#contextInitialized启动Spring容器,再初始化DispatcherServlet来启动web容器的

关于@EnableWebMvc和RequestMappingHandlerMapping和RequestMappingHandlerAdapter

我们知道,在我们使用xml配置的时候,我们获取这样配置过

代码语言:javascript
复制
<!-- 注解的处理器映射器,他来解析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接口:

代码语言:javascript
复制
	@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可以不用再显示的配置出来了,而是使用下面依据配置可以代替:

代码语言:javascript
复制
<!-- 备注:此句只写在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报错的。

只要找到了原因,就很好解决了,两种方案:

一:根容器的配置文件里排除掉就行了

代码语言:javascript
复制
@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这个配置文件了

代码语言:javascript
复制
@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呢?

从上面的知识中,我们可以知道,下面这是会报错的:

代码语言:javascript
复制
    @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

代码语言:javascript
复制
    @Autowired
    private HelloController helloController;

    @ResponseBody
    @GetMapping("/hello")
    public String helloGet() {
        System.out.println(helloController == this); //true
        return "hello...Get";
    }

方案二:实现ApplicationContextAware接口,把子容器注入进来再getBean

代码语言:javascript
复制
    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()

代码语言:javascript
复制
    @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使用同一个容器管理的原因吧)

只有知己知彼,从原理的层面去了解了。出现了问题才能迅速定位,从而以最快最好的方式去解决。做到心中有数,才能更容易决胜千里

本文参与 腾讯云自媒体分享计划,分享自作者个人站点/博客。
原始发表:2019年02月23日,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 前言
  • 环境准备
  • 如何证明Spring是存在父子容器的
    • 注意:
    • 如何证明Spring的父容器不能访问子容器的Bean
    • 关于@EnableWebMvc和RequestMappingHandlerMapping和RequestMappingHandlerAdapter
    • 如何在Controller中获取到Spring子容器?如何获取到Controller这个Bean呢?
      • 问:我就要在Controller这里获取到自己(或者别的Controller),怎么办?
      • 总结
      相关产品与服务
      容器服务
      腾讯云容器服务(Tencent Kubernetes Engine, TKE)基于原生 kubernetes 提供以容器为核心的、高度可扩展的高性能容器管理服务,覆盖 Serverless、边缘计算、分布式云等多种业务部署场景,业内首创单个集群兼容多种计算节点的容器资源管理模式。同时产品作为云原生 Finops 领先布道者,主导开源项目Crane,全面助力客户实现资源优化、成本控制。
      领券
      问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档