精盡Spring MVC源碼分析 - ViewResolver 組件

該系列文檔是本人在學習 Spring MVC 的源碼過程當中總結下來的,可能對讀者不太友好,請結合個人源碼註釋 Spring MVC 源碼分析 GitHub 地址 進行閱讀html

Spring 版本:5.2.4.RELEASEjava

該系列其餘文檔請查看:《精盡 Spring MVC 源碼分析 - 文章導讀》git

ViewResolver 組件

ViewResolver 組件,視圖解析器,根據視圖名和國際化,得到最終的視圖 View 對象github

回顧

先來回顧一下在 DispatcherServlet 中處理請求的過程當中哪裏使用到 ViewResolver 組件,能夠回到《一個請求的旅行過程》中的 DispatcherServletrender 方法中看看,以下:web

protected void render(ModelAndView mv, HttpServletRequest request, HttpServletResponse response) throws Exception {
    // Determine locale for request and apply it to the response.
    // <1> 解析 request 中得到 Locale 對象,並設置到 response 中
    Locale locale = (this.localeResolver != null ? this.localeResolver.resolveLocale(request) : request.getLocale());
    response.setLocale(locale);

    // 得到 View 對象
    View view;
    String viewName = mv.getViewName();
    // 狀況一,使用 viewName 得到 View 對象
    if (viewName != null) {
        // We need to resolve the view name.
        // <2.1> 使用 viewName 得到 View 對象
        view = resolveViewName(viewName, mv.getModelInternal(), locale, request);
        if (view == null) { // 獲取不到,拋出 ServletException 異常
            throw new ServletException("Could not resolve view with name '" + mv.getViewName() +
                    "' in servlet with name '" + getServletName() + "'");
        }
    }
    // 狀況二,直接使用 ModelAndView 對象的 View 對象
    else {
        // No need to lookup: the ModelAndView object contains the actual View object.
        // 直接使用 ModelAndView 對象的 View 對象
        view = mv.getView();
        if (view == null) { // 獲取不到,拋出 ServletException 異常
            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 {
        // <3> 設置響應的狀態碼
        if (mv.getStatus() != null) {
            response.setStatus(mv.getStatus().value());
        }
        // <4> 渲染頁面
        view.render(mv.getModelInternal(), request, response);
    }
    catch (Exception ex) {
        if (logger.isDebugEnabled()) {
            logger.debug("Error rendering view [" + view + "]", ex);
        }
        throw ex;
    }
}

@Nullable
protected View resolveViewName(String viewName, @Nullable Map<String, Object> model,
        Locale locale, HttpServletRequest request) throws Exception {

    if (this.viewResolvers != null) {
        // 遍歷 ViewResolver 數組
        for (ViewResolver viewResolver : this.viewResolvers) {
            // 根據 viewName + locale 參數,解析出 View 對象
            View view = viewResolver.resolveViewName(viewName, locale);
            // 解析成功,直接返回 View 對象
            if (view != null) {
                return view;
            }
        }
    }
    return null;
}

若是 ModelAndView 對象不爲null,且須要進行頁面渲染,則調用 render 方法,若是設置的 View 對象是 String 類型,也就是 viewName,則須要調用 resolveViewName 方法,經過 ViewResolver 根據 viewNamelocale 解析出對應的 View 對象spring

這是先後端未分離的狀況下重要的一個組件後端

ViewResolver 接口

org.springframework.web.servlet.ViewResolver,視圖解析器,根據視圖名和國際化,得到最終的視圖 View 對象,代碼以下:數組

public interface ViewResolver {
	/**
	 * 根據視圖名和國際化,得到最終的 View 對象
	 */
	@Nullable
	View resolveViewName(String viewName, Locale locale) throws Exception;
}

ViewResolver 接口體系的結構以下:緩存

ViewResolver 的實現類比較多,其中 Spring MVC 默認使用 org.springframework.web.servlet.view.InternalResourceViewResolver 這個實現類app

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 組件,方法以下:

private void initViewResolvers(ApplicationContext context) {
    // 置空 viewResolvers 處理
    this.viewResolvers = null;

    // 狀況一,自動掃描 ViewResolver 類型的 Bean 們
    if (this.detectAllViewResolvers) {
        // Find all ViewResolvers in the ApplicationContext, including ancestor contexts.
        Map<String, ViewResolver> matchingBeans = BeanFactoryUtils.beansOfTypeIncludingAncestors(context, 
                                                                                                 ViewResolver.class, true, false);
        if (!matchingBeans.isEmpty()) {
            this.viewResolvers = new ArrayList<>(matchingBeans.values());
            // We keep ViewResolvers in sorted order.
            AnnotationAwareOrderComparator.sort(this.viewResolvers);
        }
    }
    // 狀況二,得到名字爲 VIEW_RESOLVER_BEAN_NAME 的 Bean 們
    else {
        try {
            ViewResolver vr = context.getBean(VIEW_RESOLVER_BEAN_NAME, ViewResolver.class);
            this.viewResolvers = Collections.singletonList(vr);
        }
        catch (NoSuchBeanDefinitionException ex) {
            // Ignore, we'll add a default ViewResolver later.
        }
    }

    // Ensure we have at least one ViewResolver, by registering
    // a default ViewResolver if no other resolvers are found.
    /**
     * 狀況三,若是未得到到,則得到默認配置的 ViewResolver 類
     * {@link org.springframework.web.servlet.view.InternalResourceViewResolver}
     */
    if (this.viewResolvers == null) {
        this.viewResolvers = getDefaultStrategies(context, ViewResolver.class);
        if (logger.isTraceEnabled()) {
            logger.trace("No ViewResolvers declared for servlet '" + getServletName() +
                    "': using default strategies from DispatcherServlet.properties");
        }
    }
}
  1. 若是「開啓」探測功能,則掃描已註冊的 ViewResolver 的 Bean 們,添加到 viewResolvers 中,默認開啓

  2. 若是「關閉」探測功能,則得到 Bean 名稱爲 "viewResolver" 對應的 Bean ,將其添加至 viewResolvers

  3. 若是未得到到,則得到默認配置的 ViewResolver 類,調用 getDefaultStrategies(ApplicationContext context, Class<T> strategyInterface) 方法,就是從 DispatcherServlet.properties 文件中讀取 ViewResolver 的默認實現類,以下:

    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 和拓展後綴

構造方法

public class ContentNegotiatingViewResolver extends WebApplicationObjectSupport
		implements ViewResolver, Ordered, InitializingBean {

	@Nullable
	private ContentNegotiationManager contentNegotiationManager;
	/**
	 * ContentNegotiationManager 的工廠,用於建立 {@link #contentNegotiationManager} 對象
	 */
	private final ContentNegotiationManagerFactoryBean cnmFactoryBean = new ContentNegotiationManagerFactoryBean();
	/**
	 * 在找不到 View 對象時,返回 {@link #NOT_ACCEPTABLE_VIEW}
	 */
	private boolean useNotAcceptableStatusCode = false;
	/**
	 * 默認 View 數組
	 */
	@Nullable
	private List<View> defaultViews;
	/**
	 * ViewResolver 數組
	 */
	@Nullable
	private List<ViewResolver> viewResolvers;
	/**
	 * 順序,優先級最高
	 */
	private int order = Ordered.HIGHEST_PRECEDENCE;
}
  • 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) 方法

@Override
protected void initServletContext(ServletContext servletContext) {
    // <1> 掃描全部 ViewResolver 的 Bean 們
    Collection<ViewResolver> matchingBeans = BeanFactoryUtils.
        beansOfTypeIncludingAncestors(obtainApplicationContext(), ViewResolver.class).values();
    // <1.1> 狀況一,若是 viewResolvers 爲空,則將 matchingBeans 做爲 viewResolvers 。
    // BeanNameViewResolver、ThymeleafViewResolver、ViewResolverComposite、InternalResourceViewResolver
    if (this.viewResolvers == null) {
        this.viewResolvers = new ArrayList<>(matchingBeans.size());
        for (ViewResolver viewResolver : matchingBeans) {
            if (this != viewResolver) { // 排除本身
                this.viewResolvers.add(viewResolver);
            }
        }
    }
    // <1.2> 狀況二,若是 viewResolvers 非空,則和 matchingBeans 進行比對,判斷哪些未進行初始化,進行初始化
    else {
        for (int i = 0; i < this.viewResolvers.size(); i++) {
            ViewResolver vr = this.viewResolvers.get(i);
            // 已存在在 matchingBeans 中,說明已經初始化,則直接 continue
            if (matchingBeans.contains(vr)) {
                continue;
            }
            // 不存在在 matchingBeans 中,說明還未初始化,則進行初始化
            String name = vr.getClass().getName() + i;
            obtainApplicationContext().getAutowireCapableBeanFactory().initializeBean(vr, name);
        }

    }
    // <1.3> 排序 viewResolvers 數組
    AnnotationAwareOrderComparator.sort(this.viewResolvers);
    // <2> 設置 cnmFactoryBean 的 servletContext 屬性
    this.cnmFactoryBean.setServletContext(servletContext);
}
  1. 掃描全部 ViewResolver 的 Bean 們 matchingBeans

    1. 狀況一,若是 viewResolvers 爲空,則將 matchingBeans 做爲 viewResolvers
    2. 狀況二,若是 viewResolvers 非空,則和 matchingBeans 進行比對,判斷哪些未進行初始化,進行初始化
    3. 排序 viewResolvers 數組
  2. 設置 cnmFactoryBeanservletContext 屬性爲當前 Servlet 上下文

afterPropertiesSet

由於 ContentNegotiatingViewResolver 實現了 InitializingBean 接口,在 Sping 初始化該 Bean 的時候,會調用該方法,完成一些初始化工做,方法以下:

@Override
public void afterPropertiesSet() {
    // 若是 contentNegotiationManager 爲空,則進行建立
    if (this.contentNegotiationManager == null) {
        this.contentNegotiationManager = this.cnmFactoryBean.build();
    }
    if (this.viewResolvers == null || this.viewResolvers.isEmpty()) {
        logger.warn("No ViewResolvers configured");
    }
}

resolveViewName

實現 resolveViewName(String viewName, Locale locale) 方法,代碼以下:

@Override
@Nullable
public View resolveViewName(String viewName, Locale locale) throws Exception {
    RequestAttributes attrs = RequestContextHolder.getRequestAttributes();
    Assert.state(attrs instanceof ServletRequestAttributes, "No current ServletRequestAttributes");
    // <1> 得到 MediaType 數組
    List<MediaType> requestedMediaTypes = getMediaTypes(((ServletRequestAttributes) attrs).getRequest());
    if (requestedMediaTypes != null) {
        // <2> 得到匹配的 View 數組
        List<View> candidateViews = getCandidateViews(viewName, locale, requestedMediaTypes);
        // <3> 篩選最匹配的 View 對象
        View bestView = getBestView(candidateViews, requestedMediaTypes, attrs);
        // 若是篩選成功,則返回
        if (bestView != null) {
            return bestView;
        }
    }

    String mediaTypeInfo = logger.isDebugEnabled() && requestedMediaTypes != null ? " given " + requestedMediaTypes.toString() : "";

    // <4> 若是匹配不到 View 對象,則根據 useNotAcceptableStatusCode ,返回 NOT_ACCEPTABLE_VIEW 或 null 
    if (this.useNotAcceptableStatusCode) {
        if (logger.isDebugEnabled()) {
            logger.debug("Using 406 NOT_ACCEPTABLE" + mediaTypeInfo);
        }
        return NOT_ACCEPTABLE_VIEW;
    }
    else {
        logger.debug("View remains unresolved" + mediaTypeInfo);
        return null;
    }
}
  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 變量,代碼以下:

    private static final View NOT_ACCEPTABLE_VIEW = new View() {
        @Override
        @Nullable
        public String getContentType() {
            return null;
        }
        @Override
        public void render(@Nullable Map<String, ?> model, HttpServletRequest request, HttpServletResponse response) {
            response.setStatus(HttpServletResponse.SC_NOT_ACCEPTABLE);
        }
    };

    這個 View 對象設置狀態碼爲 406

getMediaTypes

getCandidateViews(HttpServletRequest request)方法,得到 MediaType 數組,以下:

@Nullable
protected List<MediaType> getMediaTypes(HttpServletRequest request) {
    Assert.state(this.contentNegotiationManager != null, "No ContentNegotiationManager set");
    try {
        // 建立 ServletWebRequest 對象
        ServletWebRequest webRequest = new ServletWebRequest(request);
        // 從請求中,得到可接受的 MediaType 數組。默認實現是,從請求頭 ACCEPT 中獲取
        List<MediaType> acceptableMediaTypes = this.contentNegotiationManager.resolveMediaTypes(webRequest);
        // 得到可產生的 MediaType 數組
        List<MediaType> producibleMediaTypes = getProducibleMediaTypes(request);
        // 經過 acceptableTypes 來比對,將符合的 producibleType 添加到 mediaTypesToUse 結果數組中
        Set<MediaType> compatibleMediaTypes = new LinkedHashSet<>();
        for (MediaType acceptable : acceptableMediaTypes) {
            for (MediaType producible : producibleMediaTypes) {
                if (acceptable.isCompatibleWith(producible)) {
                    compatibleMediaTypes.add(getMostSpecificMediaType(acceptable, producible));
                }
            }
        }
        // 按照 MediaType 的 specificity、quality 排序
        List<MediaType> selectedMediaTypes = new ArrayList<>(compatibleMediaTypes);
        MediaType.sortBySpecificityAndQuality(selectedMediaTypes);
        return selectedMediaTypes;
    }
    catch (HttpMediaTypeNotAcceptableException ex) {
        if (logger.isDebugEnabled()) {
            logger.debug(ex.getMessage());
        }
        return null;
    }
}

getCandidateViews

getCandidateViews(String viewName, Locale locale, List<MediaType> requestedMediaTypes)方法,得到匹配的 View 數組,以下:

private List<View> getCandidateViews(String viewName, Locale locale, List<MediaType> requestedMediaTypes)
        throws Exception {

    // 建立 View 數組
    List<View> candidateViews = new ArrayList<>();
    // <1> 來源一,經過 viewResolvers 解析出 View 數組結果,添加到 candidateViews 中
    if (this.viewResolvers != null) {
        Assert.state(this.contentNegotiationManager != null, "No ContentNegotiationManager set");
        // <1.1> 遍歷 viewResolvers 數組
        for (ViewResolver viewResolver : this.viewResolvers) {
            // <1.2> 狀況一,得到 View 對象,添加到 candidateViews 中
            View view = viewResolver.resolveViewName(viewName, locale);
            if (view != null) {
                candidateViews.add(view);
            }
            // <1.3> 狀況二,帶有拓展後綴的方式,得到 View 對象,添加到 candidateViews 中
            for (MediaType requestedMediaType : requestedMediaTypes) {
                // <1.3.2> 得到 MediaType 對應的拓展後綴的數組(默認狀況下未配置)
                List<String> extensions = this.contentNegotiationManager.resolveFileExtensions(requestedMediaType);
                // <1.3.3> 遍歷拓展後綴的數組
                for (String extension : extensions) {
                    // <1.3.4> 帶有拓展後綴的方式,得到 View 對象,添加到 candidateViews 中
                    String viewNameWithExtension = viewName + '.' + extension;
                    view = viewResolver.resolveViewName(viewNameWithExtension, locale);
                    if (view != null) {
                        candidateViews.add(view);
                    }
                }
            }
        }
    }
    // <2> 來源二,添加 defaultViews 到 candidateViews 中
    if (!CollectionUtils.isEmpty(this.defaultViews)) {
        candidateViews.addAll(this.defaultViews);
    }
    return candidateViews;
}
  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 對象,以下:

@Nullable
private View getBestView(List<View> candidateViews, List<MediaType> requestedMediaTypes, RequestAttributes attrs) {
    // <1> 遍歷 candidateView 數組,若是有重定向的 View 類型,則返回它
    for (View candidateView : candidateViews) {
        if (candidateView instanceof SmartView) {
            SmartView smartView = (SmartView) candidateView;
            if (smartView.isRedirectView()) {
                return candidateView;
            }
        }
    }
    // <2> 遍歷 MediaType 數組(MediaTy數組已經根據pespecificity、quality進行了排序)
    for (MediaType mediaType : requestedMediaTypes) {
        // <2> 遍歷 View 數組
        for (View candidateView : candidateViews) {
            if (StringUtils.hasText(candidateView.getContentType())) {
                // <2.1> 若是 MediaType 類型匹配,則返回該 View 對象
                MediaType candidateContentType = MediaType.parseMediaType(candidateView.getContentType());
                if (mediaType.isCompatibleWith(candidateContentType)) {
                    if (logger.isDebugEnabled()) {
                        logger.debug("Selected '" + mediaType + "' given " + requestedMediaTypes);
                    }
                    attrs.setAttribute(View.SELECTED_CONTENT_TYPE, mediaType, RequestAttributes.SCOPE_REQUEST);
                    return candidateView;
                }
            }
        }
    }
    return null;
}
  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 實現類

構造方法

public class BeanNameViewResolver extends WebApplicationObjectSupport implements ViewResolver, Ordered {
    /**
	 * 順序,優先級最低
	 */
	private int order = Ordered.LOWEST_PRECEDENCE;  // default: same as non-Ordered
}

resolveViewName

實現 resolveViewName(String viewName, Locale locale) 方法,根據名稱獲取 View 類型對應的 Bean(View 對象),以下:

@Override
@Nullable
public View resolveViewName(String viewName, Locale locale) throws BeansException {
    ApplicationContext context = obtainApplicationContext();
    // 若是對應的 Bean 對象不存在,則返回 null
    if (!context.containsBean(viewName)) {
        // Allow for ViewResolver chaining...
        return null;
    }
    // 若是 Bean 對應的 Bean 類型不是 View ,則返回 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;
    }
    // 得到 Bean 名字對應的 View 對象
    return context.getBean(viewName, View.class);
}

