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

前言

各位老铁们是否遇曾经遇到过这样的疑惑:同样是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()方法里可以看出:

	@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的,如下:

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:

@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:

@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,如下:

@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里注入这两个类如下:

@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配置的时候,我们获取这样配置过

<!-- 注解的处理器映射器,他来解析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使用同一个容器管理的原因吧)

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

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏Java大联盟

徒手撸一个Spring MVC框架

今天我们来仿写一个 Spring MVC 框架,用到的技术比较简单,只需要 XML 解析+反射就可以完成,不需要 JDK 动态代理。

13020
来自专栏编程一生

Spring参数的自解析--还在自己转换?你out了!

背景前段时间开发一个接口,因为调用我接口的同事脾气特别好,我也就不客气,我就直接把源代码发给他当接口定义了。

8530
来自专栏后端开发你必须学会的干货

Spring与后端模板引擎的故事

现在很多开发,都采用了前后端完全分离的模式,随着近几年前端工程化工具和MVC框架的完善,使得这种模式的维护成本逐渐降低。但是这种模式目前并不利于SEO(前后端分...

13830
来自专栏明丰随笔

MVC和Webapi的区别

Mvc主要用于构建网站,在后端实现了一套完整的MVC开发框架,默认使用Razor视图引擎。

39620
来自专栏全栈开发之路

SpringMVC笔记

1、SpringMVC.xml文件 两种方式把Controller里的java文件注册上来 1)<bean> 写法:<bean name="/loanv1...

8730
来自专栏格姗知识圈

放弃JSP吧--否则你无路可走

自从在知乎回答问题以来,以及根据最近几年给企业做技术咨询的情况,发现JSP还是一个经常被提到的问题。希望能在这篇文章里把关于JSP的问题集中说明一下。我的观点很...

21820
来自专栏Java研发军团

SSHM(SPRING+STRUTS+MYBATIS+HIBERNATE)书籍介绍

持久化——数据在程序实例之外留存的功能——是现代应用程序的核心。Hibernate是最流行的Java持久化工具,提供了自动且透明的对象/关系映射,使得在Java...

10620
来自专栏后端开发你必须学会的干货

SpringIoC和SpringMVC的快速入门

IoC和AOP是Spring框架的两大特性,IoC和MVC的流程密不可分,可以看作是面向对象编程的实现;而AOP特性则是面向切面编程的体现,也是前者的补充,所以...

6420
来自专栏Java后端技术栈cwnait

Nginx+Tomcat搭建集群,Spring Session+Redis实现Session共享

小伙伴们好久不见!最近略忙,博客写的有点少,嗯,要加把劲。OK,今天给大家带来一个JavaWeb中常用的架构搭建,即Nginx+Tomcat搭建服务集群,然后通...

9820
来自专栏算法之名

Spring MVC的模板方法模式 顶

模板方法模式是由抽象类或接口定义好执行顺序,由子类去实现,但无论子类如何实现,他都得按照抽象类或者接口定义好的顺序去执行。实例代码请参考 设计模式整理 ,Ser...

13820

扫码关注云+社区

领取腾讯云代金券

年度创作总结 领取年终奖励