day04-檢視和檢視解析器

一刀一個小西瓜發表於2023-02-07

檢視和檢視解析器

1.基本介紹

  1. 在SpringMVC中的目標方法,最終返回的都是一個檢視(有各種檢視)

    注意,這裡的檢視是一個類物件,不是一個頁面!!

  2. 返回的檢視都會由一個檢視解析器來處理(檢視解析器有很多種)

    day04-檢視和檢視解析器

2.自定義檢視

2.1為什麼需要自定義檢視

  1. 在預設情況下,我們都是返回預設的檢視,然後返回的檢視交由 SpringMVC 的 InternalResourcesViewResolver 預設檢視解析器來處理的:

    image-20230206213632558
  2. 在實際開發中,因為業務需求,我們有時候需要自定義檢視解析器

  3. 檢視解析器可以配置多個,按照指定的順序來對檢視進行解析。如果上一個檢視解析器不匹配,下一個檢視解析器就會去解析檢視,以此類推。

2.2應用例項

執行流程:

image-20230207171939607
  1. 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>
    
  2. 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";//自定義檢視名
        }
    }
    
  3. 自定義檢視 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);
        }
    }
    
  4. 結果頁面 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>
    
  5. 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/>
    
  6. 測試,訪問view.jsp,點選超連結

    image-20230207175529158
  7. 成功跳轉到 my_view.jsp

    image-20230207175601984
  8. 後臺輸出如下:說明整個執行流程如圖所示。

    image-20230207175629231

2.3建立自定義檢視的步驟

  1. 自定義一個檢視:建立一個 View 的 bean,該 bean 需要繼承自 AbstractView,並實現renderMergedOutputModel方法
  2. 並把自定義 View 加入到 IOC 容器中
  3. 自定義檢視的檢視解析器,使用 BeanNameViewResolver,這個檢視解析器也需要配置到 ioc 容器檔案中
  4. BeanNameViewResolver 的呼叫優先順序需要設定一下,設定 order 比 Integer.MAX_VALUE 小的值,以確保其在預設的檢視解析器之前被呼叫

2.4Debug原始碼-自定義檢視解析器執行流程

自定義檢視-工作流程:

  1. SpringMVC 呼叫目標方法,返回自定義 View 在 IOC 容器中的 id
  2. SpringMVC 呼叫 BeanNameViewResolver 檢視解析器:從 IOC 容器中獲取返回 id 值對應的 bean,即自定義的 View 的物件
  3. SpringMVC 呼叫自定義檢視的 renderMergedOutputModel 方法,渲染檢視
  4. 說明:如果 SpringMVC 呼叫 Handler 的目標方法時,返回的自定義 View ,在 IOC 容器中的 id 不存在,則仍然按照預設的檢視解析器機制處理。

Debug-01

(1)在GoodsHandler的目標方法中打上斷點:

image-20230207181443199

(2)點選debug,訪問view.jsp,點選超連結,可以看到後臺游標跳轉到斷點處:

image-20230207182111043

(3)在原始碼 BeanNameViewResolver 的 resolveViewName 方法處打上斷點:

image-20230207181949587

(4)點選Resume,游標跳轉到了這個斷點處,viewName 的值就是自定義檢視物件的 id:這裡完成檢視解析

image-20230207182357898

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);
}
image-20230207183628224

(5)在自定義檢視物件裡打上斷點:

image-20230207183815264

(6)點選 resume,游標跳轉到該斷點:在這裡完成檢視渲染,並轉發到結果頁面

image-20230207183923620 image-20230207184246555

(7)最後由 tomcat 將資料返回給客戶端:

image-20230207184459844

2.5Debug原始碼-預設檢視解析器執行流程

將預設檢視解析器的優先順序調高:

image-20230207184950399

debug-02

(1)仍然在GoodsHandler中新增斷點:

image-20230207192822730

(2)瀏覽器訪問 view.jsp,可以看到後臺游標跳轉到了斷點處:

image-20230207193027494

(3)分別在預設檢視解析器(InternalResourceViewResolver)和自定義檢視解析器(BeanNameViewResolver) 中的方法中打上斷點:

