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

多种传参方式

在前一篇文章互联网应用主流框架整合之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&&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,所以获取参数也响应的发生了变化,代码如下

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

相关推荐
谦风(主Java)(接口开发)1 个月前
记一次重定向问题(浏览器安全)解决
java·重定向
Amd7942 个月前
Django国际化与本地化指南
性能优化·国际化·多语言·表单验证·翻译工具·本地化-l10n·django-i18n
Airs_Gao2 个月前
A Dexterous Hand-Arm Teleoperation System
重定向·遥操作·灵巧手
小康师兄2 个月前
IoTDB 入门教程⑤——数据模型和基础概念
数据库·物联网·时序数据库·iotdb·数据模型
ModelWhale2 个月前
和鲸科技出席第五届空间数据智能学术会议,执行总裁殷自强受邀发表主题报告
大数据·人工智能·数据模型·空间数据
景天科技苑4 个月前
【python】flask服务端响应与重定向处理
开发语言·python·flask·重定向
赢乐4 个月前
前端开发中HTTP状态码的含义和用途,及常见错误码产生的原因和解决方法
网络·网络协议·http·状态码·重定向·404·500
诡异森林。4 个月前
Linux--文件(2)-重定向和文件缓冲
linux·服务器·重定向·文件缓冲
椿融雪6 个月前
【计算机网络】HTTP协议以及简单的HTTP服务器实现
服务器·计算机网络·http·url·长连接·重定向·会话保持