ViewResolverComposite

org.springframework.web.servlet.view.ViewResolverComposite,實現 ViewResolver、Ordered、InitializingBean、ApplicationContextAware、ServletContextAware 接口,複合的 ViewResolver 實現類

構造方法

public class ViewResolverComposite implements ViewResolver, Ordered, InitializingBean,
		ApplicationContextAware, ServletContextAware {
	/**
	 * ViewResolver 數組
	 */
	private final List<ViewResolver> viewResolvers = new ArrayList<>();

	/**
	 * 順序,優先級最低
	 */
	private int order = Ordered.LOWEST_PRECEDENCE;
}

afterPropertiesSet

由於 ViewResolverComposite 實現了 InitializingBean 接口,在 Sping 初始化該 Bean 的時候,會調用該方法,完成一些初始化工做,方法以下:

@Override
public void afterPropertiesSet() throws Exception {
    for (ViewResolver viewResolver : this.viewResolvers) {
        if (viewResolver instanceof InitializingBean) {
            ((InitializingBean) viewResolver).afterPropertiesSet();
        }
    }
}

resolveViewName

實現 resolveViewName(String viewName, Locale locale) 方法,代碼以下:

@Override
@Nullable
public View resolveViewName(String viewName, Locale locale) throws Exception {
    // 遍歷 viewResolvers 數組,逐個進行解析,但凡成功,則返回該 View 對象
    for (ViewResolver viewResolver : this.viewResolvers) {
        // 執行解析
        View view = viewResolver.resolveViewName(viewName, locale);
        // 解析成功,則返回該 View 對象
        if (view != null) {
            return view;
        }
    }
    return null;
}

