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

news2025/1/10 18:36:11

多种传参方式

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

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

接收普通请求参数

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

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

/**
 * 参数处理控制器,负责处理各种请求参数的场景,包括路径变量、请求体、请求参数等的不同组合。
 */
@Controller
@RequestMapping("/params")
public class ParamsController {

    @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的能力

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

package com.sma.vo;

/**
 * 角色参数类
 * 用于封装角色相关参数及分页参数的实体类。
 */
public class RoleParams {
    // 角色名称
    private String roleName;
    // 角色备注信息
    private String note;

    /**
     * 获取角色名称
     * @return 角色名称
     */
    public String getRoleName() {
        return roleName;
    }

    /**
     * 设置角色名称
     * @param roleName 角色名称
     */
    public void setRoleName(String roleName) {
        this.roleName = roleName;
    }

    /**
     * 获取角色备注信息
     * @return 角色备注信息
     */
    public String getNote() {
        return note;
    }

    /**
     * 设置角色备注信息
     * @param note 角色备注信息
     */
    public void setNote(String note) {
        this.note = note;
    }
}

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

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

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

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

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

注解@RequestParam获取参数

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

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

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

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

使用URL传递参数

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

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

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

传递JSON参数

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

/**
 * 分页参数类
 * 用于封装分页查询时的起始位置和每页记录数。
 */
package com.sma.vo;

public class PageParams {
    private int start; // 起始位置,表示从第几条记录开始查询
    private int limit; // 每页记录数,表示每页最多显示多少条记录

    /**
     * 获取起始位置
     * @return 起始位置的索引值
     */
    public int getStart() {
        return start;
    }

    /**
     * 设置起始位置
     * @param start 起始位置的索引值,用于指定从哪条记录开始查询
     */
    public void setStart(int start) {
        this.start = start;
    }

    /**
     * 获取每页记录数
     * @return 每页显示的记录数量
     */
    public int getLimit() {
        return limit;
    }

    /**
     * 设置每页记录数
     * @param limit 每页显示的记录数量,用于指定每页最多显示多少条记录
     */
    public void setLimit(int limit) {
        this.limit = limit;
    }
}

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

package com.sma.vo;

/**
 * 角色参数类
 * 用于封装角色相关参数及分页参数的实体类。
 */
public class RoleParams {
    // 角色名称
    private String roleName;
    // 角色备注信息
    private String note;

    // 分页参数对象,用于角色列表的分页查询
    private PageParams pageParams = null;

    /**
     * 获取角色名称
     * @return 角色名称
     */
    public String getRoleName() {
        return roleName;
    }

    /**
     * 设置角色名称
     * @param roleName 角色名称
     */
    public void setRoleName(String roleName) {
        this.roleName = roleName;
    }

    /**
     * 获取角色备注信息
     * @return 角色备注信息
     */
    public String getNote() {
        return note;
    }

    /**
     * 设置角色备注信息
     * @param note 角色备注信息
     */
    public void setNote(String note) {
        this.note = note;
    }

    /**
     * 获取分页参数对象
     * @return 分页参数对象
     */
    public PageParams getPageParams() {
        return pageParams;
    }

    /**
     * 设置分页参数对象
     * @param pageParams 分页参数对象
     */
    public void setPageParams(PageParams pageParams) {
        this.pageParams = pageParams;
    }
}

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

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

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

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


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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

重定向

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

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

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

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

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

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

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

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

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

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

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

属性标签

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

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

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

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

@RequestAttribute

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

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

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

@Controller
@RequestMapping("/attribute")
public class AttributeController {

    // 角色服务
    @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的会话对象中,如下代码所示

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代码如下

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

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

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

<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%>
<%@ page import="java.io.*" %>
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
<html>
<head>
	<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
	<title>session</title>
</head>
<body>
<%
	// 设置会话属性,将一个长整型值1存储到会话中,键为"id"
	session.setAttribute("id", 1L);

	// 向Dispatcher请求将请求转发到指定的资源,这里是处理会话参数的控制器
	request.getRequestDispatcher("/mvc/attribute/session/param").forward(request, response);

	// 清空输出缓冲区,确保之前的输出不会影响后续的页面渲染
	out.clear();

	// 重新设置输出流,以便可以继续向客户端输出内容
	out = pageContext.pushBody();
%>
</body>
</html>

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


    // 访问session_attribute.jsp
    @RequestMapping("/session/page")
    public ModelAndView sessionPage() {
        ModelAndView mv = new ModelAndView("session_attribute");
        return mv;
    }

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

@CookieValue@RequestHeader

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

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

表单验证

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

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

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

JSR303注解验证输入内容

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

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

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

<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%>
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<title>validate</title>
</head>
<body>

