前言
Spring的一个优秀之处在于,把view层技术与MVC框架的其他部分离开来。 例如,选择使用Velocity或者XSLT来代替已有的JSP方式只需要修改配置就可以实现。
前面已经讲解了Spring MVC对Handler返回值的处理:
【小家Spring】Spring MVC容器的web九大组件之—HandlerAdapter源码详解—一篇文章带你读懂返回值处理器HandlerMethodReturnValueHandler
我们知道,当我们对SpringMVC控制的资源发起请求时,这些请求都会被SpringMVC的DispatcherServlet处理。接着它会根据请求的URL经过HandlerMapping处理,匹配上一个最合适的HandlerExecutionChain(它是一个拦截器+handler的组合)。
然后再通过Handler拿到一个HandlerAdapter,HandlerAdapter再对Handler进行执行、处理之后会统一返回一个ModelAndView对象。
在获得了ModelAndView对象之后,SpringMVC就需要把该View渲染给用户,即返回给浏览器。在这个渲染的过程中,发挥作用的就是ViewResolver和View,本文就是讲解ViewResolver。
当Handler返回的ModelAndView中不包含真正的视图,只返回一个逻辑视图(比如返回一个字符串)名称的时候,ViewResolver就会把该逻辑视图名称解析为真正的视图View对象。
View是真正的进行视图渲染(对response里写东西),把结果返回给浏览器的
ViewResolver
SpringMVC 用于处理视图最重要的两个接口是 ViewResolver 和 View ,ViewResolver 的主要作用是把一个逻辑上的视图名称解析为一个真正的视图(View )。SpringMVC 中用于把 View 对象呈现给客户端的是 View 对象本身,而 ViewResolver 只是把逻辑视图名称解析为对象的View对象。 View 接口的主要作用是用于处理视图,然后返回给客户端。
Spring MVC为我们定义了非常多的视图解析器,下面重点就是看看该接口本身以及它的实现类们:
// 这个接口非常简单,就一个方法:把一个逻辑视图viewName解析为一个真正的视图View,Local表示国际化相关内容~ public interface ViewResolver { @Nullable View resolveViewName(String viewName, Locale locale) throws Exception; }
看看它的继承树:
此处需要注意的是,我上面的截图用的是Spring5.x版本,下面我截图一个Spring4.x的作为对比:
可以看到曾经非常火的页面渲染技术:velocity在Spring5里面已经被完全抛弃了。根本原因在于velocity社区太不活跃了,上十年都不更新。虽然2018年左右社区又启动了维护,但显然已经不能让Spring回头了
在Spring4.x版本中虽然没有删除掉Velocity的包,但也都标记为过时了~~~
关于Apache的title技术,我今天刚打开官网,却发现一行大红字:
看来它也寿终正寝了,现在处于交替期,不建议大家在新项目中使用了。
现在推荐使用新一代高性能渲染引擎:Thymeleaf,这也是Spring(Boot)的推荐~
AbstractCachingViewResolver 非常重要
这是一个抽象类,这种视图解析器会把它曾经解析过的视图缓存起来(从命名caching也能看出来)。然后每次要解析视图的时候先从缓存里面找,如果找到了对应的视图就直接返回,如果没有就创建一个新的视图对象,然后把它放到一个用于缓存的 map 中,接着再把新建的视图返回
使用这种视图缓存的方式可以把解析视图的性能问题降到最低,所以它是Spring MVC最为主要的渲染方式
// 该首相类完成的主要是缓存的相关逻辑~~~ public abstract class AbstractCachingViewResolver extends WebApplicationObjectSupport implements ViewResolver { // Map的最大值,1024我觉得还是挺大的了~ /** Default maximum number of entries for the view cache: 1024. */ public static final int DEFAULT_CACHE_LIMIT = 1024; // 表示没有被解析过的View~~~ private static final View UNRESOLVED_VIEW = new View() { @Override @Nullable public String getContentType() { return null; } @Override public void render(@Nullable Map<String, ?> model, HttpServletRequest request, HttpServletResponse response) { } }; private volatile int cacheLimit = DEFAULT_CACHE_LIMIT; private boolean cacheUnresolved = true; // 此处使用的是ConcurrentHashMap,key是Object private final Map<Object, View> viewAccessCache = new ConcurrentHashMap<>(DEFAULT_CACHE_LIMIT); // 通过它来实现缓存最大值: removeEldestEntry表示当你往里put成为为true的时候,会执行它 // 此处可以看到,当size大于1024时,会把Map里面最老的那个值给remove掉~~~viewAccessCache.remove(eldest.getKey()); private final Map<Object, View> viewCreationCache = new LinkedHashMap<Object, View>(DEFAULT_CACHE_LIMIT, 0.75f, true) { @Override protected boolean removeEldestEntry(Map.Entry<Object, View> eldest) { if (size() > getCacheLimit()) { viewAccessCache.remove(eldest.getKey()); return true; } else { return false; } } }; ... // 通过逻辑视图,来找到一个View真正的视图~~~~ @Override @Nullable public View resolveViewName(String viewName, Locale locale) throws Exception { if (!isCache()) { return createView(viewName, locale); } else { // cacheKey其实就是 viewName + '_' + locale 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); // 此处需要注意:若调用者返回的是null,并且cacheUnresolved,那就返回一个未经处理的视图~~~~ if (view == null && this.cacheUnresolved) { view = UNRESOLVED_VIEW; } // 缓存起来~~~~ if (view != null) { this.viewAccessCache.put(cacheKey, view); this.viewCreationCache.put(cacheKey, view); } } } } else { if (logger.isTraceEnabled()) { logger.trace(formatKey(cacheKey) + "served from cache"); } } // 这个很重要,因为没有被解析过 都会返回null // 而再真正责任链处理的时候,第一个不返回null的view,最终就会被返回了~~~ return (view != UNRESOLVED_VIEW ? view : null); } } // 逻辑比较简单~~~ public void removeFromCache(String viewName, Locale locale) { ... } public void clearCache() { logger.debug("Clearing all views from the cache"); synchronized (this.viewCreationCache) { this.viewAccessCache.clear(); this.viewCreationCache.clear(); } } }
课件此抽象类完成的是缓存相关的维护逻辑,而子类只需要专注在createView这件事情上了。
UrlBasedViewResolver
它是对 ViewResolver 的一种简单实现,而且继承了AbstractCachingViewResolver ,主要就是提供的一种拼接 URL 的方式来解析视图,它可以让我们通过 prefix 属性指定一个指定的前缀,通过 suffix 属性指定一个指定的后缀,然后把返回的逻辑视图名称加上指定的前缀和后缀就是指定的视图 URL 了。
如 prefix=/WEB-INF/jsps/ , suffix=.jsp ,返回的视图名称 viewName=test/indx ,则 UrlBasedViewResolver 解析出来的视图 URL 就是 /WEB-INF/jsps/test/index.jsp
public class UrlBasedViewResolver extends AbstractCachingViewResolver implements Ordered { // ”redirect:” 前缀 包装成一个RedirectView 最终调用 HttpServletResponse 对象的 sendRedirect 方法进行重定向 public static final String REDIRECT_URL_PREFIX = "redirect:"; // forword: 前缀的视图名称将会被封装成一个 InternalResourceView 对象 服务器端利用 `RequestDispatcher`的forword方式跳转到指定的地址 public static final String FORWARD_URL_PREFIX = "forward:"; // 这个三个属性是最重要的~~~ @Nullable private Class<?> viewClass; private String prefix = ""; private String suffix = ""; // 其它属性值非常多 // the content type for all views,若view自己设置了此值就用自己的,否则是它 @Nullable private String contentType; //重定向的时候,是否把/解释为相对当前ServletContext的路径 // 直接关系RedirectView#setContextRelative这个值 private boolean redirectContextRelative = true; // 设置重定向是否应与HTTP 1.0客户端保持兼容 private boolean redirectHttp10Compatible = true; // 配置与应用程序关联的一个或多个主机 @since 4.3 @Nullable private String[] redirectHosts; // Set the name of the RequestContext attribute for all views @Nullable private String requestContextAttribute; /** Map of static attributes, keyed by attribute name (String). */ // 保存一些全局属性~~~ private final Map<String, Object> staticAttributes = new HashMap<>(); // 指定此解析程序解析的视图是否应向模型添加路径变量 // {@code true} - all Views resolved by this resolver will expose path variables // {@code false} - no Views resolved by this resolver will expose path variables // {@code null} - individual Views can decide for themselves (this is used by the default) 默认值是这个 @Nullable private Boolean exposePathVariables; // 设置是否将应用程序上下文中的所有SpringBean作为请求属性进行访问 // This will make all such beans accessible in plain {@code ${...}} expressions in a JSP 2.0 page, as well as in JSTL's {@code c:out} value expressions //AbstractView#setExposeContextBeansAsAttributes 默认值是false @Nullable private Boolean exposeContextBeansAsAttributes; // 在应该公开的上下文中指定bean的名称 如果不为空,则只有指定的bean才有资格作为属性进行暴露 @Nullable private String[] exposedContextBeanNames; // Set the view names (or name patterns) that can be handled by this ViewResolver // View names can contain simple wildcards such that 'my*', '*Report' and '*Repo*' will all match the view name 'myReport'. @Nullable private String[] viewNames; // 你指定的viewClass必须是AbstractUrlBasedView的子类 protected Class<?> requiredViewClass() { return AbstractUrlBasedView.class; } // 把Properties 保存起来,放在群居的map里 public void setAttributes(Properties props) { CollectionUtils.mergePropertiesIntoMap(props, this.staticAttributes); } public void setAttributesMap(@Nullable Map<String, ?> attributes) { if (attributes != null) { this.staticAttributes.putAll(attributes); } } // 从这里可以看出viewClass属性,如果你在Spring容器里面使用,它是必须的~~~ @Override protected void initApplicationContext() { super.initApplicationContext(); if (getViewClass() == null) { throw new IllegalArgumentException("Property 'viewClass' is required"); } } // 这个方法注意:复写的是父类的crateView方法,而不是loadView方法(loadView才是抽象方法~~~)注意这个涉及技巧~~~ 分层次进行处理 @Override protected View createView(String viewName, Locale locale) throws Exception { // canHandle表示:viewNames没配置 或者 匹配上了 就返回true if (!canHandle(viewName, locale)) { return null; } // Check for special "redirect:" prefix. // 最终被转换成一个RedirectView,可以看到这里很多属性都是为它而准备的~~~比如getRedirectHosts这种属性值~~~ 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打头的用的就是`InternalResourceView ` 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); } // 执行容器内此Bean的声明周期方法,也就是view的声明周期方法。比如@Postconstruct、XXXAware这种方法 // 可议看到它调用的是initializeBean,可议知道我们的View并不需要交给容器管理,但我们却能够享受它的一些声明周期方法~~~~~ protected View applyLifecycleMethods(String viewName, AbstractUrlBasedView view) { ApplicationContext context = getApplicationContext(); if (context != null) { Object initialized = context.getAutowireCapableBeanFactory().initializeBean(view, viewName); if (initialized instanceof View) { return (View) initialized; } } return view; } // 实现了父类的loadView方法~ @Override protected View loadView(String viewName, Locale locale) throws Exception { AbstractUrlBasedView view = buildView(viewName); View result = applyLifecycleMethods(viewName, view); // 这一步非常关键,它调用了view的checkResource方法,而这个方法的默认实现是永远返回true的 // 所以请注意:特别是在你自定义视图的时候,注意重写此方法。只有资源真的存在的时候,你才去返回,否则让返回null,交给别的视图解析器继续去处理~~~ // 自己处理不了的,自己就不要勉强了~~~~ return (view.checkResource(locale) ? result : null); } // 构建一个View,注意此处的返回值为AbstractUrlBasedView~~ 合理主要工作就是把属性都设置进去~~~ protected AbstractUrlBasedView buildView(String viewName) throws Exception { // 我们必须配置的viewClass属性~~~~ 然后反射创建一个实例~~ Class<?> viewClass = getViewClass(); Assert.state(viewClass != null, "No view class"); AbstractUrlBasedView view = (AbstractUrlBasedView) BeanUtils.instantiateClass(viewClass); view.setUrl(getPrefix() + viewName + getSuffix()); String contentType = getContentType(); if (contentType != null) { view.setContentType(contentType); } view.setRequestContextAttribute(getRequestContextAttribute()); view.setAttributesMap(getAttributesMap()); 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; } }
使用 UrlBasedViewResolver 的时候必须指定属性viewClass,表示解析成哪种视图,一般使用较多的就是InternalResourceView ,利用它来展现 jsp 。但是当我们要使用 JSTL 的时候我们必须使用 JstlView(JstlView是InternalResourceView的子类)
ScriptTemplateViewResolver
个脚本渲染有关的一个处理器。处理成ScriptTemplateView(自定义前缀、后缀)
// @since 4.2 是一个非常新的View处理器~~~ public class ScriptTemplateViewResolver extends UrlBasedViewResolver { public ScriptTemplateViewResolver() { setViewClass(requiredViewClass()); } public ScriptTemplateViewResolver(String prefix, String suffix) { this(); setPrefix(prefix); setSuffix(suffix); } // ScriptTemplateView的父类是AbstractUrlBasedView @Override protected Class<?> requiredViewClass() { return ScriptTemplateView.class; } }
InternalResourceViewResolver
这个视图处理器最为重要,它也是Spring MVC默认给装配的视图解析器。
public class InternalResourceViewResolver extends UrlBasedViewResolver { // 如果你导入了JSTL的相关的包,这个解析器也会支持JSTLView的~~~~ private static final boolean jstlPresent = ClassUtils.isPresent( "javax.servlet.jsp.jstl.core.Config", InternalResourceViewResolver.class.getClassLoader()); // 指定是否始终包含视图而不是转发到视图 // 默认值为“false”。打开此标志以强制使用servlet include,即使可以进行转发 // InternalResourceView#setAlwaysInclude @Nullable private Boolean alwaysInclude; @Override protected Class<?> requiredViewClass() { return InternalResourceView.class; } // 默认情况下,它可能会设置一个JstlView 或者 InternalResourceView public InternalResourceViewResolver() { Class<?> viewClass = requiredViewClass(); if (InternalResourceView.class == viewClass && jstlPresent) { viewClass = JstlView.class; } setViewClass(viewClass); } public InternalResourceViewResolver(String prefix, String suffix) { this(); // 先调用空构造 setPrefix(prefix); setSuffix(suffix); } // 在父类实现的记仇上,设置上了alwaysInclude,并且view.setPreventDispatchLoop(true) @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; } }
因为它是默认就被装配进去的,所以啥都不说了,这么写:
@GetMapping("/index") public String index() { return "index.jsp"; }
这样我们访问http://localhost:8080/demo_war_war/index
就能顺利的展示这个页面了
理论上我们的JSP页面都应该放在WEB-INF目录下,避免直接访问。此处因为只是Demo,我就先不遵守了~