AbstractCachingViewResolver

org.springframework.web.servlet.view.AbstractCachingViewResolver,實現 ViewResolver 接口,繼承 WebApplicationObjectSupport 抽象類,提供通用的緩存的 ViewResolver 抽象類。對於相同的視圖名,返回的是相同的 View 對象,因此經過緩存,能夠進一步提供性能。

構造方法

public abstract class AbstractCachingViewResolver extends WebApplicationObjectSupport implements ViewResolver {

	/** Default maximum number of entries for the view cache: 1024. */
	public static final int DEFAULT_CACHE_LIMIT = 1024;

	/** Dummy marker object for unresolved views in the cache Maps. */
	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) {
		}
	};

	/** The maximum number of entries in the cache. */
	private volatile int cacheLimit = DEFAULT_CACHE_LIMIT; // 緩存上限。若是 cacheLimit = 0 ,表示禁用緩存

	/** Whether we should refrain from resolving views again if unresolved once. */
	private boolean cacheUnresolved = true; // 是否緩存空 View 對象

	/** Fast access cache for Views, returning already cached instances without a global lock. */
	private final Map<Object, View> viewAccessCache = new ConcurrentHashMap<>(DEFAULT_CACHE_LIMIT); // View 的緩存的映射

	/** Map from view key to View instance, synchronized for View creation. */
	// View 的緩存的映射。相比 {@link #viewAccessCache} 來講,增長了 synchronized 鎖
	@SuppressWarnings("serial")
	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;
					}
				}
			};
}

