SpringMVC解析视图概述
- 不论控制器返回一个
String
、ModelAndView
、View
都会转换为ModelAndView
对象,由视图解析器解析视图,然后,进行页面的跳转 - 视图解析两个重要的接口(
View
、ViewResolver
)view
:视图的作用是渲染模型数据,将模型里的数据以某种形式呈现给客户
public interface View { String RESPONSE_STATUS_ATTRIBUTE = View.class.getName() + ".responseStatus"; String PATH_VARIABLES = View.class.getName() + ".pathVariables"; String SELECTED_CONTENT_TYPE = View.class.getName() + ".selectedContentType"; // 如果预先确定,则返回视图的内容类型。可用于预先检查视图的内容类型,即在实际渲染尝试之前。 default String getContentType() { return null; } // 根据指定的模型渲染视图 void render(@Nullable Map<String, ?> model, HttpServletRequest request, HttpServletResponse response) throws Exception; }
ViewResolver
:Spring MVC 借助视图解析器(ViewResolver
)得到最终的视图对象(View
),最终的视图可以是 JSP ,也可能是 Excel、JFreeChart等各种表现形式的视图
public interface ViewResolver { // 按名称解析给定视图 @Nullable View resolveViewName(String viewName, Locale locale) throws Exception; }
视图常用实现类
视图解析器
- SpringMVC 为逻辑视图名的解析提供了不同的策略,可以在 Spring WEB 上下文中配置一种或多种解析策略,并指定他们之间的先后顺序。每一种映射策略对应一个具体的视图解析器实现类。
- 每个视图解析器都实现了 Ordered 接口并开放出一个 order 属性,可以通过 order 属性指定解析器的优先顺序,order 越小优先级越高。
JstlView
- 若项目中使用了JSTL,则SpringMVC 会自动把视图由
InternalResourceView
转为JstlView
代码如下
大致过程与Java Web原生的国际化过程相同i18n 国际化
- 导包
<!-- https://mvnrepository.com/artifact/javax.servlet/jstl --> <dependency> <groupId>javax.servlet</groupId> <artifactId>jstl</artifactId> <version>1.2</version> </dependency>
- 在resources文件夹中,放入i18n的配置文件
- 在spring-mvc的配置文件中,注册
messageSource
<bean id="messageSource" class="org.springframework.context.support.ResourceBundleMessageSource"> <property name="basename" value="i18n"/> <!-- 支持UTF-8的中文 --> <property name="defaultEncoding" value="UTF-8"/> </bean>
- 编写一个控制器
@RequestMapping("/viewAndViewResolverTest") public String viewAndViewResolverTest() { System.out.println("viewAndViewResolverTest"); return "success"; }
- 与Java Web不同的是,在jsp中无需声明地区以及基础名称basename,可以直接通过message调用,获取配置文件的内容。
<%@ page contentType="text/html;charset=UTF-8" language="java" %> <%@taglib prefix="fmt" uri="http://java.sun.com/jsp/jstl/fmt" %> <html> <head> <title>success</title> </head> <body> <h1> <fmt:message key="username"/> </h1> <h1> <fmt:message key="password"/> </h1> </body> </html>
mvc:view-controller标签
- 若希望直接响应通过 SpringMVC 渲染的页面,可以使用 mvc:view-controller 标签实现(即省去写Controller的代码,直接调用Spring MVC内部的Controller代码)
- 在spring-mvc的配置文件中,加入以下代码(页面为
success.jsp
,url为/success
)<!-- 直接配置响应的页面:无需经过控制器来执行结果 --> <mvc:view-controller path="/success" view-name="success"/> <mvc:annotation-driven/>
注意,必须添加
<mvc:annotation-driven/>
这句话,否则除了该页面以外,所有页面都会失效。
自定义视图
- 自定义视图(需要加入SpringMVC,那么,一定需要实现框架的接口)
简单案例
- 自己写一个view,发送请求之后返回该view
- 首先,定义view
@Component public class HelloView implements View { // 返回内容类型 @Override public String getContentType() { return "text/html;charset=utf-8"; } @Override public void render(Map<String, ?> model, HttpServletRequest request, HttpServletResponse response) throws Exception { response.setContentType("text/html;charset=utf-8"); // 必须设置,否则会出现中文乱码现象 response.getWriter().write("自定义view测试"); // 自定义页面内容 } }
即使存在过滤器
CharacterEncodingFilter
,仍要在响应中设置该返回类型。因为在过滤器中,只设置了编码格式,没有设置响应类型。详见Spring MVC解决中文乱码问题 - 在sring-mvc配置文件中,设置
BeanNameViewResolver
视图解析器。并将优先级设为100(其实多少数字都可以,只要比internalResourceViewResolver
的oder小就行)。
在<bean class="org.springframework.web.servlet.view.BeanNameViewResolver"> <property name="order" value="100"/> </bean> <bean class="org.springframework.web.servlet.view.InternalResourceViewResolver" id="internalResourceViewResolver"> <property name="prefix" value="/WEB-INF/views/"/> <property name="suffix" value=".jsp"/> </bean>
InternalResourceViewResolver
的父类中,将order
设置为private int order = Ordered.LOWEST_PRECEDENCE;
(int LOWEST_PRECEDENCE = 2147483647;
) - 控制器
@RequestMapping("/testView") public String testView(){ System.out.println("testView..."); return "helloView"; //与视图Bean对象的id一致 }
重定向
- 之前都是返回一个字符串,然后视图解析器通过拼接,获取真实资源的位置,并转发到此处。
- 我们也可以让视图解析器解析为重定向的方式。
@RequestMapping("redirectTest") public String redirectTest() { System.out.println("redirectTest"); return "redirect:/index.jsp"; }
使用此方法之后,不会视图解析器不会使用拼接的,而是直接根据url寻找资源。
转发
- 同理,也可以自己写一个转发的地址,不需要视图解析器帮忙拼接。
@RequestMapping("forwardTest") public String forwardTest() { System.out.println("forwardTest"); return "forward:/index.jsp"; }
源码分析
- 在
DispatcherServlet
中,doDispatch()
方法,在接近末尾的地方,有一个processDispatchResult()
方法; processDispatchResult()
方法调用render()
方法渲染;render()
方法中,调用resolveViewName()
通过View
的名字,解析出View
对象;并在末尾,调用View
的rend()
方法,开启重定向或转发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 != null ? this.localeResolver.resolveLocale(request) : request.getLocale()); response.setLocale(locale); View view; String viewName = mv.getViewName(); if (viewName != null) { // 通过viewname解析出view view = resolveViewName(viewName, 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.isTraceEnabled()) { logger.trace("Rendering view [" + view + "] "); } try { if (mv.getStatus() != null) { response.setStatus(mv.getStatus().value()); } // 执行view view.render(mv.getModelInternal(), request, response); } catch (Exception ex) { if (logger.isDebugEnabled()) { logger.debug("Error rendering view [" + view + "]", ex); } throw ex; } }
- 在
resolveViewName()
方法中,遍历所有视图解析器,尝试通过view
的名字,解析出结果。一旦解析出来,就返回view
。@Nullable protected View resolveViewName(String viewName, @Nullable Map<String, Object> model, Locale locale, HttpServletRequest request) throws Exception { if (this.viewResolvers != null) { // 遍历所有视图解析器 for (ViewResolver viewResolver : this.viewResolvers) { View view = viewResolver.resolveViewName(viewName, locale); if (view != null) { return view; } } } return null; }
InternalResourceViewResolver
- 继承关系
- 所有ViewResolver都需要实现resolveViewName()方法
AbstractCachingViewResolver
AbstractCachingViewResolver
首先实现了这个方法,在此其中,最重要的是createView()
方法,用于创造View
;而该方法又调用一个loadView()
的方法(该方法是一个抽象方法)。@Override @Nullable public View resolveViewName(String viewName, Locale locale) throws Exception { if (!isCache()) { // 创造View 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) { // Ask the subclass to create the View object. view = createView(viewName, locale); if (view == null && this.cacheUnresolved) { view = UNRESOLVED_VIEW; } if (view != null && this.cacheFilter.filter(view, viewName, locale)) { this.viewAccessCache.put(cacheKey, view); this.viewCreationCache.put(cacheKey, view); } } } } else { if (logger.isTraceEnabled()) { logger.trace(formatKey(cacheKey) + "served from cache"); } } return (view != UNRESOLVED_VIEW ? view : null); } }
@Nullable protected View createView(String viewName, Locale locale) throws Exception { return loadView(viewName, locale); }
UrlBasedResolver
- 子类
UrlBasedResolver
实现了loadView()
方法,该方法又调用一个buildView()
方法实现。@Override protected View loadView(String viewName, Locale locale) throws Exception { AbstractUrlBasedView view = buildView(viewName); View result = applyLifecycleMethods(viewName, view); return (view.checkResource(locale) ? result : null); }
buildView()
是最重要的实现方法。通过BeanUtils.instantiateClass()
方法实例化一个view对象,通过各类get
和set
方法,给view
注入多个参数(基本上响应有什么,就注入什么)。protected AbstractUrlBasedView buildView(String viewName) throws Exception { Class<?> viewClass = getViewClass(); Assert.state(viewClass != null, "No view class"); // 实例化一个view AbstractUrlBasedView view = (AbstractUrlBasedView) BeanUtils.instantiateClass(viewClass); view.setUrl(getPrefix() + viewName + getSuffix()); view.setAttributesMap(getAttributesMap()); String contentType = getContentType(); if (contentType != null) { view.setContentType(contentType); } String requestContextAttribute = getRequestContextAttribute(); if (requestContextAttribute != null) { view.setRequestContextAttribute(requestContextAttribute); } Boolean exposePathVariables = getExposePathVariables(); if (exposePathVariables != null) { view.setExposePathVariables(exposePathVariables); } Boolean exposeContextBeansAsAttributes = getExposeContextBeansAsAttributes(); if (exposeContextBeansAsAttributes != null) { view.setExposeContextBeansAsAttributes(exposeContextBeansAsAttributes); } String[] exposedContextBeanNames = getExposedContextBeanNames(); if (exposedContextBeanNames != null) { view.setExposedContextBeanNames(exposedContextBeanNames); } return view; }
- 值得一提的是,该类还重写了父类的
createView()
方法,加入了viewName
的判断@Override protected View createView(String viewName, Locale locale) throws Exception { // If this resolver is not supposed to handle the given view, // return null to pass on to the next resolver in the chain. if (!canHandle(viewName, locale)) { return null; } // Check for special "redirect:" prefix. 判断是否以redirect开头 if (viewName.startsWith(REDIRECT_URL_PREFIX)) { String redirectUrl = viewName.substring(REDIRECT_URL_PREFIX.length()); RedirectView view = new RedirectView(redirectUrl, isRedirectContextRelative(), isRedirectHttp10Compatible()); String[] hosts = getRedirectHosts(); if (hosts != null) { view.setHosts(hosts); } return applyLifecycleMethods(REDIRECT_URL_PREFIX, view); } // Check for special "forward:" prefix.判断是否以forward开头 if (viewName.startsWith(FORWARD_URL_PREFIX)) { String forwardUrl = viewName.substring(FORWARD_URL_PREFIX.length()); InternalResourceView view = new InternalResourceView(forwardUrl); return applyLifecycleMethods(FORWARD_URL_PREFIX, view); } // Else fall back to superclass implementation: calling loadView. return super.createView(viewName, locale); }
InternalResourceViewResolver
- 在真正的
ViewResolver
中,就简单地调用父类的buildView()
方法,即可实现功能。@Override protected AbstractUrlBasedView buildView(String viewName) throws Exception { InternalResourceView view = (InternalResourceView) super.buildView(viewName); //调用父类的方法实现 if (this.alwaysInclude != null) { view.setAlwaysInclude(this.alwaysInclude); } view.setPreventDispatchLoop(true); return view; }
BeanNameViewResolver
- 继承关系
- 该类相较于上面庞大的类来说,简单得多。基本上就是将整个
view
实例化一遍,然后还回去。public class BeanNameViewResolver extends WebApplicationObjectSupport implements ViewResolver, Ordered { private int order = Ordered.LOWEST_PRECEDENCE; // default: same as non-Ordered /** * Specify the order value for this ViewResolver bean. * <p>The default value is {@code Ordered.LOWEST_PRECEDENCE}, meaning non-ordered. * @see org.springframework.core.Ordered#getOrder() */ public void setOrder(int order) { this.order = order; } @Override public int getOrder() { return this.order; } @Override @Nullable public View resolveViewName(String viewName, Locale locale) throws BeansException { ApplicationContext context = obtainApplicationContext(); if (!context.containsBean(viewName)) { // Allow for ViewResolver chaining... return null; } if (!context.isTypeMatch(viewName, View.class)) { if (logger.isDebugEnabled()) { logger.debug("Found bean named '" + viewName + "' but it does not implement View"); } // Since we're looking into the general ApplicationContext here, // let's accept this as a non-match and allow for chaining as well... return null; } return context.getBean(viewName, View.class); } }
InternalResourceView
- 继承关系
AbstractView
- 该类首先实现了
rend()
方法,并调用了抽象方法renderMergedOutputModel()
@Override public void render(@Nullable Map<String, ?> model, HttpServletRequest request, HttpServletResponse response) throws Exception { if (logger.isDebugEnabled()) { logger.debug("View " + formatViewName() + ", model " + (model != null ? model : Collections.emptyMap()) + (this.staticAttributes.isEmpty() ? "" : ", static attributes " + this.staticAttributes)); } Map<String, Object> mergedModel = createMergedOutputModel(model, request, response); prepareResponse(request, response); renderMergedOutputModel(mergedModel, getRequestToExpose(request), response); }
InternalResourceView
- 该类就直接实现了
renderMergedOutputModel()
方法,并将大部分model的信息,都保存在request中。 - 最后获取
Dispatcher
,并forward
到指定的url中。@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 [" + getUrl() + "]"); } rd.include(request, response); } else { // Note: The forwarded resource is supposed to determine the content type itself. if (logger.isDebugEnabled()) { logger.debug("Forwarding to [" + getUrl() + "]"); } rd.forward(request, response); } }