本文共 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 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 如侵犯您的版权,请留言回复原文章的地址,我们会给您删除此文章,给您带来不便请您谅解!