經過 viewAccessCache 屬性,提供更快的訪問 View 緩存

經過 viewCreationCache 屬性,提供緩存的上限的功能

KEY 是經過 getCacheKey(String viewName, Locale locale) 方法,得到緩存 KEY,方法以下:

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

resolveViewName

實現 resolveViewName(String viewName, Locale locale) 方法,代碼以下:

@Override
@Nullable
public View resolveViewName(String viewName, Locale locale) throws Exception {
    // 若是禁用緩存,則建立 viewName 對應的 View 對象
    if (!isCache()) {
        return createView(viewName, locale);
    }
    else {
        // 得到緩存 KEY
        Object cacheKey = getCacheKey(viewName, locale);
        // 從 viewAccessCache 緩存中,得到 View 對象
        View view = this.viewAccessCache.get(cacheKey);
        // 若是得到不到緩存,則從 viewCreationCache 中,得到 View 對象
        if (view == null) {
            synchronized (this.viewCreationCache) {
                // 從 viewCreationCache 中,得到 View 對象
                view = this.viewCreationCache.get(cacheKey);
                if (view == null) {
                    // Ask the subclass to create the View object.
                    // 建立 viewName 對應的 View 對象
                    view = createView(viewName, locale);
                    // 若是建立失敗,可是 cacheUnresolved 爲 true ,則設置爲 UNRESOLVED_VIEW
                    if (view == null && this.cacheUnresolved) {
                        view = UNRESOLVED_VIEW;
                    }
                    // 若是 view 非空,則添加到 viewAccessCache 緩存中
                    if (view != null) {
                        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);
}
@Nullable
protected abstract View loadView(String viewName, Locale locale) throws Exception;

邏輯比較簡單,主要是緩存的處理,須要經過子類去建立對應的 View 對象

UrlBasedViewResolver

org.springframework.web.servlet.view.UrlBasedViewResolver,實現 Ordered 接口,繼承 AbstractCachingViewResolver 抽象類,基於 Url 的 ViewResolver 實現類

構造方法

public class UrlBasedViewResolver extends AbstractCachingViewResolver implements Ordered {

	public static final String REDIRECT_URL_PREFIX = "redirect:";

	public static final String FORWARD_URL_PREFIX = "forward:";

	/**
	 * View 的類型,不一樣的實現類,會對應一個 View 的類型
	 */
	@Nullable
	private Class<?> viewClass;
	/**
	 * 前綴
	 */
	private String prefix = "";
	/**
	 * 後綴
	 */
	private String suffix = "";
	/**
	 * ContentType 類型
	 */
	@Nullable
	private String contentType;

	private boolean redirectContextRelative = true;

	private boolean redirectHttp10Compatible = true;

	@Nullable
	private String[] redirectHosts;
	/**
	 * RequestAttributes 暴露給 View 使用時的屬性
	 */
	@Nullable
	private String requestContextAttribute;

	/** Map of static attributes, keyed by attribute name (String). */
	private final Map<String, Object> staticAttributes = new HashMap<>();
	/**
	 * 是否暴露路徑變量給 View 使用
	 */
	@Nullable
	private Boolean exposePathVariables;

	@Nullable
	private Boolean exposeContextBeansAsAttributes;

	@Nullable
	private String[] exposedContextBeanNames;
	/**
	 * 是否只處理指定的視圖名們
	 */
	@Nullable
	private String[] viewNames;
	/**
	 * 順序,優先級最低
	 */
	private int order = Ordered.LOWEST_PRECEDENCE;
}

initApplicationContext

實現 initApplicationContext() 方法,進一步初始化,代碼以下:

在父類 WebApplicationObjectSupport 的父類 ApplicationObjectSupport 中能夠看到,由於實現了 ApplicationContextAware 接口,則在初始化該 Bean 的時候會調用 setApplicationContext(@Nullable ApplicationContext context) 方法,在這個方法中會調用 initApplicationContext(ApplicationContext context) 這個方法,這個方法又會調用initApplicationContext() 方法

@Override
protected void initApplicationContext() {
    super.initApplicationContext();
    if (getViewClass() == null) {
        throw new IllegalArgumentException("Property 'viewClass' is required");
    }
}

在子類中會看到 viewClass 屬性通常會在構造中法中設置

getCacheKey

重寫 getCacheKey(String viewName, Locale locale) 方法,忽略 locale 參數,僅僅使用 viewName 做爲緩存 KEY,以下:

@Override
protected Object getCacheKey(String viewName, Locale locale) {
    // 重寫了父類的方法,去除locale直接返回viewName
    return viewName;
}

也就是說,不支持 Locale 特性

canHandle

canHandle(String viewName, Locale locale) 方法,判斷傳入的視圖名是否能夠被處理,以下:

protected boolean canHandle(String viewName, Locale locale) {
    String[] viewNames = getViewNames();
    return (viewNames == null || PatternMatchUtils.simpleMatch(viewNames, viewName));
}

@Nullable
protected String[] getViewNames() {
    return this.viewNames;
}

通常狀況下,viewNames 指定的視圖名們爲空,因此會知足 viewNames == null 代碼塊。也就說,全部視圖名均可以被處理

applyLifecycleMethods

applyLifecycleMethods(String viewName, AbstractUrlBasedView view) 方法,代碼以下:

protected View applyLifecycleMethods(String viewName, AbstractUrlBasedView view) {
    // 狀況一,若是 viewName 有對應的 View Bean 對象,則使用它
    ApplicationContext context = getApplicationContext();
    if (context != null) {
        Object initialized = context.getAutowireCapableBeanFactory().initializeBean(view, viewName);
        if (initialized instanceof View) {
            return (View) initialized;
        }
    }
    // 狀況二,直接返回 view
    return view;
}

createView

重寫 createView(String viewName, Locale locale) 方法,增長了對 REDIRECT、FORWARD 的狀況的處理,以下:

@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.
    if (viewName.startsWith(REDIRECT_URL_PREFIX)) { // 若是是 REDIRECT 開頭,建立 RedirectView 視圖
        String redirectUrl = viewName.substring(REDIRECT_URL_PREFIX.length());
        RedirectView view = new RedirectView(redirectUrl, isRedirectContextRelative(), isRedirectHttp10Compatible());
        String[] hosts = getRedirectHosts();
        if (hosts != null) {
            // 設置 RedirectView 對象的 hosts 屬性
            view.setHosts(hosts);
        }
        // 應用
        return applyLifecycleMethods(REDIRECT_URL_PREFIX, view);
    }

    // Check for special "forward:" prefix.
    if (viewName.startsWith(FORWARD_URL_PREFIX)) { // 若是是 FORWARD 開頭,建立 InternalResourceView 視圖
        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.
    // 建立視圖名對應的 View 對象
    return super.createView(viewName, locale);
}

loadView

實現 loadView(String viewName, Locale locale) 方法,加載 viewName 對應的 View 對象,方法以下:

@Override
protected View loadView(String viewName, Locale locale) throws Exception {
    // <x> 建立 viewName 對應的 View 對象
    AbstractUrlBasedView view = buildView(viewName);
    // 應用
    View result = applyLifecycleMethods(viewName, view);
    return (view.checkResource(locale) ? result : null);
}

其中,<x> 處,調用 buildView(String viewName) 方法,建立 viewName 對應的 View 對象,方法以下:

protected AbstractUrlBasedView buildView(String viewName) throws Exception {
    Class<?> viewClass = getViewClass();
    Assert.state(viewClass != null, "No view class");

    // 建立 AbstractUrlBasedView 對象
    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;
}

requiredViewClass

requiredViewClass() 方法,定義了產生的視圖,代碼以下:

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

InternalResourceViewResolver

org.springframework.web.servlet.view.InternalResourceViewResolver,繼承 UrlBasedViewResolver 類,解析出 JSP 的 ViewResolver 實現類

構造方法

public class InternalResourceViewResolver extends UrlBasedViewResolver {

	/**
	 * 判斷 javax.servlet.jsp.jstl.core.Config 是否存在
	 */
	private static final boolean jstlPresent = ClassUtils.isPresent(
			"javax.servlet.jsp.jstl.core.Config", InternalResourceViewResolver.class.getClassLoader());

	@Nullable
	private Boolean alwaysInclude;

	public InternalResourceViewResolver() {
		// 得到 viewClass
		Class<?> viewClass = requiredViewClass();
		if (InternalResourceView.class == viewClass && jstlPresent) {
			viewClass = JstlView.class;
		}
		// 設置 viewClass
		setViewClass(viewClass);
	}
}

從構造方法中,能夠看出,視圖名會是 InternalResourceView 或 JstlView 類。😈 實際上,JstlView 是 InternalResourceView 的子類。

buildView

重寫 buildView(String viewName) 方法,代碼以下:

@Override
protected AbstractUrlBasedView buildView(String viewName) throws Exception {
    // 調用父方法
    InternalResourceView view = (InternalResourceView) super.buildView(viewName);
    if (this.alwaysInclude != null) {
        view.setAlwaysInclude(this.alwaysInclude);
    }
    // 設置 View 對象的相關屬性
    view.setPreventDispatchLoop(true);
    return view;
}

設置兩個屬性

View

org.springframework.web.servlet.View,Spring MVC 中的視圖對象,用於視圖渲染,代碼以下:

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

	@Nullable
	default String getContentType() {
		return null;
	}

	/**
	 * 渲染視圖
	 */
	void render(@Nullable Map<String, ?> model, HttpServletRequest request, HttpServletResponse response)
			throws Exception;
}

View 接口體系的結構以下:

能夠看到 View 的實現類很是多,本文不會詳細分析,簡單講解兩個方法

在 DispatcherServlet 中會直接調用 View 的 render(@Nullable Map<String, ?> model, HttpServletRequest request, HttpServletResponse response) 來進行渲染頁面

// AbstractView.java
@Override
public void render(@Nullable Map<String, ?> model, HttpServletRequest request,
      HttpServletResponse response) throws Exception {

   // 合併返回結果,將 Model 中的靜態數據和請求中的動態數據進行合併
   Map<String, Object> mergedModel = createMergedOutputModel(model, request, response);
   // 進行一些準備工做(修復 IE 中存在的 BUG)
   prepareResponse(request, response);
   // 進行渲染
   renderMergedOutputModel(mergedModel, getRequestToExpose(request), response);
}
  1. 將 Model 對象與請求中的數據進行合併,生成一個 Map 對象,保存進入頁面的一些數據

  2. 進行一些準備工做(修復 IE 中存在的 BUG)

  3. 調用 renderMergedOutputModel(Map<String, Object> model, HttpServletRequest request, HttpServletResponse response) 方法,頁面渲染,以下:

    // InternalResourceView.java
    @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.
       // 往請求中設置一些屬性,Locale、TimeZone、LocalizationContext
       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);
       }
    }

