该系列文档是本人在学习 Spring MVC 的源码过程中总结下来的,可能对读者不太友好,请结合我的源码注释 Spring MVC 源码分析 GitHub 地址 进行阅读

Spring 版本:5.2.4.RELEASE

该系列其他文档请查看:《精尽 Spring MVC 源码分析 - 文章导读》

ViewResolver 组件

ViewResolver 组件,视图解析器,根据视图名和国际化,获得最终的视图 View 对象

回顾

先来回顾一下在 DispatcherServlet 中处理请求的过程中哪里使用到 ViewResolver 组件,可以回到《一个请求的旅行过程》中的 DispatcherServletrender 方法中看看,如下:

  1. protected void render(ModelAndView mv, HttpServletRequest request, HttpServletResponse response) throws Exception {
  2. // Determine locale for request and apply it to the response.
  3. // <1> 解析 request 中获得 Locale 对象,并设置到 response 中
  4. Locale locale = (this.localeResolver != null ? this.localeResolver.resolveLocale(request) : request.getLocale());
  5. response.setLocale(locale);
  6. // 获得 View 对象
  7. View view;
  8. String viewName = mv.getViewName();
  9. // 情况一,使用 viewName 获得 View 对象
  10. if (viewName != null) {
  11. // We need to resolve the view name.
  12. // <2.1> 使用 viewName 获得 View 对象
  13. view = resolveViewName(viewName, mv.getModelInternal(), locale, request);
  14. if (view == null) { // 获取不到,抛出 ServletException 异常
  15. throw new ServletException("Could not resolve view with name '" + mv.getViewName() +
  16. "' in servlet with name '" + getServletName() + "'");
  17. }
  18. }
  19. // 情况二,直接使用 ModelAndView 对象的 View 对象
  20. else {
  21. // No need to lookup: the ModelAndView object contains the actual View object.
  22. // 直接使用 ModelAndView 对象的 View 对象
  23. view = mv.getView();
  24. if (view == null) { // 获取不到,抛出 ServletException 异常
  25. throw new ServletException("ModelAndView [" + mv + "] neither contains a view name nor a " +
  26. "View object in servlet with name '" + getServletName() + "'");
  27. }
  28. }
  29. // Delegate to the View object for rendering.
  30. // 打印日志
  31. if (logger.isTraceEnabled()) {
  32. logger.trace("Rendering view [" + view + "] ");
  33. }
  34. try {
  35. // <3> 设置响应的状态码
  36. if (mv.getStatus() != null) {
  37. response.setStatus(mv.getStatus().value());
  38. }
  39. // <4> 渲染页面
  40. view.render(mv.getModelInternal(), request, response);
  41. }
  42. catch (Exception ex) {
  43. if (logger.isDebugEnabled()) {
  44. logger.debug("Error rendering view [" + view + "]", ex);
  45. }
  46. throw ex;
  47. }
  48. }
  49. @Nullable
  50. protected View resolveViewName(String viewName, @Nullable Map<String, Object> model,
  51. Locale locale, HttpServletRequest request) throws Exception {
  52. if (this.viewResolvers != null) {
  53. // 遍历 ViewResolver 数组
  54. for (ViewResolver viewResolver : this.viewResolvers) {
  55. // 根据 viewName + locale 参数,解析出 View 对象
  56. View view = viewResolver.resolveViewName(viewName, locale);
  57. // 解析成功,直接返回 View 对象
  58. if (view != null) {
  59. return view;
  60. }
  61. }
  62. }
  63. return null;
  64. }

如果 ModelAndView 对象不为null,且需要进行页面渲染,则调用 render 方法,如果设置的 View 对象是 String 类型,也就是 viewName,则需要调用 resolveViewName 方法,通过 ViewResolver 根据 viewNamelocale 解析出对应的 View 对象

这是前后端未分离的情况下重要的一个组件

ViewResolver 接口

org.springframework.web.servlet.ViewResolver,视图解析器,根据视图名和国际化,获得最终的视图 View 对象,代码如下:

  1. public interface ViewResolver {
  2. /**
  3. * 根据视图名和国际化,获得最终的 View 对象
  4. */
  5. @Nullable
  6. View resolveViewName(String viewName, Locale locale) throws Exception;
  7. }

ViewResolver 接口体系的结构如下:

ViewResolver 的实现类比较多,其中 Spring MVC 默认使用 org.springframework.web.servlet.view.InternalResourceViewResolver 这个实现类

Spring Boot 中的默认实现类如下:

可以看到有三个实现类:

  • org.springframework.web.servlet.view.ContentNegotiatingViewResolver

  • org.springframework.web.servlet.view.ViewResolverComposite,默认没有实现类

  • org.springframework.web.servlet.view.BeanNameViewResolver

  • org.springframework.web.servlet.view.InternalResourceViewResolver

初始化过程

