跳转至

视图和视图解析器

1.基本介绍

  1. 在SpringMVC中的目标方法,最终返回的都是一个**视图**(有各种视图)

注意,这里的视图是一个类对象,不是一个页面!!

  1. 返回的视图都会由一个视图解析器来处理(视图解析器有很多种)

2.自定义视图

2.1为什么需要自定义视图

  1. 在默认情况下,我们都是返回默认的视图,然后返回的视图交由 SpringMVC 的 InternalResourcesViewResolver 默认视图解析器来处理的:

image-20230206213632558

  1. 在实际开发中,因为业务需求,我们有时候需要自定义视图解析器

  2. 视图解析器可以配置多个,按照指定的顺序来对视图进行解析。如果上一个视图解析器不匹配,下一个视图解析器就会去解析视图,以此类推。

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>
  1. 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";//自定义视图名
    }
}
  1. 自定义视图 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);
    }
}
  1. 结果页面 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>
  1. 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/>
  1. 测试,访问view.jsp,点击超链接

image-20230207175529158

  1. 成功跳转到 my_view.jsp

image-20230207175601984

  1. 后台输出如下:说明整个执行流程如图所示。

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>
  1. 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";
    }
}
  1. 访问 view.jsp,点击超链接:

image-20230207210928762

  1. 成功进入到请求转发的页面:

image-20230207210953307

请求转发也可以转发到WEB-INF目录之外的页面。

  1. 修改 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";
}
  1. redeployTomcat,访问 view.jsp,点击超链接,可以看到成功重定向到 login.jsp页面

image-20230207212618227

image-20230207212638759

3.2Debug-指定请求转发流程分析

通过debug查看SpringMVC指定的请求转发的方式,是如何执行到我们原生的请求转发方法的:

(1)在GoodsHandler.java的目标方法中打上断点:

image-20230208191749490

(2)点击debug,在浏览器访问 view.jsp,点击该页面的链接,访问目标方法,后台的光标跳转到断点处:

image-20230208192127650

(3)在源码 DispatcherServlet.java 中打上断点:

image-20230208192401169

(4)点击 resume,光标跳转到断点处,点击step over,可以看到 传入的viewName 为 "forward:/WEB-INF/pages/my_view.jsp"

image-20230208192819535

(5)返回的 View 视图对象如下:view中了一个名为 "forward:" 的 bean 对象

image-20230208193409132

(6)点击 step over,光标跳转到 DispatcherServlet.java 的 render() 方法,即视图的渲染方法:

image-20230208193751059

(7)点击step into,光标跳转到 AbstractView 的 render() 方法中:

image-20230208194023198

(8)该方法有一个 renderMergedOutputModel() 方法:

image-20230208194112837

(9)step into 该方法,光标跳转到 InternalResourceView(默认视图解析器)的 renderMergedOutputModel() 方法中,该方法**最终调用了 Servlet 原生的请求转发方法**:

image-20230208194634474

image-20230208194640512

然后将结果返回给 tomcat,tomcat 再把结果响应给浏览器。

3.3Debug-指定重定向流程分析

(1)还是在 GoodsHandler.java 的目标方法中打上断点:

image-20230208195457552

(2)点击debug,访问 view.jsp,点击给页面的超链接以访问目标方法,可以看到光标跳转到断点处:

image-20230208195733483

(3)仍然在 DispatcherServlet.java 的 resolveViewName() 方法中打上断点:

image-20230208195931898

(4)点击resume,点击 step over,当前的 viewName 如下:

image-20230208200131037

(5)生成的视图对象 view:view对象中包含了一个名为 "redirect:" 的 bean 对象

image-20230208200320856

(6)点击 step over,光标跳转到 DispatcherServlet.java 的 render() 方法,即视图的渲染方法:

image-20230208193751059

(7)点击step into,光标跳转到 AbstractView 的 render() 方法中:

image-20230208194023198

(8)该方法有一个 renderMergedOutputModel() 方法:

image-20230208194112837

(9)step into 该方法,这次光标跳转到 RedirectView 的 renderMergedOutputModel() 方法中,然后调用 Servlet 原生的 sendRedirect() 方法。

image-20230208201207354

4.练习

  1. 将之前的 SpringMVC映射数据请求,数据模型,视图和视图解析 的案例再写一遍。
  2. Debug过的源码再走一遍
  3. 完成一个简单的用户登录案例:
  4. (1)编写登录页面 login.jsp
  5. (2)LoginHandler 的 doLogin() 方法,如果用户输入用户名为 olien,密码为 123 就可以登录
  6. (3)创建 Javabean:User.java
  7. (4)表单提交数据到 doLogin 方法,以 User 对象接收
  8. (5)登录成功,到页面 login_ok.jsp,并显示登录欢迎信息
  9. (6)登录失败,到页面 login_error.jsp,并给出重新登录的超链接

login.jsp:

<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
    <title>登录</title>
</head>
<body>
<h3>登录页面</h3>
<form action="user/login" method="post">
    u:<input name="username" type="text"/> <br/>
    p:<input name="pwd" type="password"/> <br/>
    <input type="submit" value="登录"/>
</form>
</body>
</html>

LoginHandler.java:

package com.li.web.hw;

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;

/**
 * @author 李
 * @version 1.0
 */
@Controller
@RequestMapping(value = "/user")
public class LoginHandler {
    @RequestMapping(value = "/login")
    public String doLogin(User user) {//自动匹配到形参
        if ("olien".equals(user.getUsername()) && "123".equals(user.getPwd())) {
            //默认将数据放入request域
            return "forward:/WEB-INF/pages/login_ok.jsp";
        } else {
            return "forward:/WEB-INF/pages/login_error.jsp";
        }
    }
}

User.java:

package com.li.web.hw;

/**
 * @author 李
 * @version 1.0
 */
public class User {
    private String username;
    private String pwd;

    public String getUsername() {
        return username;
    }

    public void setUsername(String username) {
        this.username = username;
    }

    public String getPwd() {
        return pwd;
    }

    public void setPwd(String pwd) {
        this.pwd = pwd;
    }
}

login_ok.jsp:

<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
    <title>登录成功</title>
</head>
<body>
<h2>欢迎 ${requestScope.user.username}登录成功!</h2>
<h2>您的密码为 ${requestScope.user.pwd}</h2>
</body>
</html>

login_error.jsp:

<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
    <title>登录失败</title>
</head>
<body>
<h1>登录失败</h1>
<%--浏览器会解析为 http://localhost:8080/springmvc/login.jsp--%>
<a href="<%=request.getContextPath()%>/login.jsp">登录失败,点击返回重新登录</a>
</body>
</html>

测试: