Spring注解驱动开发第53讲——Servlet 3.0与Spring MVC的整合分析
发布日期:2021-06-30 17:56:36 浏览次数:3 分类:技术文章

本文共 11826 字,大约阅读时间需要 39 分钟。

写在前面

在前两讲中,我们说了一下ServletContainerInitializer机制以及如何利用ServletContext向web容器中注册Servlet、Listener以及Filter这三大组件。而在这一讲中,我们就来详细分析下Servlet 3.0是如何利用ServletContainerInitializer机制来整合Spring MVC的。

注意,这儿还只是来研究分析整合Spring MVC的底层,并没有开始正式整合Spring MVC哟🙃,这是下一讲的事情。

Servlet 3.0与Spring MVC的整合分析

首先,我们来创建一个新的maven工程,例如springmvc-annotation-liayun,注意其打包方式是war。该maven工程创建好了之后,发现它里面报了一个小小的错误,如下图所示。

在这里插入图片描述

看到没有,在pom.xml文件报了web.xml is missing and <failOnMissingWebXml> is set to true这样一个错误。

为什么会报这个错误呢?这是因为maven插件在编译maven工程的时候,发现工程的webapp目录下没有web.xml文件。

那么,如何解决这个问题呢?我们可以在pom.xml文件中添加如下一个编译war的maven插件。

4.0.0
com.meimeixia
springmvc-annotation-liayun
0.0.1-SNAPSHOT
war
org.apache.maven.plugins
maven-war-plugin
2.4
false

注意<configuration>标签里面的<failOnMissingWebXml>false</failOnMissingWebXml>这个配置哟,它是来告诉maven工程即使没有web.xml文件,也不要报错误哟😄

配置好之后,立马更新一下新建的maven工程,那么错误就会消失不见了,如下图所示。

在这里插入图片描述

不过这个时候我们发现新建的maven工程的编译版本是1.5。那这还玩个毛啊!要知道现在都2021年3月中旬了,Java JDK迭代版本不知道有多高了,但为了稳定,公司里面现在一般使用的都是Java 8,所以在这里我们需要设置编译版本为1.8,即在pom.xml文件中添加maven插件指定编译时候使用的JDK版本。

4.0.0
com.meimeixia
springmvc-annotation-liayun
0.0.1-SNAPSHOT
war
org.apache.maven.plugins
maven-compiler-plugin
3.5.1
1.8
1.8
UTF-8
org.apache.maven.plugins
maven-war-plugin
2.4
false

以上只是调整局部JDK版本。这时,你会发现新建的maven工程依然会报错,如下图所示。

在这里插入图片描述

这个报错你也不用担心,因为有时候加依赖或者插件,maven工程就会报错。这时,就需要时不时地更新一下工程了,更新工程之后,你就会发现工程的编译版本为1.8了,如下图所示。

在这里插入图片描述

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

org.springframework
spring-webmvc
4.3.11.RELEASE

这样,我们就把Spring MVC的webmvc包,以及它所依赖的其他jar包,都一并导入进来了。

再来导入对servlet api的依赖,注意其版本是3.1.0,因为我们现在是在用Servlet 3.0以上的特性。

org.springframework
spring-webmvc
4.3.11.RELEASE
javax.servlet
javax.servlet-api
3.1.0
provided

此外,还要注意<scope>provided</scope>配置哟!由于Tomcat服务器里面也有servlet api,即目标环境已经该jar包了,所以我们在这儿将以上servlet api的scope设置成provided。这样的话,我们的项目在被打成war包时,就不会带上该jar包了,否则就会引起jar包冲突。

依赖都导完以后,接下来我们来讲些什么呢?就讲一下Servlet 3.0整合Spring MVC的底层原理。

我们不妨去Spring官网看看Spring的官方文档,Spring的官网地址是,进去之后,你能看到如下图所示的页面。

在这里插入图片描述

看到网页顶部导航栏中的Projects菜单没有,将鼠标光标放在它上面,你就能看到如下所示的下拉列表了,然后点击其中的Overview选项。

在这里插入图片描述

这时,你应该能看到如下图所示的页面了。

在这里插入图片描述

接着,点击箭头所指的Spring Framework,来到Spring框架的介绍页面,如下图所示,可以看到Spring框架的最新版本是5.3.5

在这里插入图片描述

紧接着,点击箭头所指的LEARN切换到如下所示的选项卡中,点击Reference Doc.超链接,我们就能查看Spring Framework 5.3.5版本的Spring官方文档了。

在这里插入图片描述

Spring官方文档分类是非常详细的,如下图所示,不过这儿我们只关注Web Servlet这一分类,因为它主要是来讲述Spring MVC、WebSocket等等的。

在这里插入图片描述

点击Web Servlet之后,你就应该能看到如下页面了。

在这里插入图片描述

从上图可以看到,在Spring Web MVC这一部分下有这样1.1. DispatcherServlet一个小节,这一小节主要是来讲述配置DispatcherServlet的,我们不妨看一看该小节中的内容,如下图所示。

在这里插入图片描述

你要是没有耐心看那些文字描述,不如集中精力看Java代码。这块的Java代码我得来好好说说,摘抄如下:

// 我们可以编写一个类来实现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); ServletRegistration.Dynamic registration = servletContext.addServlet("app", servlet); registration.setLoadOnStartup(1); // 这儿是来配置DispatcherServlet的映射信息的 registration.addMapping("/app/*"); }}

当然了,如果你使用以上这种编码方式来整合Spring MVC,那也不是不可以,只不过我们并不会用这种方式而已。

其实,这种方式类似于我们以前整合Spring MVC时在web.xml文件中写的如下配置,这你应该就很熟悉了吧!