DispatcherServletinitViewResolvers(ApplicationContext context) 方法,初始化 ViewResolver 组件,方法如下:

  1. private void initViewResolvers(ApplicationContext context) {
  2. // 置空 viewResolvers 处理
  3. this.viewResolvers = null;
  4. // 情况一,自动扫描 ViewResolver 类型的 Bean 们
  5. if (this.detectAllViewResolvers) {
  6. // Find all ViewResolvers in the ApplicationContext, including ancestor contexts.
  7. Map<String, ViewResolver> matchingBeans = BeanFactoryUtils.beansOfTypeIncludingAncestors(context,
  8. ViewResolver.class, true, false);
  9. if (!matchingBeans.isEmpty()) {
  10. this.viewResolvers = new ArrayList<>(matchingBeans.values());
  11. // We keep ViewResolvers in sorted order.
  12. AnnotationAwareOrderComparator.sort(this.viewResolvers);
  13. }
  14. }
  15. // 情况二,获得名字为 VIEW_RESOLVER_BEAN_NAME 的 Bean 们
  16. else {
  17. try {
  18. ViewResolver vr = context.getBean(VIEW_RESOLVER_BEAN_NAME, ViewResolver.class);
  19. this.viewResolvers = Collections.singletonList(vr);
  20. }
  21. catch (NoSuchBeanDefinitionException ex) {
  22. // Ignore, we'll add a default ViewResolver later.
  23. }
  24. }
  25. // Ensure we have at least one ViewResolver, by registering
  26. // a default ViewResolver if no other resolvers are found.
  27. /**
  28. * 情况三,如果未获得到,则获得默认配置的 ViewResolver 类
  29. * {@link org.springframework.web.servlet.view.InternalResourceViewResolver}
  30. */
  31. if (this.viewResolvers == null) {
  32. this.viewResolvers = getDefaultStrategies(context, ViewResolver.class);
  33. if (logger.isTraceEnabled()) {
  34. logger.trace("No ViewResolvers declared for servlet '" + getServletName() +
  35. "': using default strategies from DispatcherServlet.properties");
  36. }
  37. }
  38. }
  1. 如果“开启”探测功能,则扫描已注册的 ViewResolver 的 Bean 们,添加到 viewResolvers 中,默认开启

  2. 如果“关闭”探测功能,则获得 Bean 名称为 "viewResolver" 对应的 Bean ,将其添加至 viewResolvers

  3. 如果未获得到,则获得默认配置的 ViewResolver 类,调用 getDefaultStrategies(ApplicationContext context, Class<T> strategyInterface) 方法,就是从 DispatcherServlet.properties 文件中读取 ViewResolver 的默认实现类,如下:

    1. org.springframework.web.servlet.ViewResolver=org.springframework.web.servlet.view.InternalResourceViewResolver

在 Spring Boot 不是通过这样初始化的,感兴趣的可以去看看

ContentNegotiatingViewResolver

org.springframework.web.servlet.view.ContentNegotiatingViewResolver,实现 ViewResolver、Ordered、InitializingBean 接口,继承 WebApplicationObjectSupport 抽象类,基于内容类型来获取对应 View 的 ViewResolver 实现类。其中,内容类型指的是 Content-Type 和拓展后缀

构造方法

  1. public class ContentNegotiatingViewResolver extends WebApplicationObjectSupport
  2. implements ViewResolver, Ordered, InitializingBean {
  3. @Nullable
  4. private ContentNegotiationManager contentNegotiationManager;
  5. /**
  6. * ContentNegotiationManager 的工厂,用于创建 {@link #contentNegotiationManager} 对象
  7. */
  8. private final ContentNegotiationManagerFactoryBean cnmFactoryBean = new ContentNegotiationManagerFactoryBean();
  9. /**
  10. * 在找不到 View 对象时,返回 {@link #NOT_ACCEPTABLE_VIEW}
  11. */
  12. private boolean useNotAcceptableStatusCode = false;
  13. /**
  14. * 默认 View 数组
  15. */
  16. @Nullable
  17. private List<View> defaultViews;
  18. /**
  19. * ViewResolver 数组
  20. */
  21. @Nullable
  22. private List<ViewResolver> viewResolvers;
  23. /**
  24. * 顺序,优先级最高
  25. */
  26. private int order = Ordered.HIGHEST_PRECEDENCE;
  27. }
  • viewResolvers:ViewResolver 数组。对于来说,ContentNegotiatingViewResolver 会使用这些 ViewResolver们,解析出所有的 View 们,然后基于内容类型,来获取对应的 View 们。此时的 View 结果,可能是一个,可能是多个,所以需要比较获取到最优的 View 对象。
  • defaultViews:默认 View 数组。那么此处的默认是什么意思呢?在 viewResolvers 们解析出所有的 View 们的基础上,也会添加 defaultViews 到 View 结果中
  • order:顺序,优先级最高。所以,这也是为什么它排在最前面

在上图中可以看到,在 Spring Boot 中 viewResolvers 属性有三个实现类,分别是 BeanNameViewResolverViewResolverCompositeInternalResourceViewResolver

initServletContext

实现 initServletContext(ServletContext servletContext) 方法,初始化 viewResolvers 属性,方法如下:

在父类 WebApplicationObjectSupport 的父类 ApplicationObjectSupport 中可以看到,因为实现了 ApplicationContextAware 接口,则在初始化该 Bean 的时候会调用 setApplicationContext(@Nullable ApplicationContext context) 方法,在这个方法中会调用 initApplicationContext(ApplicationContext context) 这个方法,这个方法又会调用initServletContext(ServletContext servletContext) 方法

  1. @Override
  2. protected void initServletContext(ServletContext servletContext) {
  3. // <1> 扫描所有 ViewResolver 的 Bean 们
  4. Collection<ViewResolver> matchingBeans = BeanFactoryUtils.
  5. beansOfTypeIncludingAncestors(obtainApplicationContext(), ViewResolver.class).values();
  6. // <1.1> 情况一,如果 viewResolvers 为空,则将 matchingBeans 作为 viewResolvers 。
  7. // BeanNameViewResolver、ThymeleafViewResolver、ViewResolverComposite、InternalResourceViewResolver
  8. if (this.viewResolvers == null) {
  9. this.viewResolvers = new ArrayList<>(matchingBeans.size());
  10. for (ViewResolver viewResolver : matchingBeans) {
  11. if (this != viewResolver) { // 排除自己
  12. this.viewResolvers.add(viewResolver);
  13. }
  14. }
  15. }
  16. // <1.2> 情况二,如果 viewResolvers 非空,则和 matchingBeans 进行比对,判断哪些未进行初始化,进行初始化
  17. else {
  18. for (int i = 0; i < this.viewResolvers.size(); i++) {
  19. ViewResolver vr = this.viewResolvers.get(i);
  20. // 已存在在 matchingBeans 中,说明已经初始化,则直接 continue
  21. if (matchingBeans.contains(vr)) {
  22. continue;
  23. }
  24. // 不存在在 matchingBeans 中,说明还未初始化,则进行初始化
  25. String name = vr.getClass().getName() + i;
  26. obtainApplicationContext().getAutowireCapableBeanFactory().initializeBean(vr, name);
  27. }
  28. }
  29. // <1.3> 排序 viewResolvers 数组
  30. AnnotationAwareOrderComparator.sort(this.viewResolvers);
  31. // <2> 设置 cnmFactoryBean 的 servletContext 属性
  32. this.cnmFactoryBean.setServletContext(servletContext);
  33. }
  1. 扫描所有 ViewResolver 的 Bean 们 matchingBeans

    1. 情况一,如果 viewResolvers 为空,则将 matchingBeans 作为 viewResolvers
    2. 情况二,如果 viewResolvers 非空,则和 matchingBeans 进行比对,判断哪些未进行初始化,进行初始化
    3. 排序 viewResolvers 数组
  2. 设置 cnmFactoryBeanservletContext 属性为当前 Servlet 上下文

