云计算百科
云计算领域专业知识百科平台

Spring MVC 核心机制剖析:视图实现、请求转发/重定向与 RESTful 设计

一、SpringMVC中视图的实现原理

1.1 Spring MVC视图支持可配置

以下的配置表明当前SpringMVC框架使用的视图View是Thymeleaf的。
如果你需要换成其他的视图View,修改以下的配置即可。这种设计是完全符合OCP开闭原则的。视图View和框架是解耦合的,耦合度低扩展能力强。视图View可以通过配置文件进行灵活切换。

<!–视图解析器–>
<bean id="thymeleafViewResolver" class="org.thymeleaf.spring6.view.ThymeleafViewResolver">
<!–作用于视图渲染的过程中,可以设置视图渲染后输出时采用的编码字符集–>
<property name="characterEncoding" value="UTF-8"/>
<!–如果配置多个视图解析器,它来决定优先使用哪个视图解析器,它的值越小优先级越高–>
<property name="order" value="1"/>
<!–当 ThymeleafViewResolver 渲染模板时,会使用该模板引擎来解析、编译和渲染模板–>
<property name="templateEngine">
<bean class="org.thymeleaf.spring6.SpringTemplateEngine">
<!–用于指定 Thymeleaf 模板引擎使用的模板解析器。模板解析器负责根据模板位置、模板资源名称、文件编码等信息,加载模板并对其进行解析–>
<property name="templateResolver">
<bean class="org.thymeleaf.spring6.templateresolver.SpringResourceTemplateResolver">
<!–设置模板文件的位置(前缀)–>
<property name="prefix" value="/WEB-INF/templates/"/>
<!–设置模板文件后缀(后缀),Thymeleaf文件扩展名不一定是html,也可以是其他,例如txt,大部分都是html–>
<property name="suffix" value=".html"/>
<!–设置模板类型,例如:HTML,TEXT,JAVASCRIPT,CSS等–>
<property name="templateMode" value="HTML"/>
<!–用于模板文件在读取和解析过程中采用的编码字符集–>
<property name="characterEncoding" value="UTF-8"/>
</bean>

</property>

</bean>

</property>

</bean>

1.2 实现视图机制的核心接口

