5.SpringMVC异常处理 5.1.异常分类 1.可预知异常: Java编译时可检测异常,例如:IOException、SQLException等。 自定义异常(继承Exception父类的自定义类即为自定义异常)。 2.不可预知异常: Java运行时异常,例如:NullPointerException、IndexOutOfBoundsException等。 5.2.SpringMVC异常处理 在JavaEE项目的开发中,不管是持久层的数据库操作过程,还是业务层的处理过程,还是控制层的处理过程,都不可避免的遇到各种可预知的、不可预知的异常需要处理。如果每个过程都单独处理异常,那么系统的代码冗余度会很高,工作量大且不好统一,维护的工作量也很大。 那么,能不能将所有类型的异常处理从各处理过程提取出来呢?如果能提取出来,那么既保证了各层程序的处理逻辑的功能较单一(只专注业务逻辑的实现),也实现了异常信息的统一处理和维护。答案是肯定的。下面将介绍使用Spring MVC统一处理异常的解决和实现过程。 5.2.1.SpringMVC异常处理方式 SpringMVC异常处理的思路总的来说就是dao、service、controller层的程序出现异常都通过throws Exception向外抛出,抛出的异常就会逐层向它的上层传递,最后异常有DispatcherServlet接收,它接到之后就会转给统一的异常处理组件HandlerExceptionResolver(处理器异常解析器)进行异常处理,如下图: 5.2.2.自定义异常解析器 因为HandlerExceptionResolver(处理器异常解析器)只是一个接口,SpringMVC不提供实现类进行异常处理,所以异常的具体处理需要由我们继承这个接口自己实现。 在实现自定义异常解析器之前要明确一点认识: 我们不能把404、500这样的错误异常信息展示给用户,也就一旦展示给用户会产生很不友好的印象。说的不好听点就是对外要掩饰错误,给出一些善意的托词,比如:系统繁忙,请稍后再试,或者一个可爱卖萌的动画图片等等。目的是求得用户暂时的理解。 创建package【cn.baidu.exception】在其中创建【CustomExceptionResolver.java】 package cn.baidu.exception; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import org.springframework.web.servlet.HandlerExceptionResolver; import org.springframework.web.servlet.ModelAndView; public class CustomExceptionResolver implements HandlerExceptionResolver { @Override public ModelAndView resolveException(HttpServletRequest arg0, HttpServletResponse arg1, Object arg2, Exception exc) { // 异常信息 String msg = "系统繁忙,请稍候再试"; ModelAndView modelAndView = new ModelAndView(); modelAndView.addObject("msg", msg); modelAndView.setViewName("common/error"); return modelAndView; } } 5.2.3.配置异常解析器 【SpringMVC.xml】 <!-- 配置自定义异常解析器 --> <bean class="cn.baidu.exception.CustomExceptionResolver" /> 只要在SpringMVC核心配置文件中把这个bean配置上就可以。由于它继承了HandlerExceptionResolver,所以SpringMVC可以自动加载这个自定义的组件。 5.2.4.错误页面 <%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%> <%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c" %> <%@ taglib uri="http://java.sun.com/jsp/jstl/fmt" prefix="fmt"%> <!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> ${msg } </body> </html> 5.2.5.异常测试 特意把Controller中的一个方法改错,【ItemsController.java】:运行时异常 @RequestMapping("/list") public ModelAndView itemsList() throws Exception { // 程序错误,自动抛出异常 int i = 0 / 0; List<Items> itemsList = itemsService.findItemsList(); // 1. 设置返回页面需要的数据 2. 指定返回页面的地址 ModelAndView modelAndView = new ModelAndView(); // 1. 设置返回页面需要的数据 modelAndView.addObject("itemList", itemsList); // 2. 逻辑视图名称的设置(就是视图文件的文件名) modelAndView.setViewName("itemList"); return modelAndView; } 画面显示了【系统繁忙,请稍候再试】,而不是丑陋的500异常信息,就是因为有了整个系统的统一异常处理。 如果去掉这个统一的异常处理,比如讲SpringMVC.xml中的配置去掉,然后在请求这个页面就会出现丑陋的500: 5.2.6.SpringMVC异常处理方式的好处 各层都throws Exception,最后由DispatcherServlet交给HandlerExceptionResolver的实现类来处理的好处: 异常信息统一处理,更易于维护。 避免将500、404这样的错误信息返回给用户。 可以判断自定义异常,用异常机制控制业务违规的限制。 5.3.自定义异常类 我们还可以自定义异常类,那自定义异常类究竟有什么作用呢?——自定义异常只是希望利用java异常机制做一些特殊业务的限制,这样的业务限制不是程序bug。比如秒杀活动中的限购提示或者取钱时余额不足时中断处理并提示余额不足等。这些并不是程序的bug,都是业务范畴的限制。我们就可以利用java的异常机制,自定义一种异常,一旦业务出现违规就抛出这个特殊的异常,当系统捕获到这个特殊异常时就做对应的业务违规提示。 自定义异常【CustomException.java】 package cn.baidu.exception; /** * 自定义异常类 * @author Derek Sun * */ public class CustomException extends Exception { private String message; /** * @return the message */ public String getMessage() { return message; } /** * @param message the message to set */ public void setMessage(String message) { this.message = message; } } 在程序中造一个业务业务违规。由于是业务违规都是先进行判断,并在判断条件为true的逻辑中设置业务违规的具体信息,然后再抛出自定义异常。 【ItemsController.java】:做一个假的业务违规逻辑 @RequestMapping("/list") public ModelAndView itemsList() throws Exception { // 自定义异常 if (true) { CustomException exc = new CustomException(); exc.setMessage("请不要太贪心,您已经购买了一台!"); throw exc; } List<Items> itemsList = itemsService.findItemsList(); // 1. 设置返回页面需要的数据 2. 指定返回页面的地址 ModelAndView modelAndView = new ModelAndView(); // 1. 设置返回页面需要的数据 modelAndView.addObject("itemList", itemsList); // 2. 逻辑视图名称的设置(就是视图文件的文件名) modelAndView.setViewName("itemList"); return modelAndView; } 异常抛出后最终还是会由自定义的异常处理解析器捕获,因此需要在异常处理解析器中增加自定义异常处理的逻辑判断:【CustomExceptionResolver.java】 package cn.baidu.exception; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import org.springframework.web.servlet.HandlerExceptionResolver; import org.springframework.web.servlet.ModelAndView; public class CustomExceptionResolver implements HandlerExceptionResolver { @Override public ModelAndView resolveException(HttpServletRequest arg0, HttpServletResponse arg1, Object arg2, Exception exc) { // 异常信息 String msg = ""; // 判断传入的异常种类 // 如果是自定义异常直接抛出对应的业务违规信息 // 如果是程序异常就提示:系统繁忙,请稍后再试 if (exc instanceof CustomException) { // 自定义异常 msg = exc.getMessage(); } else { // 运行时异常 msg = "系统繁忙,请稍候再试"; } ModelAndView modelAndView = new ModelAndView(); modelAndView.addObject("msg", msg); modelAndView.setViewName("error"); return modelAndView; } } 再次运行tomcat测试,结果显示【请不要太贪心,您已经购买了一台!】 5.4.架构级别异常处理总结 SpringMVC的异常处理思想其实就是架构级别的异常处理思想,是从JavaEE架构整体的角度去统一异常处理。这是一个系统架构处理异常的最重要思想。 6.上传图片 本章所讲的图片上传方法是JavaWeb传统的上传方式,即前台页面提交一个可以包含图片的特殊form,后台处理需要具有处理特殊form的能力,将form中的图片提取出来交给后台程序处理。 6.1.服务器端配置文件访问服务 上传的图片应该在画面上显示出来,在web页面中访问一个图片是使用一个url的。Tomcat提供一种设置虚拟URL和实际图片保存的磁盘路径的映射关系,这样在web页面访问这个虚拟url就相当于访问实际磁盘的路径,就可以访问到指定的图片。 如何创建一个web虚拟url路径和一个磁盘物理路径的映射关系呢?——在web服务器中可以指定它们之间的映射关系,比如我们的tomcat就可以创建: 点击Modules 将上面指定的实际保存图片的物理路【C:\mydir\03_workspace\image】与这个虚拟url路径【/pic】关联到一起。这样当通过url:http://localhost:8080/pic/xxxx.jpg就可以访问的对应的图片了,并显示到浏览器中。就相当于访问C:\mydir\03_workspace\image\xxxx.jpg。 这里的物理路径:C:\mydir\03_workspace\image 映射后url路径:/pic 可以启动tomcat试一下: 先找一个图片放到C:\mydir\03_workspace\image目录下 然后启动tomcat 在浏览器访问http://localhost:8080/pic/xxx.jpg 注意:这个虚拟url路径是tomcat本身自己的配置,和任何web工程无关,所以任何web工程都可以使用这个虚拟路径。 这样在页面上就可以在回显的img的src中这样写: <%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%> <%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c" %> <%@ taglib uri="http://java.sun.com/jsp/jstl/fmt" prefix="fmt"%> <!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> <!-- 上传图片是需要指定属性 enctype="multipart/form-data" --> <form id="itemForm" action="${pageContext.request.contextPath }/items/it/${item.id }" method="post" enctype="multipart/form-data"> <%-- <form id="itemForm" action="${pageContext.request.contextPath }/items/update.action" method="post"> --%> <%-- <input type="hidden" name="id" value="${item.id }" /> --%> 修改商品信息: <table width="100%" border=1> <tr> <td>商品名称</td> <td><input type="text" name="name" value="${item.name }" /></td> </tr> <tr> <td>商品价格</td> <td><input type="text" name="price" value="${item.price }" /></td> </tr> <tr> <td>商品生产日期</td> <td><input type="text" name="createtime" value="<fmt:formatDate value="${item.createtime}" pattern="yyyy-MM-dd HH:mm:ss"/>" /></td> </tr> <tr> <td>商品图片</td> <td> <c:if test="${item.pic !=null}"> <img src="/pic/${item.pic}" width=100 height=100/> <br/> </c:if> <input type="file" name="pictureFile"/> </td> </tr> <tr> <td>商品简介</td> <td><textarea rows="3" cols="30" name="detail">${item.detail }</textarea> </td> </tr> <tr> <td colspan="2" align="center"><input type="submit" value="提交" /> </td> </tr> </table> </form> </body> </html> 6.2.图片上传的过程 6.2.1.前台上传与图片显示 在jsp页面中,form的【enctype="multipart/form-data"】属性,作用是表示该表单可以提交多媒体文件,比如图片 修改【editItem.jsp】,给form添加这个属性,使得它能够处理图片上传。 <%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%> <%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c" %> <%@ taglib uri="http://java.sun.com/jsp/jstl/fmt" prefix="fmt"%> <!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> <!-- 上传图片是需要指定属性 enctype="multipart/form-data" --> <form id="itemForm" action="${pageContext.request.contextPath }/items/it/${item.id }" method="post" enctype="multipart/form-data"> <%-- <form id="itemForm" action="${pageContext.request.contextPath }/items/update.action" method="post"> --%> <%-- <input type="hidden" name="id" value="${item.id }" /> --%> 修改商品信息: <table width="100%" border=1> <tr> <td>商品名称</td> <td><input type="text" name="name" value="${item.name }" /></td> </tr> <tr> <td>商品价格</td> <td><input type="text" name="price" value="${item.price }" /></td> </tr> <tr> <td>商品生产日期</td> <td><input type="text" name="createtime" value="<fmt:formatDate value="${item.createtime}" pattern="yyyy-MM-dd HH:mm:ss"/>" /></td> </tr> <tr> <td>商品图片</td> <td> <c:if test="${item.pic !=null}"> <img src="${item.pic}" width=100 height=100/> <br/> </c:if> <input type="file" name="pictureFile"/> </td> </tr> <tr> <td>商品简介</td> <td><textarea rows="3" cols="30" name="detail">${item.detail }</textarea> </td> </tr> <tr> <td colspan="2" align="center"><input type="submit" value="提交" /> </td> </tr> </table> </form> </body> </html> 上传过程只是强调一点:提交表单,前台将图片转换成二进制流并提交。 注意:图片上传必须通过post方式提交多媒体类型的form表单,其他方式,包括get都不允许提交多媒体的form,否则会报500错误(The current request is not a multipart request) 6.2.2.多媒体解析器——配置 SpringMVC对上传的图片提供后台的解析支持,使用的解析器是:org.springframework.web.multipart.commons.CommonsMultipartResolver,但是解析器需要依赖commons-fileupload和commons-io两个第三方的jar包,因此需要导入它们: 然后SpringMVC需要配置一下这个解析器才能生效: 【SpringMVC.xml】 <!-- 文件上传 --> <bean id="multipartResolver" class="org.springframework.web.multipart.commons.CommonsMultipartResolver"> <!-- 设置上传文件的最大尺寸为5MB --> <property name="maxUploadSize"> <value>5242880</value> </property> </bean> 这里限制了文件上传的大小,不能太大,否则容易造成服务器的磁盘负担超大。 6.2.3.后台图片处理——编码 SpringMVC中配置了多媒体解析器后,Controller方法中就可以使用【MultipartFile】类型定义一个形参接收图片,并调用这个形参对象的方法处理图片。 ·传参规范:页面上传控件的name属性值必须等于Controller方法中MultipartFile形参的变量名。 【ItemsController.java】:修改updateItems方法如下: /** * 演示图片上传的后台处理 */ @RequestMapping("/update") public String updateItems(MultipartFile pictureFile, Items items, Model model) throws Exception { // 1. 获取图片原始的文件名 String fileName = pictureFile.getOriginalFilename(); // 2. 随机生成字符串 + 原文件的扩展名组成新的文件名称 String newFileName = UUID.randomUUID().toString() + fileName.substring(fileName.lastIndexOf(".")); // 3. 将图片保存到磁盘 pictureFile.transferTo(new File("C:\\mydir\\03_workspace\\image\\" + newFileName)); // 4. 将新的图片名称保存到数据库 items.setPic("http://localhost:8080/image/" + newFileName); itemsService.updateItems(items); // 在底层仍然是将model中设置的这个属性名和属性值设置到request对象中,所以无论是请求转发还是 // 重定向都可以将需要的数据通过model带到他们对应的request对象中,这样数据就被带到请求转发或 // 者重定向后的方法中去了。 model.addAttribute("id", items.getId()); return "redirect:toEdit.action"; } 6.3.注意 在项目实际中这种传统的上传方式已经不适用了,作为SpringMVC的一个比较重要的插件,这里只是作为一个SpringMVC的知识点把SpringMVC对上传图片的支持介绍给大家,大家作为一个知识了解一下即可。 因为在当今实际项目中,都采用js端的上传插件,图片选择完成后直接上传,后台需要提前编写一个独立的Controller类并定义一个方法来处理上传,直接保存到文件服务器,然后返回对应的url给页面。这时在整个页面完整信息进行提交保存时,form表单中只包含图片的url字符串和其他业务信息,这个form就不需要指定多媒体类型的属性了,没有了多媒体类型的属性的form就可以不局限于只运行post提交了,这就给处理带来了便利。尤其是解决了RESTful的更新表单提交问题(这个在RESTful中再详细说明)。 7.json的数据交互 7.1.json的数据格式 1. JSON数据格式:键值对的形式承载数据,即{k1:v1,k2:v2,...} 2. JSON的起源:JavaScript 3. JSON目的:是用字符串的形式表示一个JavaScript对象,即对象的序列化。序列化的好处是便于对象的传输交互 4. JSON的本质:JSON本质就是一个字符串。因此JSON在JS代码程序中要以字符串的形式出现,其中key名、字符串类型的value值都要用双引号括起来,包括大括号在内整体json串要包在一对单引号中。 例如:'{"name":"测试商品", "price":99.9}',key名name和price也都要表示成字符串所以要加双引号,value值99.9是数值,所以不用加双引号。整体放到一对单引号中。 如果不按照上面的格式写,SpringMVC在配置接收JSON类型参数时就会报400错误。 7.2.json数据格式的好处 比xml更小、更高效,构上结和pojo类似,可以借助工具进行相互转换。 7.3.支持json所需要的jar包 在SpringMVC中要想使用json必须导入一下jar包: jackson包的作用:帮我们在json与pojo对象之间做转化的。 a)将页面传入的json格式的字符串自动转换成java对象即pojo对象。 b)将Controller中处理好的pojo对象自动转换成json格式字符串返回给页面使用。 在SpringMVC中利用jackson的jar包可以完美的支持json与pojo的互转,连配置都不需要,导入jackson的三个jar包即可。 7.4.SpringMVC中怎么传入和返回json json数据在客户端都是通过js的ajax提交的。 1.jsonpojo: 用@RequestBody注解修饰方法的pojo类型形参,作用是接收json数据并自动转换成pojo对象传入方法 2.pojojson: 把@ResponseBody注解加在pojo类型返回值的方法定义的上面,作用是把pojo对象结果自动转换成json,写入到Response对象的body数据区。 数据成功写入Response对象的body数据区后,Response对象中的状态信息就是success了,就会激活ajax的回调函数,jquery就会从Response对象的body数据区中把json字符串拿出来转换成合适的对象参数传给回调函数。此时SpringMVC方法返回后就不会走视图解析器的处理流程了。 ajax回调函数的参数是什么取决于SpringMVC方法的返回值类型是什么,此时SpringMVC方法可以直接返回一个pojo对象,也可以返回一个字符串,并且SpringMVC方法返回啥,ajax回调函数中data参数就是啥。 【代码示例】 1.随便在itemList.jsp页面上添加一个button,然后在jsp中用jquery定义一个js函数里面定义一个ajax作为客户端,点击添加的button进行ajax提交。 <%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%> <%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c" %> <%@ taglib uri="http://java.sun.com/jsp/jstl/fmt" prefix="fmt"%> <!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"> <script type="text/javascript" src="${pageContext.request.contextPath }/js/jquery-1.4.4.min.js"></script> <script type="text/javascript"> function sendJson() { $.ajax({ type:"post", url:"${pageContext.request.contextPath }/items/sendJson.action", contentType:"application/json;charset=utf-8", // 指定从页面传给Controller的数据格式是什么样的 //dataType:"", // 从Controller返回给页面的数据格式是什么样的,一般可以不写,它可以自动jquery可以自动匹配 data:'{"name":"测试商品","price":99.9}', success:function(data){ alert(data); } }); } </script> <title>查询商品列表</title> </head> <body> <input type="button" value="sendJson" onclick="sendJson()"> <form action="${pageContext.request.contextPath }/items/search.action" method="post"> 查询条件: <table width="100%" border=1> 。。。。。。。。 </table> </form> </body> </html> 2.在后台Controller中定义一个新方法来响应这个ajax提交: 【ItemsController.java】形式一:@ResponseBody放在了方法定义上面 /** * json数据交互 */ @RequestMapping("/sendJson") @ResponseBody public Items sendJson(@RequestBody Items items) throws Exception { System.out.println(items); items.setDetail("aaaa"); return items; } 【ItemsController.java】形式二:@ResponseBody放在了方法返回类型前面 /** * json数据交互 */ @RequestMapping("/sendJson") public @ResponseBody Items sendJson(@RequestBody Items items) throws Exception { System.out.println(items); items.setDetail("aaaa"); return items; } 注意: 如果在后台方法中使用了@RequestBody,此时前端必须要提交严格的json格式和请求的类型,否则就会报错。 严格的json格式和请求类型是: 1)【type:"post"】 2)【contentType:"application/json;charset=utf-8"】 3)【data:'{"name":"测试商品","price":99.9}'】 以上三者缺一不可。 如果type用get提交,或者data写成{"name":"测试商品","price":99.9},提交时都不会是按照json提交,那样如果后台配置了@RequestBody就会报错。 3.注意:要想让上面的两个注解发挥处理json的作用在SpringMVC配置文件中必须要有注解驱动的配置,即:<mvc:annotation-driven />,否则上面两个注解将会失效。 4.总结 以上示例中我们主要使用了SpringMVC中的两个注解: @RequestBody作用: 就是将页面传入的json格式字符串自动转换成pojo对象,要求json的key必须等于pojo的属性名,否则会造成数据丢失。 @ResponseBody作用: 是把pojo对象结果转换成json,并写入到Response对象的body数据区。后台方法返回什么前台回调函数中就接收什么。 附:json不用配置的解释 1.为什么要想使用处理json的这两个注解就必须要配置注解驱动呢? 实际上这两个注解是通过SpringMVC提供的接口org.springframework.http.converter.HttpMessageConverter<T>进行json处理的,并实现json与Controller中方法的形参pojo和返回值pojo进行相互转化。 但接口不可能进行实际的工作,需要实现类来执行具体的工作。在SpringMVC内部有多个这个接口的实现类都可以处理json格式的数据,而当前版本的spring默认使用的实现类是org.springframework.http.converter.json.MappingJackson2HttpMessageConverter,用他对json进行转换。而SpringMVC配置了注解驱动后就会默认使用MappingJackson2HttpMessageConverter来进行json数据处理。这也是我们最常用的配置方式。 2.为什么要引入jackson那三个jar包,似乎我们没有用到? 因为默认的实现类MappingJackson2HttpMessageConverter里面需要用到jackson的类进行json数据的处理,所以需要导入jackson的三个jar包。 3.SpringMVC提供HttpMessageConverter接口的好处是什么? 向外提供接口的好处是可以增加系统的扩展性,可以使用第三方开发的接口实现类进行json格式数据的处理。如果不使用默认的实现类就需要显示的配置: <!--注解适配器 --> <bean class="org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter"> <property name="messageConverters"> <list> <bean class="org.springframework.http.converter.json.MappingJackson2HttpMessageConverter"></bean> </list> </property> </bean> 8.RESTful支持 RESTful从英语上讲是一个形容词,它的名词是REST,加ful即成为形容词。 REST可以简单理解成是设计如何定位资源的一些建议,按照这些建议设计的系统架构我们就说这个系统具有REST风格,注意这只是一种风格不是强制的标准或者协议。基于这种风格设计的系统可以更简洁,更有层次,更方便扩展,对缓存的支持会更好。而这种REST风格,我们给它一个英文单词来表达这种风格,即RESTful。 8.1.现阶段如何学习RESTful RESTful的概念过于高大,属于系统架构级别的东西,需要我们有深厚的系统架构经验和相关的知识才能逐渐的理解。这对于现阶段的我们是无法达到的,因此我们只要从一点来掌握RESTful就可以: 学习如何把我们的url变成REST风格,即把url变得RESTful。 8.1.1.RESTful的url 1.RESTful中一个建议就是将互联网上所有的一切都看作为资源,url就是描述这些资源的地址。 (URL的百度百科https://baike.baidu.com/item/url/110640?fr=aladdin) 因为凡是地址没有使用动词的,所以RESTful的URL的第一个建议:URL使用名词而不是动词。 2.RESTful的URL第二个建议:用HTTP的请求动词(GET:查询、POST:新增、PUT:更新、DELETE:删除)描述对URL指定资源的操作。 3.也就是说如果我们想完整阐述对一个url的处理时,需要URL + HTTP请求动词。 4.在我们的代码示例中正常的url: http://localhost:8080/ssm-2/items/list.action(查询,GET) http://localhost:8080/ssm-2/items/itemEdit.action?id=1(查询,GET) http://localhost:8080/ssm-2/items/itemUpdate.action(更新,POST) http://localhost:8080/ssm-2/items/sendJson.action(模拟删除,POST) 把上面url变成RESTful样式如下: http://localhost:8080/ssm-2/items/list(查询,GET) http://localhost:8080/ssm-2/items/detail/1(查询,GET) http://localhost:8080/ssm-2/items/detail(更新,PUT) http://localhost:8080/ssm-2/items/detail(模拟删除,DELETE) 注意:删除和更新的url是相同的,这时就使用HTTP动词来区分不同。 URL改成restful后变得更加简洁了。 5.综上得出RESTful的url特点: 1)请求的url,除了静态资源文件的url外不允许有后缀名 2)Get请求url后面附带的参数必须在url后面用斜杠/分隔,可以传递多个,但先后顺序不要记错了,在SpringMVC方法中接收时候要对号入座的。 3)用名词组成的URL定位资源,用HTTP动词(GET、POST、PUT、DELETE)描述操作。 8.1.2.让前端控制器可以接收RESTful的url 想要SpringMVC前端控制器可以接收RESTful的url必须修改web.xml中的<servlet-mapping>: <servlet-mapping> <servlet-name>springmvc</servlet-name> <!-- DispatcherServlet拦截接收所有url请求,但只放行以.jsp为结尾的url, 其他资源文件后缀的url不放行 --> <url-pattern>/</url-pattern> </servlet-mapping> 8.1.3.手动配置放行的资源文件 【SpringMVC.xml】 <!-- 配置放行的资源文件目录 --> <!-- 放行js资源文件的配置,也可以理解为为location对应的目录配置对应的url访问路径 location:表示js所在的相对目录(以web根目录为基准) mapping:表示url中对应的路径名,**表示所有的js文件均被放行。 对css、jsp、pdf等,继续增加<mvc:resources>标签的配置项即可。 --> <mvc:resources location="/js/" mapping="/js/**"/> 此处可以试一试: 启动tomcat后,直接访问一个js文件,应该是可以访问到的,但是如果把这个配置注视掉,再启动tomcat后,就访问不到了。 8.1.4.改造url 【itemList.jsp】 ajax支持四种HTTP动词,可以直接写: <script type="text/javascript"> function sendJson() { $.ajax({ type:'delete', url:'${pageContext.request.contextPath }/items/detail', contentType:'application/json;charset=utf-8', data:'{"name":"测试商品", "price":99.9}', success:function(data) { alert(data.name + '---' + data.price); } }); } </script> <tr> <td>${item.name }</td> <td>${item.price }</td> <td><fmt:formatDate value="${item.createtime}" pattern="yyyy-MM-dd HH:mm:ss"/></td> <td>${item.detail }</td> <td><a href="${pageContext.request.contextPath }/items/detail/${item.id}">修改</a></td> </tr> 面对表单提交只能是GET或POST,DELETE或PUT不直接支持,所以想要DELETE和PUT提交只能是将POST转换成PUT或者DELETE。需要在【web.xml】中配置一个过滤器:这个配置用时拷贝即可。 <!-- 将POST请求转化为DELETE或者是PUT,要在页面指定一个_method名称的hidden变量来指定真正的请求参数 --> <filter> <filter-name>HiddenHttpMethodFilter</filter-name> <filter-class>org.springframework.web.filter.HiddenHttpMethodFilter</filter-class> </filter> <filter-mapping> <filter-name>HiddenHttpMethodFilter</filter-name> <url-pattern>/*</url-pattern> </filter-mapping> 【editItem.jsp】 <body> <!-- 上传图片是需要指定属性 enctype="multipart/form-data" --> <form id="itemForm" action="${pageContext.request.contextPath }/items/detail" method="post" enctype="multipart/form-data"> <input type="hidden" name="_method" value="PUT" /> 。。。。。。 </form> 注意:由于上传图片的多媒体form必须是post动作提交才行,所以这里将图片上传的功能暂时取消掉,恢复成正常的form。 【ItemsController.java】 在Controller方法中接收get url【http://localhost:8080/ssm255-2/items/itemEdit/1】的参数【1】时, 1.先在@RequestMapping中的url中对应参数的部分加一个{自定义接收的变量名称} 2.然后在方法的形参中定义个形参,类型要相符合 3.再在形参前面加一个注解@PathVariable(“同自定义接收的变量名称”) 4.如果{}中的变量名称和形参变量名称相同,则@PathVariable("id")可以省略成@PathVariable @RequestMapping(value="/itemEdit/{itemsId}", method=RequestMethod.GET) public String getItemsById(@PathVariable("itemsId") Integer id, HttpServletRequest request, Model model) throws Exception { // Integer id = Integer.valueOf(request.getParameter("id")); Items items = itemsService.getItemsById(id); model.addAttribute("item", items); return "items/editItem"; } 或者 @RequestMapping(value="/itemEdit/{id}", method=RequestMethod.GET) public String getItemsById(@PathVariable Integer id, HttpServletRequest request, Model model) throws Exception { // Integer id = Integer.valueOf(request.getParameter("id")); Items items = itemsService.getItemsById(id); model.addAttribute("item", items); return "items/editItem"; } 如果想加多个参数:【http://localhost:8080/ssm255-2/items/itemEdit/1/鼠标/键盘】 对应@RequestMapping(value="/itemEdit/{itemsId}/{mouse}/{keyborad}", method=RequestMethod.GET) 然后用@PathVariable(“自定义接收的变量名称”)对号入座的取来使用。 @RequestMapping(value="/detail", method=RequestMethod.PUT) public String updateItemsById2(Items items) throws Exception { itemsService.updateItemsById(items); // model.addAttribute("id", items.getId()); return "redirect:/items/itemEdit/" + items.getId(); } 注意: RESTful的url中用PUT表示更新,但是如果是多媒体表单提交即使你做了PUT的相关设置也是无效的,只要是多媒体form提交只认POST类型,因此前面的【editItem.jsp】我们取消了多媒体form,这里的方法的MultipartFile类型的参数以及图片保存处理部分的代码我们也删除不用。 在图片上传部分我们已经说了,当今实际项目的图片上传是通过js插件做的。 @RequestMapping(value="/detail", method=RequestMethod.DELETE) @ResponseBody public Items sendJsonTest(@RequestBody Items items) throws Exception { items.setDetail("json test"); return items; } 附1:为什么会出现REST这个概念 这跟我们软件系统的演变有关系:C/S单机结构 -> B/S网络结构 -> C/S互联网结构 C/S互联网结构: 一个后台系统服务多种客户端,甚至还出现了一些面向大众的公共服务平台,比如Facebook platform,微博开放平台,微信公共平台等,它们不需要有显式的前端,只有一套提供服务的接口,用户可以利用这些平台进行基于平台的应用开发。 这些新的互联网的演化,要求我们的服务端架构设计要进行调整,以适应各种不同的C(客户)。于是一哥们在他的毕业论文中提出了REST概念,即以网络资源(数据、文件、图片、视频、音频)为核心的一种思想。 Roy Fielding的毕业论文。这哥们参与设计HTTP协议,也是Apache Web Server项目的co-founder。PhD的毕业学校是 UC Irvine,Irvine在加州,有着充裕的阳光和美丽的海滩,是著名的富人区。Oculus VR 的总部就坐落于此(虚拟现实眼镜,被FB收购,CTO为Quake和Doom的作者 John Carmack)。 附2:REST 1.全称: Resource Representational State Transfer(资源表现的状态转移),通俗讲就是资源在网络中以某种表现形式进行状态转移。它认为网络中的核心是资源。 2.解释: Resource:资源,即数据,比如商品信息、用户信息、一个图片、一个视频等、一个pdf文件等。互联网中的一切都是资源。 Representational:某种表现形式,比如用JSON、XML、JPEG、PDF等; State Transfer:状态变化。通过HTTP动词(GET、POST、PUT、DELETE等)实现。即通过CRUD的动作对数据产生的变化。比如:苹果从青到红到烂,就是苹果的状态变化,是细菌和氧气对苹果的产生的动作作用的结果。同理通过HTTP协议中的动作对网络资源进行CRUD的操作,使得资源发生变化,即为状态变化。 3.怎样理解: 小的方面:就是围绕着网络资源的状态变化,通过某种表现形式表现出来。 大的方面:就是为了达到网络资源为核心的目的,并能更好的为各种客户端提供服务,需要对web系统架构进行重组,基于此大牛架构师先行者们提出了一些建议,使得REST成为一种如何组织web服务架构的建议的代名词,它不是强制性的标准,更不是一种新的技术,只是一种建议或者叫做风格。 附3:RESTful 从小的方面入手就是用URL定位资源,用HTTP动词(GET、POST、PUT、DELETE等)描述操作。 从大的方面入手就是形容web系统符合了REST风格就称为RESTful。 附4:RESTful的URL 大的方面需要多年的开发积累和自己的对系统架构的不断研究学习才能有所体会的。因此我们从小的方面讲RESTful,即解决如何使我们的url变得RESTful 先来看一个RESTful风格URL的例子:知乎的某问题的url: http://www.zhihu.com/question/28557115。 根据用URL定位资源,用HTTP动词描述操作原则,组合如下: 创建:POST http://www.zhihu.com/question/28557115 删除:DELETE http://www.zhihu.com/question/28557115 (可以用POST代替) 更新:PUT http://www.zhihu.com/question/28557115 (可以用POST代替) 取得:GET http://www.zhihu.com/question/28557115 由上面的叙述可知:URL中只需要描述你需要访问的资源在哪,即: http://www.jd.com/drinks/beers/local/list http://www.jd.com/drinks/beers/qingdao/1903/1 如何使我们的URL变得RESTful?(两点) RESTful的URL中使用名词而不是动词,且推荐用复数,不要有参数。 RESTful中的资源要分类分层次(什么分类下什么层次下的什么资源名中的具体哪个资源对象) 注意: 不要有参数即不要有Get请求中那样的参数:http://www.a.com/goods/list.action?id=aaa&name=bbb RESTful中的参数全被视为资源定位的名词描述 URL示例: Bad: http://www.jd.com/beer/getqingdao/1903/1 http://www.a.com/toys/cars/list.action?name=bmw&color=red Good: http://www.jd.com/beers/qingdao/1903/1 http://www.a.com/toys/cars/list/bmw/red 附5:REST建议 1.使用客户/服务器模型: 客户和服务器之间通过一个统一的接口来互相通讯。 2.层次化的系统: 在一个REST系统中,客户端并不会固定地与一个服务器打交道。 3.无状态: 在一个REST系统中,服务端并不会保存有关客户的任何状态。也就是说,客户端自身负责用户状态的维持,并在每次发送请求时都需要提供足够的信息。 4.可缓存: REST系统需要能够恰当地缓存请求,以尽量减少服务端和客户端之间的信息传输,以提高性能。 5.统一的接口: 一个REST系统需要使用一套统一的接口来完成子系统之间以及服务与用户之间的交互。这使得REST系统中的各个子系统可以独自完成演化。 参考网页: https://www.zhihu.com/question/28557115 http://www.cnblogs.com/loveis715/p/4669091.html http://www.cnblogs.com/rainy-shurun/p/5412162.html 9.拦截器 9.1.作用 拦截请求,类似于Servlet 开发中的过滤器Filter,用于对处理器进行预处理和后处理。一般在权限验证的时候使用较多。 SpringMVC第一天学习的转换器仅仅是处理参数的,拦截器的功能更加强大。 9.2.拦截器定义 自定义拦截器都要实现org.springframework.web.servlet.HandlerInterceptor接口: 在工程中创建连接器: 【Interceptor1.java】 package interceptor; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import org.springframework.web.servlet.HandlerInterceptor; import org.springframework.web.servlet.ModelAndView; public class Interceptor1 implements HandlerInterceptor { /** * 执行时机:Controller方法已经执行,处理结果也已经返回。 * 应用场景:这里可以记录操作日志,记录下来的日志可以用于用户行为分析。 */ @Override public void afterCompletion(HttpServletRequest arg0, HttpServletResponse arg1, Object arg2, Exception arg3) throws Exception { System.out.println("======Interceptor1=============afterCompletion======"); } /** * 执行时机:Controller方法已经执行,但处理结果没有返回,所以它可以拦截住返回的ModelAndView。 * 应用场景:这里可以返回用户前对模型数据进行加工处理,比如这里加入公共信息以便页面显示 */ @Override public void postHandle(HttpServletRequest arg0, HttpServletResponse arg1, Object arg2, ModelAndView arg3) throws Exception { System.out.println("======Interceptor1=============postHandle======"); // Items items = (Items)arg3.getModel().get("item"); // System.out.println(items.getName()); } /** * 返回布尔类型的结果,返回true放行,返回false拦截后续所有的方法执行。 * * 执行时机:在Controller方法执行之前。 * 应用场景:这里可以加入权限验证,登录验证等,使用的是最多的,拦截所有请求并判断是否具有访问权限。 */ @Override public boolean preHandle(HttpServletRequest arg0, HttpServletResponse arg1, Object arg2) throws Exception { System.out.println("======Interceptor1=============preHandle======"); return true; } } 9.3.配置拦截器 【SpringMVC.xml】 <!-- 配置全局拦截器 --> <mvc:interceptors> <mvc:interceptor> <!-- 配置拦截器能够拦截的url --> <!-- /**表示拦截所有请求 --> <mvc:mapping path="/**"/> <bean class="interceptor.Interceptor1" /> </mvc:interceptor> <mvc:interceptor> <!-- 配置拦截器能够拦截的url --> <!-- /**表示拦截所有请求 --> <mvc:mapping path="/**"/> <bean class="interceptor.Interceptor2" /> </mvc:interceptor> </mvc:interceptors> 9.4.正常流程测试 这里了解一下单个拦截器中和多个拦截器并存时三个方法的执行顺序的规律,主要是想让大家把握住拦截器执行的详细顺序,尤其是多个拦截器共同工作的时候,以免使用时由于不清楚顺序而拦截失败或拦截了不该拦截的东西。 1.单个拦截器的执行顺序: 先定义一个拦截器:Interceptor1.java测试它里面三个方法的拦截顺序 ======Interceptor1=============preHandle====== ======Interceptor1=============postHandle====== ======Interceptor1=============afterCompletion====== 2.多个拦截器的执行顺序: a)两个拦截器中preHandle方法都返回true时:在配置文件中配置顺序是先1后2 preHandle:(配置的正序) ======Interceptor1=============preHandle====== ======Interceptor2=============preHandle====== postHandle:(配置的反序) ======Interceptor2=============postHandle====== ======Interceptor1=============postHandle====== afterCompletion:(配置的反序) ======Interceptor2=============afterCompletion====== ======Interceptor1=============afterCompletion====== b)两个拦截器中preHandle方法都返回true时:在配置文件中配置顺序是先2后1 preHandle:(配置的正序) ======Interceptor2=============preHandle====== ======Interceptor1=============preHandle====== postHandle:(配置的反序) ======Interceptor1=============postHandle====== ======Interceptor2=============postHandle====== afterCompletion:(配置的反序) ======Interceptor1=============afterCompletion====== ======Interceptor2=============afterCompletion====== 当都所有拦截器都返回true时,此时总的规律:先开始的后结束。 9.5.中断流程测试 1.让Interceptor2的preHandle方法返回false时:(配置顺序中不是第一个的拦截器) ======Interceptor1=============preHandle====== ======Interceptor2=============preHandle====== ======Interceptor1=============afterCompletion====== 说明: 首先拦截器2的preHandle返回false,它自己的后续方法全部中断。 其次拦截器1的preHandle返回true,但是它的postHandle也没有执行,说明postHandle受到所有拦截器的preHandle方法返回值的影响 再次拦截器1的afterCompletion方法却执行了,说明afterCompletion不受其他拦截器的preHandle方法返回值的影响。 结论: postHandle受所有拦截器的preHandle执行结果的影响,只有全部preHandle都返回true时才执行 afterCompletion只受它自己所属拦截器中preHandle的影响,preHandle返回true时执行。 2.让Interceptor1的preHandle方法返回false时:(配置顺序中的第一个拦截器) ======Interceptor1=============preHandle====== 结论: 配置顺序第一个拦截器的preHandle返回了false,则中断所有后续处理。 9.6.拦截器应用 9.6.1.处理流程 1.有一个登录页面,需要写一个Controller访问登录页面 2.登录页面有一提交表单的动作。需要在Controller中处理。 a)判断用户名密码是否正确(在控制台打印) b)如果正确,向session中写入用户信息(写入用户名username) c)跳转到商品列表 3.拦截器 a)访问商品列表画面时,拦截用户请求,判断用户是否登录(登录请求不能拦截) b)如果用户已经登录。放行 c)如果用户未登录,跳转到登录页面。 9.6.2.JSP页面 【login.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>Insert title here</title> </head> <body> <form action="${pageContext.request.contextPath }/user/login.action"> <label>用户名:</label> <br> <input type="text" name="username"> <br> <label>密码:</label> <br> <input type="password" name="password"> <br> <input type="submit"> </form> </body> </html> 9.6.3.用户登录Controller 【UserController.java】 package cn.baidu.controller; import javax.servlet.http.HttpSession; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.RequestMapping; @Controller @RequestMapping("/user") public class UserController { /** * 跳转到登录页面 */ @RequestMapping("/toLogin") public String toLogin() { return "items/login"; } /** * 用户登录 */ @RequestMapping("/login") public String login(String username, String password, HttpSession session) { // 校验用户登录 System.out.println(username); System.out.println(password); // 把用户名放到session中 if (username != null && !"".equals(username)) { session.setAttribute(session.getId(), username); } return "redirect:/items/list.action"; } } 9.6.4.编写拦截器 @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object arg2) throws Exception { // 从request中获取session HttpSession session = request.getSession(); // 从session中根据Session id取得用户登录信息 Object user = session.getAttribute(session.getId()); // 判断user是否为null if (user != null) { // 如果不为空则放行 return true; } else { // 如果为空则跳转到登录页面 response.sendRedirect(request.getContextPath() + "/user/toLogin.action"); } return false; } 9.6.5.配置拦截器 拦截商品业务中的url 因为ItemController做了url窄化限定, 所以配置文件中如下配置:表明url以/items/开头的均被拦截。 <mvc:interceptor> <!-- 配置商品被拦截器拦截 --> <mvc:mapping path="/items/**"/> <!-- 配置具体的拦截器 --> <bean class="cn.baidu.interceptor.LoginHandlerInterceptor"/> </mvc:interceptor>