afterPropertiesSet

因为 ContentNegotiatingViewResolver 实现了 InitializingBean 接口,在 Sping 初始化该 Bean 的时候,会调用该方法,完成一些初始化工作,方法如下:

  1. @Override
  2. public void afterPropertiesSet() {
  3. // 如果 contentNegotiationManager 为空,则进行创建
  4. if (this.contentNegotiationManager == null) {
  5. this.contentNegotiationManager = this.cnmFactoryBean.build();
  6. }
  7. if (this.viewResolvers == null || this.viewResolvers.isEmpty()) {
  8. logger.warn("No ViewResolvers configured");
  9. }
  10. }

resolveViewName

实现 resolveViewName(String viewName, Locale locale) 方法,代码如下:

  1. @Override
  2. @Nullable
  3. public View resolveViewName(String viewName, Locale locale) throws Exception {
  4. RequestAttributes attrs = RequestContextHolder.getRequestAttributes();
  5. Assert.state(attrs instanceof ServletRequestAttributes, "No current ServletRequestAttributes");
  6. // <1> 获得 MediaType 数组
  7. List<MediaType> requestedMediaTypes = getMediaTypes(((ServletRequestAttributes) attrs).getRequest());
  8. if (requestedMediaTypes != null) {
  9. // <2> 获得匹配的 View 数组
  10. List<View> candidateViews = getCandidateViews(viewName, locale, requestedMediaTypes);
  11. // <3> 筛选最匹配的 View 对象
  12. View bestView = getBestView(candidateViews, requestedMediaTypes, attrs);
  13. // 如果筛选成功,则返回
  14. if (bestView != null) {
  15. return bestView;
  16. }
  17. }
  18. String mediaTypeInfo = logger.isDebugEnabled() && requestedMediaTypes != null ? " given " + requestedMediaTypes.toString() : "";
  19. // <4> 如果匹配不到 View 对象,则根据 useNotAcceptableStatusCode ,返回 NOT_ACCEPTABLE_VIEW 或 null
  20. if (this.useNotAcceptableStatusCode) {
  21. if (logger.isDebugEnabled()) {
  22. logger.debug("Using 406 NOT_ACCEPTABLE" + mediaTypeInfo);
  23. }
  24. return NOT_ACCEPTABLE_VIEW;
  25. }
  26. else {
  27. logger.debug("View remains unresolved" + mediaTypeInfo);
  28. return null;
  29. }
  30. }
  1. 调用 getMediaTypes(HttpServletRequest request) 方法,获得 MediaType 数组,详情见下文

  2. 调用 getCandidateViews(String viewName, Locale locale, List<MediaType> requestedMediaTypes) 方法,获得匹配的 View 数组,详情见下文

  3. 调用 getBestView(List<View> candidateViews, List<MediaType> requestedMediaTypes, RequestAttributes attrs) 方法,筛选出最匹配的 View 对象,如果筛选成功则直接返回,详情见下文

  4. 如果匹配不到 View 对象,则根据 useNotAcceptableStatusCode,返回 NOT_ACCEPTABLE_VIEWnull,其中NOT_ACCEPTABLE_VIEW 变量,代码如下:

    1. private static final View NOT_ACCEPTABLE_VIEW = new View() {
    2. @Override
    3. @Nullable
    4. public String getContentType() {
    5. return null;
    6. }
    7. @Override
    8. public void render(@Nullable Map<String, ?> model, HttpServletRequest request, HttpServletResponse response) {
    9. response.setStatus(HttpServletResponse.SC_NOT_ACCEPTABLE);
    10. }
    11. };

    这个 View 对象设置状态码为 406

getMediaTypes

getCandidateViews(HttpServletRequest request)方法,获得 MediaType 数组,如下:

  1. @Nullable
  2. protected List<MediaType> getMediaTypes(HttpServletRequest request) {
  3. Assert.state(this.contentNegotiationManager != null, "No ContentNegotiationManager set");
  4. try {
  5. // 创建 ServletWebRequest 对象
  6. ServletWebRequest webRequest = new ServletWebRequest(request);
  7. // 从请求中,获得可接受的 MediaType 数组。默认实现是,从请求头 ACCEPT 中获取
  8. List<MediaType> acceptableMediaTypes = this.contentNegotiationManager.resolveMediaTypes(webRequest);
  9. // 获得可产生的 MediaType 数组
  10. List<MediaType> producibleMediaTypes = getProducibleMediaTypes(request);
  11. // 通过 acceptableTypes 来比对,将符合的 producibleType 添加到 mediaTypesToUse 结果数组中
  12. Set<MediaType> compatibleMediaTypes = new LinkedHashSet<>();
  13. for (MediaType acceptable : acceptableMediaTypes) {
  14. for (MediaType producible : producibleMediaTypes) {
  15. if (acceptable.isCompatibleWith(producible)) {
  16. compatibleMediaTypes.add(getMostSpecificMediaType(acceptable, producible));
  17. }
  18. }
  19. }
  20. // 按照 MediaType 的 specificity、quality 排序
  21. List<MediaType> selectedMediaTypes = new ArrayList<>(compatibleMediaTypes);
  22. MediaType.sortBySpecificityAndQuality(selectedMediaTypes);
  23. return selectedMediaTypes;
  24. }
  25. catch (HttpMediaTypeNotAcceptableException ex) {
  26. if (logger.isDebugEnabled()) {
  27. logger.debug(ex.getMessage());
  28. }
  29. return null;
  30. }
  31. }

