一、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 实现视图机制的核心接口
实现视图的核心类与接口包括:
- 职责:在整个Spring MVC执行流程中,负责中央调度。
- 核心方法:doDispatch
- 职责:负责将逻辑视图名转换为物理视图名,最终创建View接口的实现类,即视图实现类对象。
- 核心方法:resolveViewName
- 职责:负责将模型数据Model渲染为视图格式(HTML代码),并最终将生成的视图(HTML代码)输出到客户端。(它负责将模板语言转换成HTML代码)
- 核心方法:render
- 负责在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 转发和重定向区别
| 请求次数 | 一次请求 | 两次请求 |
| 浏览器地址栏 | 地址不变 | 地址变为目标 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 是基于资源的结构和状态进行操作的。
| 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。
最后感谢动力节点提供的优质学习资源,让我们在技术道路上能够站在巨人的肩膀上继续前行。本资料仅为学习过程的副产品,希望能帮助到更多同样在努力学习的开发者。
网硕互联帮助中心




评论前必须登录!
注册