Spring注解驱动之Servlet3.0与SpringMVC的整合分析

导读:本篇文章讲解 Spring注解驱动之Servlet3.0与SpringMVC的整合分析,希望对大家有帮助,欢迎收藏,转发!站点地址:www.bmabk.com

概述

之前我们说了下ServletContainerInitializer机制以及如何利用ServletContext向web容器中注册Servlet、Listener以及Filter三大组件。下面我们来详细分析下Servlet3.0是如何利用ServletContainerInitializer机制来整合SpringMVC的。

Servlet 3.0与Spring MVC的整合分析

首先,我们来创建一个新的maven工程,例如springmvc-annotation-liayun,注意其打包方式是war。该maven工程创建好了之后,发现它里面报了一个小小的错误,如下图所示。
在这里插入图片描述
在pom.xml文件报了web.xml is missing and is set to true这样一个错误。
我们可以在pom.xml文件中添加如下一个编译war的maven插件即可。

<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
	xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
	<modelVersion>4.0.0</modelVersion>
	<groupId>com.meimeixia</groupId>
	<artifactId>springmvc-annotation-liayun</artifactId>
	<version>0.0.1-SNAPSHOT</version>
	<packaging>war</packaging>
	
	<build>
		<plugins>
			<plugin>
				<groupId>org.apache.maven.plugins</groupId>
				<artifactId>maven-war-plugin</artifactId>
				<version>2.4</version>
				<configuration>
					<failOnMissingWebXml>false</failOnMissingWebXml>
				</configuration>
			</plugin>
		</plugins>
	</build>
</project>

注意标签里面的false这个配置,它是来告诉maven工程即使没有web.xml文件。

导入相关依赖

先导入对spring-webmvc的依赖,注意其版本是4.3.11.RELEASE。

<dependencies>
    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-webmvc</artifactId>
        <version>4.3.11.RELEASE</version>
    </dependency>
</dependencies>

这样,我们就把Spring MVC的webmvc包,以及它所依赖的其他jar包,都一并导入进来了。
再来导入对servlet api的依赖,注意其版本是3.1.0,因为我们现在是在用Servlet 3.0以上的特性。

<dependencies>
    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-webmvc</artifactId>
        <version>4.3.11.RELEASE</version>
    </dependency>
    <dependency>
        <groupId>javax.servlet</groupId>
        <artifactId>javax.servlet-api</artifactId>
        <version>3.1.0</version>
        <scope>provided</scope>
    </dependency>
</dependencies>

注意下provided配置,说明该Jar已经提供了,只是编译是使用,是不需要打入war包中的。
由于Tomcat服务器里面也有servlet api,即目标环境已经提高该Jar包了,所以把servlet api的scope设置成provided。这样我们项目在打成war包时,就不会带上jar包了,否则就会引起jar包冲突。

Servlet3.0整合SpringMVC的底层原理

Spring的官网地址是:https://spring.io/
看到网页顶部导航栏中的Projects菜单没有,将鼠标光标放在它上面,你就能看到如下所示的下拉列表了,然后点击其中的Overview选项。
在这里插入图片描述
这时,你应该能看到如下图所示的页面了。
在这里插入图片描述
接着,点击箭头所指的Spring Framework,来到Spring框架的介绍页面,如下图所示。
在这里插入图片描述
紧接着,点击箭头所指的LEARN切换到如下所示的选项卡中,点击Reference Doc.
在这里插入图片描述
Spring官方文档分类是非常详细的,如下图所示,不过这儿我们只关注Web Servlet这一分类,因为它主要是来讲述Spring MVC、WebSocket等等的。
在这里插入图片描述
在Spring Web MVC这一部分下有这样1.1. DispatcherServlet一个小节,这一小节主要是来讲述配置DispatcherServlet的,我们不妨看一看该小节中的内容,如下图所示。
在这里插入图片描述
上图中代码摘抄如下:

// 我们可以编写一个类来实现WebApplicationInitializer接口哟,当然了,你也可以编写一个类来实现ServletContainerInitializer接口
public class MyWebApplicationInitializer implements WebApplicationInitializer {

