[Spring MVC]Spring MVC与Servlet标准
Tomcat容器
image.pngspring整合Tomcat经常看到的web.xml
<web-app>
<listener>
<listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
</listener>
<context-param>
<param-name>contextConfigLocation</param-name>
<param-value>/WEB-INF/app-context.xml</param-value>
</context-param>
<servlet>
<servlet-name>app</servlet-name>
<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
<init-param>
<param-name>contextConfigLocation</param-name>
<param-value></param-value>
</init-param>
<load-on-startup>1</load-on-startup>
</servlet>
<servlet-mapping>
<servlet-name>app</servlet-name>
<url-pattern>/app/*</url-pattern>
</servlet-mapping>
</web-app>
Servler3.0后,提供了注解+SPI的机制来配置servlet.
ServletContainerInitializer
允许库/运行时被通知 Web 应用程序的启动阶段并执行任何必需的 servlet、过滤器和侦听器的编程注册以响应它的接口。
该接口的实现可以使用HandlesTypes进行注释,以便(在它们的onStartup方法中)接收实现、扩展或已使用注释指定的类类型进行注释的应用程序类集。
如果此接口的实现不使用HandlesTypes批注,或者没有任何应用程序类与批注指定的类匹配,则容器必须将空类集传递给onStartup 。
在检查应用程序的类以查看它们是否与ServletContainerInitializer的HandlesTypes注释指定的任何条件匹配时,如果缺少任何应用程序的可选 JAR 文件,容器可能会遇到类加载问题。 因为容器无法决定这些类型的类加载失败是否会阻止应用程序正常工作,所以它必须忽略它们,同时提供一个配置选项来记录它们。
此接口的实现必须由位于META-INF/services目录内的 JAR 文件资源声明,并以此接口的完全限定类名命名,并将使用运行时的服务提供者查找机制或容器特定机制发现在语义上等同于它。 在任一情况下,必须忽略从绝对排序中排除的 Web 片段 JAR 文件中的ServletContainerInitializer服务,并且发现这些服务的顺序必须遵循应用程序的类加载委托模型。
ServletContainerInitializer 是 Servlet 3.0 新增的一个接口,主要用于在容器启动阶段通过编程风格注册Filter, Servlet以及Listener,以取代通过web.xml配置注册.
Tomcat启动的时候会通过JAR API来发现实现这类接口的类进行配置加载
WebApplicationInitializer和SpringServletContainerInitializer
- org.springframework.web.SpringServletContainerInitializer#onStartup
@Override
public void onStartup(@Nullable Set<Class<?>> webAppInitializerClasses, ServletContext servletContext)
throws ServletException {
List<WebApplicationInitializer> initializers = new LinkedList<>();
if (webAppInitializerClasses != null) {
for (Class<?> waiClass : webAppInitializerClasses) {
// Be defensive: Some servlet containers provide us with invalid classes,
// no matter what @HandlesTypes says...
if (!waiClass.isInterface() && !Modifier.isAbstract(waiClass.getModifiers()) &&
WebApplicationInitializer.class.isAssignableFrom(waiClass)) {
try {
initializers.add((WebApplicationInitializer)
ReflectionUtils.accessibleConstructor(waiClass).newInstance());
}
catch (Throwable ex) {
throw new ServletException("Failed to instantiate WebApplicationInitializer class", ex);
}
}
}
}
if (initializers.isEmpty()) {
servletContext.log("No Spring WebApplicationInitializer types detected on classpath");
return;
}
servletContext.log(initializers.size() + " Spring WebApplicationInitializers detected on classpath");
AnnotationAwareOrderComparator.sort(initializers);
// 激活WebApplicationInitializer#onStartup
for (WebApplicationInitializer initializer : initializers) {
initializer.onStartup(servletContext);
}
}
SpringServletContainerInitializer实现了ServletContainerInitializer
,在其onStartup
方法中,通过@HandlesTypes(WebApplicationInitializer.class)
注入WebApplicationInitializer
到Tomcat容器.激活WebApplicationInitializer#onStartup
.所以实现了WebApplicationInitializer
的类都会被加载.
WebApplicationInitializer家族成员
image.pngServlet WebApplicationContext 和 Root WebApplicationContext
image.png- org.springframework.web.servlet.support.AbstractAnnotationConfigDispatcherServletInitializer#createServletApplicationContext
protected WebApplicationContext createServletApplicationContext() {
AnnotationConfigWebApplicationContext context = new AnnotationConfigWebApplicationContext();
// 获取配置类并且注入到容器中
Class<?>[] configClasses = getServletConfigClasses();
if (!ObjectUtils.isEmpty(configClasses)) {
context.register(configClasses);
}
return context;
}
- org.springframework.web.servlet.support.AbstractAnnotationConfigDispatcherServletInitializer#createRootApplicationContext
protected WebApplicationContext createRootApplicationContext() {
Class<?>[] configClasses = getRootConfigClasses();
if (!ObjectUtils.isEmpty(configClasses)) {
AnnotationConfigWebApplicationContext context = new AnnotationConfigWebApplicationContext();
context.register(configClasses);
return context;
}
else {
return null;
}
}
Servlet WebApplicationContext:
将Controller、ViewResolver、HandleMapping相关的类扫描进Servlet的web容器上下文中.
Root WebApplicationContext:
将Service、Repositories.这类对象交由Root WebApplicationContext管理.
总体来说,是做了业务对象的分层.
Spring MVC的大致流程
- 建立请求(RequestMapping)和Controller方法的映射集合的流程
- 根据请求(RequestURI)查找对应的Controller方法的流程
- 请求参数绑定到方法形参,执行方法处理请求,渲染视图
流程图
image.png
- 请求先经过浏览器访问tomcat容器,tomcat委派给线程池响应当前请求调用servlet进行处理。
Spring MVC
通过DispatcherServlet
拦截了所有的请求,通过当前请求的路径与IoC提前解析好的HanlderMapping
进行对比,进而定位到Controller的method进行响应.- method经过响应后,会返回一个
ModelAndView
实例.DispatcherServlet
会委派给ViewResolver
进行视图解析.- 返回
View
,放入响应流中响应给浏览器.
注解配置的容器入口-AbstractDispatcherServletInitializer
注解的配置会优于XML的配置执行.SpringServletContainerInitializer
实现了ServletContainerInitializer
接口,通过@HandlesTypes(WebApplicationInitializer.class)
的SPI发现机制,可以将实现该接口的Class对象传递到onStartup
中.
- org.springframework.web.SpringServletContainerInitializer#onStartup
@Override
public void onStartup(@Nullable Set<Class<?>> webAppInitializerClasses, ServletContext servletContext)
throws ServletException {
List<WebApplicationInitializer> initializers = new LinkedList<>();
if (webAppInitializerClasses != null) {
for (Class<?> waiClass : webAppInitializerClasses) {
// Be defensive: Some servlet containers provide us with invalid classes,
// no matter what @HandlesTypes says...
if (!waiClass.isInterface() && !Modifier.isAbstract(waiClass.getModifiers()) &&
WebApplicationInitializer.class.isAssignableFrom(waiClass)) {
try {
initializers.add((WebApplicationInitializer)
ReflectionUtils.accessibleConstructor(waiClass).newInstance());
}
catch (Throwable ex) {
throw new ServletException("Failed to instantiate WebApplicationInitializer class", ex);
}
}
}
}
if (initializers.isEmpty()) {
servletContext.log("No Spring WebApplicationInitializer types detected on classpath");
return;
}
servletContext.log(initializers.size() + " Spring WebApplicationInitializers detected on classpath");
AnnotationAwareOrderComparator.sort(initializers);
// 激活WebApplicationInitializer#onStartup
for (WebApplicationInitializer initializer : initializers) {
initializer.onStartup(servletContext);
}
}
image.png
在对这些
initializers
进行排序后,最后就会依次激活所有WebApplicationInitializer
的onStartup
方法.
- org.springframework.web.servlet.support.AbstractDispatcherServletInitializer#onStartup
@Override
public void onStartup(ServletContext servletContext) throws ServletException {
super.onStartup(servletContext);
registerDispatcherServlet(servletContext);
}
- org.springframework.web.servlet.support.AbstractDispatcherServletInitializer#registerDispatcherServlet
protected void registerDispatcherServlet(ServletContext servletContext) {
String servletName = getServletName();
Assert.hasLength(servletName, "getServletName() must not return null or empty");
// 创建spring web IoC 容器
WebApplicationContext servletAppContext = createServletApplicationContext();
Assert.notNull(servletAppContext, "createServletApplicationContext() must not return null");
// 创建dispatcherServlet实例
FrameworkServlet dispatcherServlet = createDispatcherServlet(servletAppContext);
Assert.notNull(dispatcherServlet, "createDispatcherServlet(WebApplicationContext) must not return null");
dispatcherServlet.setContextInitializers(getServletApplicationContextInitializers());
ServletRegistration.Dynamic registration = servletContext.addServlet(servletName, dispatcherServlet);
if (registration == null) {
throw new IllegalStateException("Failed to register servlet with name '" + servletName + "'. " +
"Check if there is another servlet registered under the same name.");
}
registration.setLoadOnStartup(1);
// 路径映射
registration.addMapping(getServletMappings());
registration.setAsyncSupported(isAsyncSupported());
// 请求拦截器,可以在此处控制编码
Filter[] filters = getServletFilters();
if (!ObjectUtils.isEmpty(filters)) {
for (Filter filter : filters) {
registerServletFilter(servletContext, filter);
}
}
customizeRegistration(registration);
}
这是一个模板方法,其中
createServletApplicationContext
可以通过子类去实现.支持注解形式的实现类是AbstractAnnotationConfigDispatcherServletInitializer
XML配置的容器入口-ContextLoaderListener
ContextLoaderListener
实现了Servlet的ServletContextListener
,Tomcat在启动的时候,会优先加载Servlet的监听器组件,以确保在Servlet被创建之前,调用监听器的contextInitialized
方法,Spring正是在ContextLoaderListener
中进行了initWebApplicationContext
的调用,即启动的时候,进行容器的refresh操作.
- org.springframework.web.context.ContextLoaderListener#contextInitialized
@Override
public void contextInitialized(ServletContextEvent event) {
initWebApplicationContext(event.getServletContext());
}
- org.springframework.web.context.ContextLoader#initWebApplicationContext
public WebApplicationContext initWebApplicationContext(ServletContext servletContext) {
// 从servletContext中查找,是否存在以WebApplicationContext的ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE为key的值
if (servletContext.getAttribute(WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE) != null) {
throw new IllegalStateException(
"Cannot initialize context because there is already a root application context present - " +
"check whether you have multiple ContextLoader* definitions in your web.xml!");
}
servletContext.log("Initializing Spring root WebApplicationContext");
Log logger = LogFactory.getLog(ContextLoader.class);
if (logger.isInfoEnabled()) {
logger.info("Root WebApplicationContext: initialization started");
}
long startTime = System.currentTimeMillis();
try {
// Store context in local instance variable, to guarantee that
// it is available on ServletContext shutdown.
// 在AbstractContextLoaderInitializer#onStartup
// ContextLoaderListener listener = new ContextLoaderListener(rootAppContext);
// 进行了初始化
if (this.context == null) {
this.context = createWebApplicationContext(servletContext);
}
if (this.context instanceof ConfigurableWebApplicationContext) {
ConfigurableWebApplicationContext cwac = (ConfigurableWebApplicationContext) this.context;
if (!cwac.isActive()) {
// The context has not yet been refreshed -> provide services such as
// setting the parent context, setting the application context id, etc
if (cwac.getParent() == null) {
// The context instance was injected without an explicit parent ->
// determine parent for root web application context, if any.
ApplicationContext parent = loadParentContext(servletContext);
cwac.setParent(parent);
}
// 配置并刷新容器
configureAndRefreshWebApplicationContext(cwac, servletContext);
}
}
// 将WebApplicationContext放入servletContext中
// 其key为ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE
servletContext.setAttribute(WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE, this.context);
ClassLoader ccl = Thread.currentThread().getContextClassLoader();
if (ccl == ContextLoader.class.getClassLoader()) {
currentContext = this.context;
}
else if (ccl != null) {
currentContextPerThread.put(ccl, this.context);
}
if (logger.isInfoEnabled()) {
long elapsedTime = System.currentTimeMillis() - startTime;
logger.info("Root WebApplicationContext initialized in " + elapsedTime + " ms");
}
return this.context;
}
catch (RuntimeException | Error ex) {
logger.error("Context initialization failed", ex);
servletContext.setAttribute(WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE, ex);
throw ex;
}
}
- 先看看容器是否被创建出来了,如果没有则调用
createWebApplicationContext
来创建容器.- 容器是否属于
ConfigurableWebApplicationContext
类型,对容器进行强转,然后查看是否处于激活的状态(如果经过了refresh,容器会进入active状态).- 是否存在父容器,如果有进行
setParent
- 配置并refresh容器
又是refresh
Spring会依次建立两个上下文,一个是Root WebApplicationContext
.另一个是WebApplicationContext For Dispatcher-servlet
.无论哪个,最终还是会回到org.springframework.context.support.AbstractApplicationContext#refresh
这个方法中.
- 第一次refresh是Root WebApplicationContext
- 第二次refresh是Servlet WebApplicationContext
这里出现了几个关键的对象:
FrameworkerServlet
、HttpServletBean
.
再从执行的调用栈中看看调用过程:
- org.springframework.web.servlet.HttpServletBean#init
- org.springframework.web.servlet.FrameworkServlet#initServletBean
- org.springframework.web.servlet.FrameworkServlet#initWebApplicationContext
- org.springframework.web.servlet.FrameworkServlet#configureAndRefreshWebApplicationContext
image.png我们再用UML来梳理这些类之间的关系:
可以看到,
DispatcherServlet
继承自FrameworkServlet
,而FrameworkServlet
又继承自HttpServletBean
,最后HttpServletBean
继承了HttpServlet
.
同时,实现了Aware
接口,就可以从容器中获取到环境变量、容器上下文等资源.
这样来看,调用顺序就很好理解了.
HttpServletBean
实现了HttpServlet
接口,就可以重写其init
方法,在init
方法里面调用一个钩子方法initServletBean
(HttpServletBean不负责实现,由子类实现).
FrameworkServlet
重写了initServletBean
方法,着手初始化容器的事项.
注意,这里提到的FrameworkSerlvet
和HttpSerlvetBean
都是抽象类,真正的实例是-DispatcherSerlvet
.
下一章,我们一起来看看
DispatcherSerlvet
是如何初始化其他MVC组件的.