实现视图的核心类与接口包括:

  • DispatcherServlet类(前端控制器):
    • 职责:在整个Spring MVC执行流程中,负责中央调度。
    • 核心方法:doDispatch
  • ViewResolver接口(视图解析器):
    • 职责:负责将逻辑视图名转换为物理视图名,最终创建View接口的实现类,即视图实现类对象。
    • 核心方法:resolveViewName
  • View接口(视图):
    • 职责:负责将模型数据Model渲染为视图格式(HTML代码),并最终将生成的视图(HTML代码)输出到客户端。(它负责将模板语言转换成HTML代码)
    • 核心方法:render
  • ViewResolverRegistry(视图解析器注册器):
    • 负责在web容器(Tomcat)启动的时候,完成视图解析器的注册。如果有多个视图解析器,会将视图解析器对象按照order的配置放入List集合。
  • 1.3 实现视图机制的原理描述

    public class DispatcherServlet extends FrameworkServlet {

    // 前端控制器的核心代码,处理请求,返回代码,渲染视图,都是在这个方法中完成的
    protected void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception {

    // 根据请求路径调用映射的处理器方法,处理器方法执行结束之后,返回逻辑视图名称
    // 返回逻辑视图名称之后,DispatcherServlet会将逻辑视图名称Viemname + Model,将其封装为ModelAndView对象
    mv = ha.handle(processedRequest, response, mappedHandler.getHandler());

    // 这行代码的作用是处理视图
    this.processDispatchResult(processedRequest, response, mappedHandler, mv, dispatchException);
    }

    private void processDispatchResult(HttpServletRequest request, HttpServletResponse response,
    @Nullable HandlerExecutionChain mappedHandler,
    @Nullable ModelAndView mv, @Nullable Exception exception) throws Exception {
    // 渲染页面(将模板字符串转换成html代码相应到浏览器)
    this.render(mv, request, response);
    }

    protected void render(ModelAndView mv, HttpServletRequest request, HttpServletResponse response) throws Exception {
    // 这个方法的作用是将逻辑视图名称转换成物理视图名称,并且最终返回视图对象View
    View view = this.resolveViewName(viewName, mv.getModelInternal(), locale, request);

    // 真正的将模板字符串转换成html代码,并且将html代码相应给浏览器
    view.render(mv.getModelInternal(), request, response);
    }

    protected View resolveViewName(String viewName, @Nullable Map<String, Object> model,
    Locale locale, HttpServletRequest request) throws Exception {
    //
    return this.resolveViewNameInternal(viewName, locale);
    }

    private View resolveViewNameInternal(String viewName, Locale locale) throws Exception {
    if (this.viewResolvers != null) {
    for(ViewResolver viewResolver : this.viewResolvers) {

    // 真正将逻辑视图名称转换成物理视图名称,返回视图对象View
    // 如果使用的是Thymeleaf,那么返回的视图对象:ThymeleafView对象
    View view = viewResolver.resolveViewName(viewName, locale);
    if (view != null) {
    return view;
    }
    }
    }

    return null;
    }

    // 这是一个接口(负责视图解析)
    public interface ViewResolver {
    // 这个方法就是将逻辑视图名称转换成物理视图名称,并且最终返回视图对象View
    View resolveViewName(String viewName, Locale locale) throws Exception;
    }

    // 这是一个接口(负责将模板字符串转换成html代码,响应给浏览器)
    public interface View {
    void render(@Nullable Map<String, ?> model, HttpServletRequest request, HttpServletResponse response) throws Exception;
    }

    }

    在浏览器访问后端服务器时,请求会被前端控制器DispatcherServlet类处理,通过核心方法doDispatch(),将请求路径调用映射的处理器方法,处理器方法执行结束之后,返回逻辑视图名称,通过代码mv = ha.handle(processedRequest, response, mappedHandler.getHandler());将其封装为ModelAndView对象,而后通过processDispatchResult(processedRequest, response, mappedHandler, mv, dispatchException);处理视图。processDispatchResult()调用render()方法,在render()中先后调用了resolveViewName(viewName, mv.getModelInternal(), locale, request);和render(mv.getModelInternal(), request, response);
    resolveViewName()方法调用resolveViewNameInternal(),执行其中代码View view = viewResolver.resolveViewName(viewName, locale); 真正将逻辑视图名称转换成物理视图名称,并返回视图对象View。这里尽管使用的也是resolveViewName()方法,但是并不是递归,如果使用的是Thymeleaf视图,这里调用的是ThymeleafViewResolver的resolveViewName()方法,返回ThymeleafView对象。
    render()同样如果使用的是Thymeleaf视图,这里就是ThymeleafView的render()方法,也不是递归。是真正将模板字符串转换成html代码,并且将html代码相应给浏览器 。

    1.4 逻辑视图名到物理视图名的转换

    关键在于springmvc.xml文件中视图解析器的配置,假如视图解析器配置的是ThymeleafViewResolver,例:

    <bean id="thymeleafViewResolver" class="org.thymeleaf.spring6.view.ThymeleafViewResolver">
    <property name="characterEncoding" value="UTF-8"/>
    <property name="order" value="1"/>
    <property name="templateEngine">
    <bean class="org.thymeleaf.spring6.SpringTemplateEngine">
    <property name="templateResolver">
    <bean class="org.thymeleaf.spring6.templateresolver.SpringResourceTemplateResolver">
    <property name="prefix" value="/WEB-INF/templates/"/>
    <property name="suffix" value=".html"/>
    <property name="templateMode" value="HTML"/>
    <property name="characterEncoding" value="UTF-8"/>
    </bean>

    </property>

    </bean>

    </property>

    </bean>

    如果逻辑视图名"index" 转换为物理视图名:/WEB-INF/templates/index.html。

    二、转发与重定向

    2.1 转发和重定向区别

    特性转发(Forward)重定向(Redirect)
    请求次数 一次请求 两次请求
    浏览器地址栏 地址不变 地址变为目标 URL
    代码实现 request.getRequestDispatcher(“/index”).forward(request, response); response.sendRedirect(“/webapproot/index”);
    跳转控制方 服务器内部控制 浏览器控制
    跨域访问 不可实现跨域 可实现跨域跳转
    访问 WEB-INF 可访问受保护资源 无法访问受保护资源
    原理 服务器内部转发请求,浏览器只发一次请求 服务器返回新路径,浏览器重新发起请求
    适用场景 内部资源跳转,数据共享 外部跳转,防止表单重复提交

    2.2 forward

    在Spring MVC中默认就是转发的方式,我们之前所写的程序,都是转发的方式。只不过都是转发到Thymeleaf的模板文件xxx.html上。
    使用forward可以实现在Spring MVC中如何转发到另一个Controller上:

    @Controller
    public class IndexController {

    @RequestMapping("/a")
    public String toA(){
    return "forward:/b";
    }

    @RequestMapping("/b")
    public String toB(){
    return "b";
    }
    }

    通过源码的跟踪得知:整个请求处理过程中,一共创建了两个视图对象InternalResourceView,ThymeleafView
    这说明转发底层创建的视图对象是:InternalResourceView。

    思考:既然会创建InternalResourceView,应该会对应一个视图解析器呀(InternalResourceViewResolver)?
    原因并不是在springmvc.xml文件中只配置了ThymeleafViewResolver,并没有配置InternalResourceViewResolver,而是这是因为forward: 后面的不是逻辑视图名,而是一个请求路径。因此转发是不需要视图解析器的。另外,转发使用的是InternalResourceView,也说明了转发是内部资源的跳转。

    2.3 redirect

    redirect是专门完成重定向效果的,例:

    @Controller
    public class IndexController {

    @RequestMapping("/a")
    public String toA(){
    return "redirect:/b";
    }

    @RequestMapping("/b")
    public String toB(){
    return "b";
    }
    }

    通过源码的跟踪得知:当重定向的时候,SpringMVC会创建一个重定向视图对象:RedirectView。这个视图对象也是SpringMVC框架内置的。

    注意:从springmvc应用重定向到springmvc2应用(跨域),语法是:redirect:http://localhost:8080/springmvc2/b

    三、RESTFul编程风格

    RESTful 的英文全称是 Representational State Transfer(表述性状态转移)。简称REST。通过 URI + 请求方式 来控制服务器端数据的变化。
    RESTful是WEB服务接口的一种设计风格。RESTful定义了一组约束条件和规范,可以让WEB服务接口更加简洁、易于理解、易于扩展、安全可靠。

    3.1 RESTFul风格与传统方式对比

    传统的 URL 与 RESTful URL 的区别是传统的 URL 是基于方法名进行资源访问和操作,而 RESTful URL 是基于资源的结构和状态进行操作的。

    传统的 URLREST风格的URL
    GET /getUserById?id=1 GET /user/1
    GET /getAllUser GET /user
    POST /addUser POST /user
    POST /modifyUser PUT /user
    DELETE /deleteUserById?id=1 DELETE /user/1

    从上表中我们可以看出,传统的URL是基于动作的,而 RESTful URL 是基于资源和状态的。

    3.2 RESTFul方式演示

    3.2.1 查询(GET)

    @Controller
    public class UserController {

    @RequestMapping(value = "/api/user/{id}", method = RequestMethod.GET)
    public String getById(@PathVariable("id") Integer id){
    System.out.println("根据用户id查询用户信息,用户id是" + id);
    return "ok";
    }

    }

    3.2.2 查询所有(GET)

    @RequestMapping(value = "/api/users", method = RequestMethod.GET)
    public String getAllUsers(Model model){
    System.out.println("查询所有用户信息");

    // 调用service查询所有用户
    List<User> userList = userService.findAll();

    // 将数据存入Model,传递到视图
    model.addAttribute("users", userList);
    return "userList";
    }

    3.2.3 增加(POST)

    @RequestMapping(value = "/api/user", method = RequestMethod.POST)
    public String save(){
    System.out.println("保存用户信息");
    return "ok";
    }

    3.2.4 修改(PUT)

    如何发送PUT请求?
    第一步:首先你必须是一个POST请求。
    第二步:在发送POST请求的时候,提交这样的数据:_method=PUT
    第三步:在web.xml文件配置SpringMVC提供的过滤器:HiddenHttpMethodFilter

    <!–修改用户–>
    <hr>
    <form th:action="@{/api/user}" method="post">
    <!–隐藏域的方式提交 _method=put –>
    <input type="hidden" name="_method" value="put">
    用户名:<input type="text" name="username"><br>
    <input type="submit" th:value="修改">
    </form>

    <!–隐藏的HTTP请求方式过滤器–>
    <filter>
    <filter-name>hiddenHttpMethodFilter</filter-name>

    <filter-class>org.springframework.web.filter.HiddenHttpMethodFilter</filter-class>

    </filter>

    <filter-mapping>
    <filter-name>hiddenHttpMethodFilter</filter-name>

    <url-pattern>/*</url-pattern>

    </filter-mapping>

    @RequestMapping(value = "/api/user", method = RequestMethod.PUT)
    public String update(String username){
    System.out.println("修改用户信息,用户名:" + username);
    return "ok";
    }

    3.3 HiddenHttpMethodFilter(过滤器)

    HiddenHttpMethodFilter是Spring MVC框架提供的,专门用于RESTFul编程风格。
    在这里插入图片描述
    通过源码可以看到,可以看到this.methodParam是 _method,这样就要求我们在提交请求方式的时候必须采用这个格式:_method=put。其中ALLOWED_METHODS = List.of(HttpMethod.PUT.name(), HttpMethod.DELETE.name(), HttpMethod.PATCH.name());
    在这里插入图片描述
    首先初始化一个对象,默认使用原始请求对象。第一个if检查当前请求是否是 POST 方法并且不是错误转发请求;第二个if检查 _method 参数是否有值(不为null且不为空字符串),然后将参数值转为大写(确保 “put”、“Put”、“PUT” 都能被识别);第三个if检查这个转换后的方法是否在允许的列表中,再创建HttpMethodRequestWrapper对象

    在这里插入图片描述
    HttpMethodRequestWrapper 是 HiddenHttpMethodFilter 的内部类,它将指定的 method替换掉先前method,重写了 getMethod() 方法,返回我们指定的 method(如 “PUT”),这样method就从POST变成了:PUT/DELETE/PATCH。

    字符编码过滤器执行之前不能调用 request.getParameter方法,如果提前调用了,乱码问题就无法解决了。因为request.setCharacterEncoding()方法的执行必须在所有request.getParameter()方法之前执行。因此这两个过滤器就有先后顺序的要求,在web.xml文件中,应该先配置CharacterEncodingFilter,然后再配置HiddenHttpMethodFilter。

    最后感谢动力节点提供的优质学习资源,让我们在技术道路上能够站在巨人的肩膀上继续前行。本资料仅为学习过程的副产品,希望能帮助到更多同样在努力学习的开发者。

    赞(0)
    未经允许不得转载:网硕互联帮助中心 » Spring MVC 核心机制剖析:视图实现、请求转发/重定向与 RESTful 设计
    分享到: 更多 (0)

    评论 抢沙发

    评论前必须登录!