精盡Spring MVC原始碼分析 - ViewResolver 元件
阿新 • • 發佈:2020-12-23
> 該系列文件是本人在學習 Spring MVC 的原始碼過程中總結下來的,可能對讀者不太友好,請結合我的原始碼註釋 [Spring MVC 原始碼分析 GitHub 地址](https://github.com/liu844869663/spring-framework) 進行閱讀
>
> Spring 版本:5.2.4.RELEASE
>
> 該系列其他文件請檢視:[**《精盡 Spring MVC 原始碼分析 - 文章導讀》**](https://www.cnblogs.com/lifullmoon/p/14123963.html)
## ViewResolver 元件
`ViewResolver` 元件,檢視解析器,根據檢視名和國際化,獲得最終的檢視 View 物件
### 回顧
先來回顧一下在 `DispatcherServlet` 中處理請求的過程中哪裡使用到 `ViewResolver` 元件,可以回到[**《一個請求的旅行過程》**](https://www.cnblogs.com/lifullmoon/p/14131862.html)中的 `DispatcherServlet` 的 `render` 方法中看看,如下:
```java
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 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 物件,程式碼如下:
```java
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 元件,方法如下:
```java
private void initViewResolvers(ApplicationContext context) {
// 置空 viewResolvers 處理
this.viewResolvers = null;
// 情況一,自動掃描 ViewResolver 型別的 Bean 們
if (this.detectAllViewResolvers) {
// Find all ViewResolvers in the ApplicationContext, including ancestor contexts.
Map 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 strategyInterface)` 方法,就是從 `DispatcherServlet.properties` 檔案中讀取 ViewResolver 的預設實現類,如下:
```properties
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` 和拓展字尾
#### 構造方法
```java
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 defaultViews;
/**
* ViewResolver 陣列
*/
@Nullable
private List 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)` 方法
```java
@Override
protected void initServletContext(ServletContext servletContext) {
// <1> 掃描所有 ViewResolver 的 Bean 們
Collection 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. 設定 `cnmFactoryBean` 的 `servletContext` 屬性為當前 Servlet 上下文
#### afterPropertiesSet
因為 ContentNegotiatingViewResolver 實現了 InitializingBean 介面,在 Sping 初始化該 Bean 的時候,會呼叫該方法,完成一些初始化工作,方法如下:
```java
@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)` 方法,程式碼如下:
```java
@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 requestedMediaTypes = getMediaTypes(((ServletRequestAttributes) attrs).getRequest());
if (requestedMediaTypes != null) {
// <2> 獲得匹配的 View 陣列
List 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 requestedMediaTypes)` 方法,獲得匹配的 View 陣列,詳情見下文
3. 呼叫 `getBestView(List candidateViews, List requestedMediaTypes, RequestAttributes attrs)` 方法,篩選出最匹配的 View 物件,如果篩選成功則直接返回,詳情見下文
4. 如果匹配不到 View 物件,則根據 `useNotAcceptableStatusCode`,返回 `NOT_ACCEPTABLE_VIEW` 或 `null`,其中`NOT_ACCEPTABLE_VIEW` 變數,程式碼如下:
```java
private static final View NOT_ACCEPTABLE_VIEW = new View() {
@Override
@Nullable
public String getContentType() {
return null;
}
@Override
public void render(@Nullable Map model, HttpServletRequest request, HttpServletResponse response) {
response.setStatus(HttpServletResponse.SC_NOT_ACCEPTABLE);
}
};
```
這個 View 物件設定狀態碼為 `406`
#### getMediaTypes
`getCandidateViews(HttpServletRequest request)`方法,獲得 MediaType 陣列,如下:
```java
@Nullable
protected List getMediaTypes(HttpServletRequest request) {
Assert.state(this.contentNegotiationManager != null, "No ContentNegotiationManager set");
try {
// 建立 ServletWebRequest 物件
ServletWebRequest webRequest = new ServletWebRequest(request);
// 從請求中,獲得可接受的 MediaType 陣列。預設實現是,從請求頭 ACCEPT 中獲取
List acceptableMediaTypes = this.contentNegotiationManager.resolveMediaTypes(webRequest);
// 獲得可產生的 MediaType 陣列
List producibleMediaTypes = getProducibleMediaTypes(request);
// 通過 acceptableTypes 來比對,將符合的 producibleType 新增到 mediaTypesToUse 結果陣列中
Set compatibleMediaTypes = new LinkedHashSet<>();
for (MediaType acceptable : acceptableMediaTypes) {
for (MediaType producible : producibleMediaTypes) {
if (acceptable.isCompatibleWith(producible)) {
compatibleMediaTypes.add(getMostSpecificMediaType(acceptable, producible));
}
}
}
// 按照 MediaType 的 specificity、quality 排序
List 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 requestedMediaTypes)`方法,獲得匹配的 View 陣列,如下:
```java
private List getCandidateViews(String viewName, Locale locale, List requestedMediaTypes)
throws Exception {
// 建立 View 陣列
List 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 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 candidateViews` 中
1. 遍歷 `viewResolvers` 陣列
2. 情況一,通過當前 ViewResolver 實現類獲得 View 物件,新增到 `candidateViews` 中
3. 情況二,遍歷入參 `List requestedMediaTypes`,將帶有拓展字尾的型別再通過當前 ViewResolver 實現類獲得 View 物件,新增到 `candidateViews` 中
2. 獲得 MediaType 對應的拓展字尾的陣列(預設情況下未配置)
3. 遍歷拓展字尾的陣列
4. 帶有拓展字尾的方式,通過當前 ViewResolver 實現類獲得 View 物件,新增到 `candidateViews` 中
2. 來源二,新增 `defaultViews` 到 `candidateViews` 中
#### getBestView
`getBestView(List candidateViews, List requestedMediaTypes, RequestAttributes attrs)`方法,篩選出最匹配的 View 物件,如下:
```java
@Nullable
private View getBestView(List candidateViews, List 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陣列已經根據`pespecificity`、`quality`進行了排序)和 `candidateView` 陣列
1. 如果 MediaType 型別匹配該 View 物件,則返回該 View 物件。也就是說,優先順序的匹配規則,由 ViewResolver 在 `viewResolvers` 的位置,越靠前,優先順序越高。
### BeanNameViewResolver
`org.springframework.web.servlet.view.BeanNameViewResolver`,實現 ViewResolver、Ordered 介面,繼承 WebApplicationObjectSupport 抽象類,基於 Bean 的名字獲得 View 物件的 ViewResolver 實現類
#### 構造方法
```java
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 物件),如下:
```java
@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 實現類
#### 構造方法
```java
public class ViewResolverComposite implements ViewResolver, Ordered, InitializingBean,
ApplicationContextAware, ServletContextAware {
/**
* ViewResolver 陣列
*/
private final List viewResolvers = new ArrayList<>();
/**
* 順序,優先順序最低
*/
private int order = Ordered.LOWEST_PRECEDENCE;
}
```
#### afterPropertiesSet
因為 ViewResolverComposite 實現了 InitializingBean 介面,在 Sping 初始化該 Bean 的時候,會呼叫該方法,完成一些初始化工作,方法如下:
```java
@Override
public void afterPropertiesSet() throws Exception {
for (ViewResolver viewResolver : this.viewResolvers) {
if (viewResolver instanceof InitializingBean) {
((InitializingBean) viewResolver).afterPropertiesSet();
}
}
}
```
#### resolveViewName
實現 `resolveViewName(String viewName, Locale locale)` 方法,程式碼如下:
```java
@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 物件,所以通過快取,可以進一步提供效能。
#### 構造方法
```java
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 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