互联网应用主流框架整合之SpringMVC基础组件开发

多种传参方式

在前一篇文章互联网应用主流框架整合之SpringMVC初始化及各组件工作原理中讨论了最简单的参数传递,而实际情况要复杂的多,比如REST风格,它往往会将参数写入请求路径中,而不是以HTTP请求参数传递;比如查询客户,查询参数可能很多,需要传递JSON,需要分页,然后将数据集组装并传递分页参数;比如有时候需要传递多个对象等等,实际场景比想象的要多

SpringMVC提供了诸多的注解来解析参数,其目的是在于把控制器从复杂的Servlet API中剥离出来,这样就可以在非Web容器环境中重用这些控制器,同时也方便测试工程师进行有效地测试

接收普通请求参数

<%@page contentType="text/html" pageEncoding="UTF-8"%>
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<title>参数</title>
<!-- 加载Query文件-->
<script type="text/javascript" src="https://code.jquery.com/jquery-3.2.0.js"></script>
<!-- 此处插入JavaScript脚本  暂时忽略-->
</head>
<body><form id="form" action="./common"><table><tr><td>角色名称</td><td><input id="roleName" name="roleName" value="" /></td></tr><tr><td>备注</td><td><input id="note" name="note" /></td></tr><tr><td></td><td align="right"><input id="commit" type="button" value="提交" /></td></tr></table></form>
</body>
</html>

如果代码所示这是一个非常简单的表单,它传递了两个HTTP参数角色名称和备注,响应请求的是"./common", 也就是提交表单后,它就会请求到对应的URL上,对应的Controller如下代码所示

/*** 参数处理控制器,负责处理各种请求参数的场景,包括路径变量、请求体、请求参数等的不同组合。*/
@Controller
@RequestMapping("/params")
public class ParamsController {@Autowiredprivate RoleService roleService;/*** 首页请求处理方法,返回角色管理页面。** @return 视图模型,指向角色页面。*/@RequestMapping("/index")public ModelAndView index() {return new ModelAndView("role");}/*** 处理带有普通请求参数的请求,演示如何获取和使用这些参数。** @param roleName 角色名称参数* @param note 备注参数* @return 视图模型,用于重定向或显示结果。*/@RequestMapping("/common")public ModelAndView commonParams(String roleName, String note) {// 简单演示如何使用参数System.out.println("roleName =>" + roleName);System.out.println("note =>" + note);ModelAndView mv = new ModelAndView();mv.setViewName("index");return mv;}

此类情况是通过参数名称和HTTP请求参数的名称保持一致,来获取参数,如果不一致则无法获取参数,这样的方法允许参数为空;虽然这种方式能够满足大部分表单请求,但在有些场景下并不适合,比如新增一个用户,可能需要N多个字段,用这种方式传输,参数会非常多,这个时候就需要考虑用一个POJO来管理这些参数,在不借助其他注解的情况下,SpringMVC也有映射POJO的能力

新建一个角色参数类,代码如下所示

package com.sma.vo;/*** 角色参数类* 用于封装角色相关参数及分页参数的实体类。*/
public class RoleParams {// 角色名称private String roleName;// 角色备注信息private String note;/*** 获取角色名称* @return 角色名称*/public String getRoleName() {return roleName;}/*** 设置角色名称* @param roleName 角色名称*/public void setRoleName(String roleName) {this.roleName = roleName;}/*** 获取角色备注信息* @return 角色备注信息*/public String getNote() {return note;}/*** 设置角色备注信息* @param note 角色备注信息*/public void setNote(String note) {this.note = note;}
}

这个POJO中除了分页参数外,POJO的属性和HTTP参数一一对应了,接着在控制器中增加一个方法来通过这个POJO获取HTTP请求参数

