檢視和檢視解析器
1.基本介紹
-
在SpringMVC中的目標方法,最終返回的都是一個檢視(有各種檢視)
注意,這裡的檢視是一個類物件,不是一個頁面!!
-
返回的檢視都會由一個檢視解析器來處理(檢視解析器有很多種)
2.自定義檢視
2.1為什麼需要自定義檢視
-
在預設情況下,我們都是返回預設的檢視,然後返回的檢視交由 SpringMVC 的
InternalResourcesViewResolver
預設檢視解析器來處理的: -
在實際開發中,因為業務需求,我們有時候需要自定義檢視解析器
-
檢視解析器可以配置多個,按照指定的順序來對檢視進行解析。如果上一個檢視解析器不匹配,下一個檢視解析器就會去解析檢視,以此類推。
2.2應用例項
執行流程:
-
view.jsp,請求到 Handler
<%@ page contentType="text/html;charset=UTF-8" language="java" %> <html> <head> <title>自定義檢視測試</title> </head> <body> <h1>自定義檢視測試</h1> <a href="goods/buy">點選到自定義檢視</a> </body> </html>
-
GoodsHandler.java
package com.li.web.viewresolver; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.RequestMapping; /** * @author 李 * @version 1.0 */ @RequestMapping(value = "/goods") @Controller public class GoodsHandler { @RequestMapping(value = "/buy") public String buy(){ System.out.println("----------buy()---------"); return "liView";//自定義檢視名 } }
-
自定義檢視 MyView.java
package com.li.web.viewresolver; import org.springframework.stereotype.Component; import org.springframework.web.servlet.view.AbstractView; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.util.Map; /** * @author 李 * @version 1.0 * 1. MyView 繼承了AbstractView,就可以作為了一個檢視使用 * 2. @Component(value = "myView") ,該檢視會注入到容器中,id為 liView */ @Component(value = "liView") public class MyView extends AbstractView { @Override protected void renderMergedOutputModel(Map<String, Object> model, HttpServletRequest request, HttpServletResponse response) throws Exception { //1.完成檢視渲染 System.out.println("進入到自己的檢視..."); //2.並且確定我們要跳轉的頁面,如 /WEB-INF/pages/my_view.jsp /* * 1.下面就是請求轉發到 /WEB-INF/pages/my_view.jsp * 2.該路徑會被springmvc解析成 /web工程路徑/WEB-INF/pages/my_view.jsp */ request.getRequestDispatcher("/WEB-INF/pages/my_view.jsp") .forward(request, response); } }
-
結果頁面 my_view.jsp
<%@ page contentType="text/html;charset=UTF-8" language="java" %> <html> <head> <title>my_view</title> </head> <body> <h1>進入到my_view頁面</h1> <p>從自定義檢視來的...</p> </body> </html>
-
springDispatcherServlet-servlet.xml 配置自定義檢視解析器
<!--1.指定掃描的包--> <context:component-scan base-package="com.li.web"/> <!--2.配置檢視解析器[預設的檢視解析器]--> <bean class="org.springframework.web.servlet.view.InternalResourceViewResolver"> <!--配置屬性 suffix(字尾) 和 prefix(字首)--> <property name="prefix" value="/WEB-INF/pages/"/> <property name="suffix" value=".jsp"/> </bean> <!--3. 3.1 配置自定義檢視解析器 BeanNameViewResolver 3.2 BeanNameViewResolver 可以解析我們自定義的檢視 3.3 屬性 order 表示檢視節解析器執行的順序,值越小優先順序越高 3.4 order 的預設值為最低優先順序-LOWEST_PRECEDENCE 3.5 預設的檢視解析器就是最低優先順序,因此我們的自定義解析器會先執行 --> <bean class="org.springframework.web.servlet.view.BeanNameViewResolver"> <property name="order" value="99"/> </bean> <!--4.加入兩個常規的配置--> <!--支援SpringMVC的高階功能,比如:JSR303校驗,對映動態請求--> <mvc:annotation-driven></mvc:annotation-driven> <!--將SpringMVC不能處理的請求,交給tomcat處理,比如css,js--> <mvc:default-servlet-handler/>
-
測試,訪問view.jsp,點選超連結
-
成功跳轉到 my_view.jsp
-
後臺輸出如下:說明整個執行流程如圖所示。
2.3建立自定義檢視的步驟
- 自定義一個檢視:建立一個 View 的 bean,該 bean 需要繼承自 AbstractView,並實現renderMergedOutputModel方法
- 並把自定義 View 加入到 IOC 容器中
- 自定義檢視的檢視解析器,使用 BeanNameViewResolver,這個檢視解析器也需要配置到 ioc 容器檔案中
- BeanNameViewResolver 的呼叫優先順序需要設定一下,設定 order 比 Integer.MAX_VALUE 小的值,以確保其在預設的檢視解析器之前被呼叫
2.4Debug原始碼-自定義檢視解析器執行流程
自定義檢視-工作流程:
- SpringMVC 呼叫目標方法,返回自定義 View 在 IOC 容器中的 id
- SpringMVC 呼叫 BeanNameViewResolver 檢視解析器:從 IOC 容器中獲取返回 id 值對應的 bean,即自定義的 View 的物件
- SpringMVC 呼叫自定義檢視的 renderMergedOutputModel 方法,渲染檢視
- 說明:如果 SpringMVC 呼叫 Handler 的目標方法時,返回的自定義 View ,在 IOC 容器中的 id 不存在,則仍然按照預設的檢視解析器機制處理。
Debug-01
(1)在GoodsHandler的目標方法中打上斷點:
(2)點選debug,訪問view.jsp,點選超連結,可以看到後臺游標跳轉到斷點處:
(3)在原始碼 BeanNameViewResolver 的 resolveViewName 方法處打上斷點:
(4)點選Resume,游標跳轉到了這個斷點處,viewName 的值就是自定義檢視物件的 id:這裡完成檢視解析
resolveViewName 方法如下:
@Override
@Nullable
public View resolveViewName(String viewName, Locale locale) throws BeansException {
//獲取ioc容器物件
ApplicationContext context = obtainApplicationContext();
//如果容器物件中不存在 目標方法返回的自定義檢視物件id
if (!context.containsBean(viewName)) {
// Allow for ViewResolver chaining...
//就返回null,讓預設的檢視解析器處理該檢視
return null;
}
//判斷自定義的檢視是不是 org.springframework.web.servlet.View 型別
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;
}
//如果是,就返回這個自定義檢視物件
return context.getBean(viewName, View.class);
}
(5)在自定義檢視物件裡打上斷點:
(6)點選 resume,游標跳轉到該斷點:在這裡完成檢視渲染,並轉發到結果頁面
(7)最後由 tomcat 將資料返回給客戶端:
2.5Debug原始碼-預設檢視解析器執行流程
將預設檢視解析器的優先順序調高:
debug-02
(1)仍然在GoodsHandler中新增斷點:
(2)瀏覽器訪問 view.jsp,可以看到後臺游標跳轉到了斷點處:
(3)分別在預設檢視解析器(InternalResourceViewResolver)和自定義檢視解析器(BeanNameViewResolver) 中的方法中打上斷點:
(4)點選resume,可以看到游標先跳到了預設檢視解析器的 buildView 方法中:因為預設解析器的優先順序在之前設定為最高。
buildView 方法:
@Override
protected AbstractUrlBasedView buildView(String viewName) throws Exception {
//根據目標方法返回的viewName建立一個View物件
InternalResourceView view = (InternalResourceView) super.buildView(viewName);
if (this.alwaysInclude != null) {
view.setAlwaysInclude(this.alwaysInclude);
}
view.setPreventDispatchLoop(true);
return view;
}
這個 View 物件的 url 是按照你配置的字首和字尾,拼接完成的 url
(5)之後就會到該View物件進行檢視渲染,然後由Tomcat將資料返回給客戶端。
但是如果該url下沒有/WEB-INF/pages/liView.jsp檔案,就會報錯:
2.6Debug原始碼-自定義View不存在,會走預設檢視解析機制
檢視解析器可以配置多個,按照指定的順序來對檢視進行解析。如果上一個檢視解析器不匹配,下一個檢視解析器就會去解析檢視,以此類推:
-
在容器檔案中,將預設的檢視解析器呼叫優先順序降低,提高自定義檢視解析器的呼叫優先順序。見2.2的容器檔案配置
-
刪除2.2中的自定義檢視MyView.java。也就是說,自定義檢視解析器解析目標方法返回的檢視物件時,將會無法解析該檢視,因為它不存在。
-
這時就會去呼叫下一個優先順序的檢視解析器,即預設檢視解析器。
debug-03
(1)仍然在GoodsHandler中新增斷點:
(2)瀏覽器訪問 view.jsp,可以看到後臺游標跳轉到了斷點處:
(3)在自定義的檢視解析器 BeanNameViewResolver 中打上斷點:
(4)點選resume,可以看到游標跳轉到該斷點處:
(5)因為在容器檔案中找不到該檢視物件的id了,因此會進入第一個分支,方法直接返回 null
(6)點選step over,游標跳轉到中央控制器的 resolveViewName 方法中:
@Nullable
protected View resolveViewName(String viewName, @Nullable Map<String, Object> model,
Locale locale, HttpServletRequest request) throws Exception {
if (this.viewResolvers != null) {
//迴圈呼叫檢視解析器,直到某個檢視解析器返回的view不為null
for (ViewResolver viewResolver : this.viewResolvers) {
View view = viewResolver.resolveViewName(viewName, locale);
if (view != null) {
return view;
}
}
}
return null;
}
因為自定義檢視解析器會返回 null,因此這裡進入第二次迴圈,由預設的檢視解析器去進行解析,然後返回對應的檢視:
(7)在該方法中打上斷點,點選 resume,可以看到此時 view 是由預設的檢視解析器返回的檢視物件,走的是預設機制。
(8)下一個就按照預設機制拼接的 url 去訪問該頁面,並進行渲染。然後由Tomcat返回給客戶端。如果根據 url 找不到該頁面,就報404錯誤。
補充:如果預設檢視解析器優先順序高,自定義的檢視解析器優先順序低,但是預設檢視解析器返回的的View為null,這時候會繼續呼叫自定義的檢視解析器嗎?
答:事實上,預設檢視解析器返回的 View 不會為 null。
因為它是根據目標方法返回的字串+你配置的前字尾進行 url 的拼接。只要目標方法返回了一個字串,預設檢視處理器就不會返回 null。
如果目標方法返回的是 null 呢?將會以目標方法的路徑名稱+配置的前字尾作為尋找頁面的 url
因此在迴圈呼叫檢視處理器的時候,一旦迴圈到預設檢視處理器,就不會呼叫後面的自定義檢視解析器。
3.目標方法直接指定轉發或重定向
3.1使用例項
目標方法中指定轉發或者重定向:
- 預設返回的方式是請求轉發,然後用檢視處理器進行處理。
- 但是也可以在目標方法中直接指定重定向或者轉發的 url 的地址。
- 注意:如果指定重定向,則不能定向到 /WEB-INF 目錄中。因為該目錄為 Tomcat 的內部目錄。
例子
-
view.jsp
<%@ page contentType="text/html;charset=UTF-8" language="java" %> <html> <head> <title>請求轉發或重定向</title> </head> <body> <h1>請求轉發或重定向</h1> <a href="goods/order">測試在目標方法找中指定請求轉發或重定向</a> </body> </html>
-
GoodsHandler.java
package com.li.web.viewresolver; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.RequestMapping; /** * @author 李 * @version 1.0 */ @RequestMapping(value = "/goods") @Controller public class GoodsHandler { //演示直接指定請求轉發或者重定向 @RequestMapping(value = "/order") public String order() { System.out.println("=========order()========="); //請求轉發到 /WEB-INF/pages/my_view.jsp //下面的路徑會被解析/web工程路徑/WEB-INF/pages/my_view.jsp return "forward:/WEB-INF/pages/my_view.jsp"; } }
-
訪問 view.jsp,點選超連結:
-
成功進入到請求轉發的頁面:
請求轉發也可以轉發到WEB-INF目錄之外的頁面。
-
修改 GoodsHandler.java 的 order 方法:
//演示直接指定請求轉發或者重定向 @RequestMapping(value = "/order") public String order() { System.out.println("=========order()========="); //重定向 //1.對於重定向來說,不能重定向到/WEB-INF/目錄下 //2.redirect 為重定向的關鍵字 //3./login.jsp 是在伺服器解析的,解析為 /web工程路徑/login.jsp return "redirect:/login.jsp"; }
-
redeployTomcat,訪問 view.jsp,點選超連結,可以看到成功重定向到 login.jsp頁面