getCandidateViews

getCandidateViews(String viewName, Locale locale, List<MediaType> requestedMediaTypes)方法,获得匹配的 View 数组,如下:

  1. private List<View> getCandidateViews(String viewName, Locale locale, List<MediaType> requestedMediaTypes)
  2. throws Exception {
  3. // 创建 View 数组
  4. List<View> candidateViews = new ArrayList<>();
  5. // <1> 来源一,通过 viewResolvers 解析出 View 数组结果,添加到 candidateViews 中
  6. if (this.viewResolvers != null) {
  7. Assert.state(this.contentNegotiationManager != null, "No ContentNegotiationManager set");
  8. // <1.1> 遍历 viewResolvers 数组
  9. for (ViewResolver viewResolver : this.viewResolvers) {
  10. // <1.2> 情况一,获得 View 对象,添加到 candidateViews 中
  11. View view = viewResolver.resolveViewName(viewName, locale);
  12. if (view != null) {
  13. candidateViews.add(view);
  14. }
  15. // <1.3> 情况二,带有拓展后缀的方式,获得 View 对象,添加到 candidateViews 中
  16. for (MediaType requestedMediaType : requestedMediaTypes) {
  17. // <1.3.2> 获得 MediaType 对应的拓展后缀的数组(默认情况下未配置)
  18. List<String> extensions = this.contentNegotiationManager.resolveFileExtensions(requestedMediaType);
  19. // <1.3.3> 遍历拓展后缀的数组
  20. for (String extension : extensions) {
  21. // <1.3.4> 带有拓展后缀的方式,获得 View 对象,添加到 candidateViews 中
  22. String viewNameWithExtension = viewName + '.' + extension;
  23. view = viewResolver.resolveViewName(viewNameWithExtension, locale);
  24. if (view != null) {
  25. candidateViews.add(view);
  26. }
  27. }
  28. }
  29. }
  30. }
  31. // <2> 来源二,添加 defaultViews 到 candidateViews 中
  32. if (!CollectionUtils.isEmpty(this.defaultViews)) {
  33. candidateViews.addAll(this.defaultViews);
  34. }
  35. return candidateViews;
  36. }
  1. 来源一,通过 viewResolvers 解析出 View 数组结果,添加到 List<View> candidateViews

    1. 遍历 viewResolvers 数组
    2. 情况一,通过当前 ViewResolver 实现类获得 View 对象,添加到 candidateViews
    3. 情况二,遍历入参 List<MediaType> requestedMediaTypes,将带有拓展后缀的类型再通过当前 ViewResolver 实现类获得 View 对象,添加到 candidateViews

      2. 获得 MediaType 对应的拓展后缀的数组(默认情况下未配置)

      3. 遍历拓展后缀的数组

      4. 带有拓展后缀的方式,通过当前 ViewResolver 实现类获得 View 对象,添加到 candidateViews
  2. 来源二,添加 defaultViewscandidateViews

getBestView

getBestView(List<View> candidateViews, List<MediaType> requestedMediaTypes, RequestAttributes attrs)方法,筛选出最匹配的 View 对象,如下:

  1. @Nullable
  2. private View getBestView(List<View> candidateViews, List<MediaType> requestedMediaTypes, RequestAttributes attrs) {
  3. // <1> 遍历 candidateView 数组,如果有重定向的 View 类型,则返回它
  4. for (View candidateView : candidateViews) {
  5. if (candidateView instanceof SmartView) {
  6. SmartView smartView = (SmartView) candidateView;
  7. if (smartView.isRedirectView()) {
  8. return candidateView;
  9. }
  10. }
  11. }
  12. // <2> 遍历 MediaType 数组(MediaTy数组已经根据pespecificity、quality进行了排序)
  13. for (MediaType mediaType : requestedMediaTypes) {
  14. // <2> 遍历 View 数组
  15. for (View candidateView : candidateViews) {
  16. if (StringUtils.hasText(candidateView.getContentType())) {
  17. // <2.1> 如果 MediaType 类型匹配,则返回该 View 对象
  18. MediaType candidateContentType = MediaType.parseMediaType(candidateView.getContentType());
  19. if (mediaType.isCompatibleWith(candidateContentType)) {
  20. if (logger.isDebugEnabled()) {
  21. logger.debug("Selected '" + mediaType + "' given " + requestedMediaTypes);
  22. }
  23. attrs.setAttribute(View.SELECTED_CONTENT_TYPE, mediaType, RequestAttributes.SCOPE_REQUEST);
  24. return candidateView;
  25. }
  26. }
  27. }
  28. }
  29. return null;
  30. }
  1. 遍历 candidateView 数组,如果有重定向的 View 类型,则返回它。也就是说,重定向的 View ,优先级更高。
  2. 遍历 MediaType 数组(MediaTy数组已经根据pespecificityquality进行了排序)和 candidateView 数组
    1. 如果 MediaType 类型匹配该 View 对象,则返回该 View 对象。也就是说,优先级的匹配规则,由 ViewResolver 在 viewResolvers 的位置,越靠前,优先级越高。

BeanNameViewResolver