image-20230207193313100 image-20230207193321152

(4)點選resume,可以看到游標先跳到了預設檢視解析器的 buildView 方法中:因為預設解析器的優先順序在之前設定為最高。

image-20230207193631707

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

image-20230207194350281

(5)之後就會到該View物件進行檢視渲染,然後由Tomcat將資料返回給客戶端。

但是如果該url下沒有/WEB-INF/pages/liView.jsp檔案,就會報錯:

image-20230207195118671

2.6Debug原始碼-自定義View不存在,會走預設檢視解析機制

檢視解析器可以配置多個,按照指定的順序來對檢視進行解析。如果上一個檢視解析器不匹配,下一個檢視解析器就會去解析檢視,以此類推:

image-20230207195521100
  1. 在容器檔案中,將預設的檢視解析器呼叫優先順序降低,提高自定義檢視解析器的呼叫優先順序。見2.2的容器檔案配置

  2. 刪除2.2中的自定義檢視MyView.java。也就是說,自定義檢視解析器解析目標方法返回的檢視物件時,將會無法解析該檢視,因為它不存在。

  3. 這時就會去呼叫下一個優先順序的檢視解析器,即預設檢視解析器。

debug-03

(1)仍然在GoodsHandler中新增斷點:

image-20230207192822730

(2)瀏覽器訪問 view.jsp,可以看到後臺游標跳轉到了斷點處:

image-20230207193027494

(3)在自定義的檢視解析器 BeanNameViewResolver 中打上斷點:

image-20230207200552990

(4)點選resume,可以看到游標跳轉到該斷點處:

image-20230207200655556

(5)因為在容器檔案中找不到該檢視物件的id了,因此會進入第一個分支,方法直接返回 null

image-20230207200857797

(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;
}
image-20230207201505383

因為自定義檢視解析器會返回 null,因此這裡進入第二次迴圈,由預設的檢視解析器去進行解析,然後返回對應的檢視:

image-20230207202159365

(7)在該方法中打上斷點,點選 resume,可以看到此時 view 是由預設的檢視解析器返回的檢視物件,走的是預設機制。

image-20230207203001876

(8)下一個就按照預設機制拼接的 url 去訪問該頁面,並進行渲染。然後由Tomcat返回給客戶端。如果根據 url 找不到該頁面,就報404錯誤。


補充:如果預設檢視解析器優先順序高,自定義的檢視解析器優先順序低,但是預設檢視解析器返回的的View為null,這時候會繼續呼叫自定義的檢視解析器嗎?

答:事實上,預設檢視解析器返回的 View 不會為 null。

因為它是根據目標方法返回的字串+你配置的前字尾進行 url 的拼接。只要目標方法返回了一個字串,預設檢視處理器就不會返回 null。

如果目標方法返回的是 null 呢?將會以目標方法的路徑名稱+配置的前字尾作為尋找頁面的 url

因此在迴圈呼叫檢視處理器的時候,一旦迴圈到預設檢視處理器,就不會呼叫後面的自定義檢視解析器。

3.目標方法直接指定轉發或重定向

3.1使用例項

目標方法中指定轉發或者重定向:

  1. 預設返回的方式是請求轉發,然後用檢視處理器進行處理。
  2. 但是也可以在目標方法中直接指定重定向或者轉發的 url 的地址。
  3. 注意:如果指定重定向,則不能定向到 /WEB-INF 目錄中。因為該目錄為 Tomcat 的內部目錄。

例子

  1. 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>
    
  2. 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";
        }
    }
    
  3. 訪問 view.jsp,點選超連結:

    image-20230207210928762
  4. 成功進入到請求轉發的頁面:

    image-20230207210953307

    請求轉發也可以轉發到WEB-INF目錄之外的頁面。

  5. 修改 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";
    }
    
  6. redeployTomcat,訪問 view.jsp,點選超連結,可以看到成功重定向到 login.jsp頁面

    image-20230207212618227 image-20230207212638759

3.2Debug-指定請求轉發流程分析

相關文章