Spring MVC 中,我們在返回邏輯視圖時,框架會通過 viewResolver 來解析得到具體的 View,然后向瀏覽器渲染。假設(shè)邏輯視圖名為 hello,通過配置,我們
配置某個 ViewResolver 如下:
-
<
bean
?
class
=
"org.springframework.web.servlet.view.InternalResourceViewResolver"
>
??
-
????
<
description
>
??
-
????????假如邏輯試圖名為?"hello",因此?viewResolver?將解析成?/WEB-INF/jsp/hello.jsp ??
-
????
</
description
>
??
-
????
<
property
?
name
=
"order"
?
value
=
"10"
?
/>
??
-
????
<
property
?
name
=
"prefix"
?
value
=
"/WEB-INF/jsp/"
?
/>
??
-
????
<
property
?
name
=
"suffix"
?
value
=
".jsp"
?
/>
??
-
</
bean
>
??
<bean class="org.springframework.web.servlet.view.InternalResourceViewResolver">
<description>
假如邏輯試圖名為 "hello",因此 viewResolver 將解析成 /WEB-INF/jsp/hello.jsp
</description>
<property name="order" value="10" />
<property name="prefix" value="/WEB-INF/jsp/" />
<property name="suffix" value=".jsp" />
</bean>
??????? 實(shí)際上,框架還是通過 forward 的方式轉(zhuǎn)發(fā)到了 /WEB-INF/jsp/hello.jsp。如果邏輯視圖名是 /hello,實(shí)際還是轉(zhuǎn)發(fā)到了 /WEB-INF/jsp/hello.jsp,即 /WEB-INF/jsp//hello.jsp 等同于 /WEB-INF/jsp/hello.jsp。
??????? 現(xiàn)在有個問題,如果 /hello 就是某個 controller 的映射,我想轉(zhuǎn)發(fā)到這個 controller,怎么辦?我們可以通過
forward 前綴來達(dá)到轉(zhuǎn)發(fā)到其它資源的目的:
-
public
?String?handle()?{ ??
-
????
??
-
????
??
-
????
??
-
????
return
?
"forward:/hello"
; ??
-
}??
public String handle() {
// return "forward:/hello" => 轉(zhuǎn)發(fā)到能夠匹配 /hello 的 controller 上
// return "hello" => 實(shí)際上還是轉(zhuǎn)發(fā),只不過是框架會找到該邏輯視圖名對應(yīng)的 View 并渲染
// return "/hello" => 同 return "hello"
return "forward:/hello";
}
??????? 同理,如果我們想重定向到某個資源,我們可以通過
redirect 前綴來達(dá)到重定向到其它資源的目的:
-
public
?String?handle()?{ ??
-
????
??
-
????
return
?
"redirect:/hello"
; ??
-
}??
public String handle() {
// 重定向到 /hello 資源
return "redirect:/hello";
}
??????? 還記得
java web 中的轉(zhuǎn)發(fā)和重定向
這篇文章嗎?我強(qiáng)調(diào)過,如果想做轉(zhuǎn)發(fā)操作,不需要寫 contextPath;如果想做重定向操作,推薦寫包括 contextPath 在內(nèi)的 url。因此,
在使用 Spring MVC 的 redirect 前綴時,里面是有坑的!
??????? 仍然假設(shè)應(yīng)用程序的 contextPath 為 /ctx。我們來看看
RedirectView.renderMergedOutputModel 的片段:
-
protected
?
void
?renderMergedOutputModel( ??
-
????Map<String,?Object>?model,?HttpServletRequest?request,?HttpServletResponse?response) ??
-
????
throws
?IOException?{ ??
-
??
-
??
??
-
??StringBuilder?targetUrl?=?
new
?StringBuilder(); ??
-
??
if
?(
this
.contextRelative?&&?getUrl().startsWith(
"/"
))?{ ??
-
????
??
-
????targetUrl.append(request.getContextPath()); ??
-
??} ??
-
??targetUrl.append(getUrl()); ??
-
??
-
??
??
-
??
-
??sendRedirect(request,?response,?targetUrl.toString(),?
this
.http10Compatible); ??
-
} ??
-
??
-
protected
?
void
?sendRedirect( ??
-
????HttpServletRequest?request,?HttpServletResponse?response,?String?targetUrl,?
boolean
?http10Compatible) ??
-
????
throws
?IOException?{ ??
-
??
-
??
if
?(http10Compatible)?{ ??
-
????
??
-
????response.sendRedirect(response.encodeRedirectURL(targetUrl)); ??
-
??} ??
-
??
else
?{ ??
-
????HttpStatus?statusCode?=?getHttp11StatusCode(request,?response,?targetUrl); ??
-
????response.setStatus(statusCode.value()); ??
-
????response.setHeader(
"Location"
,?response.encodeRedirectURL(targetUrl)); ??
-
??} ??
-
}??
protected void renderMergedOutputModel(
Map<String, Object> model, HttpServletRequest request, HttpServletResponse response)
throws IOException {
// Prepare target URL.
StringBuilder targetUrl = new StringBuilder();
if (this.contextRelative && getUrl().startsWith("/")) {
// Do not apply context path to relative URLs.
targetUrl.append(request.getContextPath());
}
targetUrl.append(getUrl());
// ...
sendRedirect(request, response, targetUrl.toString(), this.http10Compatible);
}
protected void sendRedirect(
HttpServletRequest request, HttpServletResponse response, String targetUrl, boolean http10Compatible)
throws IOException {
if (http10Compatible) {
// Always send status code 302.
response.sendRedirect(response.encodeRedirectURL(targetUrl));
}
else {
HttpStatus statusCode = getHttp11StatusCode(request, response, targetUrl);
response.setStatus(statusCode.value());
response.setHeader("Location", response.encodeRedirectURL(targetUrl));
}
}
??????? sendRedirect 方法沒什么特別的,它就是調(diào)用 HttpServletResponse 的 sendRedirect 方法而已。因此,關(guān)鍵點(diǎn)就是 renderMergedOutputModel 方法對轉(zhuǎn)發(fā)的資源的 url 進(jìn)行處理了。最終的 url 與 contextRelative 和你要重定向的資源是否以 / 開頭有關(guān)!當(dāng)且僅當(dāng) renderMergedOutputModel 為 true,并且你要重定向的資源是以 / 開頭,spring 會在該資源前添加 contextPath。
??????? response.sendRedirect() 的參數(shù),如果不以 / 開頭,那么容器最終計算出來的資源是相對于
做重定向操作的資源的 url
;如果以 / 開頭,容器將它視為相對于主機(jī)的 url。如此說來,spring 的 RedirectView 怎么著都只能將資源重定向到當(dāng)前應(yīng)用程序上。將 url 開頭的 / 去掉不是解決之道,因此本機(jī)的其它應(yīng)用程序的 contextPath 必定是以 / 開頭,因此我們要想辦法設(shè)置 contextRelative 了。
??????? RedirectView 自身持有 contextRelative 屬性,用于在程序中通過 new 操作符來構(gòu)造一個 RedirectView 并可以設(shè)置 contextRelative。當(dāng)處理請求的方法返回類型為 String 時,是通過 viewResolver 來解析得到 View 的。UrlBasedViewResolver 就是能夠解析出 RedirectView 的 viewResolver。該 viewResolver 持有 redirectContextRelative 屬性,當(dāng)它發(fā)現(xiàn)邏輯視圖名以 "redirect:" 開頭時,會將自身持有的 redirectContextRelative 傳入 RedirectView 的構(gòu)造函數(shù)以創(chuàng)建 RedirectView。因此我們通過注冊 UrlBasedViewResolver 時設(shè)置 redirectContextRelative 以達(dá)到控制 RedirectView 修改 url 的行為。
UrlBasedViewResolver 解析出 View:
-
protected
?View?createView(String?viewName,?Locale?locale)?
throws
?Exception?{ ??
-
??
??
-
??
??
-
??
if
?(!canHandle(viewName,?locale))?{ ??
-
????
return
?
null
; ??
-
??} ??
-
??
??
-
??
if
?(viewName.startsWith(REDIRECT_URL_PREFIX))?{ ??
-
????String?redirectUrl?=?viewName.substring(REDIRECT_URL_PREFIX.length()); ??
-
????
return
?
new
?RedirectView(redirectUrl,?isRedirectContextRelative(),?isRedirectHttp10Compatible()); ??
-
??} ??
-
??
??
-
??
if
?(viewName.startsWith(FORWARD_URL_PREFIX))?{ ??
-
????String?forwardUrl?=?viewName.substring(FORWARD_URL_PREFIX.length()); ??
-
????
return
?
new
?InternalResourceView(forwardUrl); ??
-
??} ??
-
??
??
-
??
return
?
super
.createView(viewName,?locale); ??
-
}??
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)) {
String redirectUrl = viewName.substring(REDIRECT_URL_PREFIX.length());
return new RedirectView(redirectUrl, isRedirectContextRelative(), isRedirectHttp10Compatible());
}
// Check for special "forward:" prefix.
if (viewName.startsWith(FORWARD_URL_PREFIX)) {
String forwardUrl = viewName.substring(FORWARD_URL_PREFIX.length());
return new InternalResourceView(forwardUrl);
}
// Else fall back to superclass implementation: calling loadView.
return super.createView(viewName, locale);
}
??????? UrlBasedViewResolver 的 redirectContextRelative 的默認(rèn)值為 true,這意味著,只要重定向的資源以 / 開頭,那么 spring 會幫你添加 contextPath。站在 Spring MVC 的角度上來說,/ 開頭的資源就是相對于當(dāng)前應(yīng)用程序,這和 forward 一樣了。因此,如果你確定重定向操作是在同一應(yīng)用程序中操作,那就使用 Spring MVC 的默認(rèn)值吧,這樣就不需要你寫 contextPath 了。注意,這樣做有隱患!當(dāng)重定向的資源是其它應(yīng)用程序時,除非你了解機(jī)制,否則請不要這么做!