org.springframework.web.servlet.view.BeanNameViewResolver,实现 ViewResolver、Ordered 接口,继承 WebApplicationObjectSupport 抽象类,基于 Bean 的名字获得 View 对象的 ViewResolver 实现类

构造方法

  1. public class BeanNameViewResolver extends WebApplicationObjectSupport implements ViewResolver, Ordered {
  2. /**
  3. * 顺序,优先级最低
  4. */
  5. private int order = Ordered.LOWEST_PRECEDENCE; // default: same as non-Ordered
  6. }

resolveViewName

实现 resolveViewName(String viewName, Locale locale) 方法,根据名称获取 View 类型对应的 Bean(View 对象),如下:

  1. @Override
  2. @Nullable
  3. public View resolveViewName(String viewName, Locale locale) throws BeansException {
  4. ApplicationContext context = obtainApplicationContext();
  5. // 如果对应的 Bean 对象不存在,则返回 null
  6. if (!context.containsBean(viewName)) {
  7. // Allow for ViewResolver chaining...
  8. return null;
  9. }
  10. // 如果 Bean 对应的 Bean 类型不是 View ,则返回 null
  11. if (!context.isTypeMatch(viewName, View.class)) {
  12. if (logger.isDebugEnabled()) {
  13. logger.debug("Found bean named '" + viewName + "' but it does not implement View");
  14. }
  15. // Since we're looking into the general ApplicationContext here,
  16. // let's accept this as a non-match and allow for chaining as well...
  17. return null;
  18. }
  19. // 获得 Bean 名字对应的 View 对象
  20. return context.getBean(viewName, View.class);
  21. }

ViewResolverComposite

org.springframework.web.servlet.view.ViewResolverComposite,实现 ViewResolver、Ordered、InitializingBean、ApplicationContextAware、ServletContextAware 接口,复合的 ViewResolver 实现类

构造方法

  1. public class ViewResolverComposite implements ViewResolver, Ordered, InitializingBean,
  2. ApplicationContextAware, ServletContextAware {
  3. /**
  4. * ViewResolver 数组
  5. */
  6. private final List<ViewResolver> viewResolvers = new ArrayList<>();
  7. /**
  8. * 顺序,优先级最低
  9. */
  10. private int order = Ordered.LOWEST_PRECEDENCE;
  11. }

afterPropertiesSet

因为 ViewResolverComposite 实现了 InitializingBean 接口,在 Sping 初始化该 Bean 的时候,会调用该方法,完成一些初始化工作,方法如下:

  1. @Override
  2. public void afterPropertiesSet() throws Exception {
  3. for (ViewResolver viewResolver : this.viewResolvers) {
  4. if (viewResolver instanceof InitializingBean) {
  5. ((InitializingBean) viewResolver).afterPropertiesSet();
  6. }
  7. }
  8. }

resolveViewName

实现 resolveViewName(String viewName, Locale locale) 方法,代码如下:

  1. @Override
  2. @Nullable
  3. public View resolveViewName(String viewName, Locale locale) throws Exception {
  4. // 遍历 viewResolvers 数组,逐个进行解析,但凡成功,则返回该 View 对象
  5. for (ViewResolver viewResolver : this.viewResolvers) {
  6. // 执行解析
  7. View view = viewResolver.resolveViewName(viewName, locale);
  8. // 解析成功,则返回该 View 对象
  9. if (view != null) {
  10. return view;
  11. }
  12. }
  13. return null;
  14. }

AbstractCachingViewResolver

org.springframework.web.servlet.view.AbstractCachingViewResolver,实现 ViewResolver 接口,继承 WebApplicationObjectSupport 抽象类,提供通用的缓存的 ViewResolver 抽象类。对于相同的视图名,返回的是相同的 View 对象,所以通过缓存,可以进一步提供性能。

构造方法

  1. public abstract class AbstractCachingViewResolver extends WebApplicationObjectSupport implements ViewResolver {
  2. /** Default maximum number of entries for the view cache: 1024. */
  3. public static final int DEFAULT_CACHE_LIMIT = 1024;
  4. /** Dummy marker object for unresolved views in the cache Maps. */
  5. private static final View UNRESOLVED_VIEW = new View() {
  6. @Override
  7. @Nullable
  8. public String getContentType() {
  9. return null;
  10. }
  11. @Override
  12. public void render(@Nullable Map<String, ?> model, HttpServletRequest request, HttpServletResponse response) {
  13. }
  14. };
  15. /** The maximum number of entries in the cache. */
  16. private volatile int cacheLimit = DEFAULT_CACHE_LIMIT; // 缓存上限。如果 cacheLimit = 0 ,表示禁用缓存
  17. /** Whether we should refrain from resolving views again if unresolved once. */
  18. private boolean cacheUnresolved = true; // 是否缓存空 View 对象
  19. /** Fast access cache for Views, returning already cached instances without a global lock. */
  20. private final Map<Object, View> viewAccessCache = new ConcurrentHashMap<>(DEFAULT_CACHE_LIMIT); // View 的缓存的映射
  21. /** Map from view key to View instance, synchronized for View creation. */
  22. // View 的缓存的映射。相比 {@link #viewAccessCache} 来说,增加了 synchronized 锁
  23. @SuppressWarnings("serial")
  24. private final Map<Object, View> viewCreationCache =
  25. new LinkedHashMap<Object, View>(DEFAULT_CACHE_LIMIT, 0.75f, true) {
  26. @Override
  27. protected boolean removeEldestEntry(Map.Entry<Object, View> eldest) {
  28. if (size() > getCacheLimit()) {
  29. viewAccessCache.remove(eldest.getKey());
  30. return true;
  31. }
  32. else {
  33. return false;
  34. }
  35. }
  36. };
  37. }

通过 viewAccessCache 属性,提供更快的访问 View 缓存