    /*** 通过POJO对象接收请求体中的参数,便于处理复杂或多个参数的情况。** @param roleParams 包含角色参数和备注的VO对象* @return 视图模型,用于显示结果或进行重定向。*/@RequestMapping("/common/pojo")public ModelAndView commonParamPojo(RoleParams roleParams) {// 使用POJO对象获取参数System.out.println("roleName =>" + roleParams.getRoleName());System.out.println("note =>" + roleParams.getNote());ModelAndView mv = new ModelAndView();mv.setViewName("index");return mv;}

请求路径变为/common/pojo, 修改一下对应的form请求的action

<body><form id="form" action="./common/pojo"><table><tr><td>角色名称</td><td><input id="roleName" name="roleName" value="" /></td></tr><tr><td>备注</td><td><input id="note" name="note" /></td></tr><tr><td></td><td align="right"><input id="commit" type="button" value="提交" /></td></tr></table></form>
</body>

通过这样的方式可以将多个参数组织为一个POJO,以便于在参数较多时进行管理,这里需要注意的是POJO的属性也要和HTTP请求保持一致,它们也能够有效传递参数,但是有时候前端的参数命名规则和后端不一样,比如前端把角色名称的参数命名为role_name,这个时候就要进行转换,Spring MVC提供了诸多注解来实现各类转换规则

注解@RequestParam获取参数

把jsp代码中的角色名称参数名roleName改为role_name,获取参数会失败,SpringMVC提供了注解@RequestParam来处理这种情况,进行重新绑定规则,代码如下

    /*** 处理使用@RequestParam注解的请求参数,可以指定参数名称和是否必须。** @param roleName 角色名称请求参数* @param note 备注请求参数* @return 视图模型,用于显示结果或进行重定向。*/@RequestMapping("/request")public ModelAndView requestParam(@RequestParam("role_name") String roleName, String note) {// 使用@RequestParam注解获取参数System.out.println("roleName =>" + roleName);System.out.println("note =>" + note);ModelAndView mv = new ModelAndView();mv.setViewName("index");return mv;}

如果参数被@RequestParam注解,在默认情况下该参数不能为空,如果为空则会抛异常,如果要允许它为空,需要加上required=false,如下代码所示

public ModelAndView requestParam(@RequestParam(value = "role_name", required = false) String roleName, String note)

使用URL传递参数

使用URL的形式传递参数,这符合REST风格,对于一些业务比较简单的应用也十分常见,SpringMVC也对这种形式提供了良好的支持,如下代码所示

    /*** 通过路径变量获取URL中指定的id值,用于展示或操作特定ID的角色。** @param id 角色的ID,从URL路径中获取* @return 视图模型,包含角色信息的JSON视图。*/@RequestMapping("/role/{id}")public ModelAndView pathVariable(@PathVariable("id") Long id) {Role role = roleService.getRole(id);ModelAndView mv = new ModelAndView();mv.addObject(role);mv.setView(new MappingJackson2JsonView());return mv;}

在注解@RequestMapping的路径配置中的{id}表示控制器需要URL带有这个名为id的参数一起请求,方法中的@PathVariable("id")表示将获取这个在注解@RequestMapping中带过来的名为id的参数,然后通过角色服务类获取角色对象,并将其绑定到视图中,将视图设置为JSON;注意@PathVariable允许对应的参数为空

传递JSON参数

首先定义一个分页参数类PageParams,代码如下

/*** 分页参数类* 用于封装分页查询时的起始位置和每页记录数。*/
package com.sma.vo;public class PageParams {private int start; // 起始位置,表示从第几条记录开始查询private int limit; // 每页记录数,表示每页最多显示多少条记录/*** 获取起始位置* @return 起始位置的索引值*/public int getStart() {return start;}/*** 设置起始位置* @param start 起始位置的索引值,用于指定从哪条记录开始查询*/public void setStart(int start) {this.start = start;}/*** 获取每页记录数* @return 每页显示的记录数量*/public int getLimit() {return limit;}/*** 设置每页记录数* @param limit 每页显示的记录数量,用于指定每页最多显示多少条记录*/public void setLimit(int limit) {this.limit = limit;}
}

在角色参数类中添加分页属性,代码如下

package com.sma.vo;/*** 角色参数类* 用于封装角色相关参数及分页参数的实体类。*/
public class RoleParams {// 角色名称private String roleName;// 角色备注信息private String note;// 分页参数对象,用于角色列表的分页查询private PageParams pageParams = null;/*** 获取角色名称* @return 角色名称*/public String getRoleName() {return roleName;}/*** 设置角色名称* @param roleName 角色名称*/public void setRoleName(String roleName) {this.roleName = roleName;}/*** 获取角色备注信息* @return 角色备注信息*/public String getNote() {return note;}/*** 设置角色备注信息* @param note 角色备注信息*/public void setNote(String note) {this.note = note;}/*** 获取分页参数对象* @return 分页参数对象*/public PageParams getPageParams() {return pageParams;}/*** 设置分页参数对象* @param pageParams 分页参数对象*/public void setPageParams(PageParams pageParams) {this.pageParams = pageParams;}
}

向表单插入一段JavaScript,模拟通过jQuery传递JSON数据,代码如下

<script type="text/javascript">
$(document).ready(function() {//JSON参数和类RoleParams一一对应var data = {//角色查询参数roleName : 'role',note : 'note',//分页参数pageParams : {start : 0,limit : 20}}//Jquery的post请求$.post({url : "./roles",//此处需要告知传递参数类型为JSON,不能缺少contentType : "application/json",//将JSON转化为字符串传递data : JSON.stringify(data),//成功后的方法success : function(result) {}});
});
</script>

如代码所示,传递的JSON数据需要和对应参数的POJO保持一致,它将以请求体传递给控制器,所以只能用POST请求;在请求的时候须告知请求的参数类型为JSON,否则会引发控制器接收参数的异常;传递的参数是一个字符串,而不是JSON,所以这里使用了JSON.stringify()方法,将JSON数据转换为字符串

这个时候可以使用SpringMVC提供的注解@RequestBody接收参数,代码如下

/*** 使用@RequestBody注解从请求体中接收整个对象,适用于POST等提交复杂数据的场景。** @param roleParams 包含搜索条件的VO对象* @return 视图模型,包含搜索结果的JSON视图。*/@RequestMapping(value = "/roles", method = RequestMethod.POST)public ModelAndView findRoles(@RequestBody RoleParams roleParams) {List<Role> roleList = roleService.findRoles(roleParams);ModelAndView mv = new ModelAndView();mv.addObject("roleList", roleList);mv.setView(new MappingJackson2JsonView());return mv;}

这样SpringMVC就会把传递过来的请求体对应到POJO上了

接收列表数据和表单序列化

假如需要一次性删除多个数据,这时候可以考虑将一个数据传递给后端,同样的新增也是同样的情况,这就需要使用Java的集合或者数组保存对应的参数

SpringMVC对类似场景也有良好的支撑,如下JavaScript模拟传递数组给后端

<script type="text/javascript">$(document).ready(function() {//删除角色数组var idList = [ 1, 2, 3 ];//jQuery的post请求$.post({url : "./remove/roles",//将JSON转化为字符串传递data : JSON.stringify(idList),//指定传递数据类型,不可缺少contentType : "application/json",//成功后的方法success : function(result) {}});});
</script>

控制器接收数组参数代码如下

    /*** 删除角色,通过请求体接收一个角色ID列表,进行批量删除操作。** @param idList 角色ID列表,用于删除多个角色* @return 视图模型,包含删除总数的JSON视图。*/@RequestMapping(value = "/remove/roles", method = RequestMethod.POST)public ModelAndView removeRoles(@RequestBody List<Long> idList) {ModelAndView mv = new ModelAndView();int total = roleService.deleteRoles(idList);mv.addObject("total", total);mv.setView(new MappingJackson2JsonView());return mv;}

SpringMVC 通过@RequestBody注解,将传递过来的JSON数组数据转换为对应的Java集合类型

新增多个数据也是一样的逻辑

 <script type="text/javascript">
$(document).ready(function () {//新增角色数组var roleList = [{roleName: 'role_name_1', note: 'note_1'},{roleName: 'role_name_2', note: 'note_2'},{roleName: 'role_name_3', note: 'note_3'}];//jQuery的post请求$.post({url: "./insert/roles",//将JSON转化为字符串传递data: JSON.stringify(roleList),contentType: "application/json",//成功后的方法success: function (result) {}});
});
</script>
    /*** 新增角色,通过请求体接收一个角色列表,进行批量插入操作。** @param roleList 角色列表,用于批量插入新角色* @return 视图模型,包含插入总数的JSON视图。*/@RequestMapping(value = "/insert/roles", method = RequestMethod.POST)public ModelAndView insertRoles(@RequestBody List<Role> roleList) {ModelAndView mv = new ModelAndView();int total = roleService.insertRoles(roleList);mv.addObject("total", total);mv.setView(new MappingJackson2JsonView());return mv;}

通过表单序列化可以将表单数据转换为字符串传递给后端,因为一些特殊的字符需要进行一定的转换提交给后端,所以有时候需要在用户点击提交按钮后,通过序列化提交表单数据,代码如下

<%@page contentType="text/html" pageEncoding="UTF-8"%>
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<title>参数</title>
<!-- 加载Query文件-->
<script type="text/javascript" src="https://code.jquery.com/jquery-3.2.0.js"></script><script type="text/javascript">// 页面加载完成后执行的函数$(document).ready(function() {// 点击提交按钮时执行的函数$("#commit").click(function() {// 将表单数据序列化为字符串var str = $("form").serialize();// 使用jQuery的post方法提交表单数据// 提交表单$.post({// 设置提交的URL地址url:"./serialize/params",// 设置提交的数据,这里使用serialize方法获取的表单数据字符串// 将form数据序列化,传递给后台,// 则将数据以roleName=xxx&&note=xxx传递data:$("form").serialize(),// 设置提交成功后的回调函数success:function(result) {// 处理提交成功后的逻辑,这里留空}});});});</script></head>
<body><form id="form" action="./common"><table><tr><td>角色名称</td><td><input id="roleName" name="roleName" value="" /></td></tr><tr><td>备注</td><td><input id="note" name="note" /></td></tr><tr><td></td><td align="right"><input id="commit" type="button" value="提交" /></td></tr></table></form>
</body>
</html>

序列化之后,传递规则变为了roleName=xxx&&note=xxx,所以获取参数也响应的发生了变化,代码如下

    /*** 处理序列化参数的请求,展示如何接收和处理通过@RequestParam注解的序列化参数。** @param roleName 角色名称请求参数* @param note 备注请求参数* @return 视图模型,包含序列化参数的JSON视图。*/@RequestMapping(value = "/serialize/params", method = RequestMethod.POST)public ModelAndView serializeParams(@RequestParam("roleName") String roleName, @RequestParam("note") String note) {ModelAndView mv = new ModelAndView();mv.setView(new MappingJackson2JsonView());mv.addObject("roleName", roleName);mv.addObject("note", note);return mv;}

重定向

首先看一段将角色信息转化为JSON视图的功能代码,如下所示

    /*** 展示特定角色的JSON信息,通过路径变量获取角色ID。** @param id 角色ID* @param roleName 角色名称* @param note 角色备注* @return 视图模型,包含角色信息的JSON视图。*/@RequestMapping("/role/info")public ModelAndView showRoleJsonInfo(Long id, String roleName, String note) {ModelAndView mv = new ModelAndView();mv.setView(new MappingJackson2JsonView());mv.addObject("id", id);mv.addObject("roleName", roleName);mv.addObject("note", note);return mv;}

有一个这样的需求,每当新增一个角色信息时,需要将新增的数据以JSON视图的形式展示给请求者,在数据保存到数据库后,由数据库返回角色编号,再将角色信息传递给showRoleJsonInfo方法,就可以展示JSON视图给请求者,代码如下

    /*** 插入角色并重定向到角色信息页面,展示如何在插入后获取并使用新角色的ID。** @param model Spring的Model接口,用于向视图传递数据* @param roleName 角色名称* @param note 角色备注* @return 重定向到角色信息页面的字符串路径。*/@RequestMapping("/role/insert")public String insertRole(Model model, String roleName, String note) {Role role = new Role();role.setRoleName(roleName);role.setNote(note);roleService.insertRole(role);model.addAttribute("roleName", roleName);model.addAttribute("note", note);model.addAttribute("id", role.getId());return "redirect:./info";}

这里用到了Model,它代表数据模型,可以给它附上对应的数据模型,然后通过返回字符串实现重定向的功能,Spring MVC有一个约定,当返回的字符串以redirect为前缀时,就会被认为请求最后需要重定向;不仅仅可以通过返回字符串来实现重定向,也可以通过返回视图来实现重定向,代码如下

    /*** 另一种插入角色的方法,使用ModelAndView对象进行重定向并传递数据。** @param mv ModelAndView对象,用于设置重定向视图和附加数据* @param roleName 角色名称* @param note 角色备注* @return 视图模型,用于重定向到角色信息页面。*/@RequestMapping("/role/insert2")public ModelAndView insertRole2(ModelAndView mv, String roleName, String note) {Role role = new Role();role.setRoleName(roleName);role.setNote(note);roleService.insertRole(role);mv.setViewName("redirect:./info");mv.addObject("roleName", roleName);mv.addObject("note", note);mv.addObject("id", role.getId());return mv;}

这样可以将参数顺利的传给重定向的地址,同样的如果参数比较多,有些时候要传递POJO来完成,而不是一个个字段传递,代码如下

    /*** 通过角色信息展示JSON格式的数据。* 此方法处理请求的URL路径为/role/info2,旨在返回一个包含角色信息的JSON对象。* 使用MappingJackson2JsonView将模型对象转换为JSON格式,以便在客户端如JavaScript中使用。* @param role 角色对象,包含需要展示的角色信息。* @return ModelAndView 对象,配置了JSON视图和角色对象。*/@RequestMapping("/role/info2")public ModelAndView showRoleJsonInfo2(Role role) {ModelAndView mv = new ModelAndView();mv.setView(new MappingJackson2JsonView());mv.addObject("role", role);return mv;}

在RUL重定向的过程中,并不能有效地传递对象,因为HTTP的重定向参数是以字符串的形式传递的,这个时候Spring MVC提供了一个方法,就是flash属性,需要的数据模型是RedirectAttribute,代码如下

    /*** 使用RedirectAttributes进行角色插入并重定向,展示如何在重定向中携带额外信息。* @param ra RedirectAttributes接口,用于在重定向中添加闪现属性,SpringMVC会自动初始化它* @param roleName 角色名称* @param note 角色备注* @return 重定向到角色信息页面的字符串路径。*/@RequestMapping("/role/insert3")public String insertRole3(RedirectAttributes ra, String roleName, String note) {Role role = new Role(roleName, note);roleService.insertRole(role);ra.addFlashAttribute("role", role);return "redirect:./info2";}

这样就能传递POJO对象,使用addFlashAttribute方法后,Spring MVC会将数据保存到Session中,Session会在一个会话期有效,重定向后,就会将其清除,这样就能传递给下一个地址了
在这里插入图片描述

属性标签

有时候我们会将数据暂存到HTTP的request对象或者Session对象中,同样在开发控制器时,有时候也需要将对应的数据保存到这些对象中去,或者从它们当中读取数据,为此,SpringMVC有良好的支持,主要注解有@RequestAttribute@SessionAttribute@SessionAttributes

这三个注解都是Spring MVC框架中用于处理HTTP请求时,向控制器方法传递属性的注解,它们主要用于不同范围内的数据传递和管理。下面是这三个注解的解释:

  • @RequestAttribute
    • 用途: 该注解用于从HttpServletRequest的属性中获取数据,并将其绑定到控制器方法的参数上。它允许你在请求级别传递数据,这意味着这些属性只存在于当前HTTP请求的生命周期内
    • 应用场景: 当你需要在同一个请求的不同处理环节间传递数据时非常有用,比如在拦截器、过滤器或是在重定向之前设置某些属性供后续处理使用
  • @SessionAttribute
    • 用途: 此注解用于从HttpSession中获取属性值,并将其绑定到控制器方法的参数上。与@RequestAttribute不同,它操作的是session范围的数据,意味着这些属性可以在用户的整个会话期间保持有效,即使跨多个请求
    • 应用场景: 当你需要在用户的不同请求之间保持某些状态信息时,比如用户登录信息、购物车等,可以使用此注解来实现
  • @SessionAttributes
    • 用途: 这是一个类级别的注解,用于声明哪些模型属性(即控制器方法添加到Model中的属性)需要存储在HttpSession中。与单个方法上的@SessionAttribute不同,它定义了一个更大的作用域,影响控制器类中的所有处理器方法
    • 应用场景: 当你的控制器中有多个方法需要共享一些数据,且这些数据需要在用户的整个会话期间持久化时,使用@SessionAttributes可以在类级别声明这些共享的session属性。这在处理多步表单、维护临时的用户选择状态等场景中非常有用

总结来说,@RequestAttribute适用于单次请求内部的数据传递,@SessionAttribute用于单个方法中从session获取数据,而@SessionAttributes则是在控制器类级别管理那些需要跨请求持久化的数据。选择哪个注解取决于你希望数据存活的作用域以及具体的应用场景。

@RequestAttribute

这个注解的作用是从HTTP的请求对象(HttpServletRequest)中取出请求属性,只是它的有效性是在一次请求中存在,先建一个/WEB-INF/jsp/request_attribute.jsp文件,代码如下

<%@page contentType="text/html" pageEncoding="UTF-8"%> <!-- 设置页面的Content-Type和字符编码 -->
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd"> <!-- 定义HTML文档的类型和版本 -->
<html>
<head><meta http-equiv="Content-Type" content="text/html; charset=UTF-8"> <!-- 再次声明页面的Content-Type和字符编码,确保浏览器正确解析 --><title>SMA</title> <!-- 页面标题 -->
</head>
<body><%// 设置请求属性,这里将一个长整型数值1赋值给"id"属性request.setAttribute("id", 1L);// 向前转发请求到指定的资源,这里是处理请求参数的控制器或处理器request.getRequestDispatcher("/mvc/attribute/request/param").forward(request, response);// 清空输出缓冲区,避免之前的内容影响后续的输出out.clear();// 重新获取页面输出流,以便继续向客户端输出内容out = pageContext.pushBody();%>
</body>
</html>

代码中首先设置了id为1L的请求属性,然后进行了转发控制器,这样将由对应的控制器处理业务逻辑,代码如下

@Controller
@RequestMapping("/attribute")
public class AttributeController {// 角色服务@Autowiredprivate RoleService roleService = null;// 访问页面request_attribute.jsp@RequestMapping("/request/page")public ModelAndView requestPage() {return new ModelAndView("request_attribute");}/*** 测试@RequestAttribute* @param id 角色编号* @return ModelAndView*/@RequestMapping("/request/param")public ModelAndView requestAttribute(@RequestAttribute(value="id", required = false) Long id) {ModelAndView mv = new ModelAndView();Role role = roleService.getRole(id);mv.addObject("role", role);mv.setView(new MappingJackson2JsonView());return mv;}

@SessionAttribute@SessionAttributes

这两个注解和HTTP的会话对象(HttpSession)有关,在浏览器和服务器保持联系的时候HTTP会创建一个会话对象,这样可以让浏览器和服务器会话期间,通过它读/写会话对象的属性,缓存一定的数据信息

在控制器总可以使用注解@SessionAttributes设置对应的键值对,不过这个注解只能对类进行标注,不能对方法或者参数进行注解,它可以配置属性名称或者属性类型,其作用是当用它标注了某个类,SpringMVC执行完控制器的逻辑后,将数据模型中对应的属性名称或者属性类型保存到HTTP的会话对象中,如下代码所示

package com.sma.controller;import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.CookieValue;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestAttribute;
import org.springframework.web.bind.annotation.RequestHeader;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.SessionAttribute;
import org.springframework.web.bind.annotation.SessionAttributes;
import org.springframework.web.servlet.ModelAndView;
import org.springframework.web.servlet.view.json.MappingJackson2JsonView;import com.sma.pojo.Role;
import com.sma.service.RoleService;@Controller
@RequestMapping("/attribute")
//可以配置数据模型的名称和类型,两者取或关系
@SessionAttributes(names = { "id" }, types = { Role.class })
public class AttributeController {// 角色服务@Autowiredprivate RoleService roleService = null;... .../*** 测试@SessionAttributes* @param id 角色编号* @return ModelAndView*/@RequestMapping("/session/{id}")public ModelAndView sessionAttrs(@PathVariable("id") Long id) {ModelAndView mv = new ModelAndView();Role role = roleService.getRole(id);// 根据类型,Session将会保存角色信息mv.addObject("role", role);// 根据名称,Session将会保存idmv.addObject("id", id);// 视图名称,定义跳转到一个JSP文件上mv.setViewName("session_show");return mv;}

这个时候请求/mvc/attribute/session/1,那么请求会进入sessionAttrs方法中,数据模型保存了一个id和角色,由于它们都满足了注解@SessionAttributes的配置,所以最后请求会保存到Session对象中,视图名称设置为session_show,说明要进一步跳转到/WEB-INF/jsp/session_show.jsp中,这样就可以通过JSP文件去验证注解@SessionAttributes的配置是否有效了,session_show.jsp代码如下

<%@ page language="java" import="com.sma.pojo.Role" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%>
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<title>Show Session Attribute</title>
</head>
<body><%// 从session中获取Role对象,用于后续显示角色信息Role role = (Role) session.getAttribute("role");// 输出角色的id,以便用户确认当前角色的标识out.println("id = " + role.getId() + "<p/>");// 输出角色的名称,以便用户了解当前角色的名称out.println("roleName = " + role.getRoleName() + "<p/>");// 输出角色的备注信息,以便用户了解角色的详细描述out.println("note = " + role.getNote() + "<p/>");// 从session中获取用户id,用于后续显示或验证用户身份Long id = (Long) session.getAttribute("id");// 输出用户id,以便用户确认当前登录的用户标识out.println("id = " + id + "<p/>");%>
</body>
</html>

这样就可以在控制器内不使用给Servlet的API造成侵入的HttpSession对象设置Session的属性了;既然有了设置Session的属性,自然有读取Session属性的要求,SpringMVC是通过注解@SessionAttribute实现的

首先写个/WEB-INF/jsp/session_attribute.jsp,让它保存Session的属性,代码如下

<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%>
<%@ page import="java.io.*" %>
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
<html>
<head><meta http-equiv="Content-Type" content="text/html; charset=UTF-8"><title>session</title>
</head>
<body>
<%// 设置会话属性,将一个长整型值1存储到会话中,键为"id"session.setAttribute("id", 1L);// 向Dispatcher请求将请求转发到指定的资源,这里是处理会话参数的控制器request.getRequestDispatcher("/mvc/attribute/session/param").forward(request, response);// 清空输出缓冲区,确保之前的输出不会影响后续的页面渲染out.clear();// 重新设置输出流,以便可以继续向客户端输出内容out = pageContext.pushBody();
%>
</body>
</html>

当请求JSP时,它会在Session中设置一个属性id,然后跳转到对应的控制器上,在控制器中加入对应的方法,并在方法的参数中通过注解@SessionAttribute来获取Session属性值,如下代码所示

// 访问session_attribute.jsp@RequestMapping("/session/page")public ModelAndView sessionPage() {ModelAndView mv = new ModelAndView("session_attribute");return mv;}/*** 测试@SessionAttribute* @param id 角色名称* @return ModelAndView*/@RequestMapping("/session/param")public ModelAndView sessionParam(@SessionAttribute(value = "id", required = false) Long id) {ModelAndView mv = new ModelAndView();Role role = roleService.getRole(id);mv.addObject("role", role);mv.setView(new MappingJackson2JsonView());return mv;}

@CookieValue@RequestHeader

这两个注解分别用于从Cookie和HTTP请求头获取对应的请求信息,它们用法比较简单,且大同小异,只是对于Cookie而言,需要考虑的是用户是可以禁用的

    /***  获取Cookie和请求头(RequestHeader)属性* @param userAgent 用户代理* @param jsessionId 会话编号* @return ModelAndView*/@RequestMapping("/header/cookie")public ModelAndView testHeaderAndCookie(@RequestHeader(value = "User-Agent",required = false,defaultValue = "attribute") String userAgent,@CookieValue(value = "JSESSIONID",required = true,defaultValue = "MyJsessionId") String jsessionId) {ModelAndView mv = new ModelAndView();mv.setView(new MappingJackson2JsonView());mv.addObject("User-Agent", userAgent);mv.addObject("JSESSIONID", jsessionId);return mv;}

表单验证

在实际的工作中,得到数据后的第一步就是验证数据的正确性,如果存在录入上的问,那么一般会通过注解验证,发现错误后返回给用户,但是对于一些逻辑错误就很难使用注解方式验证,这个时候可以使用Spring提供的验证器(Validator)规则去验证,Spring的验证规则符合JSR(Java Specification Requests), 但是它只是一个提案,存在多种实现,目前业界广泛使用的是Hibernate Validator

在Spring MVC中,所有的验证都需要先注册验证器,验证器是由Spring MVC自动注册和加载的,不需要用户处理,为了使用JSR功能,需要引入如下依赖

    <dependency><groupId>org.hibernate.validator</groupId><artifactId>hibernate-validator</artifactId><version>6.1.0.Final</version></dependency>

JSR303注解验证输入内容

JSR 303, 正式名称为 Bean Validation (在 JSR 349 中更新为 Bean Validation 1.1, 而最新的版本是 JSR 380, 也称为 Bean Validation 2.0), 是Java为企业级应用提供的一个数据验证的标准规范。它允许开发者使用注解来声明性地规定数据验证规则,而无需在业务逻辑中混入验证代码。

  • 常用的JSR 303验证注解包括但不限于:
    • @Null:被注释的元素必须为 null。
    • @NotNull:被注释的元素必须不为 null。
    • @AssertTrue:被注释的元素必须为 true。
    • @AssertFalse:被注释的元素必须为 false。
    • @Min(value):被注释的元素必须是一个数字,其值必须大于等于指定的最小值。
    • @Max(value):被注释的元素必须是一个数字,其值必须小于等于指定的最大值。
    • @Size(min=, max=):被注释的元素的大小必须在指定的范围内。
    • @Length(min=, max=):字符串的长度限制,与 @Size 类似,但仅用于字符串。
    • @Pattern(regex=, flags=):被注释的字符串必须符合指定的正则表达式。
    • @Email:被注释的字符串必须是电子邮箱地址。
  • 自定义验证注解:除了上述内置注解外,JSR 303 还支持自定义验证注解。自定义注解需要配合以下元注解使用:
    • @Constraint(validatedBy = {YourValidator.class}):指定实现约束验证的类。
    • @Target({ElementType.TYPE, ElementType.FIELD, ElementType.ANNOTATION_TYPE, ElementType.CONSTRUCTOR, ElementType.METHOD, ElementType.PARAMETER, ElementType.TYPE_USE}):定义该注解可以应用到哪些程序元素上。
    • @Retention(RetentionPolicy.RUNTIME):确保注解在运行时可见。
    • @Documented:表示这个注解应该被 javadoc 工具记录。
    • message:默认的错误消息模板。
    • groups 和 payload:用于分组验证和携带额外的元数据。

Spring 提供了Bean的功能验证,通过注解@Valid标明哪个Bean需要启用注解式的验证,在javax.validation.constraints.*中定义了一系列的JSR规范给出的注解
在这里插入图片描述
org.hibernate.validator.constraints.*中也定义了一系列的JSR规范给出的注解
在这里插入图片描述
实际使用,假设有这样一个表单/WEB-INF/jsp/validation.jsp

<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%>
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<title>validate</title>
</head>
<body><form id="form" method="post" action="./validator"><table><tr><td>产品编号:</td><td><input name="productId" id="productId" /></td></tr><tr><td>用户编号:</td><td><input name="userId" id="userId" /></td></tr><tr><td>交易日期:</td><td><input name="date" id="date" /></td></tr><tr><td>价格:</td><td><input name="price" id="price" /></td></tr><tr><td>数量:</td><td><input name="quantity" id="quantity" /></td></tr><tr><td>交易金额:</td><td><input name="amount" id="amount" /></td></tr><tr><td>用户邮件:</td><td><input name="email" id="email" /></td></tr><tr><td>备注:</td><td><textarea id="note" name="note" cols="20" rows="5"></textarea></td></tr><tr><td colspan="2" align="right"><input type="submit" value="提交" /></tr></table></form>
</body>
</html>

对应的POJO代码如下

package com.sma.pojo;import java.util.Date;import javax.validation.constraints.DecimalMax;
import javax.validation.constraints.DecimalMin;
import javax.validation.constraints.Future;
import javax.validation.constraints.Max;
import javax.validation.constraints.Min;
import javax.validation.constraints.NotNull;
import javax.validation.constraints.Pattern;
import javax.validation.constraints.Size;
import org.springframework.format.annotation.DateTimeFormat;public class Transaction {@NotNull //不能为空private Long productId;@NotNull //不能为空private Long userId;@Future //只能是将来的日期@DateTimeFormat(pattern = "yyyy-MM-dd")//日期格式化转换@NotNull //不能为空private Date date;@NotNull //不能为空@DecimalMin(value = "0.1") //最小值0.1元private Double price;@Min(1) //最小值为1@Max(100)//最大值@NotNull //不能为空private Integer quantity;@NotNull //不能为空@DecimalMax("500000.00") //最大金额为5万元@DecimalMin("1.00") //最小交易金额1元private Double amount;@Pattern(regexp = "^([a-zA-Z0-9]*[-_]?[a-zA-Z0-9]+)*@"+ "([a-zA-Z0-9]*[-_]?[a-zA-Z0-9]+)+"+ "[\\.][A-Za-z]{2,3}([\\.] [A-Za-z]{2})?$",message="不符合邮件格式")private String email;@Size(min = 0, max = 256) //0到255个字符private String note;public Long getProductId() {return productId;}public void setProductId(Long productId) {this.productId = productId;}public Long getUserId() {return userId;}public void setUserId(Long userId) {this.userId = userId;}public Date getDate() {return date;}public void setDate(Date date) {this.date = date;}public Double getPrice() {return price;}public void setPrice(Double price) {this.price = price;}public Integer getQuantity() {return quantity;}public void setQuantity(Integer quantity) {this.quantity = quantity;}public Double getAmount() {return amount;}public void setAmount(Double amount) {this.amount = amount;}public String getEmail() {return email;}public void setEmail(String email) {this.email = email;}public String getNote() {return note;}public void setNote(String note) {this.note = note;}
}

这样就加入了对每个字段的验证,它会生成默认的错误消息,邮件的验证还是用了配置项message来重新定义验证失败后的错误信息,如此便能启动Spring的验证规则来验证表单了,配以如下控制器

package com.sma.controller;import java.util.List;import javax.validation.Valid;import org.springframework.stereotype.Controller;
import org.springframework.validation.DataBinder;
import org.springframework.validation.Errors;
import org.springframework.validation.FieldError;
import org.springframework.web.bind.annotation.InitBinder;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.servlet.ModelAndView;
import org.springframework.web.servlet.view.json.MappingJackson2JsonView;import com.sma.pojo.Transaction;
import com.sma.validator.TransactionValidator;@Controller
@RequestMapping("/validate")
public class ValidateController {// 表单页面@RequestMapping("/form")public ModelAndView formPage() {return new ModelAndView("validation");}/*** Spring验证(JSR 303)* @param trans 交易* @param errors 错误* @return*/@RequestMapping("/annotation")public ModelAndView annotationValidate(@Valid Transaction trans, Errors errors) {ModelAndView mv = new ModelAndView();// 是否存在错误if (errors.hasErrors()) {// 获取错误信息List<FieldError> errorList = errors.getFieldErrors();for (FieldError error : errorList) {// 获取错误信息mv.addObject(error.getField(), error.getDefaultMessage());}}mv.setView(new MappingJackson2JsonView());return mv;}

@Valid Transaction trans, Errors errors标明这个Bean将会被验证,另一个类型为Errors的参数则用于记录是否存在错误信息,也就是当采用JSR规范进行验证后,它会将错误信息保存到这个参数中,进入方法后使用Errors对象的hasErrors方法,便能够判断其验证是否出现错误,启动项目,访问/mvc/validate/form进入表单,输入数据,点击提交按钮,数据会提交到控制器中,页面会给出对应的错误信息

自定义验证器

创建一个检查手机号码格式的自定义注解 @IsMobile

import javax.validation.Constraint;
import javax.validation.Payload;
import java.lang.annotation.*;@Documented
@Constraint(validatedBy = IsMobileValidator.class)
@Target({ElementType.FIELD, ElementType.PARAMETER})
@Retention(RetentionPolicy.RUNTIME)
public @interface IsMobile {String message() default "手机号码格式不正确";Class<?>[] groups() default {};Class<? extends Payload>[] payload() default {};
}

然后实现对应的验证器 IsMobileValidator:

import javax.validation.ConstraintValidator;
import javax.validation.ConstraintValidatorContext;public class IsMobileValidator implements ConstraintValidator<IsMobile, String> {private static final String MOBILE_PATTERN = "^1[3-9]\\d{9}$";@Overridepublic void initialize(IsMobile constraintAnnotation) {}@Overridepublic boolean isValid(String mobile, ConstraintValidatorContext context) {return mobile != null && mobile.matches(MOBILE_PATTERN);}
}

这样,你就可以在需要验证手机号码的字段或参数上使用@IsMobile注解了

Spring验证器

Spring提供了Validator接口实现验证,它将在进入控制器逻辑之前对参数的合法性进行验证,Validator接口是Spring MVC验证表单逻辑的核心接口,其接口代码如下所示

/*** 验证器接口,用于验证特定类型的对象。* 实现这个接口的类必须提供支持验证的类型,并执行实际的验证逻辑。*/
package org.springframework.validation;
/*** Validator接口定义了验证器的行为,验证器用于验证给定对象是否符合特定的验证规则。*/
public interface Validator {/*** 检查此验证器是否支持给定的类* 通过此方法,可以确定验证器是否适用于特定类型的对象验证* @param clazz 需要验证的对象的类。参数类型使用通配符?,表示支持任何类* @return 如果验证器支持该类,则返回true;否则返回false*/boolean supports(Class<?> var1);/*** 验证给定对象是否符合特定的验证规则* 此方法将验证逻辑委托给实现类,实现类负责实际的验证工作,并通过Errors参数报告任何验证错误* @param object 待验证的对象* @param errors 用于收集验证过程中发现的错误的Errors实例*/void validate(Object var1, Errors var2);
}

Validator接口实例是一个具体的验证器,在Spring中最终被注册到验证器列表中,这样就可以提供给各个控制器使用,它通过supports方法判定是否会启用验证器验证数据,对于逻辑的验证咋通过validate方法实现,接口实例如下代码所示

package com.sma.validator;import org.springframework.validation.Errors;
import org.springframework.validation.Validator;import com.sma.pojo.Transaction;/*** 交易验证器类,用于验证Transaction对象的合法性。* 实现了Spring的Validator接口,用于在业务逻辑中进行数据验证。*/
public class TransactionValidator implements Validator {/*** 判断当前验证器是否支持指定的类。* 本验证器仅支持com.sma.pojo.Transaction类的验证。** @param clazz 需要验证的类* @return 如果clazz等于Transaction类,则返回true;否则返回false。*/@Overridepublic boolean supports(Class<?> clazz) {// 匹配为交易记录类型return Transaction.class.equals(clazz);}/*** 对Transaction对象进行验证。* 验证交易金额是否等于价格乘以数量。* 如果不相等,则认为数据不合法,将错误信息添加到Errors对象中。** @param target 需要验证的对象,类型应为Transaction* @param errors 用于收集验证过程中发现的错误信息的对象*/@Overridepublic void validate(Object target, Errors errors) {// 强制转换类型Transaction trans = (Transaction) target;// 计算交易金额与价格乘以数量的差值// 求交易金额和价格×数量的差额double dis = trans.getAmount()- (trans.getPrice() * trans.getQuantity());// 如果差值的绝对值大于0.01,认为交易金额与价格乘以数量不匹配,添加错误信息// 如果差额大于0.01,则认为业务错误if (Math.abs(dis) > 0.01) {//加入错误信息errors.rejectValue("amount", null, "交易金额和购买数量与价格不匹配");}}
}

这样这个验证器就会现在supports方法中判断是否为Transaction对象,如果判断为是,才会进行后面的逻辑验证,SpringMVC提供了注解@InitBinder,通过它可以将验证器和控制器绑定到一起,这样就能验证表单请求了,控制器代码如下所示

    @InitBinderpublic void initBinder(DataBinder binder) {//数据绑定器加入验证器binder.setValidator(new TransactionValidator());}@RequestMapping("/validator")public ModelAndView validator(@Valid Transaction trans, Errors errors) {ModelAndView mv = new ModelAndView();//是否存在错误if (errors.hasErrors()) {//获取错误信息List<FieldError>errorList = errors.getFieldErrors();for (FieldError error : errorList) {// 获取错误信息mv.addObject(error.getField(), error.getDefaultMessage());}}mv.setView(new MappingJackson2JsonView());return mv;}
}

这样把表单的请求URL修改为./validator,就能够请求得到我们的validator方法了

JSR注解方式和验证器方式不能同时使用,不过可以在使用JSR注解方式得到基本的验证信息后,再使用自己的方法验证

数据模型

视图是业务处理后展现给用户的内容,一般伴随着业务处理返回的数据,用来给用户查看,控制器处理对应业务逻辑后,首先会将数据绑定到数据模型中,并且指定视图的信息,然后将视图名称转发到视图解析器中,通过视图解析器定位到最终视图,最后将数据模型渲染到视图中,展示最终的结果给用户

之前的代码中一直用ModelAndView定义视图类型,包括JSON视图,也用它来加载数据模型;ModelAndView有一个类型为ModelMap的属性model,而ModelMap继承了LinkedHashMap<String,Object>,因此它可以存放各种键值对,为了进一步定义数据模型功能,Spring还创建了类ExtendedModelMap,这个类实现了数据模型定义的Model接口,并且在此基础上派生了关于数据绑定的类BindingAwareModelMap,关系如下图所示
在这里插入图片描述
在控制器方法中,可以把ModelAndView、Model、ModelMap作为参数,Spring MVC在运行的时候,会自动初始化它们,Spring MVC可以选择ModelMap或者其子类作为数据模型;ModelAndView被初始化后,Model属性为空,当调用它增加数据模型的方法后,会自动创建一个ModelMap实例,用以保存数据模型,这就是数据模型之间的关系

创建一个控制器,代码如下

package com.sma.controller;import java.util.List;
import java.util.Map;import org.apache.poi.ss.usermodel.Row;
import org.apache.poi.ss.usermodel.Sheet;
import org.apache.poi.ss.usermodel.Workbook;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.ui.ModelMap;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.SessionAttributes;
import org.springframework.web.servlet.ModelAndView;
import org.springframework.web.servlet.view.json.MappingJackson2JsonView;import com.sma.pojo.Role;
import com.sma.service.ExcelExportService;
import com.sma.service.RoleService;
import com.sma.view.ExcelView;
import com.sma.vo.PageParams;
import com.sma.vo.RoleParams;@Controller
@RequestMapping("/role")
/*** 角色控制器类,负责处理与角色相关的请求。* 使用@SessionAttributes注解来指示将角色对象存储在会话中,以便在多个请求之间共享。*/
@SessionAttributes(names = "role", types = Role.class)
public class RoleController {/*** 自动注入角色服务,用于处理角色相关的业务逻辑。*/@Autowiredprivate RoleService roleService = null;/*** 根据角色ID从模型映射中获取角色信息。* @param id 角色ID* @param modelMap 模型映射对象* @return 返回包含角色信息的ModelAndView对象,以JSON格式展示。*/@RequestMapping(value = "/modelmap/{id}", method = RequestMethod.GET)public ModelAndView getRoleByModelMap(@PathVariable("id") Long id, ModelMap modelMap) {Role role = roleService.getRole(id);ModelAndView mv = new ModelAndView();modelMap.addAttribute("role", role);mv.setView(new MappingJackson2JsonView());return mv;}/*** 根据角色ID从模型中获取角色信息。* @param id 角色ID* @param model 模型对象* @return 返回包含角色信息的ModelAndView对象,以JSON格式展示。*/@RequestMapping(value = "/model/{id}", method = RequestMethod.GET)public ModelAndView getRoleByModel(@PathVariable("id") Long id, Model model) {Role role = roleService.getRole(id);ModelAndView mv = new ModelAndView();model.addAttribute("role", role);mv.setView(new MappingJackson2JsonView());return mv;}/*** 根据角色ID获取角色信息,并返回一个ModelAndView对象,该对象将被渲染为特定的页面。* @param id 角色ID* @param mv ModelAndView对象,用于存储视图和模型数据* @return 返回一个ModelAndView对象,其中包含角色信息和要显示的页面。*/@RequestMapping(value = "/mv/{id}", method = RequestMethod.GET)@ResponseBodypublic ModelAndView getRoleByMv(@PathVariable("id") Long id, ModelAndView mv) {Role role = roleService.getRole(id);mv.addObject("role", role);mv.addObject("id", id);// 跳转到具体的页面(/WEB-INF/jsp/session_show.jsp)mv.setViewName("session_show");return mv;}

无论使用Model还是ModelMap,都是BindingAwareModelMap的实例,BindingAwareModelMap是一个继承了ModelMap且实现了Model接口的类,所以就有了相互转换的功能

视图和视图解析器

视图是展示给用户的内容,在此之前,要通过控制器得到对应的数据模型,如果是非逻辑视图,就不会经过视图解析器定位视图,而是直接渲染数据模型便结束了;如果是逻辑视图,就要对其进一步解析,以定位真实视图,这就是视图解析器的作用,而视图则把控制器返回的数据模型进行渲染,从而将数据展示给用户

视图

在请求之后,SpringMVC控制器获取了对应的数据,被绑定到数据模型中,视图就可以展示数据模型的信息了;Spring MVC定义了多种视图,每一种都需要满足视图接口View的定义

/*** 接口View定义了视图的规范,视图是MVC模式中的V(视图)部分,负责渲染响应。* 它不直接与HTTP请求或响应交互,而是通过一个Map对象来获取渲染所需的数据,* 并通过HttpServletRequest和HttpServletResponse来获取或设置HTTP特定的信息。*/
package org.springframework.web.servlet;import java.util.Map;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.springframework.lang.Nullable;public interface View {// 常量RESPONSE_STATUS_ATTRIBUTE用于存储响应状态属性的名称String RESPONSE_STATUS_ATTRIBUTE = View.class.getName() + ".responseStatus";// 常量PATH_VARIABLES用于存储路径变量属性的名称String PATH_VARIABLES = View.class.getName() + ".pathVariables";// 常量SELECTED_CONTENT_TYPE用于存储选定的Content-Type属性的名称String SELECTED_CONTENT_TYPE = View.class.getName() + ".selectedContentType";/*** 获取视图的Content-Type。* 默认情况下,返回null,表示视图将自行决定或不设置Content-Type。* @return 视图的Content-Type,可能为null。*/@Nullabledefault String getContentType() {return null;}/*** 渲染视图。* 此方法使用给定的模型数据、HTTP请求和响应来呈现视图。* 模型数据是一个Map对象,包含键值对,其中键是变量名,值是变量的值。* HttpServletRequest和HttpServletResponse提供了关于HTTP请求和响应的信息,* 可以用于获取请求参数或设置响应头等操作。* @param model    包含渲染视图所需数据的Map对象。* @param request  HTTP请求对象,用于获取请求信息。* @param response HTTP响应对象,用于设置响应信息。* @throws Exception 如果渲染过程中发生错误。*/void render(@Nullable Map<String, ?> model, HttpServletRequest request, HttpServletResponse response) throws Exception;
}

在这里插入图片描述
图中是常用的视图类及其关系,Spring MVC还有其他的视图类,例报表使用的AbstractJasperReportsSingleFormatView等等;JstlView和InternalResourceView是父子类,它们可以被归为一类,主要是为JSP的渲染服务的,可以使用JSTL标签库,也可以使用Spring MVC定义的标签库;MappingJackson2JsonView则是JSON视图类,它是一个非逻辑视图,木器是将数据模型转换为JSON视图,例如如下代码

    /*** 根据角色ID从模型映射中获取角色信息。* @param id 角色ID* @param modelMap 模型映射对象* @return 返回包含角色信息的ModelAndView对象,以JSON格式展示。*/@RequestMapping(value = "/modelmap/{id}", method = RequestMethod.GET)public ModelAndView getRoleByModelMap(@PathVariable("id") Long id, ModelMap modelMap) {Role role = roleService.getRole(id);ModelAndView mv = new ModelAndView();modelMap.addAttribute("role", role);mv.setView(new MappingJackson2JsonView());return mv;}

mv.setView(new MappingJackson2JsonView());指定了具体视图的类型,由于MappingJackson2JsonView是非逻辑视图,所以在没有视图解析器的情况下也可以渲染,最终将其绑定的数据模型转换为JSON数据

InternalResourceView是一个逻辑视图,它需要一个视图解析器,通常会在dispatcher-servlet.xml中进行如下配置

<?xml version='1.0' encoding='UTF-8' ?>
<beans xmlns="http://www.springframework.org/schema/beans"xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"xmlns:p="http://www.springframework.org/schema/p"xmlns:tx="http://www.springframework.org/schema/tx"xmlns:context="http://www.springframework.org/schema/context"xmlns:mvc="http://www.springframework.org/schema/mvc"xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-4.0.xsdhttp://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx-4.0.xsdhttp://www.springframework.org/schema/contexthttp://www.springframework.org/schema/context/spring-context-4.0.xsdhttp://www.springframework.org/schema/mvc http://www.springframework.org/schema/mvc/spring-mvc-4.0.xsd"><!-- 使用注解驱动 --><mvc:annotation-driven /><!-- 定义扫描装载的包 --><context:component-scan base-package="com.*" /><!-- 定义视图解析器 --><!-- 找到Web工程/WEB-INF/JSP文件夹,且文件结尾为jsp的文件作为映射 --><bean id="viewResolver"class="org.springframework.web.servlet.view.InternalResourceViewResolver"p:prefix="/WEB-INF/jsp/" p:suffix=".jsp" /><!-- 如果有配置数据库事务,需要开启注解事务的,需要开启这段代码 --><!-- <tx:annotation-driven transaction-manager="transactionManager" /> -->
</beans>

也可以使用Java配置的方式取代它,如下代码所示

package com.ssmvc.web.config;import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.ViewResolver;
import org.springframework.web.servlet.config.annotation.EnableWebMvc;
import org.springframework.web.servlet.view.InternalResourceViewResolver;/*** Spring MVC的配置类* 通过@Configuration注解标记这个类是一个配置类,等同于Spring XML配置文件* 使用@EnableWebMvc注解开启Spring MVC的功能* 使用@ComponentScan注解指定Spring要扫描的组件包,这些包中的组件会被自动注册为Spring Bean*/
@Configuration
@ComponentScan("com.ssmvc.controller")
@EnableWebMvc
public class WebConfig {/*** 配置InternalResourceViewResolver,作为Spring MVC的视图解析器* 通过@Bean注解标记这个方法会返回一个Bean,该Bean会被注册到Spring的ApplicationContext中* 方法名称viewResolver和@Bean注解中的name属性一起定义了这个Bean的名称* @return 返回一个配置好的InternalResourceViewResolver实例*/@Bean(name = "viewResolver")public ViewResolver initViewResolver() {InternalResourceViewResolver viewResolver = new InternalResourceViewResolver();viewResolver.setPrefix("/WEB-INF/jsp/");viewResolver.setSuffix(".jsp");return viewResolver;}
}

也可以通过实现WebMvcConfigurer接口来实现,代码如下

package com.ssmvc.web.config;import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.EnableWebMvc;
import org.springframework.web.servlet.config.annotation.ViewControllerRegistry;
import org.springframework.web.servlet.config.annotation.ViewResolverRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import org.springframework.web.servlet.view.InternalResourceViewResolver;/*** Spring MVC的配置类* 通过@Configuration注解标记这个类是一个配置类,等同于Spring XML配置文件* 使用@EnableWebMvc注解开启Spring MVC的功能* 使用@ComponentScan注解指定Spring要扫描的组件包,这些包中的组件会被自动注册为Spring Bean*/
@Configuration
@ComponentScan("com.ssmvc.controller")
@EnableWebMvc
public class WebConfigII implements WebMvcConfigurer {/*** 配置视图解析器* 本方法用于设置Spring MVC中视图解析的前缀和后缀,指定视图文件在项目中的位置和格式。* 使用InternalResourceViewResolver作为视图解析器,它能够处理JSP视图。* 设置前缀为"/WEB-INF/jsp/",确保视图文件位于WEB-INF目录下的jsp子目录中。* 设置后缀为".jsp",指明视图文件的格式为JSP。** @param registry ViewResolverRegistry实例,用于注册和配置视图解析器*/@Overridepublic void configureViewResolvers(ViewResolverRegistry registry) {InternalResourceViewResolver viewResolver = new InternalResourceViewResolver();viewResolver.setPrefix("/WEB-INF/jsp/");viewResolver.setSuffix(".jsp");registry.viewResolver(viewResolver);}/*** 本方法用于配置默认的控制器,即无需处理业务逻辑的简单页面跳转。* 通过ViewControllerRegistry添加一个控制器,将"/config/index"路径映射到"index"视图。* 这样当请求"/config/index"时,会直接展示对应的"index.jsp"页面,无需额外的控制器逻辑。* @param registry ViewControllerRegistry实例,用于注册和配置ViewController*/@Overridepublic void addViewControllers(ViewControllerRegistry registry) {registry.addViewController("/config/index").setViewName("index");}
}

无论使用何种方法,都是为了创建一个视图解析器,让Spring MVC可以通过前缀和后缀加上视图名称找到对应的JSP文件,然后把数据模型渲染到JSP文件中

视图解析器

非逻辑视图是不需要用视图解析器解析的,例如MappingJackson2JsonView,它的含义是把当前数据模型转化为JSON,不需要转换试图逻辑名称,但是对于逻辑视图而言,通过视图名称定位到最终视图是一个必备过程,例如InternalResourceView就是这样的一个视图,当它被配置后,就会加载到SpringMVC的视图解析器列表中,当返回ModelAndView时,SpringMVC就会在视图解析器列表中遍历,找到对应的视图解析器去解析式图,视图解析器接口源码如下

/*** 视图解析器接口,用于根据视图名称和区域设置解析视图。* 视图解析器的职责是将逻辑视图名称映射到实际的视图对象上,以便视图可以负责渲染响应。* 它是Spring MVC框架中的一部分,用于支持不同的视图技术。*/
import java.util.Locale;
import org.springframework.lang.Nullable;
public interface ViewResolver {/*** 根据给定的视图名称和区域设置解析视图。* * @param viewName 视图的逻辑名称,通常是由控制器返回的字符串。* @param locale 用户的区域设置,用于支持国际化。* @return 解析后的视图对象,如果无法解析则返回null。* @throws Exception 如果解析过程中出现错误。* * 方法注释解释了为什么方法会返回null,以及当无法解析视图时应该做什么。*/@NullableView resolveViewName(String viewName, Locale locale) throws Exception;
}

接口源码就两个参数,一个视图名,一个Locale类型,Locale类型参数是用于国际化的,这就说明了Spring MVC是支持国际化的,对于Spring MVC框架而言,它也配置了多种视图解析器,如下UML图所示
在这里插入图片描述
图中展示了Spring MVC自带的视图解析器,当控制器返回视图的逻辑名称时,通过这些解析器就能定位到具体的视图

有时候在控制器中并没有返回一个ModelAndView,而是只返回一个字符串,它也能够渲染视图,因为视图解析器定位了对应的视图,例如如下代码

    /*** 首页请求处理方法的另一种形式,返回角色管理页面的字符串路径。* @return 角色页面的字符串路径。*/@RequestMapping("/index2")public String index2() {return "role";}

由于配置了InternalResourceViewResolver,所以通过Spring MVC系统能够找到InternalResourceView视图,如果存在数据模型,那么Spring MVC会将视图和数据模型绑定到一个ModelAndView上,然后视图解析器会根据视图的名称,找到对应的视图资源,这就是视图解析器的作用

Excle视图使用

导出Excel

在实际的应用开发中,经常遇到需要导出Excel的功能,对于Excel视图的开发,Spring MVC推荐使用AbstractXlsView,它实现了视图接口,是一个抽象类,不能生成实例对象,但它自己定义了一个抽象方法buildExcelDocument去实现,代码如下

/*** 抽象类,作为生成Excel文档视图的基础。* 该类继承自AbstractView,用于处理以Excel格式响应的视图解析。* 它默认将内容类型设置为application/vnd.ms-excel,适合Excel文件下载。*/
package org.springframework.web.servlet.view.document;import java.io.IOException;
import java.util.Map;
import javax.servlet.ServletOutputStream;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.apache.poi.hssf.usermodel.HSSFWorkbook;
import org.apache.poi.ss.usermodel.Workbook;
import org.springframework.web.servlet.view.AbstractView;public abstract class AbstractXlsView extends AbstractView {/*** 构造函数,设置默认的内容类型为Excel的MIME类型。*/public AbstractXlsView() {this.setContentType("application/vnd.ms-excel");}/*** 判断视图是否生成用于下载的内容。* 对于Excel视图,通常是用于下载的。* @return 布尔值,表示是否生成下载内容。*/protected boolean generatesDownloadContent() {return true;}/*** 渲染合并后的模型数据到Excel文档。* 该方法是抽象类AbstractView中的具体实现部分,用于处理Excel文档的生成。* @param model 映射表,包含所有要呈现的数据。* @param request HTTP请求对象,可能用于获取额外的渲染上下文信息。* @param response HTTP响应对象,用于设置响应头信息和输出Excel内容。* @throws Exception 如果渲染过程中发生错误。*/protected final void renderMergedOutputModel(Map<String, Object> model, HttpServletRequest request, HttpServletResponse response) throws Exception {Workbook workbook = this.createWorkbook(model, request);this.buildExcelDocument(model, workbook, request, response);response.setContentType(this.getContentType());this.renderWorkbook(workbook, response);}/*** 创建一个空的工作簿对象。* 默认实现使用HSSFWorkbook创建一个Excel 2003及以前版本的工作簿。* @param model 渲染所需的数据模型。* @param request HTTP请求对象,可能用于获取额外的上下文信息。* @return 工作簿对象,用于构建Excel文档。*/protected Workbook createWorkbook(Map<String, Object> model, HttpServletRequest request) {return new HSSFWorkbook();}/*** 将工作簿写入HTTP响应中,完成Excel文档的渲染。* @param workbook 要写入响应的工作簿对象。* @param response HTTP响应对象,用于输出Excel内容。* @throws IOException 如果写入过程中发生I/O错误。*/protected void renderWorkbook(Workbook workbook, HttpServletResponse response) throws IOException {ServletOutputStream out = response.getOutputStream();workbook.write(out);workbook.close();}/*** 子类必须实现该方法,用于填充工作簿的具体内容。* 这是抽象方法,需要子类根据实际情况实现,以将模型数据填充到Excel工作簿中。* @param model 映射表,包含所有要呈现的数据。* @param workbook 用于构建Excel文档的工作簿对象。* @param request HTTP请求对象,可能用于获取额外的上下文信息。* @param response HTTP响应对象,可能用于设置额外的响应头信息。* @throws Exception 如果构建Excel文档过程中发生错误。*/protected abstract void buildExcelDocument(Map<String, Object> var1, Workbook var2, HttpServletRequest var3, HttpServletResponse var4) throws Exception;
}

只要完成这个buildExcelDocument方法,便能使用Excel视图功能了,该方法主要任务是创建一个Workbook,这里需要用到POI的API,需要引入如下依赖

    <!-- 引入Apache POI依赖,用于处理Microsoft Office文档 --><dependency><groupId>org.apache.poi</groupId><artifactId>poi-ooxml</artifactId><version>4.1.1</version></dependency>

假设要导出所有角色信息,先定义一个接口,该接口主要用于自定义生成Excel的规则,如下代码所示

package com.sma.service;import java.util.Map;import org.apache.poi.ss.usermodel.Workbook;public interface ExcelExportService {/****  生成Exel文档规则* @param model 数据模型* @param workbook POI的Excel workbook*/public void makeWorkBook(Map<String, Object> model, Workbook workbook);
}

然后还需要一个可实例化的Excel视图类,因为导出文档还需要一个下载文件名称,所以还需要定义一个文件名属性,由于该视图不是一个逻辑视图,所以无需解析器运行,代码如下

package com.sma.view;import java.util.Map;import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;import org.apache.poi.ss.usermodel.Workbook;
import org.springframework.util.StringUtils;
import org.springframework.web.servlet.view.document.AbstractXlsView;import com.sma.service.ExcelExportService;/*** Excel视图类,继承自AbstractXlsView,用于导出Excel文件。* 通过提供不同的构造方法,可以灵活地配置导出服务和文件名。*/
public class ExcelView extends AbstractXlsView {// Excel文件名private String fileName = null;// 导出服务接口,用于生成Excel内容private ExcelExportService excelExpService = null;/*** 构造方法,仅传入导出服务接口。* @param excelExpService 导出服务接口,用于生成Excel内容。*/public ExcelView(ExcelExportService excelExpService) {this.excelExpService = excelExpService;}/*** 构造方法,传入视图名称和导出服务接口。* @param viewName 视图名称,用于Spring MVC框架识别。* @param excelExpService 导出服务接口,用于生成Excel内容。*/public ExcelView(String viewName, ExcelExportService excelExpService) {this.setBeanName(viewName);}/*** 构造方法,传入视图名称、导出服务接口和文件名。* @param viewName 视图名称,用于Spring MVC框架识别。* @param excelExpService 导出服务接口,用于生成Excel内容。* @param fileName Excel文件名,用于设置导出文件的名称。*/public ExcelView(String viewName,ExcelExportService excelExpService, String fileName) {this.setBeanName(viewName);this.excelExpService = excelExpService;this.fileName = fileName;}// 获取文件名public String getFileName() {return fileName;}// 设置文件名public void setFileName(String fileName) {this.fileName = fileName;}// 获取导出服务接口public ExcelExportService getExcelExpService() {return excelExpService;}// 设置导出服务接口public void setExcelExpService(ExcelExportService excelExpService) {this.excelExpService = excelExpService;}/*** 覆盖父类方法,用于构建Excel文档。* @param model 视图模型,包含导出数据。* @param workbook 工作簿对象,用于存储Excel内容。* @param request HTTP请求对象,用于获取请求参数。* @param response HTTP响应对象,用于设置响应头信息。* @throws Exception 如果导出过程中发生错误。*/@Overrideprotected void buildExcelDocument(Map<String, Object> model,Workbook workbook, HttpServletRequest request,HttpServletResponse response) throws Exception {// 检查导出服务接口是否为空if (excelExpService == null) {throw new RuntimeException("导出服务接口不能为null!!");}// 文件名不为空,为空则使用请求路径中的字符串作为文件名if (!StringUtils.isEmpty(fileName)) {// 处理文件名的字符编码String reqCharset = request.getCharacterEncoding();reqCharset = reqCharset == null ? "UTF-8" : reqCharset;fileName = new String(fileName.getBytes(reqCharset), "UTF-8");// 设置响应头,指定文件名response.setHeader("Content-disposition", "attachment;filename=" + fileName);}// 回调接口方法,使用自定义生成Excel文档excelExpService.makeWorkBook(model, workbook);}}

代码实现了生成ExcelDocument方法,完成了一个视图类,然后在控制器中加入对应的方法,代码如下

    /*** 导出所有角色列表到Excel。* @return 返回一个包含角色列表的Excel文件的ModelAndView对象。*/@RequestMapping(value = "/excel/list", method = RequestMethod.GET)public ModelAndView export() {//模型和视图ModelAndView mv = new ModelAndView();//Excel视图,并设置自定义导出接口ExcelView ev = new ExcelView("role-list", exportService(), "所有角色.xlsx");//设置SQL后台参数RoleParams roleParams = new RoleParams();//限制1万条PageParams page = new PageParams();page.setStart(0);page.setLimit(10000);roleParams.setPageParams(page);//查询List<Role>roleList = roleService.findRoles(roleParams);//加入数据模型mv.addObject("roleList", roleList);mv.setView(ev);return mv;}/*** 提供一个私有的ExcelExportService实例,用于定制Excel导出的逻辑。* @return 返回一个ExcelExportService实例,用于角色列表的导出。*/@SuppressWarnings({ "unchecked"})private ExcelExportService exportService() {//使用Lambda表达式自定义导出excel规则return (Map<String, Object> model, Workbook workbook) -> {//获取用户列表List<Role>roleList = (List<Role>) model.get("roleList");//生成SheetSheet sheet= workbook.createSheet("所有角色");//加载标题Row title = sheet.createRow(0);title.createCell(0).setCellValue("编号");title.createCell(1).setCellValue("名称");title.createCell(2).setCellValue("备注");//便利角色列表,生成一行行的数据for (int i=0; i<roleList.size(); i++) {Role role = roleList.get(i);int rowIdx = i + 1;Row row = sheet.createRow(rowIdx);row.createCell(0).setCellValue(role.getId());row.createCell(1).setCellValue(role.getRoleName());row.createCell(2).setCellValue(role.getNote());}};}

如此便能够导出Excel了

上传文件

在互联网应用中,上传头像、图片等相关文件的需求十分常见,SpringMVC为上传文件提供了良好的支持,通过MultipartResolver接口来处理,源码如下

/*** 解析器接口,用于处理HTTP请求中的多部分数据,例如文件上传。* 它提供了检查请求是否包含多部分数据、解析多部分请求以及清理多部分请求数据的功能。* * 该接口的实现应该能够处理多部分请求的解析,包括但不限于文件上传。* 解析过程中可能涉及到的步骤包括识别多部分请求、分割多部分数据、为每个部分分配名称和类型等。* 实现类还需要处理解析过程中可能出现的错误,例如文件大小超出限制或文件类型不被允许。*/
package org.springframework.web.multipart;import javax.servlet.http.HttpServletRequest;public interface MultipartResolver {/*** 检查给定的HttpServletRequest是否包含多部分数据。* * @param request 要检查的HTTP请求。* @return 如果请求是多部分的,则返回true;否则返回false。*/boolean isMultipart(HttpServletRequest request);/*** 解析多部分请求,返回一个封装了多部分数据的MultipartHttpServletRequest实例。* * @param request 要解析的HTTP请求。* @return 封装了多部分数据的MultipartHttpServletRequest实例。* @throws MultipartException 如果解析过程中出现错误。*/MultipartHttpServletRequest resolveMultipart(HttpServletRequest request) throws MultipartException;/*** 清理与给定的MultipartHttpServletRequest相关的多部分数据。* 这一步骤通常包括删除临时文件,这些文件是在解析多部分请求时创建的。* * @param request 包含多部分数据的MultipartHttpServletRequest。*/void cleanupMultipart(MultipartHttpServletRequest request);
}

它有两个实现类,其中CommonsMultipartResolver依赖于Apache下的Jakarta Common FileUpload项目;StandardServletMultipartResolver则不依赖第三方包,在Spring3.1和Servlet3.0以上版本可以直接用,但在这个版本之前的只能使用CommonsMultipartResolver
在这里插入图片描述
在Spring中,既可以通过XML也可以通过Java配置MultipartResolver,对于StandardServletMultipartResolver,它的构造方法没有参数,通过注解@Bean就可以进行初始化,如下代码所示

    /*** 初始化并配置MultipartResolver bean,用于处理HTTP请求中的多部分(multipart)数据,例如文件上传。* 使用StandardServletMultipartResolver实现,这是Spring Boot默认的multipart解析器。** @return 返回一个新的StandardServletMultipartResolver实例,用于处理multipart请求。* @Bean 注解表明该方法会返回一个bean实例,并将其注册到Spring应用上下文中,名称为"multipartResolver"。*/@Bean(name = "multipartResolver")public MultipartResolver initMultipartResolver() {return new StandardServletMultipartResolver();}

multipartResolver是Spring约定好的Bean名称,不可以修改,上传文件还需要对文件进行限制,比如单个文件的大小,设置上传路径等等,在通过Java配置Spring MVC初始化的时候,只需要继承类AbstractAnnotationConfigDispatcherServletInitializer就可以,通过继承它就可以注解配置了,这个类提供了一个可以覆盖的方法customizeRegistration,它是一个用于初始化DispatcherServlet的方法(Servlet3.0以上),通过它可以配置文件上传的一些属性,Spring MVC初始化器代码如下

package com.sma.config;
import javax.servlet.MultipartConfigElement;
import javax.servlet.ServletRegistration.Dynamic;import org.springframework.web.servlet.support.AbstractAnnotationConfigDispatcherServletInitializer;import com.sma.backend.config.BackendConfig;
import com.sma.web.config.WebConfig;/*** Spring MVC应用程序的初始化器。* 继承自AbstractAnnotationConfigDispatcherServletInitializer,用于配置Spring MVC的DispatcherServlet和应用程序上下文。*/
public class MyWebAppInitializerextends AbstractAnnotationConfigDispatcherServletInitializer {/*** 配置根应用程序上下文的类。* 这些类被用来初始化ApplicationContext。* @return 根应用程序上下文的类数组。*/@Overrideprotected Class<?>[] getRootConfigClasses() {// 可以返回Spring的Java配置文件数组return new Class<?>[] {BackendConfig.class };}/*** 配置DispatcherServlet的应用程序上下文的类。* 这些类被用来初始化DispatcherServlet的ApplicationContext。* @return DispatcherServlet的应用程序上下文的类数组。*/@Overrideprotected Class<?>[] getServletConfigClasses() {// 可以返回Spring的Java配置文件数组return new Class<?>[] { WebConfig.class };}/*** 配置DispatcherServlet的URL映射。* 这些URLs会被DispatcherServlet处理。* @return URL映射的字符串数组。*/@Overrideprotected String[] getServletMappings() {return new String[] { "/mvc/*" };}/*** 自定义Servlet注册。* 这里用于配置文件上传的相关设置。* @param dynamic Servlet的动态注册接口,用于配置Servlet。*/@Overrideprotected void customizeRegistration(Dynamic dynamic) {// 文件上传路径String filepath = "e:/mvc/uploads";// 5MBLong singleMax = (long) (5*Math.pow(2, 20));// 10MBLong totalMax = (long) (10*Math.pow(2, 20));// 配置MultipartResolver,限制请求,单个文件5MB,总文件10MBdynamic.setMultipartConfig(new MultipartConfigElement(filepath, singleMax, totalMax, 0));}}

如果使用XML,就在web.xml文件中配置DispatcherServlet的地方配置就可以,如下代码所示

<?xml version="1.0" encoding="UTF-8"?>
<web-app version="3.1"xmlns="http://xmlns.jcp.org/xml/ns/javaee"xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaeehttp://xmlns.jcp.org/xml/ns/javaee/web-app_3_1.xsd"><!-- 配置Spring IoC配置文件路径 --><context-param><param-name>contextConfigLocation</param-name><param-value>/WEB-INF/applicationContext.xml</param-value></context-param><!-- 配置ContextLoaderListener用以初始化Spring IoC容器 --><listener><listener-class>org.springframework.web.context.ContextLoaderListener</listener-class></listener><!-- 配置DispatcherServlet --><servlet><!-- 注意:Spring MVC框架会根据这个名词,找到/WEB-INF/dispatcher-servlet.xml作为配置文件载入 --><servlet-name>dispatcher</servlet-name><servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class><!-- 使得Dispatcher在服务器启动的时候就初始化 --><load-on-startup>2</load-on-startup><!--MultipartResolver参数 --><multipart-config><location>e:/mvc/uploads/</location><!-- 单个文件限制5MB --><max-file-size>5242880</max-file-size><!-- 总文件限制10MB --><max-request-size>10485760</max-request-size><file-size-threshold>0</file-size-threshold></multipart-config></servlet><!-- Servlet拦截配置 --><servlet-mapping><servlet-name>dispatcher</servlet-name><!-- 拦截路径匹配 --><url-pattern>/mvc/*</url-pattern></servlet-mapping>
</web-app>

通过这样的XML配置也可以实现对MultipartResolver的配置初始化,然后通过XML或者注解生成StandardServletMultipartResolver即可

也可以使用CommonsMultipartResolver完成,但它依赖第三方包,需要导入如下依赖

    <!-- 引入Apache Commons FileUpload依赖 --><dependency><groupId>commons-fileupload</groupId><artifactId>commons-fileupload</artifactId><version>1.4</version></dependency>

使用它需要配置一个Bean, 使用Java配置文件方式,代码如下

    /*** 初始化并配置CommonsMultipartResolver,用于处理文件上传。* @return CommonsMultipartResolver 实例,配置了单个文件和总上传大小的限制,以及上传文件的临时目录。* @bean(name = "multipartResolver") 该方法标记为一个Spring Bean,命名为"multipartResolver"。*/@Bean(name = "multipartResolver")public MultipartResolver initCommonsMultipartResolver() {// 文件上传路径String filepath = "e:/mvc/uploads";// 设置单个文件的最大上传大小为5MBLong singleMax = (long) (5 * Math.pow(2, 20));// 设置总上传大小的最大限制为10MBLong totalMax = (long) (10 * Math.pow(2, 20));// 创建CommonsMultipartResolver实例CommonsMultipartResolver multipartResolver = new CommonsMultipartResolver();// 配置单个文件的最大上传大小multipartResolver.setMaxUploadSizePerFile(singleMax);// 配置总上传大小的最大限制multipartResolver.setMaxUploadSize(totalMax);try {// 设置上传文件的临时存储目录multipartResolver.setUploadTempDir(new FileSystemResource(filepath));} catch (IOException e) {// 如果设置上传目录时发生IO异常,打印异常堆栈跟踪e.printStackTrace();}// 返回配置好的CommonsMultipartResolver实例return multipartResolver;}

处理完上传后,就是处理文件解析,在Spring MVC中,对于MultipartResolver解析的调度是通过DispatcherServlet进行的,它首先判断请求是否是一种enctype="multipart/*"请求,如果是并且存在一个名为multipartResolver的Bean定义,那么它会把HttpServletRequest请求转换为MultipartHttpServletRequest请求对象,MultipartHttpServletRequest是Spring MVC的一个接口,其关系如下

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
操作文件是需要持有一定的资源的,而DispatcherServlet会在请求的最后释放掉这些资源,它还会把文件请求转换为一个MultipartFile对象,通过这个对象可以进一步操作文件

提价文件会以POST请求为主,首先建一个表单WEB-INF/jsp/file_upload.jsp,代码如下

<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%>
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" 
"http://www.w3.org/TR/html4/loose.dtd">
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<title>文件上传</title>
</head>
<body><form method="post" action="./part"enctype="multipart/form-data"><input type="file" name="file" value="请选择上传的文件" /> <inputtype="submit" value="提交" /></form>
</body>
</html>

然后开发控制器,代码如下

package com.sma.controller;import java.io.File;
import java.io.IOException;import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.Part;import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.multipart.MultipartFile;
import org.springframework.web.multipart.MultipartHttpServletRequest;
import org.springframework.web.servlet.ModelAndView;
import org.springframework.web.servlet.view.json.MappingJackson2JsonView;@Controller
@RequestMapping("/file")
public class FileController {// 文件路径private static final String FILE_PATH = "e:/mvc/uploads/";@RequestMapping(value = "/page", method = RequestMethod.GET)public String page() {return "file_upload";}@RequestMapping(value = "/upload", method = RequestMethod.POST)public ModelAndView upload(HttpServletRequest request) {// 进行转换MultipartHttpServletRequest mhsr = (MultipartHttpServletRequest) request;// 获得请求上传的文件MultipartFile file = mhsr.getFile("file");// 设置视图为JSON视图ModelAndView mv = new ModelAndView();mv.setView(new MappingJackson2JsonView());// 获取原始文件名String fileName = file.getOriginalFilename();// 目标文件File dest = new File(FILE_PATH + fileName);try {// 保存文件file.transferTo(dest);// 保存成功mv.addObject("success", true);mv.addObject("msg", "上传文件成功");} catch (IllegalStateException | IOException e) {// 保存失败mv.addObject("success", false);mv.addObject("msg", "上传文件失败");e.printStackTrace();}return mv;}

如此便可以把文件保存到指定的路径中了,但这样会有一个问题,当使用HttpServletRequest作为方法参数时,会造成API侵入,可以修改为用MultipartFile或者Part类对象实现,MultipartFile是Spring MVC提供的类,Part是Servlet API提供的类,在上面FileController的基础上,新增方法,实现代码如下

 package com.sma.controller;import java.io.File;
import java.io.IOException;import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.Part;import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.multipart.MultipartFile;
import org.springframework.web.multipart.MultipartHttpServletRequest;
import org.springframework.web.servlet.ModelAndView;
import org.springframework.web.servlet.view.json.MappingJackson2JsonView;@Controller
@RequestMapping("/file")
public class FileController {// 文件路径private static final String FILE_PATH = "e:/mvc/uploads/";@RequestMapping(value = "/page", method = RequestMethod.GET)public String page() {return "file_upload";}@RequestMapping(value = "/upload", method = RequestMethod.POST)public ModelAndView upload(HttpServletRequest request) {// 进行转换MultipartHttpServletRequest mhsr = (MultipartHttpServletRequest) request;// 获得请求上传的文件MultipartFile file = mhsr.getFile("file");// 设置视图为JSON视图ModelAndView mv = new ModelAndView();mv.setView(new MappingJackson2JsonView());// 获取原始文件名String fileName = file.getOriginalFilename();// 目标文件File dest = new File(FILE_PATH + fileName);try {// 保存文件file.transferTo(dest);// 保存成功mv.addObject("success", true);mv.addObject("msg", "上传文件成功");} catch (IllegalStateException | IOException e) {// 保存失败mv.addObject("success", false);mv.addObject("msg", "上传文件失败");e.printStackTrace();}return mv;}// 使用MultipartFile@RequestMapping("/multipart/file")public ModelAndView uploadMultipartFile(MultipartFile file) {// 定义JSON视图ModelAndView mv = new ModelAndView();mv.setView(new MappingJackson2JsonView());// 获取原始文件名String fileName = file.getOriginalFilename();file.getContentType();// 目标文件File dest = new File(FILE_PATH + fileName);try {// 保存文件file.transferTo(dest);mv.addObject("success", true);mv.addObject("msg", "上传文件成功");} catch (IllegalStateException | IOException e) {mv.addObject("success", false);mv.addObject("msg", "上传文件失败");e.printStackTrace();}return mv;}// 使用Part@RequestMapping("/part")public ModelAndView uploadPart(Part file) {ModelAndView mv = new ModelAndView();mv.setView(new MappingJackson2JsonView());// 获取原始文件名String fileName = file.getSubmittedFileName();File dest = new File(fileName);try {// 保存文件file.write(FILE_PATH + fileName);mv.addObject("success", true);mv.addObject("msg", "上传文件成功");} catch (IllegalStateException | IOException e) {mv.addObject("success", false);mv.addObject("msg", "上传文件失败");e.printStackTrace();}return mv;}
}

只需要修改表单提交地址便可以使用新的方法了,但需要注意Servlet3.0之后才支持Part

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.rhkb.cn/news/348951.html

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

[AI资讯·0612] AI测试高考物理题,最高准确率100%,OpenAI与苹果合作,将ChatGPT融入系统中,大模型在物理领域应用潜力显现

AI资讯 国产AI大战高考物理&#xff0c;第1题全对&#xff0c;第2题开始放飞终于放大招了&#xff0c;2024WWDC&#xff0c;苹果开启AI反击战苹果一夜重塑iPhone&#xff01;GPT-4o加持Siri&#xff0c;AI深入所有APPOpenAI确认苹果集成ChatGPT 还任命了两位新高管GPT-4搞不定…

大数据可视化电子沙盘:前端技术的全新演绎

随着大数据时代的到来&#xff0c;数据可视化成为了一个重要的技术趋势。数据可视化不仅可以让复杂的数据变得更加直观易懂&#xff0c;还能帮助我们更好地分析和理解数据。在本文中&#xff0c;我们将深入探讨一种基于HTML/CSS/Echarts等技术的大数据可视化电子沙盘&#xff0…

多层tablayout+ViewPager,NestedScrollView+ViewPager+RecyclerView,嵌套吸顶滑动冲突

先看实现的UI效果 其实就是仿BOSS的页面效果&#xff0c;第二层tab下的viewpager滑到最右边再右滑&#xff0c;就操作第一层viewpager滑动。页面上滑时把第一层tab和vp里的banner都推出界面&#xff0c;让第二层tab吸顶。 滑上去第二个tab块卡在顶部&#xff0c;如图 我混乱…

Unity 从0开始编写一个技能编辑器_02_Buff系统的生命周期

工作也有一年了&#xff0c;对技能编辑器也有了一些自己的看法&#xff0c;从刚接触时的惊讶&#xff0c;到大量工作时觉得有一些设计的冗余&#xff0c;在到特殊需求的修改&#xff0c;运行效率低时的优化&#xff0c;技能编辑器在我眼中已经不再是神圣不可攀的存在的&#xf…

redis 06 集群

1.节点&#xff0c;这里是把节点加到集群中的操作&#xff0c;跟主从结构不同 这里是在服务端使用命令&#xff1a; 例子&#xff1a; 2.启动节点 节点服务器 首先&#xff0c;先是服务器节点自身有一个属性来判断是不是可以使用节点功能 一般加入集群中的节点还是用r…

VMware安装ubuntu22.4虚拟机超详细图文教程

一 、下载镜像 下载地址&#xff1a;Index of /ubuntu-releases/22.04.4/ | 清华大学开源软件镜像站 | Tsinghua Open Source Mirror 二、创建虚拟机 打开VMware点击左上角文件&#xff0c;创建新的虚拟机&#xff0c;打开后如下图&#xff1a; 下一步&#xff0c;镜像文件就是…

使用代理IP常见问题有哪些?

代理IP在互联网数据收集和业务开展中发挥着重要作用&#xff0c;它充当用户客户端和网站服务器之间的“屏障”&#xff0c;可以保护用户的真实IP地址&#xff0c;并允许用户通过不同的IP地址进行操作。然而&#xff0c;在使用代理IP的过程中&#xff0c;用户经常会遇到一些问题…

弃用Docker Desktop:在WSL2中玩转Docker之Docker Engine 部署与WSL入门

Docker技术概论 在WSL2中玩转Docker之Docker Engine部署 - 文章信息 - Author: 李俊才 (jcLee95) Visit me at CSDN: https://jclee95.blog.csdn.netMy WebSite&#xff1a;http://thispage.tech/Email: 291148484163.com. Shenzhen ChinaAddress of this article:https://bl…

RabbitMQ系列-rabbitmq无法重新加入集群,启动失败的问题

当前存在3个节点&#xff1a;rabbitmq5672、rabbitmq5673、rabbitmq5674 当rabbitmq5673节点掉线之后&#xff0c;重启失败 重启的时候5672节点报错如下&#xff1a; 解决方案 在集群中取消失败节点 rabbitmqctl forget_cluster_node rabbitrabbitmq5673删除失败节点5673的…

【iOS】KVC相关总结

目录 1. 什么是KVC&#xff1f;2. 访问对象属性常用方法声明基础使用KeyPath路径多值操作 3. 访问集合属性4. 集合运算符自定义集合运算符 5. 非对象值处理访问基本数据类型访问结构体 6. 属性验证7. 设值和取值原理基本的Getter搜索模式基本的Setter搜索模式NSMutableArray搜索…

【LeetCode滑动窗口算法】长度最小的子数组 难度:中等

我们先看一下题目描述&#xff1a; 解法一&#xff1a;暴力枚举 时间复杂度&#xff1a;o(n^3) class Solution { public:int minSubArrayLen(int target, vector<int>& nums){int i 0, j 0;vector<int> v;for (;i < nums.size();i){int sum nums[i];fo…

CLIP-guided Prototype Modulating for Few-shot Action Recognition

标题&#xff1a;基于CLIP引导的原型调制用于少样本动作识别 源文链接&#xff1a;CLIP-guided Prototype Modulating for Few-shot Action Recognition | International Journal of Computer Vision (springer.com)https://link.springer.com/article/10.1007/s11263-023-019…

wireshark查看流量图

点击 菜单中的 统计 , 选择 IO 图表 项 勾选下面选项..

java.nio.charset.UnmappableCharacterException

问题 java.lang.IllegalArgumentException: java.nio.charset.UnmappableCharacterException: Input length 1 解释为编码转换有问题 问题错在位置 非汉字存在 打包的时候就会报异常

Vue基本使用-02

上节我们讲了什么是mvvm模型&#xff0c;以及我们vue的一些常用指令&#xff0c;今天给大家讲一下vue的基本使用&#xff0c;在将之前我们需要重点讲解我们的一个指令&#xff0c;v-model指令 v-model v-model 可以在组件上使用以实现双向绑定,什么是双向绑定呢?意思就是当我们…

2024年第三届数据统计与分析竞赛(B题)数学建模完整思路+完整代码全解全析

你是否在寻找数学建模比赛的突破点&#xff1f;数学建模进阶思路&#xff01; 详细请查 作为经验丰富的数学建模团队&#xff0c;我们将为你带来2024年第三届数据统计与分析竞赛&#xff08;B题&#xff09;的全面解析。这个解决方案包不仅包括完整的代码实现&#xff0c;还有…

分享一份 .NET Core 简单的自带日志系统配置,平时做一些测试或个人代码研究,用它就可以了

前言 实际上&#xff0c;.NET Core 内部也内置了一套日志系统&#xff0c;它是一个轻量级的日志框架&#xff0c;用于记录应用程序的日志信息。 它提供了 ILogger 接口和 ILoggerProvider 接口&#xff0c;以及一组内置的日志提供程序&#xff08;如 Console、Debug、EventSo…

Python学习从0开始——Kaggle时间序列002

Python学习从0开始——Kaggle时间序列002 一、作为特征的时间序列1.串行依赖周期 2.滞后序列和滞后图滞后图选择滞后 3.示例 二、混合模型1.介绍2.组件和残差3.残差混合预测4.设计混合模型5.使用 三、使用机器学习进行预测1.定义预测任务2.为预测准备数据3.多步骤预测策略3.1 M…

docker 部署nginx多级子域名(三级四级...)映射不同web项目,访问不同路径地址

一、背景 只有一台服务器&#xff0c;一个顶级域名&#xff0c;现在需要根据不同子域名访问不同web项目&#xff0c;比如 # 管理后台 cms.biacu.com# 客户端h5 h5.biacu.com# 四级域名 h5.s.biacu.com同时&#xff0c;不同web项目放在不同位置 二、 1、在云服务器上&#x…

C++——计算不同的非空子串个数

计算不同的非空子串 计算方法 这道题是我在BCSP-X小高组的题目中发现的一道 没事闲的就写了代码和思路&#xff1a; 代码 #include <iostream> #include <vector> #include <string> #include <algorithm>using namespace std;// 用于存储后缀数…