多种传参方式
在前一篇文章互联网应用主流框架整合之SpringMVC初始化及各组件工作原理中讨论了最简单的参数传递,而实际情况要复杂的多,比如REST风格,它往往会将参数写入请求路径中,而不是以HTTP请求参数传递;比如查询客户,查询参数可能很多,需要传递JSON,需要分页,然后将数据集组装并传递分页参数;比如有时候需要传递多个对象等等,实际场景比想象的要多
SpringMVC提供了诸多的注解来解析参数,其目的是在于把控制器从复杂的Servlet API中剥离出来,这样就可以在非Web容器环境中重用这些控制器,同时也方便测试工程师进行有效地测试
接收普通请求参数
js
<%@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如下代码所示
java
/**
* 参数处理控制器,负责处理各种请求参数的场景,包括路径变量、请求体、请求参数等的不同组合。
*/
@Controller
@RequestMapping("/params")
public class ParamsController {
@Autowired
private 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的能力
新建一个角色参数类,代码如下所示
java
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请求参数
java
/**
* 通过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
js
<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
来处理这种情况,进行重新绑定规则,代码如下
java
/**
* 处理使用@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
,如下代码所示
java
public ModelAndView requestParam(@RequestParam(value = "role_name", required = false) String roleName, String note)
使用URL传递参数
使用URL的形式传递参数,这符合REST风格,对于一些业务比较简单的应用也十分常见,SpringMVC也对这种形式提供了良好的支持,如下代码所示
java
/**
* 通过路径变量获取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,代码如下
java
/**
* 分页参数类
* 用于封装分页查询时的起始位置和每页记录数。
*/
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;
}
}
在角色参数类中添加分页属性,代码如下
java
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数据,代码如下
js
<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
接收参数,代码如下
java
/**
* 使用@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模拟传递数组给后端
js
<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>
控制器接收数组参数代码如下
java
/**
* 删除角色,通过请求体接收一个角色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集合类型
新增多个数据也是一样的逻辑
js
<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>
java
/**
* 新增角色,通过请求体接收一个角色列表,进行批量插入操作。
*
* @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;
}
通过表单序列化可以将表单数据转换为字符串传递给后端,因为一些特殊的字符需要进行一定的转换提交给后端,所以有时候需要在用户点击提交按钮后,通过序列化提交表单数据,代码如下
js
<%@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&¬e=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&¬e=xxx
,所以获取参数也响应的发生了变化,代码如下
java
/**
* 处理序列化参数的请求,展示如何接收和处理通过@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视图的功能代码,如下所示
java
/**
* 展示特定角色的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视图给请求者,代码如下
java
/**
* 插入角色并重定向到角色信息页面,展示如何在插入后获取并使用新角色的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
为前缀时,就会被认为请求最后需要重定向;不仅仅可以通过返回字符串来实现重定向,也可以通过返回视图来实现重定向,代码如下
java
/**
* 另一种插入角色的方法,使用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来完成,而不是一个个字段传递,代码如下
java
/**
* 通过角色信息展示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
,代码如下
java
/**
* 使用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
文件,代码如下
js
<%@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的请求属性,然后进行了转发控制器,这样将由对应的控制器处理业务逻辑,代码如下
java
@Controller
@RequestMapping("/attribute")
public class AttributeController {
// 角色服务
@Autowired
private 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的会话对象中,如下代码所示
java
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 {
// 角色服务
@Autowired
private 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将会保存id
mv.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
代码如下
js
<%@ 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的属性,代码如下
js
<%@ 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属性值,如下代码所示
java
// 访问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而言,需要考虑的是用户是可以禁用的
java
/**
* 获取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功能,需要引入如下依赖
xml
<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
:
js
<%@ 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代码如下
java
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的验证规则来验证表单了,配以如下控制器
java
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
java
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:
java
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}$";
@Override
public void initialize(IsMobile constraintAnnotation) {
}
@Override
public boolean isValid(String mobile, ConstraintValidatorContext context) {
return mobile != null && mobile.matches(MOBILE_PATTERN);
}
}
这样,你就可以在需要验证手机号码的字段或参数上使用@IsMobile
注解了
Spring验证器
Spring提供了Validator接口实现验证,它将在进入控制器逻辑之前对参数的合法性进行验证,Validator接口是Spring MVC验证表单逻辑的核心接口,其接口代码如下所示
java
/**
* 验证器接口,用于验证特定类型的对象。
* 实现这个接口的类必须提供支持验证的类型,并执行实际的验证逻辑。
*/
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方法实现,接口实例如下代码所示
java
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。
*/
@Override
public boolean supports(Class<?> clazz) {
// 匹配为交易记录类型
return Transaction.class.equals(clazz);
}
/**
* 对Transaction对象进行验证。
* 验证交易金额是否等于价格乘以数量。
* 如果不相等,则认为数据不合法,将错误信息添加到Errors对象中。
*
* @param target 需要验证的对象,类型应为Transaction
* @param errors 用于收集验证过程中发现的错误信息的对象
*/
@Override
public 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
,通过它可以将验证器和控制器绑定到一起,这样就能验证表单请求了,控制器代码如下所示
java
@InitBinder
public 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实例,用以保存数据模型,这就是数据模型之间的关系
创建一个控制器,代码如下
java
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 {
/**
* 自动注入角色服务,用于处理角色相关的业务逻辑。
*/
@Autowired
private 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)
@ResponseBody
public 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的定义
java
/**
* 接口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。
*/
@Nullable
default 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视图,例如如下代码
java
/**
* 根据角色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
<?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.xsd
http://www.springframework.org/schema/tx
http://www.springframework.org/schema/tx/spring-tx-4.0.xsd
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context-4.0.xsd
http://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配置的方式取代它,如下代码所示
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
接口来实现,代码如下
java
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实例,用于注册和配置视图解析器
*/
@Override
public 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
*/
@Override
public void addViewControllers(ViewControllerRegistry registry) {
registry.addViewController("/config/index").setViewName("index");
}
}
无论使用何种方法,都是为了创建一个视图解析器,让Spring MVC可以通过前缀和后缀加上视图名称找到对应的JSP文件,然后把数据模型渲染到JSP文件中
视图解析器
非逻辑视图是不需要用视图解析器解析的,例如MappingJackson2JsonView
,它的含义是把当前数据模型转化为JSON,不需要转换试图逻辑名称,但是对于逻辑视图而言,通过视图名称定位到最终视图是一个必备过程,例如InternalResourceView
就是这样的一个视图,当它被配置后,就会加载到SpringMVC的视图解析器列表中,当返回ModelAndView时,SpringMVC就会在视图解析器列表中遍历,找到对应的视图解析器去解析式图,视图解析器接口源码如下
java
/**
* 视图解析器接口,用于根据视图名称和区域设置解析视图。
* 视图解析器的职责是将逻辑视图名称映射到实际的视图对象上,以便视图可以负责渲染响应。
* 它是Spring MVC框架中的一部分,用于支持不同的视图技术。
*/
import java.util.Locale;
import org.springframework.lang.Nullable;
public interface ViewResolver {
/**
* 根据给定的视图名称和区域设置解析视图。
*
* @param viewName 视图的逻辑名称,通常是由控制器返回的字符串。
* @param locale 用户的区域设置,用于支持国际化。
* @return 解析后的视图对象,如果无法解析则返回null。
* @throws Exception 如果解析过程中出现错误。
*
* 方法注释解释了为什么方法会返回null,以及当无法解析视图时应该做什么。
*/
@Nullable
View resolveViewName(String viewName, Locale locale) throws Exception;
}
接口源码就两个参数,一个视图名,一个Locale类型,Locale类型参数是用于国际化的,这就说明了Spring MVC是支持国际化的,对于Spring MVC框架而言,它也配置了多种视图解析器,如下UML图所示
图中展示了Spring MVC自带的视图解析器,当控制器返回视图的逻辑名称时,通过这些解析器就能定位到具体的视图
有时候在控制器中并没有返回一个ModelAndView,而是只返回一个字符串,它也能够渲染视图,因为视图解析器定位了对应的视图,例如如下代码
java
/**
* 首页请求处理方法的另一种形式,返回角色管理页面的字符串路径。
* @return 角色页面的字符串路径。
*/
@RequestMapping("/index2")
public String index2() {
return "role";
}
由于配置了InternalResourceViewResolver,所以通过Spring MVC系统能够找到InternalResourceView视图,如果存在数据模型,那么Spring MVC会将视图和数据模型绑定到一个ModelAndView上,然后视图解析器会根据视图的名称,找到对应的视图资源,这就是视图解析器的作用
Excle视图使用
导出Excel
在实际的应用开发中,经常遇到需要导出Excel的功能,对于Excel视图的开发,Spring MVC推荐使用AbstractXlsView
,它实现了视图接口,是一个抽象类,不能生成实例对象,但它自己定义了一个抽象方法buildExcelDocument
去实现,代码如下
java
/**
* 抽象类,作为生成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,需要引入如下依赖
xml
<!-- 引入Apache POI依赖,用于处理Microsoft Office文档 -->
<dependency>
<groupId>org.apache.poi</groupId>
<artifactId>poi-ooxml</artifactId>
<version>4.1.1</version>
</dependency>
假设要导出所有角色信息,先定义一个接口,该接口主要用于自定义生成Excel的规则,如下代码所示
java
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视图类,因为导出文档还需要一个下载文件名称,所以还需要定义一个文件名属性,由于该视图不是一个逻辑视图,所以无需解析器运行,代码如下
java
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 如果导出过程中发生错误。
*/
@Override
protected 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方法,完成了一个视图类,然后在控制器中加入对应的方法,代码如下
java
/**
* 导出所有角色列表到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");
//生成Sheet
Sheet 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
接口来处理,源码如下
java
/**
* 解析器接口,用于处理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就可以进行初始化,如下代码所示
java
/**
* 初始化并配置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初始化器代码如下
java
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 MyWebAppInitializer
extends AbstractAnnotationConfigDispatcherServletInitializer {
/**
* 配置根应用程序上下文的类。
* 这些类被用来初始化ApplicationContext。
* @return 根应用程序上下文的类数组。
*/
@Override
protected Class<?>[] getRootConfigClasses() {
// 可以返回Spring的Java配置文件数组
return new Class<?>[] {BackendConfig.class };
}
/**
* 配置DispatcherServlet的应用程序上下文的类。
* 这些类被用来初始化DispatcherServlet的ApplicationContext。
* @return DispatcherServlet的应用程序上下文的类数组。
*/
@Override
protected Class<?>[] getServletConfigClasses() {
// 可以返回Spring的Java配置文件数组
return new Class<?>[] { WebConfig.class };
}
/**
* 配置DispatcherServlet的URL映射。
* 这些URLs会被DispatcherServlet处理。
* @return URL映射的字符串数组。
*/
@Override
protected String[] getServletMappings() {
return new String[] { "/mvc/*" };
}
/**
* 自定义Servlet注册。
* 这里用于配置文件上传的相关设置。
* @param dynamic Servlet的动态注册接口,用于配置Servlet。
*/
@Override
protected void customizeRegistration(Dynamic dynamic) {
// 文件上传路径
String filepath = "e:/mvc/uploads";
// 5MB
Long singleMax = (long) (5*Math.pow(2, 20));
// 10MB
Long totalMax = (long) (10*Math.pow(2, 20));
// 配置MultipartResolver,限制请求,单个文件5MB,总文件10MB
dynamic.setMultipartConfig(new MultipartConfigElement(filepath, singleMax, totalMax, 0));
}
}
如果使用XML,就在web.xml文件中配置DispatcherServlet的地方配置就可以,如下代码所示
xml
<?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/javaee
http://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完成,但它依赖第三方包,需要导入如下依赖
xml
<!-- 引入Apache Commons FileUpload依赖 -->
<dependency>
<groupId>commons-fileupload</groupId>
<artifactId>commons-fileupload</artifactId>
<version>1.4</version>
</dependency>
使用它需要配置一个Bean, 使用Java配置文件方式,代码如下
java
/**
* 初始化并配置CommonsMultipartResolver,用于处理文件上传。
* @return CommonsMultipartResolver 实例,配置了单个文件和总上传大小的限制,以及上传文件的临时目录。
* @bean(name = "multipartResolver") 该方法标记为一个Spring Bean,命名为"multipartResolver"。
*/
@Bean(name = "multipartResolver")
public MultipartResolver initCommonsMultipartResolver() {
// 文件上传路径
String filepath = "e:/mvc/uploads";
// 设置单个文件的最大上传大小为5MB
Long singleMax = (long) (5 * Math.pow(2, 20));
// 设置总上传大小的最大限制为10MB
Long 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
,代码如下
js
<%@ 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="请选择上传的文件" /> <input
type="submit" value="提交" />
</form>
</body>
</html>
然后开发控制器,代码如下
java
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的基础上,新增方法,实现代码如下
java
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