通过 viewCreationCache 属性,提供缓存的上限的功能

KEY 是通过 getCacheKey(String viewName, Locale locale) 方法,获得缓存 KEY,方法如下:

  1. protected Object getCacheKey(String viewName, Locale locale) {
  2. return viewName + '_' + locale;
  3. }

resolveViewName

实现 resolveViewName(String viewName, Locale locale) 方法,代码如下:

  1. @Override
  2. @Nullable
  3. public View resolveViewName(String viewName, Locale locale) throws Exception {
  4. // 如果禁用缓存,则创建 viewName 对应的 View 对象
  5. if (!isCache()) {
  6. return createView(viewName, locale);
  7. }
  8. else {
  9. // 获得缓存 KEY
  10. Object cacheKey = getCacheKey(viewName, locale);
  11. // 从 viewAccessCache 缓存中,获得 View 对象
  12. View view = this.viewAccessCache.get(cacheKey);
  13. // 如果获得不到缓存,则从 viewCreationCache 中,获得 View 对象
  14. if (view == null) {
  15. synchronized (this.viewCreationCache) {
  16. // 从 viewCreationCache 中,获得 View 对象
  17. view = this.viewCreationCache.get(cacheKey);
  18. if (view == null) {
  19. // Ask the subclass to create the View object.
  20. // 创建 viewName 对应的 View 对象
  21. view = createView(viewName, locale);
  22. // 如果创建失败,但是 cacheUnresolved 为 true ,则设置为 UNRESOLVED_VIEW
  23. if (view == null && this.cacheUnresolved) {
  24. view = UNRESOLVED_VIEW;
  25. }
  26. // 如果 view 非空,则添加到 viewAccessCache 缓存中
  27. if (view != null) {
  28. this.viewAccessCache.put(cacheKey, view);
  29. this.viewCreationCache.put(cacheKey, view);
  30. }
  31. }
  32. }
  33. }
  34. else {
  35. if (logger.isTraceEnabled()) {
  36. logger.trace(formatKey(cacheKey) + "served from cache");
  37. }
  38. }
  39. return (view != UNRESOLVED_VIEW ? view : null);
  40. }
  41. }
  42. @Nullable
  43. protected View createView(String viewName, Locale locale) throws Exception {
  44. return loadView(viewName, locale);
  45. }
  46. @Nullable
  47. protected abstract View loadView(String viewName, Locale locale) throws Exception;

逻辑比较简单,主要是缓存的处理,需要通过子类去创建对应的 View 对象

UrlBasedViewResolver

org.springframework.web.servlet.view.UrlBasedViewResolver,实现 Ordered 接口,继承 AbstractCachingViewResolver 抽象类,基于 Url 的 ViewResolver 实现类

构造方法

  1. public class UrlBasedViewResolver extends AbstractCachingViewResolver implements Ordered {
  2. public static final String REDIRECT_URL_PREFIX = "redirect:";
  3. public static final String FORWARD_URL_PREFIX = "forward:";
  4. /**
  5. * View 的类型,不同的实现类,会对应一个 View 的类型
  6. */
  7. @Nullable
  8. private Class<?> viewClass;
  9. /**
  10. * 前缀
  11. */
  12. private String prefix = "";
  13. /**
  14. * 后缀
  15. */
  16. private String suffix = "";
  17. /**
  18. * ContentType 类型
  19. */
  20. @Nullable
  21. private String contentType;
  22. private boolean redirectContextRelative = true;
  23. private boolean redirectHttp10Compatible = true;
  24. @Nullable
  25. private String[] redirectHosts;
  26. /**
  27. * RequestAttributes 暴露给 View 使用时的属性
  28. */
  29. @Nullable
  30. private String requestContextAttribute;
  31. /** Map of static attributes, keyed by attribute name (String). */
  32. private final Map<String, Object> staticAttributes = new HashMap<>();
  33. /**
  34. * 是否暴露路径变量给 View 使用
  35. */
  36. @Nullable
  37. private Boolean exposePathVariables;
  38. @Nullable
  39. private Boolean exposeContextBeansAsAttributes;
  40. @Nullable
  41. private String[] exposedContextBeanNames;
  42. /**
  43. * 是否只处理指定的视图名们
  44. */
  45. @Nullable
  46. private String[] viewNames;
  47. /**
  48. * 顺序,优先级最低
  49. */
  50. private int order = Ordered.LOWEST_PRECEDENCE;
  51. }

initApplicationContext

实现 initApplicationContext() 方法,进一步初始化,代码如下:

在父类 WebApplicationObjectSupport 的父类 ApplicationObjectSupport 中可以看到,因为实现了 ApplicationContextAware 接口,则在初始化该 Bean 的时候会调用 setApplicationContext(@Nullable ApplicationContext context) 方法,在这个方法中会调用 initApplicationContext(ApplicationContext context) 这个方法,这个方法又会调用initApplicationContext() 方法

  1. @Override
  2. protected void initApplicationContext() {
  3. super.initApplicationContext();
  4. if (getViewClass() == null) {
  5. throw new IllegalArgumentException("Property 'viewClass' is required");
  6. }
  7. }

在子类中会看到 viewClass 属性一般会在构造中法中设置

getCacheKey

重写 getCacheKey(String viewName, Locale locale) 方法,忽略 locale 参数,仅仅使用 viewName 作为缓存 KEY,如下:

  1. @Override
  2. protected Object getCacheKey(String viewName, Locale locale) {
  3. // 重写了父类的方法,去除locale直接返回viewName
  4. return viewName;
  5. }

也就是说,不支持 Locale 特性

canHandle

canHandle(String viewName, Locale locale) 方法,判断传入的视图名是否可以被处理,如下:

  1. protected boolean canHandle(String viewName, Locale locale) {
  2. String[] viewNames = getViewNames();
  3. return (viewNames == null || PatternMatchUtils.simpleMatch(viewNames, viewName));
  4. }
  5. @Nullable
  6. protected String[] getViewNames() {
  7. return this.viewNames;
  8. }

