对于控制器的目标方法,无论其返回值是String、View、ModelMap或是ModelAndView,SpringMVC都会在内部将它们封装为一个ModelAndView对象进行返回。 Spring MVC 借助视图解析器(ViewResolver)得到最终的视图对象(View),最终的视图可以是JSP也可是Excell、 JFreeChart等各种表现形式的视图。
1、视图(View)
视图的作用是渲染模型数据,将模型里的数据以某种形式呈现给客户。 为了实现视图模型和具体实现技术的解耦,Spring在org.springframework.web.servlet包中定义了一个高度抽象的View接口。 视图对象由视图解析器负责实例化。由于视图是无状态的,所以他们不会有线程安全的问题。所谓视图是无状态的,是指对于每一个请求,都会创建一个View对象。 JSP是最常见的视图技术。

2、视图解析器(ViewResolver)和视图(View)
- springMVC用于处理视图最重要的两个接口是
ViewResolver
和View
。

所以视图解析器的作用就是通过视图名(处理方法的返回值)生成View对象,所有的视图解析器都必须实现ViewResolver接口。 SpringMVC为逻辑视图名的解析提供了不同的策略,可以在Spring WEB上下文中配置一种或多种解析策略,并指定他们之间的先后顺序。每一种映射策略对应一个具体的视图解析器实现类。程序员可以选择一种视图解析器或混用多种视图解析器。可以通过order属性指定解析器的优先顺序,order越小优先级越高,SpringMVC会按视图解析器顺序的优先顺序对逻辑视图名进行解析,直到解析成功并返回视图对象,否则抛出ServletException异常。 在项目中可以配置InternalResourceViewResolver
作为视图解析器,在springmvc.xml中可以做如下配置:
<!--配置视图解析器-->
<bean id="viewHandler" class="org.springframework.web.servlet.view.InternalResourceViewResolver">
<property name="prefix" value="WEB-INF/pages/"/>
<property name="suffix" value=".jsp"/>
</bean>
3、forward: 和redirect:
一般情况下,控制器方法返回字符串类型的值会被当成逻辑视图名处理,会经过视图解析器拼串,但如果返回的字符串中带forward:或redirect:前缀时,SpringMVC会对它们进行特殊处理:将forward: 和redirect: 当成指示符,其后的字符串作为URL 来处理。示例如下: index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>SpringMVC给页面输出数据</title>
</head>
<body>
<center>
<a href="handler1">handler1</a><br/>
<a href="handler2">handler2</a><br/>
<a href="handler3">handler3</a><br/>
<a href="handler4">handler4</a><br/>
</center>
</body>
</html>
hello.jsp
,在当前项目的根路径下,和index.html同级
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
<title>Hello</title>
</head>
<body>
<center>
<h1>这是hello.jsp</h1>
</center>
</body>
</html>
ViewTestController.java
package com.xzy.Contorller;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
@Controller
public class ViewTestController {
/**
* handler1把请求转发到hello.jsp页面
* @return
*/
@RequestMapping("/handler1")
public String handler1(){
System.out.println("handler1");
return "forward:/hello.jsp";
}
/**
* handler把请求转发给handler1
* @return
*/
@RequestMapping("/handler2")
public String handler2(){
System.out.println("handler2");
return "forward:handler1";
}
/**
* 重定向到hello.jsp
* @return
*/
@RequestMapping("/handler3")
public String handler3(){
System.out.println("handler3");
return "redirect:/hello.jsp";
}
/**
* 重定向到handler3
* @return
*/
@RequestMapping("/handler4")
public String handler4(){
System.out.println("handler4");
return "redirect:handler3";
}
}
测试结果:
4、SpringMVC视图的解析流程(结合源码分析)
- 源码中把任何返回返回值封装为ModelAndView的实现:
protected ModelAndView handleInternal(HttpServletRequest request, HttpServletResponse response, HandlerMethod handlerMethod) throws Exception {
this.checkRequest(request);
ModelAndView mav;
if (this.synchronizeOnSession) {
HttpSession session = request.getSession(false);
if (session != null) {
Object mutex = WebUtils.getSessionMutex(session);
synchronized(mutex) {
mav = this.invokeHandlerMethod(request, response, handlerMethod);
}
} else {
mav = this.invokeHandlerMethod(request, response, handlerMethod);
}
} else {
mav = this.invokeHandlerMethod(request, response, handlerMethod);
}
if(!response.containsHeader("Cache-Control")) {
if (this.getSessionAttributesHandler(handlerMethod).hasSessionAttributes()) { this.applyCacheSeconds(response,this.cacheSecondsForSessionAttributeHandlers);
} else {
this.prepareResponse(response);
}
}
return mav;
}
这里以发出了一个GET请求为例: 首先FrameworkServlet类
会来处理这个GET请求 doGet
protected final void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
this.processRequest(request, response);
}
processRequest
protected final void processRequest(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
//省略.....
try {
//它本类中的这个方法是个抽象方法,实现这个方法的类是DispatcherServlet
this.doService(request, response);
} catch (IOException | ServletException var16) {
failureCause = var16;
throw var16;
} catch (Throwable var17) {
//省略.....var17);
} finally {
//省略.....
}
}
DispatcherServlet 类
doService方法
protected void doService(HttpServletRequest request, HttpServletResponse response) throws Exception {
protected void doService(HttpServletRequest request, HttpServletResponse response) throws Exception {
//省略.....
//给request域中设置了一些东西
try {
//调用doDispatch方法处理
this.doDispatch(request, response);
} finally {
......
}
}
doDispatch方法
protected void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception {
//省略......
this.processDispatchResult(processedRequest, response, mappedHandler, mv, dispatchException);
//省略......
}
processDispatchResult方法
,这个方法就是最终将数据交给页面的方法
private void processDispatchResult(HttpServletRequest request, HttpServletResponse response, @Nullable HandlerExecutionChain mappedHandler, @Nullable ModelAndView mv, @Nullable Exception exception) throws Exception {
boolean errorView = false;
//如果这里出现了异常就处理异常
if (exception != null) {
if (exception instanceof ModelAndViewDefiningException) {
this.logger.debug("ModelAndViewDefiningException encountered", exception);
mv = ((ModelAndViewDefiningException)exception).getModelAndView();
} else {
Object handler = mappedHandler != null ? mappedHandler.getHandler() : null;
//如果自己配置了自定义的HandlerExceptionResolver将会在这个方法里处理
mv = this.processHandlerException(request, response, handler, exception);
errorView = mv != null;
}
}
if (mv != null && !mv.wasCleared()) {
//调用render方法进行视图渲染
this.render(mv, request, response);
if (errorView) {
WebUtils.clearErrorRequestAttributes(request);
}
} else if (this.logger.isTraceEnabled()) {
this.logger.trace("No view rendering, null ModelAndView returned.");
}
//省略......
}
DispatcherServlet 类 的render方法
并没有继承View接口的render,和View接口的render不是一回事,这个render仅仅是为了命名统一而起的一个名字
protected void render(ModelAndView mv, HttpServletRequest request, HttpServletResponse response) throws Exception {
//省略......
//从ModelView中拿到视图名
String viewName = mv.getViewName();
View view;
if (viewName != null) {
//这一步就是得到一个View对象,resolveViewName的实现看下边
view = this.resolveViewName(viewName, mv.getModelInternal(), locale, request);
if (view == null) {
throw new ServletException("Could not resolve view with name '" + mv.getViewName() + "' in servlet with name '" + this.getServletName() + "'");
}
} else {
view = mv.getView();
if (view == null) {
throw new ServletException("ModelAndView [" + mv + "] neither contains a view name nor a View object in servlet with name '" + this.getServletName() + "'");
}
}
//省略......
try {
//省略......
//调用了View接口的render方法,这里实际上调用的是视图在渲染时会把Model传入
view.render(mv.getModelInternal(), request, response);
} catch (Exception var8) {
//省略......
}
}
resolveViewName方法,循环遍历你配置的视图解析器,viewResolvers是进过order排序的,这一步就是ViewResolvers是如何通过视图名产生View对象的关键
protected View resolveViewName(String viewName,Map<String, Object> model, Locale locale, HttpServletRequest request) throws Exception {
//遍历我们配置的视图解析器
for (ViewResolver viewResolver : this.viewResolvers) {
//ViewResolver根据方法的返回值,得到一个View对象,这块又有一个resolveViewName,具体的实现请往下看
View view = viewResolver.resolveViewName(viewName, locale);
if (view != null) {
return view;
}
}
return null;
}
InternalResourceViewResolver
继承了AbstractCachingViewResolver
,resolveViewName方法首先会判断有没有缓存,要是有缓存,它会先去缓存中通过viewName查找是否有View对象的存在,要是没有,它会通过viewName创建一个新的View对象,并将View对象存入缓存中,这样再次遇到同样的视图名的时候就可以直接在缓存中取出View对象了
@Override
public View resolveViewName(String viewName, Locale locale) throws Exception {
//判断有缓存中有没有view对象,有就直接拿来用
if (!isCache()) {
return createView(viewName, locale);
}
else {
Object cacheKey = getCacheKey(viewName, locale);
View view = this.viewAccessCache.get(cacheKey);
if (view == null) {
synchronized (this.viewCreationCache) {
view = this.viewCreationCache.get(cacheKey);
if (view == null) {
//根据方法的返回值创建出View对象
view = createView(viewName, locale);
if (view == null && this.cacheUnresolved) {
view = UNRESOLVED_VIEW;
}
if (view != null) {
this.viewAccessCache.put(cacheKey, view);
this.viewCreationCache.put(cacheKey, view);
if (logger.isTraceEnabled()) {
logger.trace("Cached view [" + cacheKey + "]");
}
}
}
}
}
return (view != UNRESOLVED_VIEW ? view : null);
}
}
createView的实现细节:
protected View createView(String viewName, Locale locale) throws Exception {
if (!this.canHandle(viewName, locale)) {
return null;
} else {
String forwardUrl;
//如果方法得到返回值是以redirect:开始的
if (viewName.startsWith("redirect:")) {
forwardUrl = viewName.substring("redirect:".length());
RedirectView view = new RedirectView(forwardUrl, this.isRedirectContextRelative(), this.isRedirectHttp10Compatible());
String[] hosts = this.getRedirectHosts();
if (hosts != null) {
view.setHosts(hosts);
}
return this.applyLifecycleMethods("redirect:", view);
//如果方法的返回值是以forward:开始的
} else if (viewName.startsWith("forward:")) {
forwardUrl = viewName.substring("forward:".length());
InternalResourceView view = new InternalResourceView(forwardUrl);
return this.applyLifecycleMethods("forward:", view);
} else {
//其他情况的处理,这里又有一个createView,它调用了父类的createView创建了一个默认的View对象
return super.createView(viewName, locale);
}
}
}
以下都是解析视图名的实现细节,感兴趣的可以看一下。
父类AbstractCachingViewResolver类
的createView实现细节:
protected View createView(String viewName, Locale locale) throws Exception {
return loadView(viewName, locale);
}
InternalResourceViewResolver
继承了UrlBasedViewResolver
UrlBasedViewResolver类
中loadView方法的实现:
protected View loadView(String viewName, Locale locale) throws Exception {
AbstractUrlBasedView view = buildView(viewName);
View result = applyLifecycleMethods(viewName, view);
return (view.checkResource(locale) ? result : null);
}
UrlBasedViewResolver的buildView方法会获取一个View对象,这个对象会将视图以什么格式呈现给用户,例如如果是jsp显示呈现给用户的话,那这个view对象就是JstlView,默认的是JstlView。在这个方法中我们看到了getPrefix() + viewName + getSuffix()这样一段代码,这就是对视图路径的一个拼接了,getPrefix()方法获取前缀,也就是我们在配置文件中配置的<property name="prefix" value="/WEB-INF/PAGE/"/>
的value中的值了,getSuffix()方法就是获取后缀值了,也就是我们在配置文件中配置的<property name="suffix" value=".jsp"/>
的value中的值。这样就将将视图的物理路径找到了,并赋值到View的URL属性中去。
protected AbstractUrlBasedView buildView(String viewName) throws Exception {
Class<?> viewClass = this.getViewClass();
Assert.state(viewClass != null, "No view class");
AbstractUrlBasedView view = (AbstractUrlBasedView)BeanUtils.instantiateClass(viewClass);
view.setUrl(this.getPrefix() + viewName + this.getSuffix());
String contentType = this.getContentType();
if (contentType != null) {
view.setContentType(contentType);
}
view.setRequestContextAttribute(this.getRequestContextAttribute());
view.setAttributesMap(this.getAttributesMap());
Boolean exposePathVariables = this.getExposePathVariables();
if (exposePathVariables != null) {
view.setExposePathVariables(exposePathVariables);
}
Boolean exposeContextBeansAsAttributes = this.getExposeContextBeansAsAttributes();
if (exposeContextBeansAsAttributes != null) {
view.setExposeContextBeansAsAttributes(exposeContextBeansAsAttributes);
}
String[] exposedContextBeanNames = this.getExposedContextBeanNames();
if (exposedContextBeanNames != null) {
view.setExposedContextBeanNames(exposedContextBeanNames);
}
return view;
}
就这样我们得到了一个View对象,这个视图的name就是逻辑视图名,因为当将View对象放在缓存的时候,我们可以通过逻辑视图名在缓存中找出View对象。我们在获取到View对象的时候,我们还要将View进行渲染,并呈现给用户。
View是个接口,AbstractView实现了render方法:
public void render(@Nullable Map<String, ?> model, HttpServletRequest request, HttpServletResponse response) throws Exception {
if (this.logger.isDebugEnabled()) {
this.logger.debug("View " + this.formatViewName() + ", model " + (model != null ? model : Collections.emptyMap()) + (this.staticAttributes.isEmpty() ? "" : ", static attributes " + this.staticAttributes));
}
//主要是将一些属性填充到Map中
Map<String, Object> mergedModel = this.createMergedOutputModel(model, request, response);
//对response头进行了一些属性设置
this.prepareResponse(request, response);
//渲染给页面输出的所有model数据
this.renderMergedOutputModel(mergedModel, this.getRequestToExpose(request), response);
}
最后一行的renderMergedOutputModel方法由AbstractView的孙子类InternalResourceView实现InternalResourceView的renderMergedOutputModel方法帮我们获取到视图的物理路径,然后将这段路径传给RequestDispatcher对象,再调用RequestDispatcher的forward方法将页面呈现给用户,这样就走完了视图的解析了。
@Override
protected void renderMergedOutputModel(
Map<String, Object> model, HttpServletRequest request, HttpServletResponse response) throws Exception {
// Expose the model object as request attributes.
exposeModelAsRequestAttributes(model, request);
// Expose helpers as request attributes, if any.
exposeHelpers(request);
// Determine the path for the request dispatcher.
String dispatcherPath = prepareForRendering(request, response);
// Obtain a RequestDispatcher for the target resource (typically a JSP).
RequestDispatcher rd = getRequestDispatcher(request, dispatcherPath);
if (rd == null) {
throw new ServletException("Could not get RequestDispatcher for [" + getUrl() + "]: Check that the corresponding file exists within your web application archive!");
}
// If already included or response already committed, perform include, else forward.
if (useInclude(request, response)) {
response.setContentType(getContentType());
if (logger.isDebugEnabled()) {
logger.debug("Including resource [" + getUrl() + "] in InternalResourceView '" + getBeanName() + "'");
}
rd.include(request, response);
}else {
// Note: The forwarded resource is supposed to determine the content type itself.
if (logger.isDebugEnabled()) {
logger.debug("Forwarding to resource [" + getUrl() + "] in InternalResourceView '" + getBeanName() + "'");
}
//对请求进行转发,至此结束了视图解析解析过程
rd.forward(request, response);
}
}
最后一句话总结: 视图解析器只是为了得到视图对象;视图对象才是真正的转发(将模型数据发在request域中数据)或重定向到页面(视图对象才是真正的渲染视图)。
