一、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 实现视图机制的核心接口

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

  1. DispatcherServlet类(前端控制器):
    • 职责:在整个Spring MVC执行流程中,负责中央调度。
    • 核心方法:doDispatch
  2. ViewResolver接口(视图解析器):
    • 职责:负责将逻辑视图名转换为物理视图名,最终创建View接口的实现类,即视图实现类对象。
    • 核心方法:resolveViewName
  3. View接口(视图):
    • 职责:负责将模型数据Model渲染为视图格式(HTML代码),并最终将生成的视图(HTML代码)输出到客户端。(它负责将模板语言转换成HTML代码)
    • 核心方法:render
  4. 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 是基于资源的结构和状态进行操作的。

传统的 URL REST风格的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

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

Logo

助力广东及东莞地区开发者,代码托管、在线学习与竞赛、技术交流与分享、资源共享、职业发展,成为松山湖开发者首选的工作与学习平台

更多推荐