	<form id="form" method="post" action="./validator">
		<table>
			<tr>
				<td>产品编号:</td>
				<td><input name="productId" id="productId" /></td>
			</tr>
			<tr>
				<td>用户编号:</td>
				<td><input name="userId" id="userId" /></td>
			</tr>
			<tr>
				<td>交易日期:</td>
				<td><input name="date" id="date" /></td>
			</tr>
			<tr>
				<td>价格:</td>
				<td><input name="price" id="price" /></td>
			</tr>
			<tr>
				<td>数量:</td>
				<td><input name="quantity" id="quantity" /></td>
			</tr>
			<tr>
				<td>交易金额:</td>
				<td><input name="amount" id="amount" /></td>
			</tr>
			<tr>
				<td>用户邮件:</td>
				<td><input name="email" id="email" /></td>
			</tr>
			<tr>
				<td>备注:</td>
				<td>
				    <textarea id="note" name="note" cols="20" rows="5">
				    </textarea>
				</td>
			</tr>
			<tr>
				<td colspan="2" align="right"><input type="submit" value="提交" />
			</tr>
		</table>
		</form>
</body>
</html>

对应的POJO代码如下

package com.sma.pojo;


import java.util.Date;

import javax.validation.constraints.DecimalMax;
import javax.validation.constraints.DecimalMin;
import javax.validation.constraints.Future;
import javax.validation.constraints.Max;
import javax.validation.constraints.Min;
import javax.validation.constraints.NotNull;
import javax.validation.constraints.Pattern;
import javax.validation.constraints.Size;
import org.springframework.format.annotation.DateTimeFormat;


public class Transaction {


    @NotNull //不能为空
    private Long productId;


    @NotNull //不能为空
    private Long userId;


    @Future //只能是将来的日期
    @DateTimeFormat(pattern = "yyyy-MM-dd")//日期格式化转换
    @NotNull //不能为空
    private Date date;


    @NotNull //不能为空
    @DecimalMin(value = "0.1") //最小值0.1元
    private Double price;


    @Min(1) //最小值为1
    @Max(100)//最大值
    @NotNull //不能为空
    private Integer quantity;

    @NotNull //不能为空
    @DecimalMax("500000.00") //最大金额为5万元
    @DecimalMin("1.00") //最小交易金额1元
    private Double amount;


    @Pattern(regexp = "^([a-zA-Z0-9]*[-_]?[a-zA-Z0-9]+)*@"
                    + "([a-zA-Z0-9]*[-_]?[a-zA-Z0-9]+)+"
                    + "[\\.][A-Za-z]{2,3}([\\.] [A-Za-z]{2})?$",
            message="不符合邮件格式")
    private String email;


    @Size(min = 0, max = 256) //0到255个字符
    private String note;

    public Long getProductId() {
        return productId;
    }

    public void setProductId(Long productId) {
        this.productId = productId;
    }

    public Long getUserId() {
        return userId;
    }

    public void setUserId(Long userId) {
        this.userId = userId;
    }

    public Date getDate() {
        return date;
    }

    public void setDate(Date date) {
        this.date = date;
    }

    public Double getPrice() {
        return price;
    }

    public void setPrice(Double price) {
        this.price = price;
    }

    public Integer getQuantity() {
        return quantity;
    }

    public void setQuantity(Integer quantity) {
        this.quantity = quantity;
    }

    public Double getAmount() {
        return amount;
    }

    public void setAmount(Double amount) {
        this.amount = amount;
    }

    public String getEmail() {
        return email;
    }

    public void setEmail(String email) {
        this.email = email;
    }

    public String getNote() {
        return note;
    }

    public void setNote(String note) {
        this.note = note;
    }
}

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

package com.sma.controller;

import java.util.List;

import javax.validation.Valid;

import org.springframework.stereotype.Controller;
import org.springframework.validation.DataBinder;
import org.springframework.validation.Errors;
import org.springframework.validation.FieldError;
import org.springframework.web.bind.annotation.InitBinder;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.servlet.ModelAndView;
import org.springframework.web.servlet.view.json.MappingJackson2JsonView;

import com.sma.pojo.Transaction;
import com.sma.validator.TransactionValidator;

@Controller
@RequestMapping("/validate")
public class ValidateController {

    // 表单页面
    @RequestMapping("/form")
    public ModelAndView formPage() {
        return new ModelAndView("validation");
    }

    /**
     * Spring验证(JSR 303)
     * @param trans 交易
     * @param errors 错误
     * @return
     */
    @RequestMapping("/annotation")
    public ModelAndView annotationValidate(@Valid Transaction trans, Errors errors) {
        ModelAndView mv = new ModelAndView();
        // 是否存在错误
        if (errors.hasErrors()) {
            // 获取错误信息
            List<FieldError> errorList = errors.getFieldErrors();
            for (FieldError error : errorList) {
                // 获取错误信息
                mv.addObject(error.getField(), error.getDefaultMessage());
            }
        }
        mv.setView(new MappingJackson2JsonView());
        return mv;
    }

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

自定义验证器

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

import javax.validation.Constraint;
import javax.validation.Payload;
import java.lang.annotation.*;

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

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

import javax.validation.ConstraintValidator;
import javax.validation.ConstraintValidatorContext;

public class IsMobileValidator implements ConstraintValidator<IsMobile, String> {
    private static final String MOBILE_PATTERN = "^1[3-9]\\d{9}$";

    @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验证表单逻辑的核心接口,其接口代码如下所示

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

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

package com.sma.validator;

import org.springframework.validation.Errors;
import org.springframework.validation.Validator;

import com.sma.pojo.Transaction;

/**
 * 交易验证器类,用于验证Transaction对象的合法性。
 * 实现了Spring的Validator接口,用于在业务逻辑中进行数据验证。
 */
public class TransactionValidator implements Validator {

    /**
     * 判断当前验证器是否支持指定的类。
     * 本验证器仅支持com.sma.pojo.Transaction类的验证。
     *
     * @param clazz 需要验证的类
     * @return 如果clazz等于Transaction类,则返回true;否则返回false。
     */
    @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,通过它可以将验证器和控制器绑定到一起,这样就能验证表单请求了,控制器代码如下所示

    @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实例,用以保存数据模型,这就是数据模型之间的关系

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

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的定义

/**
 * 接口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视图,例如如下代码

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

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

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

<?xml version='1.0' encoding='UTF-8' ?>
<beans xmlns="http://www.springframework.org/schema/beans"
	xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
	xmlns:p="http://www.springframework.org/schema/p"
	xmlns:tx="http://www.springframework.org/schema/tx"
	xmlns:context="http://www.springframework.org/schema/context"
	xmlns:mvc="http://www.springframework.org/schema/mvc"
	xsi:schemaLocation="http://www.springframework.org/schema/beans 
	    http://www.springframework.org/schema/beans/spring-beans-4.0.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配置的方式取代它,如下代码所示

package com.ssmvc.web.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.ViewResolver;
import org.springframework.web.servlet.config.annotation.EnableWebMvc;
import org.springframework.web.servlet.view.InternalResourceViewResolver;

/**
 * Spring MVC的配置类
 * 通过@Configuration注解标记这个类是一个配置类,等同于Spring XML配置文件
 * 使用@EnableWebMvc注解开启Spring MVC的功能
 * 使用@ComponentScan注解指定Spring要扫描的组件包,这些包中的组件会被自动注册为Spring Bean
 */
@Configuration
@ComponentScan("com.ssmvc.controller")
@EnableWebMvc
public class WebConfig {

    /**
     * 配置InternalResourceViewResolver,作为Spring MVC的视图解析器
     * 通过@Bean注解标记这个方法会返回一个Bean,该Bean会被注册到Spring的ApplicationContext中
     * 方法名称viewResolver和@Bean注解中的name属性一起定义了这个Bean的名称
     * @return 返回一个配置好的InternalResourceViewResolver实例
     */

    @Bean(name = "viewResolver")
    public ViewResolver initViewResolver() {
        InternalResourceViewResolver viewResolver = new InternalResourceViewResolver();
        viewResolver.setPrefix("/WEB-INF/jsp/");
        viewResolver.setSuffix(".jsp");
        return viewResolver;
    }
}

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

package com.ssmvc.web.config;

import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.EnableWebMvc;
import org.springframework.web.servlet.config.annotation.ViewControllerRegistry;
import org.springframework.web.servlet.config.annotation.ViewResolverRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import org.springframework.web.servlet.view.InternalResourceViewResolver;


/**
 * Spring MVC的配置类
 * 通过@Configuration注解标记这个类是一个配置类,等同于Spring XML配置文件
 * 使用@EnableWebMvc注解开启Spring MVC的功能
 * 使用@ComponentScan注解指定Spring要扫描的组件包,这些包中的组件会被自动注册为Spring Bean
 */
@Configuration
@ComponentScan("com.ssmvc.controller")
@EnableWebMvc
public class WebConfigII implements WebMvcConfigurer {