一般情况下,viewNames 指定的视图名们为空,所以会满足 viewNames == null 代码块。也就说,所有视图名都可以被处理

applyLifecycleMethods

applyLifecycleMethods(String viewName, AbstractUrlBasedView view) 方法,代码如下:

  1. protected View applyLifecycleMethods(String viewName, AbstractUrlBasedView view) {
  2. // 情况一,如果 viewName 有对应的 View Bean 对象,则使用它
  3. ApplicationContext context = getApplicationContext();
  4. if (context != null) {
  5. Object initialized = context.getAutowireCapableBeanFactory().initializeBean(view, viewName);
  6. if (initialized instanceof View) {
  7. return (View) initialized;
  8. }
  9. }
  10. // 情况二,直接返回 view
  11. return view;
  12. }

createView

重写 createView(String viewName, Locale locale) 方法,增加了对 REDIRECT、FORWARD 的情况的处理,如下:

  1. @Override
  2. protected View createView(String viewName, Locale locale) throws Exception {
  3. // If this resolver is not supposed to handle the given view,
  4. // return null to pass on to the next resolver in the chain.
  5. // 是否能处理该视图名称
  6. if (!canHandle(viewName, locale)) {
  7. return null;
  8. }
  9. // Check for special "redirect:" prefix.
  10. if (viewName.startsWith(REDIRECT_URL_PREFIX)) { // 如果是 REDIRECT 开头,创建 RedirectView 视图
  11. String redirectUrl = viewName.substring(REDIRECT_URL_PREFIX.length());
  12. RedirectView view = new RedirectView(redirectUrl, isRedirectContextRelative(), isRedirectHttp10Compatible());
  13. String[] hosts = getRedirectHosts();
  14. if (hosts != null) {
  15. // 设置 RedirectView 对象的 hosts 属性
  16. view.setHosts(hosts);
  17. }
  18. // 应用
  19. return applyLifecycleMethods(REDIRECT_URL_PREFIX, view);
  20. }
  21. // Check for special "forward:" prefix.
  22. if (viewName.startsWith(FORWARD_URL_PREFIX)) { // 如果是 FORWARD 开头,创建 InternalResourceView 视图
  23. String forwardUrl = viewName.substring(FORWARD_URL_PREFIX.length());
  24. InternalResourceView view = new InternalResourceView(forwardUrl);
  25. // 应用
  26. return applyLifecycleMethods(FORWARD_URL_PREFIX, view);
  27. }
  28. // Else fall back to superclass implementation: calling loadView.
  29. // 创建视图名对应的 View 对象
  30. return super.createView(viewName, locale);
  31. }

loadView

实现 loadView(String viewName, Locale locale) 方法,加载 viewName 对应的 View 对象,方法如下:

  1. @Override
  2. protected View loadView(String viewName, Locale locale) throws Exception {
  3. // <x> 创建 viewName 对应的 View 对象
  4. AbstractUrlBasedView view = buildView(viewName);
  5. // 应用
  6. View result = applyLifecycleMethods(viewName, view);
  7. return (view.checkResource(locale) ? result : null);
  8. }

其中,<x> 处,调用 buildView(String viewName) 方法,创建 viewName 对应的 View 对象,方法如下:

  1. protected AbstractUrlBasedView buildView(String viewName) throws Exception {
  2. Class<?> viewClass = getViewClass();
  3. Assert.state(viewClass != null, "No view class");
  4. // 创建 AbstractUrlBasedView 对象
  5. AbstractUrlBasedView view = (AbstractUrlBasedView) BeanUtils.instantiateClass(viewClass);
  6. // 设置各种属性
  7. view.setUrl(getPrefix() + viewName + getSuffix());
  8. String contentType = getContentType();
  9. if (contentType != null) {
  10. view.setContentType(contentType);
  11. }
  12. view.setRequestContextAttribute(getRequestContextAttribute());
  13. view.setAttributesMap(getAttributesMap());
  14. Boolean exposePathVariables = getExposePathVariables();
  15. if (exposePathVariables != null) {
  16. view.setExposePathVariables(exposePathVariables);
  17. }
  18. Boolean exposeContextBeansAsAttributes = getExposeContextBeansAsAttributes();
  19. if (exposeContextBeansAsAttributes != null) {
  20. view.setExposeContextBeansAsAttributes(exposeContextBeansAsAttributes);
  21. }
  22. String[] exposedContextBeanNames = getExposedContextBeanNames();
  23. if (exposedContextBeanNames != null) {
  24. view.setExposedContextBeanNames(exposedContextBeanNames);
  25. }
  26. return view;
  27. }

requiredViewClass

requiredViewClass() 方法,定义了产生的视图,代码如下:

  1. protected Class<?> requiredViewClass() {
  2. return AbstractUrlBasedView.class;
  3. }

InternalResourceViewResolver

org.springframework.web.servlet.view.InternalResourceViewResolver,继承 UrlBasedViewResolver 类,解析出 JSP 的 ViewResolver 实现类

构造方法

  1. public class InternalResourceViewResolver extends UrlBasedViewResolver {
  2. /**
  3. * 判断 javax.servlet.jsp.jstl.core.Config 是否存在
  4. */
  5. private static final boolean jstlPresent = ClassUtils.isPresent(
  6. "javax.servlet.jsp.jstl.core.Config", InternalResourceViewResolver.class.getClassLoader());
  7. @Nullable
  8. private Boolean alwaysInclude;
  9. public InternalResourceViewResolver() {
  10. // 获得 viewClass
  11. Class<?> viewClass = requiredViewClass();
  12. if (InternalResourceView.class == viewClass && jstlPresent) {
  13. viewClass = JstlView.class;
  14. }
  15. // 设置 viewClass
  16. setViewClass(viewClass);
  17. }
  18. }

