概述
过去比较流行的 SSH 技术架构,也就是 Struts + Spring + Hibernate 技术组合,它们是 Web 应用开发中最常用的技术架构之一。这个技术架构是以 Struts 作为 Web 框架来帮助应用构建 UI,Spring 作为应用层平台,Hibernate 作为 O/R 映射的数据持久化层实现。
虽然 SSH 组合中的 Web 层框架是由 Struts 来完成的,但 Spring 自己也带有 MVC 框架为用户开发 Web 应用提供支持,也就是 Spring MVC 模块。为什么叫 MVC 呢?因为它的基础就是 MVC 模式,这个模式的主要特点是:分离了模型、视图、控制器三种角色,将业务处理从 UI 设计中独立出来,封装到模型和控制器中,使得他们相互之间解耦,可以独立扩展而不需彼此依赖。
在使用 Spring MVC 的时候,需要在 web.xml 中配置 DispatcherServlet,这个 DispatcherServlet 可以看成是一个前端控制器的具体实现,还需要在 Bean 定义中配置 Web 请求和 Controller 的对应关系,以及各种视图的展现方式。在具体使用 Controller 的时候,会看到 ModelAndView 数据的生成,还会把 ModelAndView 数据交给相应的 View(视图)来进行呈现。
本章会对 Spring MVC 的设计进行详细的分析,主要分为两个部分:一个部分侧重于 Spring 的 IoC 容器是怎样在 Web 应用环境中发挥作用的,是如何与 Spring MVC、Struts 等框架进行集成的?另一个部分着重于分析 Spring MVC 框架的实现原理。
Web 环境中的 Spring MVC
在 Web 环境中,Spring MVC 是建立在 IoC 容器基础上的,Spring IoC 是一个独立的模块,并不是直接在 Web 容器中发挥作用的。如果要在 Web 环境中使用 IoC 容器,就需要在 Web 容器启动之后,把 IoC 容器载入进来并初始化。这样,才能建立起 MVC 框架的运行机制。
下面以 Tomcat 作为 Web 容器的例子进行分析:
<servlet>
<servlet-name>rootServlet</servlet-name>
<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
<init-param>
<param-name>contextConfigLocation</param-name>
<param-value>classpath:spring-mvc.xml</param-value>
</init-param>
</servlet>
<servlet-mapping>
<servlet-name>rootServlet</servlet-name>
<url-pattern>/</url-pattern>
</servlet-mapping>
<context-param>
<param-name>contextConfigLocation</param-name>
<param-value>classpath:applicationContext.xml</param-value>
</context-param>
<listener>
<listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
</listener>
在 Tomcat 中,web.xml 是应用部署描述文件,首先定义了一个 Servlet 对象,他是 Spring MVC 的 DispatcherServlet,这个 DispatcherServlet 起着分发请求的作用。在 web.xml 中还定义了 DispatcherServlet 对应的 URL 映射范围。
context-param 参数用于指定 Spring IoC 容器用于读取 bean 定义的 xml 配置文件。
最后,作为 Spring MVC 的启动类,ContextLoaderListener 被定义为一个监听器,这个监听器与 Web 服务器的生命周期相关,负责完成 IoC 容器在 Web 环境中的启动、销毁工作。
总结一下,DispatcherServlet 和 ContextLoaderListener 提供了在 Web 容器中对 Spring 的接口,IoC 容器是通过 ContextLoaderListener 的初始化来建立的,在建立 IoC 容器之后,DispatcherServlet 便负责分发 Web 请求。
上下文在 Web 容器中的启动
ContextLoaderListener 类实现了 ServletContextListener 接口,ServletContextListener 是 servlet api 中的接口,提供了与 Servlet 生命周期结合的回调,比如 contextInitialized 和 contextDestroyed 方法。
由 ContextLoaderListener 启动的是根上下文,还有一个与 MVC 相关的子上下文,保存控制器 DispatcherServlet 需要的 MVC 对象。根上下文的初始化过程如下图所示:
在 Spring 里默认创建的上下文是 XmlWebApplicationContext,该类继承了 WebApplicationContext,在其基础上增加了对 Web 环境和 xml 配置定义的处理。
Spring MVC 的设计与实现
Spring MVC 的核心便是 DispatcherServlet,DispatcherServlet 实现的是 J2EE 核心模式中的前端控制器模式,作为一个前端控制器,所有的 Web 请求都需要通过它来进行处理,进行转发、匹配、数据处理后,转由页面进行展现。
- 在 Spring MVC 中,对于不同 Web 请求的映射需求,提供了不同的 HandlerMapping 实现,可以让应用开发选取不同的映射策略。
- Spring MVC 还提供了各种视图实现,比如常用的 JSP 视图、Excel 视图、PDF 视图、Json 视图,为应用开发提供了丰富的视图选择。
- 除此之外,还提供了拦截器,允许应用对 Web 请求进行拦截,定制一些前置处理和后置处理。
DispatcherServlet 的启动和初始化
前面提到,Spring MVC 里有一个子 Context,这个 Context 就是由 DispatcherServlet 来初始化的,DispatcherServlet 类关系图如上所示,其本质上是一个 HttpServlet,具体的初始化流程如下图所示:
MVC 处理 HTTP 分发请求
前面分析了 DispatcherServlet 的初始化过程,下面再讲一下 HTTP 请求的分发处理。HTTP 请求处理的入口一般是 HttpServlet 的 doPost、doGet 等方法,这些方法的实现会调用 DispatcherServlet.doService、doDispatch 方法,主要的请求处理操作都是在 doDispatch 里做的:
protected void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception {
HttpServletRequest processedRequest = request;
HandlerExecutionChain mappedHandler = null;
boolean multipartRequestParsed = false;
WebAsyncManager asyncManager = WebAsyncUtils.getAsyncManager(request);
try {
ModelAndView mv = null;
Exception dispatchException = null;
try {
processedRequest = checkMultipart(request);
multipartRequestParsed = (processedRequest != request);
// 获取 HTTP 请求对应的处理器 Handler 信息
mappedHandler = getHandler(processedRequest);
if (mappedHandler == null || mappedHandler.getHandler() == null) {
noHandlerFound(processedRequest, response);
return;
}
// Determine handler adapter for the current request.
HandlerAdapter ha = getHandlerAdapter(mappedHandler.getHandler());
// Process last-modified header, if supported by the handler.
String method = request.getMethod();
boolean isGet = "GET".equals(method);
if (isGet || "HEAD".equals(method)) {
long lastModified = ha.getLastModified(request, mappedHandler.getHandler());
if (logger.isDebugEnabled()) {
logger.debug("Last-Modified value for [" + getRequestUri(request) + "] is: " + lastModified);
}
if (new ServletWebRequest(request, response).checkNotModified(lastModified) && isGet) {
return;
}
}
// 拦截器前置处理
if (!mappedHandler.applyPreHandle(processedRequest, response)) {
return;
}
// 执行请求处理方法
mv = ha.handle(processedRequest, response, mappedHandler.getHandler());
if (asyncManager.isConcurrentHandlingStarted()) {
return;
}
applyDefaultViewName(processedRequest, mv);
// 拦截器后处理
mappedHandler.applyPostHandle(processedRequest, response, mv);
}
catch (Exception ex) {
dispatchException = ex;
}
catch (Throwable err) {
// As of 4.3, we're processing Errors thrown from handler methods as well,
// making them available for @ExceptionHandler methods and other scenarios.
dispatchException = new NestedServletException("Handler dispatch failed", err);
}
processDispatchResult(processedRequest, response, mappedHandler, mv, dispatchException);
}
catch (Exception ex) {
triggerAfterCompletion(processedRequest, response, mappedHandler, ex);
}
catch (Throwable err) {
triggerAfterCompletion(processedRequest, response, mappedHandler,
new NestedServletException("Handler processing failed", err));
}
finally {
if (asyncManager.isConcurrentHandlingStarted()) {
// Instead of postHandle and afterCompletion
if (mappedHandler != null) {
mappedHandler.applyAfterConcurrentHandlingStarted(processedRequest, response);
}
}
else {
// Clean up any resources used by a multipart request.
if (multipartRequestParsed) {
cleanupMultipart(processedRequest);
}
}
}
}
doDispatch 方法的主要内容是:
- 先获取处理器相关的 HandlerExecutionChain
- 执行前置拦截器
- 执行请求处理方法得到 ModelAndView
- 渲染视图
- 执行后置拦截器。
HandlerExecutionChain 主要是通过 HandlerMapping 接口的 getHandler 方法来获取,输入 HTTP 请求信息,返回可以处理该请求的 HandlerExecutionChain。HandlerExecutionChain 包含了真正的处理类 handler 和拦截器 interceptorList。handler 对象实际就是 HTTP 请求对应的 Controller。
public interface HandlerMapping {
HandlerExecutionChain getHandler(HttpServletRequest request) throws Exception;
}
public class HandlerExecutionChain {
private final Object handler;
private HandlerInterceptor[] interceptors;
private List<HandlerInterceptor> interceptorList;
}
在 DispatcherServlet 初始化完成后,在上下文环境中已定义的所有 HandlerMapping 都已经被加载到一个集合 List 中,每一个 HandlerMapping 都对应了一系列从 URL 到 Controller 的映射关系。
Spring MVC 提供了一系列的 HandlerMapping 实现,如下图所示:
在 SimpleUrlHandlerMapping 实现中,维护了一个反映映射关系的 handlerMap,当需要匹配 HTTP 请求时,就来 handlerMap 中查询得到对应的 HandlerExecutionChain。这个 handlerMap 是何时配置好的呢?是在容器对 Bean 进行依赖注入时,通过 Bean 的 postProcessor 来完成的。
public final HandlerExecutionChain getHandler(HttpServletRequest request) throws Exception {
Object handler = getHandlerInternal(request);
if (handler == null) {
handler = getDefaultHandler();
}
if (handler == null) {
return null;
}
// Bean name or resolved handler?
if (handler instanceof String) {
String handlerName = (String) handler;
handler = getApplicationContext().getBean(handlerName);
}
HandlerExecutionChain executionChain = getHandlerExecutionChain(handler, request);
if (CorsUtils.isCorsRequest(request)) {
CorsConfiguration globalConfig = this.globalCorsConfigSource.getCorsConfiguration(request);
CorsConfiguration handlerConfig = getCorsConfiguration(handler, request);
CorsConfiguration config = (globalConfig != null ? globalConfig.combine(handlerConfig) : handlerConfig);
executionChain = getCorsHandlerExecutionChain(request, executionChain, config);
}
return executionChain;
}
protected HandlerExecutionChain getHandlerExecutionChain(Object handler, HttpServletRequest request) {
HandlerExecutionChain chain = (handler instanceof HandlerExecutionChain ?
(HandlerExecutionChain) handler : new HandlerExecutionChain(handler));
String lookupPath = this.urlPathHelper.getLookupPathForRequest(request);
for (HandlerInterceptor interceptor : this.adaptedInterceptors) {
if (interceptor instanceof MappedInterceptor) {
MappedInterceptor mappedInterceptor = (MappedInterceptor) interceptor;
if (mappedInterceptor.matches(lookupPath, this.pathMatcher)) {
chain.addInterceptor(mappedInterceptor.getInterceptor());
}
} else {
chain.addInterceptor(interceptor);
}
}
return chain;
}
Spring MVC 视图的呈现
前面分析了 Spring MVC 中 M(Mode) 和 C(Controller) 相关的实现,其中的 M 对应 ModelAndView 的生成,C 对应 DispatcherServlet 和与用户业务逻辑有关的 handler 实现。在 Spring MVC 框架中,DispatcherServlet 起到了非常核心的作用,是整个 MVC 框架的调度枢纽。对于下面关系的视图呈现功能,它的调用入口在 DispatcherServlet 中的 render 方法里。
protected void render(ModelAndView mv, HttpServletRequest request, HttpServletResponse response) throws Exception {
// Determine locale for request and apply it to the response.
Locale locale = this.localeResolver.resolveLocale(request);
response.setLocale(locale);
View view;
if (mv.isReference()) {
// We need to resolve the view name.
view = resolveViewName(mv.getViewName(), mv.getModelInternal(), locale, request);
if (view == null) {
throw new ServletException("Could not resolve view with name '" + mv.getViewName() +
"' in servlet with name '" + getServletName() + "'");
}
}
else {
// No need to lookup: the ModelAndView object contains the actual View object.
view = mv.getView();
if (view == null) {
throw new ServletException("ModelAndView [" + mv + "] neither contains a view name nor a " +
"View object in servlet with name '" + getServletName() + "'");
}
}
// Delegate to the View object for rendering.
if (logger.isDebugEnabled()) {
logger.debug("Rendering view [" + view + "] in DispatcherServlet with name '" + getServletName() + "'");
}
try {
if (mv.getStatus() != null) {
response.setStatus(mv.getStatus().value());
}
view.render(mv.getModelInternal(), request, response);
}
catch (Exception ex) {
if (logger.isDebugEnabled()) {
logger.debug("Error rendering view [" + view + "] in DispatcherServlet with name '" +
getServletName() + "'", ex);
}
throw ex;
}
}
从代码中可以看到,视图的呈现过程是,在 ModelAndView 中寻找视图对象的逻辑名,如果已经设置了视图对象的名称,就对这个名称进行解析,从而得到实际需要使用的视图对象。还有一种可能是,ModelAndView 中已经有了视图对象,就可以直接使用。得到视图对象后,直接调用其 render 方法来完成数据的显示过程。
下面我们看一下,如何通过解析视图的逻辑名得到视图对象:
protected View resolveViewName(String viewName, Map<String, Object> model, Locale locale,
HttpServletRequest request) throws Exception {
for (ViewResolver viewResolver : this.viewResolvers) {
View view = viewResolver.resolveViewName(viewName, locale);
if (view != null) {
return view;
}
}
return null;
}
ViewResolver 的解析过程可以参考常见的 BeanNameViewResolver:首先取得当前的 IoC 容器,然后判断容器中是否有指定名称的 Bean,如果有,则通过 getBean 方法去取。
public View resolveViewName(String viewName, Locale locale) throws BeansException {
ApplicationContext context = getApplicationContext();
if (!context.containsBean(viewName)) {
return null;
}
if (!context.isTypeMatch(viewName, View.class)) {
return null;
}
return context.getBean(viewName, View.class);
}
针对不同类型的视图,Spring MVC 提供了不同的实现。Spring 视图的继承体系如下图所示:
下面我们以最常见的 JSP 视图为例,看一下其实现原理。在 Spring MVC 中使用 JstlView 来处理 JSP 页面的视图呈现,JstlView 没有实现 render 方法,使用的 render 方法是在基类 AbstractView 中实现的。
public void render(Map<String, ?> model, HttpServletRequest request, HttpServletResponse response) throws Exception {
// 所有相关信息收集到 mergedModel
Map<String, Object> mergedModel = createMergedOutputModel(model, request, response);
// 设置响应 header
prepareResponse(request, response);
// 展现数据到视图
renderMergedOutputModel(mergedModel, getRequestToExpose(request), response);
}
基类的 render 主要负责完成一些数据的准备工作,然后调用了 renderMergedOutputModel 方法来展示数据到视图。renderMergedOutputModel 的实现位于 InternalResourceView 类:
protected void renderMergedOutputModel(
Map<String, Object> model, HttpServletRequest request, HttpServletResponse response) throws Exception {
// 将 model 里的数据写到 request 的 attributes 里
exposeModelAsRequestAttributes(model, request);
// 将一些辅助信息写到 request attributes 里
exposeHelpers(request);
// 获取已经定义好的内部资源路径
String dispatcherPath = prepareForRendering(request, response);
// 找到对应的请求处理器
RequestDispatcher rd = getRequestDispatcher(request, dispatcherPath);
// If already included or response already committed, perform include, else forward.
if (useInclude(request, response)) {
response.setContentType(getContentType());
rd.include(request, response);
} else {
rd.forward(request, response);
}
}
RequestDispatcher 是一个接口类,具体实现是由 Tomcat、Jetty 等完成的,做的事儿就是把 JSP 页面处理好写到 response 里。