    /**
     * 配置视图解析器
     * 本方法用于设置Spring MVC中视图解析的前缀和后缀,指定视图文件在项目中的位置和格式。
     * 使用InternalResourceViewResolver作为视图解析器,它能够处理JSP视图。
     * 设置前缀为"/WEB-INF/jsp/",确保视图文件位于WEB-INF目录下的jsp子目录中。
     * 设置后缀为".jsp",指明视图文件的格式为JSP。
     *
     * @param registry ViewResolverRegistry实例,用于注册和配置视图解析器
     */
    @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就会在视图解析器列表中遍历,找到对应的视图解析器去解析式图,视图解析器接口源码如下

/**
 * 视图解析器接口,用于根据视图名称和区域设置解析视图。
 * 视图解析器的职责是将逻辑视图名称映射到实际的视图对象上,以便视图可以负责渲染响应。
 * 它是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,而是只返回一个字符串,它也能够渲染视图,因为视图解析器定位了对应的视图,例如如下代码

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

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

Excle视图使用

导出Excel

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

/**
 * 抽象类,作为生成Excel文档视图的基础。
 * 该类继承自AbstractView,用于处理以Excel格式响应的视图解析。
 * 它默认将内容类型设置为application/vnd.ms-excel,适合Excel文件下载。
 */
package org.springframework.web.servlet.view.document;

import java.io.IOException;
import java.util.Map;
import javax.servlet.ServletOutputStream;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.apache.poi.hssf.usermodel.HSSFWorkbook;
import org.apache.poi.ss.usermodel.Workbook;
import org.springframework.web.servlet.view.AbstractView;

public abstract class AbstractXlsView extends AbstractView {
    
    /**
     * 构造函数,设置默认的内容类型为Excel的MIME类型。
     */
    public AbstractXlsView() {
        this.setContentType("application/vnd.ms-excel");
    }

    /**
     * 判断视图是否生成用于下载的内容。
     * 对于Excel视图,通常是用于下载的。
     * @return 布尔值,表示是否生成下载内容。
     */
    protected boolean generatesDownloadContent() {
        return true;
    }

    /**
     * 渲染合并后的模型数据到Excel文档。
     * 该方法是抽象类AbstractView中的具体实现部分,用于处理Excel文档的生成。
     * @param model 映射表,包含所有要呈现的数据。
     * @param request HTTP请求对象,可能用于获取额外的渲染上下文信息。
     * @param response HTTP响应对象,用于设置响应头信息和输出Excel内容。
     * @throws Exception 如果渲染过程中发生错误。
     */
    protected final void renderMergedOutputModel(Map<String, Object> model, HttpServletRequest request, HttpServletResponse response) throws Exception {
        Workbook workbook = this.createWorkbook(model, request);
        this.buildExcelDocument(model, workbook, request, response);
        response.setContentType(this.getContentType());
        this.renderWorkbook(workbook, response);
    }

    /**
     * 创建一个空的工作簿对象。
     * 默认实现使用HSSFWorkbook创建一个Excel 2003及以前版本的工作簿。
     * @param model 渲染所需的数据模型。
     * @param request HTTP请求对象,可能用于获取额外的上下文信息。
     * @return 工作簿对象,用于构建Excel文档。
     */
    protected Workbook createWorkbook(Map<String, Object> model, HttpServletRequest request) {
        return new HSSFWorkbook();
    }

    /**
     * 将工作簿写入HTTP响应中,完成Excel文档的渲染。
     * @param workbook 要写入响应的工作簿对象。
     * @param response HTTP响应对象,用于输出Excel内容。
     * @throws IOException 如果写入过程中发生I/O错误。
     */
    protected void renderWorkbook(Workbook workbook, HttpServletResponse response) throws IOException {
        ServletOutputStream out = response.getOutputStream();
        workbook.write(out);
        workbook.close();
    }

    /**
     * 子类必须实现该方法,用于填充工作簿的具体内容。
     * 这是抽象方法,需要子类根据实际情况实现,以将模型数据填充到Excel工作簿中。
     * @param model 映射表,包含所有要呈现的数据。
     * @param workbook 用于构建Excel文档的工作簿对象。
     * @param request HTTP请求对象,可能用于获取额外的上下文信息。
     * @param response HTTP响应对象,可能用于设置额外的响应头信息。
     * @throws Exception 如果构建Excel文档过程中发生错误。
     */
    protected abstract void buildExcelDocument(Map<String, Object> var1, Workbook var2, HttpServletRequest var3, HttpServletResponse var4) throws Exception;
}

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

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

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

package com.sma.service;


import java.util.Map;

import org.apache.poi.ss.usermodel.Workbook;

public interface ExcelExportService {
    /***
     *  生成Exel文档规则
     * @param model 数据模型
     * @param workbook POI的Excel workbook
     */
    public void makeWorkBook(Map<String, Object> model, Workbook workbook);
}

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

package com.sma.view;

import java.util.Map;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.apache.poi.ss.usermodel.Workbook;
import org.springframework.util.StringUtils;
import org.springframework.web.servlet.view.document.AbstractXlsView;

import com.sma.service.ExcelExportService;

/**
 * Excel视图类,继承自AbstractXlsView,用于导出Excel文件。
 * 通过提供不同的构造方法,可以灵活地配置导出服务和文件名。
 */
public class ExcelView extends AbstractXlsView {