从构造方法中,可以看出,视图名会是 InternalResourceView 或 JstlView 类。

精尽Spring MVC源码分析 - ViewResolver 组件的更多相关文章

  1. 精尽Spring MVC源码分析 - MultipartResolver 组件

    该系列文档是本人在学习 Spring MVC 的源码过程中总结下来的,可能对读者不太友好,请结合我的源码注释 Spring MVC 源码分析 GitHub 地址 进行阅读 Spring 版本:5.2. ...

  2. 精尽Spring MVC源码分析 - HandlerMapping 组件(一)之 AbstractHandlerMapping

    该系列文档是本人在学习 Spring MVC 的源码过程中总结下来的,可能对读者不太友好,请结合我的源码注释 Spring MVC 源码分析 GitHub 地址 进行阅读 Spring 版本:5.2. ...

  3. 精尽Spring MVC源码分析 - HandlerMapping 组件(二)之 HandlerInterceptor 拦截器

    该系列文档是本人在学习 Spring MVC 的源码过程中总结下来的,可能对读者不太友好,请结合我的源码注释 Spring MVC 源码分析 GitHub 地址 进行阅读 Spring 版本:5.2. ...

  4. 精尽Spring MVC源码分析 - HandlerMapping 组件(三)之 AbstractHandlerMethodMapping

    该系列文档是本人在学习 Spring MVC 的源码过程中总结下来的,可能对读者不太友好,请结合我的源码注释 Spring MVC 源码分析 GitHub 地址 进行阅读 Spring 版本:5.2. ...

  5. 精尽Spring MVC源码分析 - HandlerMapping 组件(四)之 AbstractUrlHandlerMapping

    该系列文档是本人在学习 Spring MVC 的源码过程中总结下来的,可能对读者不太友好,请结合我的源码注释 Spring MVC 源码分析 GitHub 地址 进行阅读 Spring 版本:5.2. ...

  6. 精尽Spring MVC源码分析 - HandlerAdapter 组件(一)之 HandlerAdapter

    该系列文档是本人在学习 Spring MVC 的源码过程中总结下来的,可能对读者不太友好,请结合我的源码注释 Spring MVC 源码分析 GitHub 地址 进行阅读 Spring 版本:5.2. ...

  7. 精尽Spring MVC源码分析 - HandlerAdapter 组件(二)之 ServletInvocableHandlerMethod

    该系列文档是本人在学习 Spring MVC 的源码过程中总结下来的,可能对读者不太友好,请结合我的源码注释 Spring MVC 源码分析 GitHub 地址 进行阅读 Spring 版本:5.2. ...

  8. 精尽Spring MVC源码分析 - HandlerAdapter 组件(三)之 HandlerMethodArgumentResolver

    该系列文档是本人在学习 Spring MVC 的源码过程中总结下来的,可能对读者不太友好,请结合我的源码注释 Spring MVC 源码分析 GitHub 地址 进行阅读 Spring 版本:5.2. ...

  9. 精尽Spring MVC源码分析 - HandlerAdapter 组件(四)之 HandlerMethodReturnValueHandler

    该系列文档是本人在学习 Spring MVC 的源码过程中总结下来的,可能对读者不太友好,请结合我的源码注释 Spring MVC 源码分析 GitHub 地址 进行阅读 Spring 版本:5.2. ...

随机推荐

  1. yum安装软件时报错libmysqlclient.so.18()(64bit)

    错误信息 yum -y install sysbench 安装sysbench提示缺少依赖包如下图: 主要原因 缺少Percona-XtraDB-Cluster-shared-55-5.5.37-25 ...

  2. 【Vue】VUE源码中的一些工具函数

    Vue源码-工具方法 /* */ //Object.freeze()阻止修改现有属性的特性和值,并阻止添加新属性. var emptyObject = Object.freeze({}); // th ...

  3. Apache Hudi初学者指南

    在深入研究Hudi机制之前,让我们首先了解Hudi正在解决的问题. 客户在使用数据湖时通常会问一个问题:当源记录被更新时,如何更新数据湖?这是一个很难解决的问题,因为一旦你写了CSV或Parquet文 ...

  4. Dotnet Core下的Channel, 你用了吗?

    今天给大家分享一个微软官方的好东西:Channel.   前言 今天给大家分享一个微软官方的生产者/消费者方案的特性解决:Channel. Channel在System.Threading.Chann ...

  5. java集合源码分析(三):ArrayList

    概述 在前文:java集合源码分析(二):List与AbstractList 和 java集合源码分析(一):Collection 与 AbstractCollection 中,我们大致了解了从 Co ...

  6. 记一次容器CPU高占用问题排查

    起因:发现docker中有两个容器的CPU持续在百分之95以上运行了一晚上 执行命令:docker stats 发现这个两个大兄弟一点没歇满负荷跑了一晚上,再这么下去怕不是要GG 容器里跑的是JAVA ...

  7. python批量definition query

    import arcpy mxd = arcpy.mapping.MapDocument("current") for lyr in arcpy.mapping.ListLayer ...

  8. 阻止brew自动更新

    export HOMEBREW_NO_AUTO_UPDATE=true  

  9. pom文件中<dependencies>和<dependencyManagement>的区别

    在父pom中,如果使用了<dependencies>标签,那么在该标签体中的所有jar包,即使子工程中没有写这些依赖,依旧会引用. 如果使用了<dependencyManagemen ...

  10. moviepy音视频剪辑:使用fl_time进行时间特效处理报错ValueError: Attribute duration not set

    专栏:Python基础教程目录 专栏:使用PyQt开发图形界面Python应用 专栏:PyQt+moviepy音视频剪辑实战 专栏:PyQt入门学习 老猿Python博文目录 老猿学5G博文目录 在使 ...