【小家Spring】Spring MVC容器的web九大组件之---ViewResolver源码详解---视图解析器ViewResolver详解(上)

简介: 【小家Spring】Spring MVC容器的web九大组件之---ViewResolver源码详解---视图解析器ViewResolver详解(上)

前言


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;
}


看看它的继承树:


image.png


image.png


此处需要注意的是,我上面的截图用的是Spring5.x版本,下面我截图一个Spring4.x的作为对比:


image.png

可以看到曾经非常火的页面渲染技术:velocity在Spring5里面已经被完全抛弃了。根本原因在于velocity社区太不活跃了,上十年都不更新。虽然2018年左右社区又启动了维护,但显然已经不能让Spring回头了


在Spring4.x版本中虽然没有删除掉Velocity的包,但也都标记为过时了~~~


关于Apache的title技术,我今天刚打开官网,却发现一行大红字:

image.png


看来它也寿终正寝了,现在处于交替期,不建议大家在新项目中使用了。


现在推荐使用新一代高性能渲染引擎: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";
    }


image.png


这样我们访问http://localhost:8080/demo_war_war/index就能顺利的展示这个页面了

理论上我们的JSP页面都应该放在WEB-INF目录下,避免直接访问。此处因为只是Demo,我就先不遵守了~



相关文章
|
10月前
|
Web App开发 前端开发 JavaScript
前端新利器:CSS容器查询——让组件真正“自适应
前端新利器:CSS容器查询——让组件真正“自适应
535 83
|
前端开发 API 开发者
harmonyOS基础- 快速弄懂HarmonyOS ArkTs基础组件、布局容器(前端视角篇)
本文由黑臂麒麟(6年前端经验)撰写,介绍ArkTS开发中的常用基础组件与布局组件。基础组件包括Text、Image、Button等,支持样式设置如字体颜色、大小和加粗等,并可通过Resource资源引用统一管理样式。布局组件涵盖Column、Row、List、Grid和Tabs等,支持灵活的主轴与交叉轴对齐方式、分割线设置及滚动事件监听。同时,Tabs组件可实现自定义样式与页签切换功能。内容结合代码示例,适合初学者快速上手ArkTS开发。参考华为开发者联盟官网基础课程。
1258 75
harmonyOS基础- 快速弄懂HarmonyOS ArkTs基础组件、布局容器(前端视角篇)
|
存储 算法 安全
JWT深度解析:现代Web身份验证的通行证为什么现在都是JWT为什么要restful-优雅草卓伊凡
JWT深度解析:现代Web身份验证的通行证为什么现在都是JWT为什么要restful-优雅草卓伊凡
672 41
JWT深度解析:现代Web身份验证的通行证为什么现在都是JWT为什么要restful-优雅草卓伊凡
|
XML JSON API
ServiceStack:不仅仅是一个高性能Web API和微服务框架,更是一站式解决方案——深入解析其多协议支持及简便开发流程,带您体验前所未有的.NET开发效率革命
【10月更文挑战第9天】ServiceStack 是一个高性能的 Web API 和微服务框架,支持 JSON、XML、CSV 等多种数据格式。它简化了 .NET 应用的开发流程,提供了直观的 RESTful 服务构建方式。ServiceStack 支持高并发请求和复杂业务逻辑,安装简单,通过 NuGet 包管理器即可快速集成。示例代码展示了如何创建一个返回当前日期的简单服务,包括定义请求和响应 DTO、实现服务逻辑、配置路由和宿主。ServiceStack 还支持 WebSocket、SignalR 等实时通信协议,具备自动验证、自动过滤器等丰富功能,适合快速搭建高性能、可扩展的服务端应用。
998 3
|
10月前
|
前端开发 Java API
Spring Cloud Gateway Server Web MVC报错“Unsupported transfer encoding: chunked”解决
本文解析了Spring Cloud Gateway中出现“Unsupported transfer encoding: chunked”错误的原因,指出该问题源于Feign依赖的HTTP客户端与服务端的`chunked`传输编码不兼容,并提供了具体的解决方案。通过规范Feign客户端接口的返回类型,可有效避免该异常,提升系统兼容性与稳定性。
730 0
|
JSON JavaScript 前端开发
蓝桥杯web组赛题解析和杯赛技巧
本文作者是一位自学前端两年半的大一学生,在第十五届蓝桥杯Web组比赛中获得省一和国三。文章详细解析了比赛题纲,涵盖HTML、CSS、JavaScript、Echarts和Vue等技术要点,并分享了备赛技巧和比赛经验。作者强调了多写代码和解题思路的重要性,同时提供了省赛和国赛的具体流程及注意事项。希望对参赛者有所帮助。
1555 11
|
安全 前端开发 Java
Web安全进阶:XSS与CSRF攻击防御策略深度解析
【10月更文挑战第26天】Web安全是现代软件开发的重要领域,本文深入探讨了XSS和CSRF两种常见攻击的原理及防御策略。针对XSS,介绍了输入验证与转义、使用CSP、WAF、HTTP-only Cookie和代码审查等方法。对于CSRF,提出了启用CSRF保护、设置CSRF Token、使用HTTPS、二次验证和用户教育等措施。通过这些策略,开发者可以构建更安全的Web应用。
1056 4
|
安全 Go PHP
Web安全进阶:XSS与CSRF攻击防御策略深度解析
【10月更文挑战第27天】本文深入解析了Web安全中的XSS和CSRF攻击防御策略。针对XSS,介绍了输入验证与净化、内容安全策略(CSP)和HTTP头部安全配置;针对CSRF,提出了使用CSRF令牌、验证HTTP请求头、限制同源策略和双重提交Cookie等方法,帮助开发者有效保护网站和用户数据安全。
880 2
|
前端开发 开发者 容器
构建响应式Web界面:Flexbox与Grid布局的深度解析
【10月更文挑战第11天】本文深入解析了CSS3中的Flexbox和Grid布局,探讨了它们的特点、应用场景及使用方法。Flexbox适用于一维布局,如导航栏;Grid布局则适用于二维布局,如复杂网格。通过示例代码和核心属性介绍,帮助开发者灵活构建响应式Web界面。
473 5
|
开发框架 前端开发 .NET
ASP.NET CORE 3.1 MVC“指定的网络名不再可用\企图在不存在的网络连接上进行操作”的问题解决过程
ASP.NET CORE 3.1 MVC“指定的网络名不再可用\企图在不存在的网络连接上进行操作”的问题解决过程
812 0