    // Excel文件名
    private String fileName = null;

    // 导出服务接口,用于生成Excel内容
    private ExcelExportService excelExpService = null;

    /**
     * 构造方法,仅传入导出服务接口。
     * @param excelExpService 导出服务接口,用于生成Excel内容。
     */
    public ExcelView(ExcelExportService excelExpService) {
        this.excelExpService = excelExpService;
    }

    /**
     * 构造方法,传入视图名称和导出服务接口。
     * @param viewName 视图名称,用于Spring MVC框架识别。
     * @param excelExpService 导出服务接口,用于生成Excel内容。
     */
    public ExcelView(String viewName, ExcelExportService excelExpService) {
        this.setBeanName(viewName);
    }

    /**
     * 构造方法,传入视图名称、导出服务接口和文件名。
     * @param viewName 视图名称,用于Spring MVC框架识别。
     * @param excelExpService 导出服务接口,用于生成Excel内容。
     * @param fileName Excel文件名,用于设置导出文件的名称。
     */
    public ExcelView(String viewName,
                     ExcelExportService excelExpService, String fileName) {
        this.setBeanName(viewName);
        this.excelExpService = excelExpService;
        this.fileName = fileName;
    }

    // 获取文件名
    public String getFileName() {
        return fileName;
    }

    // 设置文件名
    public void setFileName(String fileName) {
        this.fileName = fileName;
    }

    // 获取导出服务接口
    public ExcelExportService getExcelExpService() {
        return excelExpService;
    }

    // 设置导出服务接口
    public void setExcelExpService(ExcelExportService excelExpService) {
        this.excelExpService = excelExpService;
    }

    /**
     * 覆盖父类方法,用于构建Excel文档。
     * @param model 视图模型,包含导出数据。
     * @param workbook 工作簿对象,用于存储Excel内容。
     * @param request HTTP请求对象,用于获取请求参数。
     * @param response HTTP响应对象,用于设置响应头信息。
     * @throws Exception 如果导出过程中发生错误。
     */
    @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方法,完成了一个视图类,然后在控制器中加入对应的方法,代码如下

    /**
     * 导出所有角色列表到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接口来处理,源码如下

/**
 * 解析器接口,用于处理HTTP请求中的多部分数据,例如文件上传。
 * 它提供了检查请求是否包含多部分数据、解析多部分请求以及清理多部分请求数据的功能。
 * 
 * 该接口的实现应该能够处理多部分请求的解析,包括但不限于文件上传。
 * 解析过程中可能涉及到的步骤包括识别多部分请求、分割多部分数据、为每个部分分配名称和类型等。
 * 实现类还需要处理解析过程中可能出现的错误,例如文件大小超出限制或文件类型不被允许。
 */
package org.springframework.web.multipart;

import javax.servlet.http.HttpServletRequest;

public interface MultipartResolver {
    
    /**
     * 检查给定的HttpServletRequest是否包含多部分数据。
     * 
     * @param request 要检查的HTTP请求。
     * @return 如果请求是多部分的,则返回true;否则返回false。
     */
    boolean isMultipart(HttpServletRequest request);

    /**
     * 解析多部分请求,返回一个封装了多部分数据的MultipartHttpServletRequest实例。
     * 
     * @param request 要解析的HTTP请求。
     * @return 封装了多部分数据的MultipartHttpServletRequest实例。
     * @throws MultipartException 如果解析过程中出现错误。
     */
    MultipartHttpServletRequest resolveMultipart(HttpServletRequest request) throws MultipartException;

