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不提供實現類進行異常處理,因此異常的具體處理須要由咱們繼承這個接口本身實現。 在實現自定義異常解析器以前要明確一點認識: 咱們不能把40四、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>