是否是很熟悉?😈

經過 Servlet 的 javax.servlet.RequestDispatcher 請求派發着,轉到對應的 URL

總結

本文分析了 ViewResolver 組件,視圖解析器,根據視圖名和國際化,得到最終的視圖 View 對象。Spring MVC 執行完處理器後生成一個 ModelAndView 對象,若是該對象不爲 null 而且有對應的 viewName,那麼就須要經過 ViewResolver 根據 viewName 解析出對應的 View 對象。

在 Spring MVC 和 Spring Boot 中最主要的仍是 InternalResourceViewResolver 實現類,例如這麼配置:

<bean class="org.springframework.web.servlet.view.InternalResourceViewResolver">
    <!-- 自動給後面 action 的方法 return 的字符串加上前綴和後綴,變成一個可用的地址 -->
    <property name="prefix" value="/WEB-INF/jsp/" />
    <property name="suffix" value=".jsp" />
</bean>

當返回的視圖名稱爲 login 時,View 對象的 url 就是 /WEB-INF/jsp/login.jsp,調用 View 的 render 方法進行頁面渲染時,請求會轉發到這個 url

固然,還有其餘的 ViewResolver 實現類,例如 BeanNameViewResolver,目前大多數都是先後端分離的項目,這個組件也許你不多用到

至此,《精盡 Spring MVC 源碼分析》系列最後一篇文檔已經講述完了,筆者寫的過程當中也是懵懵懂懂的狀態,寫完以後才更加的理解,對於 Spring MVC 中大部分的內容都有分析到,你會發現 Spring MVC 原來是這麼回事,開心~ 😄😄😄 其中涉及到 Spring 相關內容在努力閱讀中,敬請期待~

但願這系列文檔可以幫助你對 Spring MVC 的理解,路漫漫其修遠兮~

參考文章:芋道源碼《精盡 Spring MVC 源碼分析》

相關文章
相關標籤/搜索