    /**
     * 清理与给定的MultipartHttpServletRequest相关的多部分数据。
     * 这一步骤通常包括删除临时文件,这些文件是在解析多部分请求时创建的。
     * 
     * @param request 包含多部分数据的MultipartHttpServletRequest。
     */
    void cleanupMultipart(MultipartHttpServletRequest request);
}

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

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

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

package com.sma.config;
import javax.servlet.MultipartConfigElement;
import javax.servlet.ServletRegistration.Dynamic;

import org.springframework.web.servlet.support.AbstractAnnotationConfigDispatcherServletInitializer;

import com.sma.backend.config.BackendConfig;
import com.sma.web.config.WebConfig;

/**
 * Spring MVC应用程序的初始化器。
 * 继承自AbstractAnnotationConfigDispatcherServletInitializer,用于配置Spring MVC的DispatcherServlet和应用程序上下文。
 */
public class 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 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完成,但它依赖第三方包,需要导入如下依赖

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

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

    /**
     * 初始化并配置CommonsMultipartResolver,用于处理文件上传。
     * @return CommonsMultipartResolver 实例,配置了单个文件和总上传大小的限制,以及上传文件的临时目录。
     * @bean(name = "multipartResolver") 该方法标记为一个Spring Bean,命名为"multipartResolver"。
     */
    @Bean(name = "multipartResolver")
    public MultipartResolver initCommonsMultipartResolver() {
        // 文件上传路径
        String filepath = "e:/mvc/uploads";
        // 设置单个文件的最大上传大小为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,代码如下

<%@ 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>

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

package com.sma.controller;


import java.io.File;
import java.io.IOException;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.Part;

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.multipart.MultipartFile;
import org.springframework.web.multipart.MultipartHttpServletRequest;
import org.springframework.web.servlet.ModelAndView;
import org.springframework.web.servlet.view.json.MappingJackson2JsonView;

@Controller
@RequestMapping("/file")
public class FileController {

    // 文件路径
    private static final String FILE_PATH = "e:/mvc/uploads/";

    @RequestMapping(value = "/page", method = RequestMethod.GET)
    public String page() {
        return "file_upload";
    }

    @RequestMapping(value = "/upload", method = RequestMethod.POST)
    public ModelAndView upload(HttpServletRequest request) {
        // 进行转换
        MultipartHttpServletRequest mhsr = (MultipartHttpServletRequest) request;
        // 获得请求上传的文件
        MultipartFile file = mhsr.getFile("file");
        // 设置视图为JSON视图
        ModelAndView mv = new ModelAndView();
        mv.setView(new MappingJackson2JsonView());
        // 获取原始文件名
        String fileName = file.getOriginalFilename();
        // 目标文件
        File dest = new File(FILE_PATH + fileName);
        try {
            // 保存文件
            file.transferTo(dest);
            // 保存成功
            mv.addObject("success", true);
            mv.addObject("msg", "上传文件成功");
        } catch (IllegalStateException | IOException e) {
            // 保存失败
            mv.addObject("success", false);
            mv.addObject("msg", "上传文件失败");
            e.printStackTrace();
        }
        return mv;
    }

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

 package com.sma.controller;


import java.io.File;
import java.io.IOException;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.Part;

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.multipart.MultipartFile;
import org.springframework.web.multipart.MultipartHttpServletRequest;
import org.springframework.web.servlet.ModelAndView;
import org.springframework.web.servlet.view.json.MappingJackson2JsonView;

@Controller
@RequestMapping("/file")
public class FileController {

    // 文件路径
    private static final String FILE_PATH = "e:/mvc/uploads/";

    @RequestMapping(value = "/page", method = RequestMethod.GET)
    public String page() {
        return "file_upload";
    }

    @RequestMapping(value = "/upload", method = RequestMethod.POST)
    public ModelAndView upload(HttpServletRequest request) {
        // 进行转换
        MultipartHttpServletRequest mhsr = (MultipartHttpServletRequest) request;
        // 获得请求上传的文件
        MultipartFile file = mhsr.getFile("file");
        // 设置视图为JSON视图
        ModelAndView mv = new ModelAndView();
        mv.setView(new MappingJackson2JsonView());
        // 获取原始文件名
        String fileName = file.getOriginalFilename();
        // 目标文件
        File dest = new File(FILE_PATH + fileName);
        try {
            // 保存文件
            file.transferTo(dest);
            // 保存成功
            mv.addObject("success", true);
            mv.addObject("msg", "上传文件成功");
        } catch (IllegalStateException | IOException e) {
            // 保存失败
            mv.addObject("success", false);
            mv.addObject("msg", "上传文件失败");
            e.printStackTrace();
        }
        return mv;
    }

    // 使用MultipartFile
    @RequestMapping("/multipart/file")
    public ModelAndView uploadMultipartFile(MultipartFile file) {
        // 定义JSON视图
        ModelAndView mv = new ModelAndView();
        mv.setView(new MappingJackson2JsonView());
        // 获取原始文件名
        String fileName = file.getOriginalFilename();
        file.getContentType();
        // 目标文件
        File dest = new File(FILE_PATH + fileName);
        try {
            // 保存文件
            file.transferTo(dest);
            mv.addObject("success", true);
            mv.addObject("msg", "上传文件成功");
        } catch (IllegalStateException | IOException e) {
            mv.addObject("success", false);
            mv.addObject("msg", "上传文件失败");
            e.printStackTrace();
        }
        return mv;
    }

    // 使用Part
    @RequestMapping("/part")
    public ModelAndView uploadPart(Part file) {
        ModelAndView mv = new ModelAndView();
        mv.setView(new MappingJackson2JsonView());
        // 获取原始文件名
        String fileName = file.getSubmittedFileName();
        File dest = new File(fileName);
        try {
            // 保存文件
            file.write(FILE_PATH + fileName);
            mv.addObject("success", true);
            mv.addObject("msg", "上传文件成功");
        } catch (IllegalStateException | IOException e) {
            mv.addObject("success", false);
            mv.addObject("msg", "上传文件失败");
            e.printStackTrace();
        }
        return mv;
    }
}

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

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

云渲染动画:C4D如何正确渲染导出动画?

​C4D是一款功能强大的3D建模、动画和渲染软件&#xff0c;在制作动画时&#xff0c;正确的渲染和导出流程至关重要&#xff0c;以确保动画质量和流畅性。 帧率概念 动画就是一幅幅图片连贯起来&#xff0c;30帧/秒&#xff0c;就是一秒出现30张图片一般国外都是30&#xff0c…

2024年6.18有必要购买正版FL Studio21吗?

对于是否需要购买FL Studio的正版软件&#xff0c;我们认为强烈推荐用户购买正版软件&#xff0c;而不是使用盗版软件。 FL Studio 21是一款功能强大的音乐编曲制作软件。尽管你可能没有接触过音乐制作&#xff0c;也能通过fl Studio 21&#xff0c;撰写&#xff0c;整理&#…

3dmax在设计3D模型时闪退解决方法---模大狮模型网

3ds Max 在设计 3D 模型时闪退可能由多种原因造成&#xff0c;以下是一些常见的解决方法&#xff1a; 更新显卡驱动程序&#xff1a; 一个过时或不稳定的显卡驱动程序可能导致 3ds Max 闪退。请确保你的显卡驱动程序是最新版本&#xff0c;并且与 3ds Max 兼容。 关闭不必要的…

轻易云-轻企AI知识库的智能创作与个性化管理

随着人工智能技术的飞速发展&#xff0c;AI助手正逐渐成为我们生活和工作中不可或缺的伙伴。轻易云AI助理&#xff0c;作为这一领域的佼佼者&#xff0c;以其无所不知、无所不能的AI创作模型&#xff0c;为用户带来了前所未有的智能体验。 一、AI创作模型的丰富性 在轻易云AI助…

CG-85D 振弦式渗压计厂家 测量孔隙水压力或液位

产品概述 振弦式渗压计适合埋设在水工建筑物和基岩内&#xff0c;或安装在测压管、钻孔、堤坝、管道或压力容器中&#xff0c;以测量孔隙水压力或液位。主要部件均采用特殊钢材制造&#xff0c;适合在各种恶劣环境中使用。特殊的稳定补偿技术使传感器具有极小的温度补偿系数。…

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

Python学习从0开始——Kaggle时间序列001 一、具有时间序列的线性回归1.时间序列2.时间序列线性回归1.时间步特征2.滞后特征 二、趋势1.介绍2.移动平均图3.设计趋向4.使用 三、季节性1.介绍2.季节图和季节指标季节性的指标 3.傅里叶特征和周期图用周期图选择傅里叶特征计算傅里…

智能生态网络(IEN)在智能城市中的应用

随着城市的发展&#xff0c;智能生态网络&#xff08;IEN&#xff09;正在改变城市的运作方式。对于城市白领来说&#xff0c;了解IEN如何提升城市生活质量、促进可持续发展和提高效率非常重要。 什么是智能生态网络&#xff08;IEN&#xff09;&#xff1f; IEN是一个将物联网…

手机如何扫描拍照?方法分享

手机如何扫描拍照&#xff1f;在数字化时代&#xff0c;手机扫描拍照软件已经成为我们日常生活和工作中不可或缺的工具。无论是快速识别纸质文档&#xff0c;还是将照片中的文字转化为可编辑的文本&#xff0c;这些软件都为我们提供了极大的便利。然而&#xff0c;市面上的手机…

【主要推荐算法概览,包括召回与排序】

文章目录 1、基于内容的推荐协同过滤推荐两大类2、召回算法2.1、基于规则策略的召回2.2、5类基础召回算法2.2.1、关联规则召回算法2.2.2、聚类召回算法2.2.3、朴素贝叶斯召回算法2.2.4、协同过滤召回算法2.2.5、矩阵分解召回算法 2.3、基于复杂算法的召回2.3.1、嵌入方法召回&a…

史上最全,呕心沥血总结oracle推进SCN方法(八)

作者介绍&#xff1a;老苏&#xff0c;10余年DBA工作运维经验&#xff0c;擅长Oracle、MySQL、PG数据库运维&#xff08;如安装迁移&#xff0c;性能优化、故障应急处理等&#xff09; 公众号&#xff1a;老苏畅谈运维 欢迎关注本人公众号&#xff0c;更多精彩与您分享。前面介…

雷达频段由来及应用

在雷达行业中&#xff0c;以雷达工作频率划分为若干的波段&#xff0c;由低到高的顺序是&#xff1a;高频&#xff08;HF&#xff09;、甚高频&#xff08;VHF&#xff09;、超高频&#xff08;UHF&#xff09;、L波段、S波段、C波段、X波段、Ku波段、K波段和Ka波段。微波波段的…

Xcode无法使用设备:Failed to prepare the device for development

问题&#xff1a; Xcode无法使用设备开发&#xff0c;失败报错如下&#xff1a; Failed to prepare the device for development. This operation can fail if the version of the OS on the device is incompatible with the installed version of Xcode. You may also need…

Pygame的基本应用

自学python如何成为大佬(目录):https://blog.csdn.net/weixin_67859959/article/details/139049996?spm1001.2014.3001.5501 Pygame有很多模块&#xff0c;每个模块又有很多方法&#xff0c;在此不能够逐一讲解&#xff0c;所以&#xff0c;我们通过一个实例来学习Pygame&…

机器学习归一化特征编码

特征缩放 因为对于大多数的机器学习算法和优化算法来说&#xff0c;将特征值缩放到相同区间可以使得获取性能更好的模型。就梯度下降算法而言&#xff0c;例如有两个不同的特征&#xff0c;第一个特征的取值范围为1——10&#xff0c;第二个特征的取值范围为1——10000。在梯度…

leetcode 122 买卖股票的最佳时机||(动态规划解法)

题目分析 题目描述的已经十分清楚了&#xff0c;不做过多阐述 算法原理 状态表示 我们假设第i天的最大利润是dp[i] 我们来画一下状态机 有两个状态&#xff0c;买入后和卖出后&#xff0c;我们就可以使用两个dp表来解决问题 f[i]表示当天买入后的最大利润 g[i]表示当天卖出…

uniapp开发微信小程序预加载分包

微信小程序分包是一种优化小程序项目结构和性能的方式。它允许开发者将小程序代码包拆分成多个子包&#xff0c;在用户需要时动态加载这些子包&#xff0c;从而减少小程序的首次加载时间和主包的体积。&#xff08;总体积不得大于20M&#xff0c;主包&#xff08;共同文件静态资…

613作业

#include "mywidget.h"MyWidget::MyWidget(QWidget *parent): QWidget(parent) {this->setWindowTitle("QQ");//窗口名this->setWindowIcon(QIcon("C:\\Users\\a8360\\Desktop\\4f603444-8a85-4d52-94d7-ab8c2f5f54a0.png"));this->res…

6.13长难句打卡

Hard times may hold you down at what usually seems like the most inopportune time, but you should remember that they won’t last forever. 艰难时刻可能会在你最不顺心的时刻让你低迷&#xff0c;但请相信&#xff0c;它们不会永远持续下去。

Vite - 项目打包从 0 到 1(完美解决打包后访问白屏问题)

目录 开始 修改资源相对地址 引入 vitejs/plugin-legacy 插件并配置 修改打包指令 修改 router 中的 history 前端配置跨域相关 打包后成功访问 开始 修改资源相对地址 在 vite.config.js 文件中配置如下&#xff1a; export default defineConfig({base: ./, //1.打包…

重复文件怎么查找并清理?6种重复文件清理方法亲测好用!

重复文件怎么查找并清理&#xff1f;重复的文件会占用计算机中不必要的空间&#xff0c;从而降低计算机速度。这些文件是您设备上现有文件的副本。您可能有照片、视频、音频、档案、文档等的文件副本。因此&#xff0c;当电脑被这些文件占用运行速度时&#xff0c;你会迫切地希…