	@Override
	public void onStartup(ServletContext servletContext) {

		// 然后,我们来创建一个AnnotationConfigWebApplicationContext对象,它应该代表的是web的IOC容器
		AnnotationConfigWebApplicationContext context = new AnnotationConfigWebApplicationContext();
		// 加载我们的配置类
		context.register(AppConfig.class);

		// 在容器启动的时候,我们自己来创建一个DispatcherServlet对象,并将其注册在ServletContext中
		DispatcherServlet servlet = new DispatcherServlet(context);
		// 向servlet容器(Tomcat容器)中添加DispatcherServlet实例
		ServletRegistration.Dynamic registration = servletContext.addServlet("app", servlet);
		registration.setLoadOnStartup(1);
		// 这儿是来配置DispatcherServlet的映射信息的
		registration.addMapping("/app/*");
	}
}

我们通常不使用上面的方式整合SpringMVC,但是通过它我们可以了解Servlet通过生命周期的Hook方法调用Spring,Spring容器的创建过程。
这种方式类似于我们以前整合Spring MVC时在web.xml文件中写的如下配置。

<!-- 使用监听器启动Spring的配置(即spring/applicationContext-*.xml),加载Spring的配置来启动Spring容器,这个容器我们叫它父容器,也可以称之为根容器 -->
<listener>
	<listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
</listener>
<!-- 初始化Spring容器 -->
<context-param>
	<param-name>contextConfigLocation</param-name>
	<param-value>classpath:spring/applicationContext-*.xml</param-value>
</context-param>
<!-- 子容器,Spring MVC子容器-->
<!-- 前端控制器 -->
<servlet>
	<servlet-name>taotao-search-web</servlet-name>
	<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
	<!-- contextConfigLocation不是必须的,如果不配置contextConfigLocation, Spring MVC的配置文件默认在:WEB-INF/servlet的name+"-servlet.xml" -->
	<init-param>
		<param-name>contextConfigLocation</param-name>
		<!-- 指定Spring MVC配置文件的路径 -->
		<param-value>classpath:spring/springmvc.xml</param-value>
	</init-param>
	<load-on-startup>1</load-on-startup>
</servlet>
<!-- 配好映射 -->
<servlet-mapping>
	<servlet-name>taotao-search-web</servlet-name>
	<!-- 伪静态化 -->
	<url-pattern>*.html</url-pattern>
</servlet-mapping>

可以看到我们以前配置的是这种父子容器,而且Spring也推荐使用父子容器的概念。

Servlet3.0整合SpringMVC的方式

既然我们不会用上述这种方式,那么得用哪种方式呢?不妨展开我们maven工程下的Maven Dependencies目录,发现我们导入了spring-web-4.3.11.RELEASE.jar这样一个jar包,如下图所示。
在这里插入图片描述
展开该jar包,发现它里面有一个META-INF/services/目录,而且在该目录下有一个名字叫javax.servlet.ServletContainerInitializer的文件,其内容如下所示。
在这里插入图片描述
我们以前说过,Servlet容器在启动应用的时候,会扫描当前应用每一个jar包里面的META-INF/services/javax.servlet.ServletContainerInitializer文件中指定的实现类,然后加载该实现类并运行它里面的方法。
那我们就来看一下spring-web-4.3.11.RELEASE.jar中META-INF/services/目录里面的javax.servlet.ServletContainerInitializer文件中到底指定的哪一个类,从上图我们可以知道其指定的是org.springframework.web.SpringServletContainerInitializer这个类。
它实现的就是ServletContainerInitializer接口。
在这里插入图片描述
Servlet容器在启动我们Spring应用之后,会传入一个我们感兴趣的类型的集合,然后在onStartup方法中拿到之后就会来挨个遍历,如果遍历出来的我们感兴趣的类型不是接口,也不是抽象类,但是WebApplicationInitializer接口旗下的,那么就会创建该类型的一个实例,并将其存储到名为initializers的LinkedList集合中。
也可以这样说,我们Spring的应用一启动就会加载感兴趣的WebApplicationInitializer接口旗下的所有组件,并且为这些WebApplicationInitializer组件创建对象,当然前提是这些组件即不是接口,也不是抽象类。
接下来,我们就来到咱们感兴趣的WebApplicationInitializer接口中,并查看该接口的继承树。
在这里插入图片描述