org.springframework.web.context.ContextLoaderListener
contextConfigLocation
classpath:spring/applicationContext-*.xml
taotao-search-web
org.springframework.web.servlet.DispatcherServlet
contextConfigLocation
classpath:spring/springmvc.xml
1
taotao-search-web
*.html

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

既然我们不会用上述这种方式,那么得用哪种方式呢?不妨展开我们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接口。

在这里插入图片描述

它里面也只有一个onStartup方法,所以我们重点来看该方法的具体实现。我也只是粗浅地来说一下,如果有说的不对的地方,还请多多指正😁。

似乎说的是,Servlet容器在启动我们Spring应用之后,会传入一个我们感兴趣的类型的集合,然后在onStartup方法中拿到之后就会来挨个遍历,如果遍历出来的我们感兴趣的类型不是接口,也不是抽象类,但是WebApplicationInitializer接口旗下的,那么就会创建该类型的一个实例,并将其存储到名为initializers的LinkedList<WebApplicationInitializer>集合中。

也可以这样说,我们Spring的应用一启动就会加载感兴趣的WebApplicationInitializer接口旗下的所有组件,并且为这些WebApplicationInitializer组件创建对象,当然前提是这些组件即不是接口,也不是抽象类。

接下来,我们就来到咱们感兴趣的WebApplicationInitializer接口中,并查看该接口的继承树(快捷键Ctrl + T),发现它下面有三个抽象类,如下图所示。

在这里插入图片描述

因此,接下来,我们就得来好好研究一下以上这三个抽象类了。

第一层抽象类,即AbstractContextLoaderInitializer

我们先来研究WebApplicationInitializer接口下面的第一层抽象类,即AbstractContextLoaderInitializer。不妨点进该抽象类里面去看一看,如下图所示,我们主要来看其onStartUp方法。

在这里插入图片描述

发现在该方法中调用了一个registerContextLoaderListener方法,见名思意,应该是来注册ContextLoaderListener的。

我们继续点进registerContextLoaderListener方法里面去看一看,发现它里面调用了一个createRootApplicationContext方法,该方法是来创建根容器的,而且该方法是一个抽象方法,需要子类自己去实现。然后,根据创建的根容器创建上下文加载监听器(即ContextLoaderListener),接着,向ServletContext中注册这个监听器。

至此,以上这个抽象类,我们算是大概地分析完了。

接下来,我们来研究一下它下面的子类,即AbstractDispatcherServletInitializer,从名字上我们应该能知道它就是一个DispatcherServlet(即Spring MVC的前端控制器)的初始化器。

第二层抽象类,即AbstractDispatcherServletInitializer

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

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

然后,继续点进registerDispatcherServlet方法里面去看一看,看看究竟是怎么向ServletContext中注册DispatcherServlet的,如下所示。

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,哎呦,你要是不信的话,那不妨点进createDispatcherServlet方法里面去看一看,如下所示,是不是在这儿new了一个DispatcherServlet啊?

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

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

至此,我们就算分析完了AbstractDispatcherServletInitializer抽象类了。

接下来,我们来研究一下它下面的子类,即AbstractAnnotationConfigDispatcherServletInitializer,从名字上我们应该能知道它就是一个注解方式配置的DispatcherServlet初始化器,它是我们本讲研究的重点。

第三层抽象类,即AbstractAnnotationConfigDispatcherServletInitializer

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

@Overrideprotected 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方法。

@Overrideprotected 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抽象类了。

总结

如果我们想以注解方式(也可以说是以配置类的方式)来整合Spring MVC,即再也不要在web.xml文件中进行配置,那么我们只需要自己来继承AbstractAnnotationConfigDispatcherServletInitializer这个抽象类就行了。继承它之后,它里面会给我们预留一些抽象方法,例如getServletConfigClasses、getRootConfigClasses以及getServletMappings等抽象方法,我们只须重写这些抽象方法即可,这样就能指定DispatcherServlet的配置信息了,随即,DispatcherServlet就会被自动地注册到ServletContext对象中。

至此,Servlet 3.0整合Spring MVC的底层原理,我们就算是分析清楚了。下一讲,我们就来正式开始Servlet 3.0与Spring MVC的整合。

最后,要不我们再来看一下Spring的官方文档吧!在1.1.1. Context Hierarchy这一小节中,我们在最显眼的位置可以看到一张图,如下所示。

在这里插入图片描述

可以看到Spring官方也推荐使用父子容器的概念,分为根容器和web容器:

  • web容器:也即子容器,只来扫描controller控制层组件(一般不包含核心业务逻辑,只有数据校验和视图渲染等工作)与视图解析器等等
  • 根容器:扫描业务逻辑核心组件,包括不同的数据源等等

继续往下看1.1.1. Context Hierarchy这一小节中的内容,发现跟我们分析的一样,要想以注解方式(也可以说是以配置类的方式)来整合Spring MVC,那么只需要我们自己来继承AbstractAnnotationConfigDispatcherServletInitializer这个抽象类就行了,这样,web容器启动的时候就能处理我们实现的这个类的内容了。

在这里插入图片描述

我是李阿昀,祝你幸福,好文记得点赞与评论哟😋

转载地址:https://liayun.blog.csdn.net/article/details/114915111 如侵犯您的版权,请留言回复原文章的地址,我们会给您删除此文章,给您带来不便请您谅解!

上一篇:Spring注解驱动开发第54讲——Servlet 3.0整合Spring MVC
下一篇:Spring注解驱动开发第52讲——使用ServletContext注册web三大组件

发表评论

最新留言

初次前来,多多关照!
[***.217.46.12]2024年04月30日 20时17分20秒