Spring MVC學(xué)習(xí)教程之視圖深入解析
前言
在RequestMappingHandlerAdapter對(duì)request進(jìn)行了適配,并且調(diào)用了目標(biāo)handler之后,其會(huì)返回一個(gè)ModelAndView對(duì)象,該對(duì)象中主要封裝了兩個(gè)屬性:view和model。其中view可以是字符串類(lèi)型也可以是View類(lèi)型,如果是字符串類(lèi)型,則表示邏輯視圖名,如果是View類(lèi)型,則其即為我們要轉(zhuǎn)換的目標(biāo)view;這里model是一個(gè)Map類(lèi)型的對(duì)象,其保存了渲染視圖所需要的屬性。本文主要講解Spring是如何通過(guò)用戶(hù)配置的ViewResolver來(lái)對(duì)視圖進(jìn)行解析,并且聲稱(chēng)頁(yè)面進(jìn)行渲染的。
首先我們來(lái)看一個(gè)比較典型的ViewResolver配置:
<bean class="org.springframework.web.servlet.view.InternalResourceViewResolver"> <property name="prefix" value="/WEB-INF/view/"/> <property name="suffix" value=".jsp"/> </bean>
這里配置的ViewResolver是InternalResourceViewResolver,其主要有兩個(gè)屬性:prefix和suffix。在進(jìn)行視圖解析時(shí),如果ModelAndView中的view是字符串類(lèi)型的,那么要解析的視圖存儲(chǔ)位置就通過(guò)“prefix + (String)view + suffix”的格式生成要解析的文件路徑,并且將其封裝為一個(gè)View對(duì)象,最后通過(guò)View對(duì)象來(lái)渲染具體的視圖。前面講到,ModelAndView中view也可以是View類(lèi)型的,如果其是View類(lèi)型的,那么這里就可以跳過(guò)第一步,直接使用其提供的View對(duì)象進(jìn)行視圖解析了。
由上面的講解可以看出,對(duì)于視圖的解析可以分為兩個(gè)步驟:①解析邏輯視圖名;②渲染視圖。對(duì)應(yīng)于這兩步,Spring也抽象了兩個(gè)接口:ViewResolver和View,這兩個(gè)接口的聲明分別如下:
public interface ViewResolver {
// 通過(guò)邏輯視圖名和用戶(hù)地區(qū)信息生成View對(duì)象
View resolveViewName(String viewName, Locale locale) throws Exception;
}
public interface View {
// 獲取返回值的contentType
default String getContentType() {
return null;
}
// 通過(guò)用戶(hù)提供的模型數(shù)據(jù)與視圖信息渲染視圖
void render(@Nullable Map<String, ?> model, HttpServletRequest request,
HttpServletResponse response) throws Exception;
}
從上面兩個(gè)接口的聲明可以看出,ViewResolver的作用主要在于通過(guò)用戶(hù)提供的邏輯視圖名根據(jù)一定的策略生成一個(gè)View對(duì)象,而View接口則負(fù)責(zé)根據(jù)視圖信息和需要填充的模型數(shù)據(jù)進(jìn)行視圖的渲染。這里我們首先看InternalResourceViewResolver是如何解析視圖名的,如下是其具體實(shí)現(xiàn)方式:
@Override
@Nullable
public View resolveViewName(String viewName, Locale locale) throws Exception {
// 判斷當(dāng)前ViewResolver是否設(shè)置了需要對(duì)需要解析的視圖進(jìn)行緩存,如果不需要緩存,
// 則每次請(qǐng)求時(shí)都會(huì)重新解析生成視圖對(duì)象
if (!isCache()) {
// 根據(jù)視圖名稱(chēng)和用戶(hù)地區(qū)信息創(chuàng)建View對(duì)象
return createView(viewName, locale);
} else {
// 如果可以對(duì)視圖進(jìn)行緩存,則首先獲取緩存使用的key,然后從緩存中獲取該key,如果沒(méi)有取到,
// 則對(duì)其進(jìn)行加鎖,再次獲取,如果還是沒(méi)有取到,則創(chuàng)建一個(gè)新的View,并且對(duì)其進(jìn)行緩存。
// 這里使用的是雙檢查法來(lái)判斷緩存中是否存在對(duì)應(yīng)的邏輯視圖。
Object cacheKey = getCacheKey(viewName, locale);
View view = this.viewAccessCache.get(cacheKey);
if (view == null) {
synchronized (this.viewCreationCache) {
view = this.viewCreationCache.get(cacheKey);
if (view == null) {
view = createView(viewName, locale);
// 這里cacheUnresolved指的是是否緩存默認(rèn)的空視圖,UNRESOLVED_VIEW是
// 一個(gè)沒(méi)有任何內(nèi)容的View
if (view == null && this.cacheUnresolved) {
view = UNRESOLVED_VIEW;
}
if (view != null) {
this.viewAccessCache.put(cacheKey, view);
this.viewCreationCache.put(cacheKey, view);
if (logger.isTraceEnabled()) {
logger.trace("Cached view [" + cacheKey + "]");
}
}
}
}
}
return (view != UNRESOLVED_VIEW ? view : null);
}
}
上面代碼中,InternalResourceViewResolver主要是判斷了當(dāng)前是否配置了需要緩存生成的View對(duì)象,如果需要緩存,則從緩存中取,如果沒(méi)有配置,則每次請(qǐng)求時(shí)都會(huì)重新生成新的View對(duì)象。這里我們繼續(xù)看其是如何創(chuàng)建視圖的:
@Override
protected View loadView(String viewName, Locale locale) throws Exception {
// 使用邏輯視圖名按照指定規(guī)則生成View對(duì)象
AbstractUrlBasedView view = buildView(viewName);
// 應(yīng)用聲明周期函數(shù),也就是調(diào)用View對(duì)象的初始化函數(shù)和Spring用于切入bean創(chuàng)建的
// Processor和Aware函數(shù)
View result = applyLifecycleMethods(viewName, view);
// 檢查view的準(zhǔn)確性,這里默認(rèn)始終返回true
return (view.checkResource(locale) ? result : null);
}
// 這里buildView()方法主要是根據(jù)邏輯視圖名生成一個(gè)View對(duì)象
protected AbstractUrlBasedView buildView(String viewName) throws Exception {
// 對(duì)于InternalResourceViewResolver而言,其返回的View對(duì)象的
// 具體類(lèi)型是InternalResourceView
Class<?> viewClass = getViewClass();
Assert.state(viewClass != null, "No view class");
// 使用反射生成InternalResourceView對(duì)象實(shí)例
AbstractUrlBasedView view = (AbstractUrlBasedView)
BeanUtils.instantiateClass(viewClass);
// 這里可以看出,InternalResourceViewResolver獲取目標(biāo)視圖的方式就是將用戶(hù)返回的
// viewName與prefix和suffix進(jìn)行拼接,以供View對(duì)象直接讀取
view.setUrl(getPrefix() + viewName + getSuffix());
// 設(shè)置View的contentType屬性
String contentType = getContentType();
if (contentType != null) {
view.setContentType(contentType);
}
// 設(shè)置contextAttribute和attributeMap等屬性
view.setRequestContextAttribute(getRequestContextAttribute());
view.setAttributesMap(getAttributesMap());
// 這了pathVariables表示request請(qǐng)求url中的屬性,這里主要是設(shè)置是否將這些屬性暴露到視圖中
Boolean exposePathVariables = getExposePathVariables();
if (exposePathVariables != null) {
view.setExposePathVariables(exposePathVariables);
}
// 這里設(shè)置的是是否將Spring的bean暴露在視圖中,以供給前端調(diào)用
Boolean exposeContextBeansAsAttributes = getExposeContextBeansAsAttributes();
if (exposeContextBeansAsAttributes != null) {
view.setExposeContextBeansAsAttributes(exposeContextBeansAsAttributes);
}
// 設(shè)置需要暴露給前端頁(yè)面的bean名稱(chēng)
String[] exposedContextBeanNames = getExposedContextBeanNames();
if (exposedContextBeanNames != null) {
view.setExposedContextBeanNames(exposedContextBeanNames);
}
return view;
}
protected View applyLifecycleMethods(String viewName, AbstractUrlBasedView view) {
ApplicationContext context = getApplicationContext();
if (context != null) {
// 對(duì)生成的View對(duì)象應(yīng)用初始化方法,主要包括InitializingBean.afterProperties()和一些
// Processor,Aware方法
Object initialized = context.getAutowireCapableBeanFactory()
.initializeBean(view, viewName);
if (initialized instanceof View) {
return (View) initialized;
}
}
return view;
}
從上面對(duì)于視圖名稱(chēng)的解析,可以看出,其主要做了四部分工作:①實(shí)例化View對(duì)象;②設(shè)置目標(biāo)視圖地址;③初始化視圖的一些基本屬性,如需要暴露的bean對(duì)象;④調(diào)用View對(duì)象的初始化方法對(duì)其進(jìn)行初始化。從這里的生成View對(duì)象的過(guò)程也可以看出,ViewResolver生成的View對(duì)象只是保存了目標(biāo)view的地址,而對(duì)其加載和渲染的過(guò)程主要是委托給了View對(duì)象進(jìn)行的。下面我們就來(lái)看一下InternalResourceView是如何結(jié)合具體的model來(lái)渲染視圖的:
@Override
public void render(@Nullable Map<String, ?> model, HttpServletRequest request,
HttpServletResponse response) throws Exception {
if (logger.isTraceEnabled()) {
logger.trace("Rendering view with name '" + this.beanName + "' with model "
+ model + " and static attributes " + this.staticAttributes);
}
// 這里主要是將request中pathVariable,staticAttribute與用戶(hù)返回的model屬性
// 合并為一個(gè)Map對(duì)象,以供給后面對(duì)視圖的渲染使用
Map<String, Object> mergedModel = createMergedOutputModel(model, request, response);
// 判斷當(dāng)前View對(duì)象的類(lèi)型是否為文件下載類(lèi)型,如果是文件下載類(lèi)型,則設(shè)置response的
// Pragma和Cache-Control等屬性值
prepareResponse(request, response);
// 通過(guò)合并的model數(shù)據(jù)以及視圖地址進(jìn)行視圖的渲染
renderMergedOutputModel(mergedModel, getRequestToExpose(request), response);
}
這里對(duì)于視圖的渲染主要分為了三步:①合并用戶(hù)返回的model數(shù)據(jù)和request中的pathVariable與staticAttribute等數(shù)據(jù);②判斷當(dāng)前是否為文件下載類(lèi)型的視圖解析,如果是,則設(shè)置Pragma和Cache-Control等header;③通過(guò)合并的模型數(shù)據(jù)和request請(qǐng)求對(duì)視圖進(jìn)行渲染。這里我們主要看一下renderMergedOutputModel()方法是如何對(duì)視圖進(jìn)行渲染的:
@Override
protected void renderMergedOutputModel(Map<String, Object> model,
HttpServletRequest request, HttpServletResponse response) throws Exception {
// 這里主要是對(duì)model進(jìn)行遍歷,將其key和value設(shè)置到request中,當(dāng)做request的
// 一個(gè)屬性供給頁(yè)面調(diào)用
exposeModelAsRequestAttributes(model, request);
// 提供的一個(gè)hook方法,默認(rèn)是空實(shí)現(xiàn),用于用戶(hù)進(jìn)行request屬性的自定義使用
exposeHelpers(request);
// 檢查當(dāng)前是否存在循環(huán)類(lèi)型的視圖名稱(chēng)解析,主要是根據(jù)相對(duì)路徑進(jìn)行判斷視圖名是無(wú)法解析的
String dispatcherPath = prepareForRendering(request, response);
// 獲取當(dāng)前request的RequestDispatcher對(duì)象,該對(duì)象有兩個(gè)方法:include()和forward(),
// 用于對(duì)當(dāng)前的request進(jìn)行轉(zhuǎn)發(fā),其實(shí)也就是將當(dāng)前的request轉(zhuǎn)發(fā)到另一個(gè)url,這里的另一個(gè)
// url就是要解析的視圖地址,也就是說(shuō)進(jìn)行視圖解析的時(shí)候請(qǐng)求的對(duì)于文件的解析實(shí)際上相當(dāng)于
// 構(gòu)造了另一個(gè)(文件)請(qǐng)求,在該請(qǐng)求中對(duì)文件內(nèi)容進(jìn)行渲染,從而得到最終的文件。這里的
// include()方法表示將目標(biāo)文件引入到當(dāng)前文件中,與jsp中的include標(biāo)簽作用相同;
// forward()請(qǐng)求則表示將當(dāng)前請(qǐng)求轉(zhuǎn)發(fā)到另一個(gè)請(qǐng)求中,也就是目標(biāo)文件路徑,這種轉(zhuǎn)發(fā)并不會(huì)
// 改變用戶(hù)瀏覽器地址欄的請(qǐng)求地址。
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!");
}
// 判斷當(dāng)前是否為include請(qǐng)求,如果是,則調(diào)用RequestDispatcher.include()方法進(jìn)行文件引入
if (useInclude(request, response)) {
response.setContentType(getContentType());
if (logger.isDebugEnabled()) {
logger.debug("Including resource [" + getUrl() + "] in InternalResourceView '"
+ getBeanName() + "'");
}
rd.include(request, response);
} else {
if (logger.isDebugEnabled()) {
logger.debug("Forwarding to resource [" + getUrl()
+ "] in InternalResourceView '" + getBeanName() + "'");
}
// 如果當(dāng)前不是include()請(qǐng)求,則直接使用forward請(qǐng)求將當(dāng)前請(qǐng)求轉(zhuǎn)發(fā)到目標(biāo)文件路徑中,
// 從而渲染該視圖
rd.forward(request, response);
}
}
上述代碼就是進(jìn)行視圖渲染的核心邏輯,上述邏輯主要分為兩個(gè)步驟:①將需要在頁(yè)面渲染使用的model數(shù)據(jù)設(shè)置到request中;②按照當(dāng)前請(qǐng)求的方式(include或forward)來(lái)將當(dāng)前請(qǐng)求轉(zhuǎn)發(fā)到目標(biāo)文件中,從而達(dá)到目標(biāo)文件的渲染。從這里可以看出,實(shí)際上對(duì)于Spring而言,其對(duì)頁(yè)面的渲染并不是在其原始的request中完成的。
本文首先講解了Spring進(jìn)行視圖渲染所需要的兩大組件ViewResolver和View的關(guān)系,然后以InternalResourceViewResolver和InternalResourceView為例講解Spring底層是如何解析一個(gè)view,并且渲染該View的。
總結(jié)
以上就是這篇文章的全部?jī)?nèi)容了,希望本文的內(nèi)容對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,如果有疑問(wèn)大家可以留言交流,謝謝大家對(duì)腳本之家的支持。
相關(guān)文章
詳解SpringBoot如何實(shí)現(xiàn)統(tǒng)一后端返回格式
在前后端分離的項(xiàng)目中后端返回的格式一定要友好,不然會(huì)對(duì)前端的開(kāi)發(fā)人員帶來(lái)很多的工作量。那么SpringBoot如何做到統(tǒng)一的后端返回格式呢?本文將為大家詳細(xì)講講2022-04-04
Mybatis resultType返回結(jié)果為null的問(wèn)題排查方式
這篇文章主要介紹了Mybatis resultType返回結(jié)果為null的問(wèn)題排查方式,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2022-03-03
SpringBoot+MySQL實(shí)現(xiàn)讀寫(xiě)分離的多種具體方案
在高并發(fā)和大數(shù)據(jù)量的場(chǎng)景下,數(shù)據(jù)庫(kù)成為了系統(tǒng)的瓶頸。為了提高數(shù)據(jù)庫(kù)的處理能力和性能,讀寫(xiě)分離成為了一種常用的解決方案,本文將介紹在Spring?Boot項(xiàng)目中實(shí)現(xiàn)MySQL數(shù)據(jù)庫(kù)讀寫(xiě)分離的多種具體方案,需要的朋友可以參考下2023-06-06
Springboot+TCP監(jiān)聽(tīng)服務(wù)器搭建過(guò)程圖解
這篇文章主要介紹了Springboot+TCP監(jiān)聽(tīng)服務(wù)器搭建過(guò)程,本文通過(guò)圖文并茂的形式給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2020-10-10
前端如何傳遞Array、Map類(lèi)型數(shù)據(jù)到Java后端
這篇文章主要給大家介紹了關(guān)于前端如何傳遞Array、Map類(lèi)型數(shù)據(jù)到Java后端的相關(guān)資料,文中通過(guò)圖文介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2024-01-01
mybatis多個(gè)plugins的執(zhí)行順序解析
這篇文章主要介紹了mybatis多個(gè)plugins的執(zhí)行順序解析,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2021-09-09
mybatis如何使用Criteria的and和or進(jìn)行聯(lián)合查詢(xún)
這篇文章主要介紹了mybatis如何使用Criteria的and和or進(jìn)行聯(lián)合查詢(xún),具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2021-12-12
IDEA 2020.2 部署JSF項(xiàng)目的詳細(xì)過(guò)程
本文通過(guò)圖文并茂的形式教大家如何在IDEA中創(chuàng)建一個(gè)JSF項(xiàng)目及遇到問(wèn)題的解決方法,感興趣的朋友跟隨小編一起看看吧2021-09-09
Spring中的@ConfigurationProperties在方法上的使用詳解
這篇文章主要介紹了Spring中的@ConfigurationProperties在方法上的使用詳解,@ConfigurationProperties應(yīng)該經(jīng)常被使用到,作用在類(lèi)上的時(shí)候,將該類(lèi)的屬性取值?與配置文件綁定,并生成配置bean對(duì)象,放入spring容器中,提供給其他地方使用,需要的朋友可以參考下2024-01-01

