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


    多种传参方式

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

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

    接收普通请求参数

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

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

    /**
     * 参数处理控制器,负责处理各种请求参数的场景,包括路径变量、请求体、请求参数等的不同组合。
     */
    @Controller
    @RequestMapping("/params")
    public class ParamsController {
    
        @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&¬e=xxx传递
    					data:$("form").serialize(),
    					// 设置提交成功后的回调函数
    					success:function(result) {
    						// 处理提交成功后的逻辑,这里留空
    					}
    				});
    			});
    		});
    	</script>
    
    </head>
    <body>
    	<form id="form" action="./common">
    		<table>
    			<tr>
    				<td>角色名称</td>
    				<td>
    				    <input id="roleName" name="roleName" value="" />
    				</td>
    			</tr>
    			<tr>
    				<td>备注</td>
    				<td><input id="note" name="note" /></td>
    			</tr>
    			<tr>
    				<td></td>
    				<td align="right">
    				    <input id="commit" type="button" value="提交" />
    				</td>
    			</tr>
    		</table>
    	</form>
    </body>
    </html>
    

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

        /**
         * 处理序列化参数的请求,展示如何接收和处理通过@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() + "

    "); // 输出角色的名称,以便用户了解当前角色的名称 out.println("roleName = " + role.getRoleName() + "

    "); // 输出角色的备注信息,以便用户了解角色的详细描述 out.println("note = " + role.getNote() + "

    "); // 从session中获取用户id,用于后续显示或验证用户身份 Long id = (Long) session.getAttribute("id"); // 输出用户id,以便用户确认当前登录的用户标识 out.println("id = " + id + "

    "); %> </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.validatorgroupId>
          <artifactId>hibernate-validatorartifactId>
          <version>6.1.0.Finalversion>
        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,因此它可以存放各种键值对,为了进一步定义数据模型功能,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中进行如下配置

    
    <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.*" />
    	
    	
    	
    	<bean id="viewResolver"
    	class="org.springframework.web.servlet.view.InternalResourceViewResolver"
    	p:prefix="/WEB-INF/jsp/" p:suffix=".jsp" />
    		
    	
    	
    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,需要引入如下依赖

        
        <dependency>
          <groupId>org.apache.poigroupId>
          <artifactId>poi-ooxmlartifactId>
          <version>4.1.1version>
        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的地方配置就可以,如下代码所示

    
    <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">
      
      <context-param>
        <param-name>contextConfigLocationparam-name>
        <param-value>/WEB-INF/applicationContext.xmlparam-value>
      context-param>
      
      <listener>
        <listener-class>
          org.springframework.web.context.ContextLoaderListener
        listener-class>
      listener>
      
      <servlet>
        
        <servlet-name>dispatcherservlet-name>
        <servlet-class>org.springframework.web.servlet.DispatcherServletservlet-class>
        
        <load-on-startup>2load-on-startup>
        
        <multipart-config>
          <location>e:/mvc/uploads/location>
          
          <max-file-size>5242880max-file-size>
          
          <max-request-size>10485760max-request-size>
          <file-size-threshold>0file-size-threshold>
        multipart-config>
      servlet>
      
      <servlet-mapping>
        <servlet-name>dispatcherservlet-name>
        
        <url-pattern>/mvc/*url-pattern>
      servlet-mapping>
    web-app>
    
    

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

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

        
        <dependency>
          <groupId>commons-fileuploadgroupId>
          <artifactId>commons-fileuploadartifactId>
          <version>1.4version>
        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

  • 相关阅读:
    k-form-desgin表格增加表格线级颜色
    P8554 心跳
    resnet_v1.resnet_v1()
    ssm+微信小程序网易云音乐设计与实现毕业设计源码261620
    C/C++总结笔记—— 关键字2:const关键字以及指针常量,常量指针,指向常量的常量指针
    前端工作方式要换了?HTMX简介:无需JavaScript的动态HTML
    新建flutter的module无法选择平台
    【LeetCode算法系列题解】第66~70题
    【JUC】原子操作类及LongAddr源码分析
    Python +大数据-知行教育(四)-意向用户主题看板_全量流程
  • 原文地址:https://blog.csdn.net/dawei_yang000000/article/details/139525801