SpringMVC 的一个案例分析

model view forward direct

Posted by gomyck on October 10, 2022

日常案例分析笔记

看案例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
@Controller
@RequestMapping("demo")
public class ForwardDemoController {

  @GetMapping("ct1")
  public String ct1(Model model) {
    model.addAttribute("name", "xxx");
    model.addAttribute("age", 123);
    System.out.println("ct1 executed...");
    return " forward:/demo1/ct2 ";
  }

  // 可以接收 queryParamater 以及 formData  x-www-form-urlencoded 类型的入参 (ServletModelAttributeMethodProcessor 处理)
  @GetMapping("ct2")
  @ResponseBody
  public String ct2(Model model, HttpServletRequest request, P p) {
    System.out.println(p.name);
    System.out.println(p.age);
    System.out.println(model); // 打印 {}
    System.out.println(CKJSON.getInstance()
                             .toJsonString(request.getAttribute("name"))); // 打印 xxx
    System.out.println("ct2 executed...");
    return "ct2返回了一条数据";
  }

  //可以接受收json 类型的入参
  @GetMapping("ct3")
  @ResponseBody
  public String ct3(Model model, HttpServletRequest request, @RequestBody P p) {
    System.out.println(p.name);
    System.out.println(p.age);
    System.out.println(model);  // 打印 {}
    System.out.println(CKJSON.getInstance()
                             .toJsonString(request.getAttribute("name"))); // 打印 xxx
    System.out.println("ct3 executed...");
    return "ct3返回了一条数据";
  }

  @Data
  public static class P {
    String name;
    String age;
  }

}

小伙伴问我 在ct1 中定义的 model, 为什么 通过ct1 forward 的请求, 在 ct2 和 3 的入参 model 不能接收到

分析

乍一听这个需求, 感觉有道理, 因为在 ct1 中定义的 model, 是可以在 2 和 3 的 HttpServletRequest 中拿到对应的信息的, 但是 model 是空对象

通过分析得出: model 只是做 render 用, 并不是接收参数的 argument, 我觉得spring 是为了避免出现属性污染, 而不做 model 透传, 因为 ct1 ct2 ct3 可能会服务于不同的视图, 贸然的透传可能带来一些莫名覆盖变量问题

forward 和 redirect 的区别:

1
2
3
4
1.forward 是 servlet 的 RequestDisptcher 的 forward 方法, redirect 是 response 的重定向方法
2.forward 是服务器内部重定向, 客户端无感知, redirecrt 是客户端重新发起请求, 是 302 行为
3.forward 在 spring 内部, 属于一个线程
4.可以使用 RedirectAttribute 来透传 model 的参数, 原理是 flashMap, 具体可以看 RequestMappingHandlerAdapter: getModelAndView方法, 这个方法也是最终生成视图的一步

mvc 是怎么处理 post 请求的

通过分析源代码, spring 在获取对应的 handler 之后, 为此 handler 进行了参数绑定, 具体可参考 RequestMappingHandlerAdapter:644

这里有个值得注意的点就是: 它在 getDefaultArgumentResolvers之中, 添加了两次 ServletModelAttributeMethodProcessor

一次入参传 true, 也就是处理注解 ModelAttribute 注解的, 一次传 false, 用来处理 formData x-www-form-urlencoded 请求的

而 ServletModelAttributeMethodProcessor的 bindRequestParameters 方法为最终为入参绑定值得处理逻辑, 是 dataBinder 最终处理的值绑定

为什么 model 里的值经过 forward 会存在于 request 中

分析源代码: DispatcherServlet:1367 render方法, 存在一个逻辑:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
view=resolveViewName(viewName,mv.getModelInternal(),locale,request);

protected View resolveViewName(String viewName, @Nullable Map<String, Object> model,Locale locale, HttpServletRequest request) throws Exception {
  if (this.viewResolvers != null) {
    for (ViewResolver viewResolver : this.viewResolvers) {
      View view = viewResolver.resolveViewName(viewName, locale);
      if (view != null) {
        return view;
      }
    }
  }
  return null;
}

// 具体确定是由哪个 resolver 来处理 是 UrlBasedViewResolver的 createView 方法确定
// DispatcherServlet 的 initViewResolvers 通过配置文件, 来确定, 默认的视图解析器都有哪些
view.render(mv.getModelInternal(), request, response);

protected void exposeModelAsRequestAttributes(Map<String, Object> model, HttpServletRequest request) throws Exception {
  model.forEach((name, value) -> {
    if (value != null) {
      request.setAttribute(name, value);
    }
    else {
      request.removeAttribute(name);
    }
  });
}

通过viewResolver 来生成最终的 view 视图, 而后调用 view.render 来最终渲染, 查看 View 的实现类, 最终确定由 InternalResourceView 来完成最终的渲染, 此类在最开始, 就遍历的 model, 把其属性存到 request 中