該系列文件是本人在學習 Spring MVC 的原始碼過程中總結下來的,可能對讀者不太友好,請結合我的原始碼註釋 Spring MVC 原始碼分析 GitHub 地址 進行閱讀
Spring 版本:5.2.4.RELEASE
該系列其他文件請檢視:《精盡 Spring MVC 原始碼分析 - 文章導讀》
ViewResolver 元件
ViewResolver
元件,檢視解析器,根據檢視名和國際化,獲得最終的檢視 View 物件
回顧
先來回顧一下在 DispatcherServlet
中處理請求的過程中哪裡使用到 ViewResolver
元件,可以回到《一個請求的旅行過程》中的 DispatcherServlet
的 render
方法中看看,如下:
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
根據 viewName
和 locale
解析出對應的 View 物件
這是前後端未分離的情況下重要的一個元件
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
這個實現類
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
初始化過程
在 DispatcherServlet
的 initViewResolvers(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");
}
}
}
-
如果“開啟”探測功能,則掃描已註冊的 ViewResolver 的 Bean 們,新增到
viewResolvers
中,預設開啟 -
如果“關閉”探測功能,則獲得 Bean 名稱為 "viewResolver" 對應的 Bean ,將其新增至
viewResolvers
-
如果未獲得到,則獲得預設配置的 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
屬性有三個實現類,分別是 BeanNameViewResolver
、ViewResolverComposite
、InternalResourceViewResolver
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);
}
-
掃描所有 ViewResolver 的 Bean 們
matchingBeans
- 情況一,如果
viewResolvers
為空,則將matchingBeans
作為viewResolvers
- 情況二,如果
viewResolvers
非空,則和matchingBeans
進行比對,判斷哪些未進行初始化,進行初始化 - 排序
viewResolvers
陣列
- 情況一,如果
-
設定
cnmFactoryBean
的servletContext
屬性為當前 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;
}
}
-
呼叫
getMediaTypes(HttpServletRequest request)
方法,獲得 MediaType 陣列,詳情見下文 -
呼叫
getCandidateViews(String viewName, Locale locale, List<MediaType> requestedMediaTypes)
方法,獲得匹配的 View 陣列,詳情見下文 -
呼叫
getBestView(List<View> candidateViews, List<MediaType> requestedMediaTypes, RequestAttributes attrs)
方法,篩選出最匹配的 View 物件,如果篩選成功則直接返回,詳情見下文 -
如果匹配不到 View 物件,則根據
useNotAcceptableStatusCode
,返回NOT_ACCEPTABLE_VIEW
或null
,其中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;
}
-
來源一,通過
viewResolvers
解析出 View 陣列結果,新增到List<View> candidateViews
中- 遍歷
viewResolvers
陣列 - 情況一,通過當前 ViewResolver 實現類獲得 View 物件,新增到
candidateViews
中 - 情況二,遍歷入參
List<MediaType> requestedMediaTypes
,將帶有擴充字尾的型別再通過當前 ViewResolver 實現類獲得 View 物件,新增到candidateViews
中
2. 獲得 MediaType 對應的擴充字尾的陣列(預設情況下未配置)
3. 遍歷擴充字尾的陣列
4. 帶有擴充字尾的方式,通過當前 ViewResolver 實現類獲得 View 物件,新增到candidateViews
中
- 遍歷
-
來源二,新增
defaultViews
到candidateViews
中
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;
}
- 遍歷
candidateView
陣列,如果有重定向的 View 型別,則返回它。也就是說,重定向的 View ,優先順序更高。 - 遍歷 MediaType 陣列(MediaTy陣列已經根據
pespecificity
、quality
進行了排序)和candidateView
陣列- 如果 MediaType 型別匹配該 View 物件,則返回該 View 物件。也就是說,優先順序的匹配規則,由 ViewResolver 在
viewResolvers
的位置,越靠前,優先順序越高。
- 如果 MediaType 型別匹配該 View 物件,則返回該 View 物件。也就是說,優先順序的匹配規則,由 ViewResolver 在
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);
}
-
將 Model 物件與請求中的資料進行合併,生成一個 Map 物件,儲存進入頁面的一些資料
-
進行一些準備工作(修復 IE 中存在的 BUG)
-
呼叫
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 原始碼分析》