第一层抽象类:AbstactContextLoaderInitializer

进入AbstractContextLoaderInitializer的源码,主要看onStartUp方法。
在这里插入图片描述
方法调用了registerContextLoaderListener方法,就是注册ContextLoaderListener。
registerContextLoaderListener掉有了createRootApplicationContext方法,该方法是来创建根容器的,而且方法是一个抽象方法,需要子类自己去实现。根据根容器创建上下文加载监听器(ContextLoaderListener),接着把ContextLoaderListener注册到ServletContext容器中。

第二层抽象类,AbstractDispatcherServletInitializer

我们也不妨点进该抽象类里面去看一看,并且主要来看其onStartUp方法,发现会注册DispatcherServlet,如下所示。

@Override
public void onStartup(ServletContext servletContext) throws ServletException {
	// 调用父类(即AbstractContextLoaderInitializer)的onStartup方法,先把根容器创建出来
	super.onStartup(servletContext);
	// 往ServletContext中注册DispatcherServlet
	registerDispatcherServlet(servletContext);
}

protected void registerDispatcherServlet(ServletContext servletContext) {
	String servletName = getServletName();
	Assert.hasLength(servletName, "getServletName() must not return empty or null");

	// 调用createServletApplicationContext方法来创建一个web的IOC容器,而且该方法还是一个抽象方法,需要子类去实现
	WebApplicationContext servletAppContext = createServletApplicationContext();
	Assert.notNull(servletAppContext,
			"createServletApplicationContext() did not return an application " +
			"context for servlet [" + servletName + "]");

	// 调用createDispatcherServlet方法来创建一个DispatcherServlet
	FrameworkServlet dispatcherServlet = createDispatcherServlet(servletAppContext);
	dispatcherServlet.setContextInitializers(getServletApplicationContextInitializers());

	// 将创建好的DispatcherServlet注册到ServletAppContext中
	ServletRegistration.Dynamic registration = servletContext.addServlet(servletName, dispatcherServlet);
	Assert.notNull(registration,
			"Failed to register servlet with name '" + servletName + "'." +
			"Check if there is another servlet registered under the same name.");

	registration.setLoadOnStartup(1);
	// 配置DispatcherServlet的映射映射信息,其中getServletMappings方法是一个抽象方法,需要由子类自己来重写
	registration.addMapping(getServletMappings());
	registration.setAsyncSupported(isAsyncSupported());

	Filter[] filters = getServletFilters();
	if (!ObjectUtils.isEmpty(filters)) {
		for (Filter filter : filters) {
			registerServletFilter(servletContext, filter);
		}
	}
	customizeRegistration(registration);
}

我们发现会先调用createServletApplicationContext方法来创建一个WebApplicationContext(即web的IOC容器),再调用createDispatcherServlet方法来创建一个DispatcherServlet。

protected FrameworkServlet createDispatcherServlet(WebApplicationContext servletAppContext) {
	// 创建一个DispatcherServlet
	return new DispatcherServlet(servletAppContext);
}

此外,我们还发现在registerDispatcherServlet方法中还会将创建好的DispatcherServlet注册到ServletAppContext中,很显然,这时会返回一个ServletRegistration.Dynamic对象,自然地就要来配置该DispatcherServlet的映射信息了,在配置该DispatcherServlet的映射信息时,还调用了一个getServletMapppings方法,不过该方法是一个抽象方法,需要由子类自己来重写。

第三层抽象类,AbstractAnnotationConfigDispatcherServletInitializer

我们也不妨点进该抽象类里面去看一看,可以看到它重写了AbstractContextLoaderInitializer抽象父类里面的createRootApplicationContext方法,如下所示,而且我们知道该方法是来创建根容器的。

@Override
protected WebApplicationContext createRootApplicationContext() {
	// 传入一个配置类(用户自定义)
	Class<?>[] configClasses = getRootConfigClasses();
	if (!ObjectUtils.isEmpty(configClasses)) {
		// 创建一个根容器
		AnnotationConfigWebApplicationContext rootAppContext = new AnnotationConfigWebApplicationContext();
		// 注册获取到的配置类,相当于注册配置类里面的组件
		rootAppContext.register(configClasses);
		return rootAppContext;
	}
	else {
		return null;
	}
}

首先获取到一个配置类,调用的可是getRootConfigClasses方法,调用该方法能传入一个配置类(其实就是我们自己写的),而且该方法还是一个抽象方法,需要由子类自己来重写。继续,要知道我们以前写的可是xml配置文件,获取到了之后,会new一个AnnotationConfigWebApplicationContext,这就相当于创建了一个根容器,然后将获取到的配置类注册进去,相当于是注册配置类里面的组件,最终返回创建的根容器。
此外,该抽象类里面还有一个createServletApplicationContext方法,如下所示,它是来创建web的IOC容器的,其实这个方法就是重写的AbstractDispatcherServletInitializer抽象类里面的createServletApplicationContext方法。

@Override
protected WebApplicationContext createServletApplicationContext() {
	// 创建一个web容器
	AnnotationConfigWebApplicationContext servletAppContext = new AnnotationConfigWebApplicationContext();
	// 获取一个配置类
	Class<?>[] configClasses = getServletConfigClasses();
	if (!ObjectUtils.isEmpty(configClasses)) {
		// 注册获取到的配置类,相当于注册配置类里面的组件
		servletAppContext.register(configClasses);
	}
	return servletAppContext;
}

可以看到,在以上方法中首先会创建一个web的IOC容器(即AnnotationConfigWebApplicationContext对象),然后再获取一个配置类,调用的是getServletConfigClasses方法,我们不妨点进该方法里面去看一下,如下所示,发现它是一个抽象方法,需要由子类自己来重写。

protected abstract Class<?>[] getServletConfigClasses();

获取到配置类之后,最终会将其注册进去。
至此,我们就算分析完了AbstractAnnotationConfigDispatcherServletInitializer抽象类了。

总结

在这里插入图片描述
如果我们想以注解方式(也就是配置类的方式)来整合SpringMVC,只需要自己来继承AbstractAnnotationConfigDispatcherServletInitializer这个抽象类就行了。
继承它之后,它里面会给我们预留一些抽象方法,例如getServletConfigClasses、getRootConfigClasses以及getServletMappings等抽象方法,我们只须重写这些抽象方法即可,这样就能指定DispatcherServlet的配置信息了,随即,DispatcherServlet就会被自动地注册到ServletContext对象中。
最后我们来看下Spring官方文档。
在这里插入图片描述
可以看到Spring官方也推荐使用父子容器的概念,分为根容器和web容器:

  • web容器:也即子容器,只来扫描controller控制层组件(一般不包含核心业务逻辑,只有数据校验和视图渲染等工作)与视图解析器等等。
  • 根容器:扫描业务逻辑核心组件,包括不同的数据源等待。
    继续往下看1.1.1. Context Hierarchy这一小节中的内容,发现跟我们分析的一样,要想以注解方式(也可以说是以配置类的方式)来整合Spring MVC,那么只需要我们自己来继承AbstractAnnotationConfigDispatcherServletInitializer这个抽象类就行了,这样,web容器启动的时候就能处理我们实现的这个类的内容了。
    在这里插入图片描述

参考

Spring注解驱动开发第53讲——Servlet 3.0与Spring MVC的整合分析

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。

文章由极客之音整理,本文链接:https://www.bmabk.com/index.php/post/99966.html

(0)
小半的头像小半

相关推荐

极客之音——专业性很强的中文编程技术网站,欢迎收藏到浏览器,订阅我们!