javascript
最全面的SpringMVC教程(二)——SpringMVC核心技术篇
前言
本文為 【SpringMVC教程】核心技術篇 相關詳細介紹,具體將對視圖和模型拆分,重定向與轉發,RequestMapping與其衍生注解,URL 模式匹配,牛逼的傳參,設定字符集,返回json數據(序列化),獲取請求中的json數據,數據轉化,數據校驗,視圖解析器詳解,全局異常捕獲,處理資源,攔截器,全局配置類等SpringMVC相關核心技術進行詳盡介紹~
📌博主主頁:小新要變強 的主頁
👉Java全棧學習路線可參考:【Java全棧學習路線】最全的Java學習路線及知識清單,Java自學方向指引,內含最全Java全棧學習技術清單~
👉算法刷題路線可參考:算法刷題路線總結與相關資料分享,內含最詳盡的算法刷題路線指南及相關資料分享~
👉Java微服務開源項目可參考:企業級Java微服務開源項目(開源框架,用于學習、畢設、公司項目、私活等,減少開發工作,讓您只關注業務!)
??本文上接:最全面的SpringMVC教程(一)——SpringMVC簡介
目錄
文章標題
- 前言
- 目錄
- 一、視圖和模型拆分
- 二、重定向與轉發
- 三、RequestMapping與其衍生注解
- 四、URL 模式匹配
- 五、牛逼的傳參
- 六、設定字符集
- 七、返回json數據(序列化)
- 八、獲取請求中的json數據
- 九、數據轉化
- 十、數據校驗
- 十一、視圖解析器詳解
- 十二、全局異常捕獲
- 十三、處理資源
- 十四、攔截器
- 十五、全局配置類
- 后記
一、視圖和模型拆分
視圖和模型相伴相生,但是springmvc給我們提供了更好的,更優雅的解決方案:
- Model會在調用handler時通過參數的形式傳入
- View可以簡化為字符串形式返回
這樣的解決方案也是企業開發中最常用的:
@RequestMapping("/test1") public String testAnnotation(Model model){model.addAttribute("hello","hello annotationMvc as string");return "annotation"; }二、重定向與轉發
在返回的字符串中,默認使用視圖解析器進行視圖跳轉。
springmvc給我們提供了更好的解決【重定向和轉發】的方案:
🍀返回視圖字符串加前綴redirect就可以進行重定向
redirect:/redirectController/redirectTest redirect:https://www.baidu.com🍀返回視圖字符串加前綴forward就可以進行請求轉發,而不走視圖解析器
// 會將請求轉發至/a/b forward:/a/b三、RequestMapping與其衍生注解
- @RequestMapping這個注解很關鍵,他不僅僅是一個方法級的注解,還是一個類級注解。
- 如果放在類上,相當于給每個方法默認都加上一個前綴url。
🍀好處
- 一個類一般處理一類業務,可以統一加上前綴,好區分
- 簡化書寫復雜度
🍀RequestMapping注解有六個屬性
value: 指定請求的實際地址,指定的地址可以是URI Template 模式(后面將會說明);
method: 指定請求的method類型, GET、POST、PUT、DELETE等;
consumes: 指定處理中的請求的內容類型(Content-Type),例如application/json;
produces: 指定返回響應的內容類型,僅當request請求頭中的(Accept)類型中包含該指定類型才返回;
params: 指定request中必須包含某些參數值處理器才會繼續執行;
headers: 指定request中必須包含某些指定的header值處理器才會繼續執行。
@RequestMapping還有幾個衍生注解,用來處理特定方法的請求:
@GetMapping("getOne") public String getOne(){return "user"; }@PostMapping("insert") public String insert(){return "user"; }@PutMapping("update") public String update(){return "user"; }@DeleteMapping("delete") public String delete(){return "user"; }源碼中能看帶GetMapping注解中有@RequestMapping作為元注解修飾:
@RequestMapping(method = {RequestMethod.GET}) public @interface GetMapping {}四、URL 模式匹配
@RequestMapping可以支持【URL模式匹配】,為此,spring提供了兩種選擇(兩個類):
- PathPattern:PathPattern是 Web 應用程序的推薦解決方案,也是 Spring WebFlux 中的唯一選擇,比較新。
- AntPathMatcher:使用【字符串模式與字符串路徑】匹配。這是Spring提供的原始解決方案,用于選擇類路徑、文件系統和其他位置上的資源。
小知識: 二者目前都存在于Spring技術棧內,做著相同的事。雖說現在還鮮有同學了解到PathPattern,我認為淘汰掉AntPathMatcher只是時間問題(特指web環境哈),畢竟后浪總歸有上岸的一天。但不可否認,二者將在較長時間內共處,那么它倆到底有何區別呢?
- (1)出現時間,AntPathMatcher是一個早在2003年(Spring的第一個版本)就已存在的路徑匹配器,而PathPattern是Spring
5新增的,旨在用于替換掉較為“古老”的AntPathMatcher。 - (2)功能差異,PathPattern去掉了Ant字樣,但保持了很好的向下兼容性:除了不支持將**寫在path中間之外,其它的匹配規則從行為上均保持和AntPathMatcher一致,并且還新增了強大的{*pathVariable}的支持,他能匹配最后的多個路勁,并獲取路徑的值。
- (3)性能差異,Spring官方說PathPattern的性能優于AntPathMatcher。
🍀一些模式匹配的示例
- “/resources/ima?e.png” - 匹配路徑段中的一個字符
- “/resources/*.png” - 匹配路徑段中的零個或多個字符
- “/resources/**” - 匹配多個路徑段
- “/projects/{project}/versions” - 匹配路徑段并將其【捕獲為變量】
- “/projects/{project:[a-z]+}/versions” - 使用正則表達式匹配并【捕獲變量】
捕獲的 URI 變量可以使用@PathVariable注解,示例例如:
@GetMapping("/owners/{ownerId}/pets/{petId}") public Pet findPet(@PathVariable Long ownerId, @PathVariable Long petId) {// ... }還可以在類和方法級別聲明 URI 變量,如以下示例所示:
@Controller @RequestMapping("/owners/{ownerId}") public class OwnerController {@GetMapping("/pets/{petId}")public Pet findPet(@PathVariable Long ownerId, @PathVariable Long petId) {// ...} }🍀一個url可以匹配到多個路由的情況
有時候會遇到一個url可以匹配到多個路由的情況,這個時候就是由Spring的AntPatternComparator完成優先級處理,大致規律如下:
比如:有兩個匹配規則一個是 /a/**,一個是 /a/b/**,還有一個是/a/b/*,如果訪問的url是/a/b/c,其實這三個路由都能匹配到,在匹配優先級中,有限級如下:
| 全路徑匹配,例如:配置路由/a/b/c | 第一優先級 |
| 帶有{}路徑的匹配,例如:/a/{b}/c | 第二優先級 |
| 正則匹配,例如:/a/{regex:\d{3}}/c | 第三優先級 |
| 帶有*路徑的匹配,例如:/a/b/* | 第四優先級 |
| 帶有**路徑的匹配,例如:/a/b/** | 第五優先級 |
| 僅僅是雙通配符:/** | 最低優先級 |
注意:
- 當有多個*和多個‘{}'時,命中單個路徑多的,優先越高;
- 多’*’的優先級高于‘**’,會優先匹配帶有*。
🍀我們還可以從一個類中看出,當一個url匹配了多個處理器時,優先級是如何考慮的,這個類是AntPathMatcher的一個內部類
protected static class AntPatternComparator implements Comparator<String> {@Overridepublic int compare(String pattern1, String pattern2) {PatternInfo info1 = new PatternInfo(pattern1);PatternInfo info2 = new PatternInfo(pattern2);.....boolean pattern1EqualsPath = pattern1.equals(this.path);boolean pattern2EqualsPath = pattern2.equals(this.path);// 完全相等,是無法比較的if (pattern1EqualsPath && pattern2EqualsPath) {return 0;}// pattern1和urlequals,返回負數 1勝出else if (pattern1EqualsPath) {return -1;}// pattern2和urlequals,返回正數,2勝出else if (pattern2EqualsPath) {return 1;}// 都是前綴匹配,長的優先 /a/b/** /a/**if (info1.isPrefixPattern() && info2.isPrefixPattern()) {return info2.getLength() - info1.getLength();}// 非前綴匹配的優先級高else if (info1.isPrefixPattern() && info2.getDoubleWildcards() == 0) {return 1;}else if (info2.isPrefixPattern() && info1.getDoubleWildcards() == 0) {return -1;}// 匹配數越少,優先級越高if (info1.getTotalCount() != info2.getTotalCount()) {return info1.getTotalCount() - info2.getTotalCount();}// 路徑越短越好if (info1.getLength() != info2.getLength()) {return info2.getLength() - info1.getLength();}// 單通配符個數,數量越少優先級越高if (info1.getSingleWildcards() < info2.getSingleWildcards()) {return -1;}else if (info2.getSingleWildcards() < info1.getSingleWildcards()) {return 1;}// url參數越少越優先if (info1.getUriVars() < info2.getUriVars()) {return -1;}else if (info2.getUriVars() < info1.getUriVars()) {return 1;}return 0;} }源碼中我們看到的信息如下:
- (1)完全匹配者,優先級最高
- (2)都是前綴匹配(/a/**), 匹配路由越長,優先級越高
- (3)前綴匹配優先級,比非前綴的低
- (4)需要匹配的數量越少,優先級越高,this.uriVars + this.singleWildcards + (2 * this.doubleWildcards);
- (5)路勁越短優先級越高
- (6)*越少優先級越高
- (7){}越少優先級越高
五、牛逼的傳參
在學習servlet時,我們是這樣獲取請求參數的:
@PostMapping("insert") public String insert(HttpServletRequest req){String username = req.getParameter("username");String password = req.getParameter("password");// 其他操作return "success"; }有了springmvc之后,我們不再需要使用getParamter一個一個獲取參數:
@Controller @RequestMapping("/user/") public class LoginController {@RequestMapping("login")public String login(String username,String password){System.out.println(username);System.out.println(password);return "login";} }如果一個表單幾十個參數怎么獲取啊?更牛的傳參方式如下:
需要提前定義一個User對象:
public class User {private String username;private String password;private int age;public String getUsername() {return username;}public void setUsername(String username) {this.username = username;}public String getPassword() {return password;}public void setPassword(String password) {this.password = password;}public int getAge() {return age;}public void setAge(int age) {this.age = age;} }直接在參數中申明user對象:
@Controller @RequestMapping("/user/") public class LoginController {@RequestMapping("register")public String register(User user){System.out.println(user);return "register";}@RequestMapping("login")public String login(String username,String password){System.out.println(username);System.out.println(password);return "login";} }🍀(1)@RequestParam
可以使用@RequestParam注解將【請求參數】(即查詢參數或表單數據)綁定到控制器中的方法參數。
@Controller @RequestMapping("/pets") public class EditPetForm {@GetMappingpublic String setupForm(@RequestParam("petId") int petId, Model model) { Pet pet = this.clinic.loadPet(petId);model.addAttribute("pet", pet);return "petForm";} }默認情況下,使用此注解的方法參數是必需的,但我們可以通過將@RequestParam注解的【required標志設置】為 false來指定方法參數是可選的。如果目標方法參數類型不是String,則應用會自動進行類型轉換,這個后邊會講。
請注意,使用@RequestParam是可選的。默認情況下,任何屬于簡單值類型且未被任何其他參數解析器解析的參數都被視為使用【@RequestParam】。
🍀(2)@RequestHeader
可以使用@RequestHeader注解將請求的首部信息綁定到控制器中的方法參數中:
假如我們的請求header如下:
Host localhost:8080 Accept text/html,application/xhtml+xml,application/xml;q=0.9 Accept-Language fr,en-gb;q=0.7,en;q=0.3 Accept-Encoding gzip,deflate Accept-Charset ISO -8859-1,utf-8;q=0.7,*;q=0.7 Keep-Alive 300以下示例獲取Accept-Encoding和Keep-Alive標頭的值:
@GetMapping("/demo") public void handle(@RequestHeader("Accept-Encoding") String encoding, @RequestHeader("Keep-Alive") long keepAlive) { //... }小知識:當@RequestHeader注解上的使用Map<String, String>, MultiValueMap<String, String>或HttpHeaders參數,則map會被填充有所有header的值。當然,我們依然可以使用requied的屬性來執行該參數不是必須的。
🍀(3)@CookieValue
可以使用@CookieValue注解將請求中的 cookie 的值綁定到控制器中的方法參數。
假設我們的請求中帶有如下cookie:
JSESSIONID=415A4AC178C59DACE0B2C9CA727CDD84以下示例顯示了如何獲取 cookie 值:
@GetMapping("/demo") public void handle(@CookieValue("JSESSIONID") String cookie) { //... }🍀(4)@ModelAttribute
可以使用@ModelAttribute注解在方法參數上來訪問【模型中的屬性】,或者在不存在的情況下對其進行實例化。模型的屬性會覆蓋來自 HTTP Servlet 請求參數的值,其名稱與字段名稱匹配,這稱為數據綁定,它使您不必【處理解析】和【轉換單個查詢參數】和表單字段。以下示例顯示了如何執行此操作:
@RequestMapping("/register") public String register(@ModelAttribute("user") UserForm user) {... }還有一個例子:
@ModelAttribute 和 @RequestMapping 注解同時應用在方法上時,有以下作用:
- 方法的【返回值】會存入到 Model 對象中,key為 ModelAttribute 的 value 屬性值。
- 方法的返回值不再是方法的訪問路徑,訪問路徑會變為 @RequestMapping 的 value值,例如:@RequestMapping(value = "/index") 跳轉的頁面是 index.jsp 頁面。
🍀(5)@SessionAttribute
如果您需要訪問全局管理的預先存在的會話屬性,并且可能存在或可能不存在,您可以@SessionAttribute在方法參數上使用注解,如下所示示例顯示:
@RequestMapping("/") public String handle(@SessionAttribute User user) { // ... }🍀(6)@RequestAttribute
和@SessionAttribute一樣,可以使用@RequestAttribute注解來訪問先前創建的存在與請求中的屬性(例如,由 ServletFilter 或HandlerInterceptor)創建或在請求轉發中添加的數據:
@GetMapping("/") public String handle(@RequestAttribute Client client) { // ... }🍀(7)@SessionAttributes
@SessionAttributes注解應用到Controller上面,可以將Model中的屬性同步到session當中:
@Controller @RequestMapping("/Demo.do") @SessionAttributes(value={"attr1","attr2"}) public class Demo {@RequestMapping(params="method=index")public ModelAndView index() {ModelAndView mav = new ModelAndView("index.jsp");mav.addObject("attr1", "attr1Value");mav.addObject("attr2", "attr2Value");return mav;}@RequestMapping(params="method=index2")public ModelAndView index2(@ModelAttribute("attr1")String attr1, @ModelAttribute("attr2")String attr2) {ModelAndView mav = new ModelAndView("success.jsp");return mav;} }附加一個注解使用的案例:
@RequestMapping("insertUser")public String insertUser(@RequestParam(value = "age",required = false) Integer age,@RequestHeader(value = "Content-Type",required = false) String contentType,@RequestHeader(required = false) String name,@CookieValue(value = "company",required = false) String company,@SessionAttribute(value = "username",required = false) String onlineUser,@RequestAttribute(required = false) Integer count,@ModelAttribute("date") Date date,@SessionAttribute(value = "date",required = false) Date sessionDate) {System.out.println("sessionDate = " + sessionDate);System.out.println("date = " + date);System.out.println("count = " + count);System.out.println("onlineUser = " + onlineUser);System.out.println("age = " + age);System.out.println("contentType = " + contentType);System.out.println("name = " + name);System.out.println("company = " + company);return "user";}🍀(8)數組的傳遞
在類似批量刪除的場景中,我們可能需要傳遞一個id數組,此時我們僅僅需要將方法的參數指定為數組即可:
@GetMapping("/array") public String testArray(@RequestParam("array") String[] array) throws Exception {System.out.println(Arrays.toString(array));return "array"; }我們可以發送如下請求,可以是多個名稱相同的key,也可以是一個key,但是值以逗號分割的參數:
http://localhost:8080/app/hellomvc?array=1,2,3,4或者
http://localhost:8080/app/hellomvc?array=1&array=3結果都是沒有問題的:
🍀(9)復雜參數的傳遞
當然我們在進行參數接收的時候,其中可能包含很復雜的參數,一個請求中可能包含很多項內容,比如以下表單:
當然我們要注意表單中的name(參數中key)的寫法:
<form action="user/queryParam" method="post">排序字段:<br><input type="text" name="sortField"><hr>數組:<br><input type="text" name="ids[0]"> <br><input type="text" name="ids[1]"><hr>user對象:<br><input type="text" name="user.username" placeholder="姓名"><br><input type="text" name="user.password" placeholder="密碼"><hr>list集合<br>第一個元素:<br><input type="text" name="userList[0].username" placeholder="姓名"><br><input type="text" name="userList[0].password" placeholder="密碼"><br>第二個元素: <br><input type="text" name="userList[1].username" placeholder="姓名"><br><input type="text" name="userList[1].password" placeholder="密碼"><hr>map集合<br>第一個元素:<br><input type="text" name="userMap['user1'].username" placeholder="姓名"><br><input type="text" name="userMap['user1'].password" placeholder="密碼"><br>第二個元素:<br><input type="text" name="userMap['user2'].username" placeholder="姓名"><br><input type="text" name="userMap['user2'].password" placeholder="密碼"><br><input type="submit" value="提交"> </form>然后我們需要搞一個實體類用來接收這個表單的參數:
@Data public class QueryVo {private String sortField;private User user;private Long[] ids;private List<User> userList;private Map<String, User> userMap; }編寫接口進行測試,我們發現表單的數據已經盡數傳遞了進來:
@PostMapping("queryParam") public String queryParam(QueryVo queryVo) {System.out.println(queryVo);return "user"; }🍀拓展知識
- VO(View Object): 視圖對象,用于展示層,它的作用是把某個指定頁面(或組件)的所有數據封裝起來。
- DTO(Data Transfer Object): 數據傳輸對象,這個概念來源于J2EE的設計模式,原來的目的是為了EJB的分布式應用提供粗粒度的數據實體,以減少分布式調用的次數,從而提高分布式調用的性能和降低網絡負載,但在這里,我泛指用于展示層與服務層之間的數據傳輸對象。
- DO(Domain Object): 領域對象,就是從現實世界中抽象出來的有形或無形的業務實體。
- PO(Persistent Object): 持久化對象,它跟持久層(通常是關系型數據庫)的數據結構形成一一對應的映射關系,如果持久層是關系型數據庫,那么,數據表中的每個字段(或若干個)就對應PO的一個(或若干個)屬性。
下面以一個時序圖建立簡單模型來描述上述對象在三層架構應用中的位置:
大致流程如下:
- 用戶發出請求(可能是填寫表單),表單的數據在展示層被匹配為VO;
- 展示層把VO轉換為服務層對應方法所要求的DTO,傳送給服務層;
- 服務層首先根據DTO的數據構造(或重建)一個DO,調用DO的業務方法完成具體業務;
- 服務層把DO轉換為持久層對應的PO(可以使用ORM工具,也可以不用),調用持久層的持久化方法,把PO傳遞給它,完成持久化操作;
- 數據傳輸順序:VO => DTO => DO => PO
相對來說越是靠近顯示層的概念越不穩定,復用度越低。分層的目的,就是復用和相對穩定性。
小知識: 一般的簡單工程中,并不會進行這樣的設計,我們可能有一個User類就可以了,并不需要什么VO、DO啥的。但是,隨著項目工程的復雜化,簡單的對象已經沒有辦法在各個層的使用,項目越是復雜,就需要越是復雜的設計方案,這樣才能滿足高擴展性和維護性。
六、設定字符集
springmvc內置了一個統一的字符集處理過濾器,我們只要在web.xml中配置即可:
<filter><filter-name>CharacterEncodingFilter</filter-name><filter-class>org.springframework.web.filter.CharacterEncodingFilter</filter-class><init-param><param-name>encoding</param-name><param-value>utf-8</param-value></init-param> </filter> <filter-mapping><filter-name>CharacterEncodingFilter</filter-name><url-pattern>/*</url-pattern> </filter-mapping>七、返回json數據(序列化)
我們經常需要使用ajax請求后臺獲取數據,而不需要訪問任何的頁面,這種場景在前后分離的項目當中尤其重要。
這種做法其實很簡單,大致步驟如下:
- 將我們的對象轉化為json字符串。
- 將返回的內容直接寫入響應體,不走視圖解析器。
- 然后將Content-Type設置為application/json即可。
為了實現這個目的,我們可以引入fastjson:
<dependency><groupId>com.alibaba</groupId><artifactId>fastjson</artifactId><version>1.2.68</version> </dependency> // produces指定了響應的Content-Type @RequestMapping(value = "getUsers",produces = {"application/json;charset=utf-8"}) @ResponseBody // 將返回的結果直接寫入響應體,不走視圖解析器 public String getUsers(){List<User> users = new ArrayList<User>(){{add(new User("Tom","2222"));add(new User("jerry","333"));}};return JSONArray.toJSONString(users); }測試: 成功!
注意:@ResponseBody能將返回的結果直接放在響應體中,不走視圖解析器。
瀏覽器中添加插件json viewer可以有如上顯示。
當然springmvc也考慮到了,每次這樣寫也其實挺麻煩,我們還可以向容器注入一個專門處理消息轉換的bean。
這個轉化器的作用就是:當不走視圖解析器時,如果發現【返回值是一個對象】,就會自動將返回值轉化為json字符序列:
<mvc:annotation-driven ><mvc:message-converters><bean id="fastjson" class="com.alibaba.fastjson.support.spring.FastJsonHttpMessageConverter"><property name="supportedMediaTypes"><list><!-- 這里順序不能反,一定先寫text/html,不然ie下會出現下載提示 --><value>text/html;charset=UTF-8</value><value>application/json;charset=UTF-8</value></list></property></bean></mvc:message-converters> </mvc:annotation-driven>以后我們的controller就可以寫成下邊的樣子了:
@RequestMapping(value = "getUsersList") @ResponseBody public List<User> getUsersList(){return new ArrayList<User>(){{add(new User("邸智偉","2222"));add(new User("劉展鵬","333"));}}; }當然我們還可以使用一個更加流行的組件jackson來處理,他的工作和fastjson一致
首先需要引入以下依賴:
<!--jackson--> <dependency><groupId>com.fasterxml.jackson.core</groupId><artifactId>jackson-core</artifactId> </dependency> <dependency><groupId>com.fasterxml.jackson.core</groupId><artifactId>jackson-annotations</artifactId> </dependency> <dependency><groupId>com.fasterxml.jackson.core</groupId><artifactId>jackson-databind</artifactId> </dependency>我們還可以對序列化的過程進行額外的一些配置:
public class CustomObjectMapper extends ObjectMapper {public CustomObjectMapper() {super();//去掉默認的時間戳格式configure(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS, false);//設置為東八區setTimeZone(TimeZone.getTimeZone("GMT+8"));//設置日期轉換yyyy-MM-dd HH:mm:sssetDateFormat(new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"));// 設置輸入:禁止把POJO中值為null的字段映射到json字符串中configure(SerializationFeature.WRITE_NULL_MAP_VALUES, false);// 空值不序列化setSerializationInclusion(JsonInclude.Include.NON_NULL);// 反序列化時,屬性不存在的兼容處理getDeserializationConfig().withoutFeatures(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES);// 序列化枚舉是以toString()來輸出,默認false,即默認以name()來輸出configure(SerializationFeature.WRITE_ENUMS_USING_TO_STRING, true);} }編寫配置文件:
<mvc:annotation-driven><mvc:message-converters><bean class="org.springframework.http.converter.json.MappingJackson2HttpMessageConverter"><!-- 自定義Jackson的objectMapper --><property name="objectMapper" ref="customObjectMapper" /><property name="supportedMediaTypes"><list><value>text/plain;charset=UTF-8</value><value>application/json;charset=UTF-8</value></list></property></bean></mvc:message-converters></mvc:annotation-driven> <!--注入我們寫的對jackson的配置的bean--> <bean name="customObjectMapper" class="com.ydlclass.CustomObjectMapper"/>測試: 成功!
八、獲取請求中的json數據
在前端發送的數據中可能會如如下情況,Contetn-Type是application/json,請求體中是json格式數據:
@RequestBody注解可以【直接獲取請求體的數據】。
如果我們配置了消息轉化器,消息轉化器會將請求體中的json數據反序列化成目標對象,如下所示:
@PostMapping("insertUser") public String insertUser(@RequestBody User user) {System.out.println(user);return "user"; }當然,我們可以把消息轉化器注解掉,直接使用一個String來接收請求體的內容。
九、數據轉化
假如有如下場景,前端傳遞過來一個日期字符串,但是后端需要使用Date類型進行接收,這時就需要一個類型轉化器進行轉化。
自定義的類型轉化器只支持從requestParam獲取的參數進行轉化,我們可以定義如下,其實學習spring時我們已經接觸過這個Converter接口:
public class StringToDateConverter implements Converter<String, Date> {@Overridepublic Date convert(String source) {SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy_MM_dd hh,mm,ss");try {return simpleDateFormat.parse(source);} catch (ParseException e) {e.printStackTrace();}return null;} }然后,我們需要在配置文件中進行配置:
<!-- 開啟mvc的注解 --> <mvc:annotation-driven conversion-service="conversionService" /><bean id="conversionService" class="org.springframework.context.support.ConversionServiceFactoryBean"><property name="converters"><set><bean id="stringToDateConverter" class="cn.itnanls.convertors.StringToDateConverter"/></set></property> </bean>對于時間類型的處理,springmvc給我們提供了一個比較完善的解決方案,使用注解@DateTimeFormat,同時配合jackson提供的@JsonFormat注解幾乎可以滿足我們的所有需求。
- @DateTimeFormat:當從requestParam中獲取string參數并需要轉化為Date類型時,會根據此注解的參數pattern的格式進行轉化。
- @JsonFormat:當從請求體中獲取json字符序列,需要反序列化為對象時,時間類型會按照這個注解的屬性內容進行處理。
這兩個注解需要加在實體類的對應字段上即可:
// 對象和json互相轉化的過程當中按照此轉化方式轉哈 @JsonFormat(pattern = "yyyy年MM月dd日",timezone = "GMT-8") // 從requestParam中獲取參數并且轉化 @DateTimeFormat(pattern = "yyyy年MM月dd日") private Date birthday;處理的過程大致如下:
十、數據校驗
- JSR 303 是 Java 為 Bean 數據合法性校驗提供的標準框架,它包含在 JavaEE 6.0 中。
- JSR 303 通過在 Bean 屬性上標注類似于 @NotNull、@Max 等標準的注解指定校驗規則,并通過標準的驗證接口對 Bean
進行驗證。
| @Null | 被注解的元素必須為 null |
| @NotNull | 被注解的元素必須不為 null |
| @AssertTrue | 被注解的元素必須為 true |
| @AssertFalse | 被注解的元素必須為 false |
| @Min(value) | 被注解的元素必須是一個數字,其值必須大于等于指定的最小值 |
| @Max(value) | 被注解的元素必須是一個數字,其值必須小于等于指定的最大值 |
| @DecimalMin(value) | 被注解的元素必須是一個數字,其值必須大于等于指定的最小值 |
| @DecimalMax(value) | 被注解的元素必須是一個數字,其值必須小于等于指定的最大值 |
| @Size(max, min) | 被注解的元素的大小必須在指定的范圍內 |
| @Digits (integer, fraction) | 被注解的元素必須是一個數字,其值必須在可接受的范圍內 |
| @Past | 被注解的元素必須是一個過去的日期 |
| @Future | 被注解的元素必須是一個將來的日期 |
| @Pattern(value) | 被注解的元素必須符合指定的正則表達式 |
🍀Hibernate Validator 擴展注解
Hibernate Validator 是 JSR 303 的一個參考實現,除支持所有標準的校驗注解外,它還支持以下的擴展注解(Hibernate Validator 附加的 constraint):
| 被注解的元素必須是電子郵箱地址 | |
| @Length | 被注解的字符串的大小必須在指定的范圍內 |
| @NotEmpty | 被注解的字符串的必須非空 |
| @Range | 被注解的元素必須在合適的范圍內 |
🍀Spring MVC 數據校驗
Spring MVC 可以對表單參數進行校驗,并將結果保存到對應的【BindingResult】或 【Errors 】對象中。
要實現數據校驗,需要引入已下依賴:
<dependency><groupId>javax.validation</groupId><artifactId>validation-api</artifactId><version>2.0.1.Final</version> </dependency> <dependency><groupId>org.hibernate</groupId><artifactId>hibernate-validator</artifactId><version>6.0.9.Final</version> </dependency>并在實體類加上特定注解:
@Data @AllArgsConstructor @NoArgsConstructor public class UserVO {@NotNull(message = "用戶名不能為空")private String username;@NotNull(message = "用戶名不能為空")private String password;@Min(value = 0, message = "年齡不能小于{value}")@Max(value = 120,message = "年齡不能大于{value}")private int age;@JsonFormat(pattern = "yyyy-MM-dd",timezone = "GMT-8")@DateTimeFormat(pattern = "yyyy-MM-dd")@Past(message = "生日不能大于今天")private Date birthday;@Pattern(regexp = "^1([358][0-9]|4[579]|66|7[0135678]|9[89])[0-9]{8}$", message = "手機號碼不正確")private String phone;@Emailprivate String email; }在配置文件中配置如下內容,增加hibernate校驗:
<bean id="localValidator" class="org.springframework.validation.beanvalidation.LocalValidatorFactoryBean"><property name="providerClass" value="org.hibernate.validator.HibernateValidator"/> </bean> <!--注冊注解驅動--> <mvc:annotation-driven validator="localValidator"/>controller使用@Validated標識驗證的對象,緊跟著的BindingResult獲取錯誤信息
@PostMapping("insert") public String insert(@Validated UserVO user, BindingResult br) {List<ObjectError> allErrors = br.getAllErrors();Iterator<ObjectError> iterator = allErrors.iterator();// 打印以下錯誤結果while (iterator.hasNext()){ObjectError error = iterator.next();log.error("user數據校驗錯誤:{}",error.getDefaultMessage());}if(allErrors.size() > 0){return "error";}System.out.println(user);return "user"; }注意: 永遠不要相信用戶的輸入,我們開發的系統凡是涉及到用戶輸入的地方,都要進行校驗,這里的校驗分為前臺校驗和后臺校驗,前臺校驗通常由javascript來完成,后臺校驗主要由java來負責,這里我們可以通過spring mvc+hibernate validator完成。
十一、視圖解析器詳解
我們默認的視圖解析器是如下的配置,它主要是處理jsp頁面的映射渲染:
<bean class="org.springframework.web.servlet.view.InternalResourceViewResolver" id="internalResourceViewResolver"><!-- 前綴 --><property name="prefix" value="/WEB-INF/page/" /><!-- 后綴 --><property name="suffix" value=".jsp" /> </bean>如果我們想添加新的視圖解析器,則需要給舊的新增一個order屬性,或者直接刪除原有的視圖解析器:
<bean class="org.springframework.web.servlet.view.InternalResourceViewResolver" id="internalResourceViewResolver"><!-- 前綴 --><property name="prefix" value="/WEB-INF/page/" /><!-- 后綴 --><property name="suffix" value=".jsp" /><property name="order" value="10"/> </bean>- 這里的order表示視圖解析的【優先級】,數字越小優先級越大(即:0為優先級最高,所以優先進行處理視圖),InternalResourceViewResolver在項目中的優先級一般要設置為最低,也就是order要最大。不然它會影響其他視圖解析器。
- 當處理器返回邏輯視圖時(也就是return “string”),要經過視圖解析器鏈,如果前面的解析器能處理,就不會繼續往下傳播。如果不能處理就要沿著解析器鏈繼續尋找,直到找到合適的視圖解析器。
如下圖所示:
然后,我們可以配置一個新的Tymeleaf視圖解析器,order設置的低一些,這樣兩個視圖解析器都可以生效:
添加兩個相關依賴:
<dependency><groupId>org.thymeleaf</groupId><artifactId>thymeleaf</artifactId><version>3.0.14.RELEASE</version> </dependency> <!-- https://mvnrepository.com/artifact/org.thymeleaf/thymeleaf-spring4 --> <dependency><groupId>org.thymeleaf</groupId><artifactId>thymeleaf-spring4</artifactId><version>3.0.14.RELEASE</version> </dependency>模板中需要添加對應的命名空間:
<html xmlns:th="http://www.thymeleaf.org" >thymeleaf官網地址:https://www.thymeleaf.org/
十二、全局異常捕獲
🍀(1)HandlerExceptionResolver
在Java中,對于異常的處理一般有兩種方式:
- 一種是當前方法捕獲處理(try-catch),這種處理方式會造成業務代碼和異常處理代碼的耦合。
- 另一種是自己不處理,而是拋給調用者處理(throws),調用者再拋給它的調用者,也就是一直向上拋,指導傳遞給瀏覽器。
被異常填充的頁面是長這個樣子的:
在這種方法的基礎上,衍生出了SpringMVC的異常處理機制。系統的dao、service、controller都通過throws Exception向上拋出,最后由springmvc前端控制器交由異常處理器進行異常處理,如下圖:
小知識: service層盡量不要處理異常,如果自己捕獲并處理了,異常就不生效了。特別是不要生吞異常。
Spring MVC的Controller出現異常的默認處理是響應一個500狀態碼,再把錯誤信息顯示在頁面上,如果用戶看到這樣的頁面,一定會覺得你這個網站太LOW了。
要解決Controller的異常問題,當然也不能在每個處理請求的方法中加上異常處理,那樣太繁瑣了。
通過源碼我們得知,需要寫一個HandlerExceptionResolver,并實現其方法:
public class GlobalExceptionResolver implements HandlerExceptionResolver {@Overridepublic ModelAndView resolveException(HttpServletRequest request,HttpServletResponse response, Object handler, Exception ex) {ModelAndView modelAndView = new ModelAndView();modelAndView.addObject("error", ex.getMessage());modelAndView.setViewName("error");return modelAndView;} } <bean id="globalExecptionResovler" class="com.lagou.exception.GlobalExecptionResovler"></bean> @Component public class GlobalExecptionResovler implements HandlerExceptionResolver {}小知識: 在web中我們也能對異常進行統一處理:
<!--處理500異常--> <error-page><error-code>500</error-code><location>/500.jsp</location> </error-page> <!--處理404異常--> <error-page><error-code>404</error-code><location>/404.jsp</location> </error-page>🍀(2)@ControllerAdvice
該注解同樣能實現異常的全局統一處理,而且實現起來更加簡單優雅,當然使用這個注解有一下三個功能:
- 處理全局異常
- 預設全局數據
- 請求參數預處理
我們主要學習其中的全局異常處理,@ControllerAdvice 配合 @ExceptionHandler 實現全局異常處理:
@Slf4j @ControllerAdvice public class GlobalExceptionResolverController {@ExceptionHandler(ArithmeticException.class)public String processArithmeticException(ArithmeticException ex){log.error("發生了數學類的異常:",ex);return "error";}@ExceptionHandler(BusinessException.class)public String processBusinessException(BusinessException ex){log.error("發生了業務相關的異常:",ex);return "error";}@ExceptionHandler(Exception.class)public String processException(Exception ex){log.error("發生了其他的異常:",ex);return "error";} }十三、處理資源
當我們使用了springmvc后,所有的請求都會交給springmvc進行管理,當然也包括靜態資源,比如/static/js/index.js,這樣的請求如果走了中央處理器,必然會拋出異常,因為沒有與之對應的controller,這樣我們可以使用一下配置進行處理:
<mvc:resources mapping="/js/**" location="/static/js/"/> <mvc:resources mapping="/css/**" location="/static/css/"/> <mvc:resources mapping="/img/**" location="/static/img/"/>十四、攔截器
- (1)SpringMVC提供的攔截器類似于JavaWeb中的過濾器,只不過SpringMVC攔截器只攔截被前端控制器攔截的請求,而過濾器攔截從前端發送的【任意】請求。
- (2)熟練掌握SpringMVC攔截器對于我們開發非常有幫助,在沒使用權限框架(shiro,spring security)之前,一般使用攔截器進行認證和授權操作。
- (3)SpringMVC攔截器有許多應用場景,比如:登錄認證攔截器,字符過濾攔截器,日志操作攔截器等等。
🍀(1)自定義攔截器
SpringMVC攔截器的實現一般有兩種方式:
- (1)自定義的Interceptor類要實現了Spring的HandlerInterceptor接口。
- (2)繼承實現了HandlerInterceptor接口的類,比如Spring已經提供的實現了HandlerInterceptor接口的抽象類HandlerInterceptorAdapter。
🍀(2)攔截器攔截流程
🍀(3)攔截器規則
我們可以配置多個攔截器,每個攔截器中都有三個方法。下面將總結多個攔截器中的方法執行規律。
- preHandle: Controller方法處理請求前執行,根據攔截器定義的順序,正向執行。
- postHandle: Controller方法處理請求后執行,根據攔截器定義的順序,逆向執行。需要所有的preHandle方法都返回true時才會調用。
- afterCompletion: View視圖渲染后處理方法:根據攔截器定義的順序,逆向執行。preHandle返回true也會調用。
🍀(4)登錄攔截器
接下來編寫一個登錄攔截器,這個攔截器可以實現認證操作。就是當我們還沒有登錄的時候,如果發送請求訪問我們系統資源時,攔截器不放行,請求失敗。只有登錄成功后,攔截器放行,請求成功。登錄攔截器只要在preHandle()方法中編寫認證邏輯即可,因為是在請求執行前攔截。代碼實現如下:
/*** 登錄攔截器*/ public class LoginInterceptor implements HandlerInterceptor {/**在執行Controller方法前攔截,判斷用戶是否已經登錄,登錄了就放行,還沒登錄就重定向到登錄頁面*/@Overridepublic boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {HttpSession session = request.getSession();User user = session.getAttribute("user");if (user == null){//還沒登錄,重定向到登錄頁面response.sendRedirect("/toLogin");}else {//已經登錄,放行return true;}}@Overridepublic void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) {}@Overridepublic void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) {} }編寫完SpringMVC攔截器,我們還需要在springmvc.xml配置文件中,配置我們編寫的攔截器,配置代碼如下:
- 配置需要攔截的路徑
- 配置不需要攔截的路徑
- 配置我們自定義的攔截器類
十五、全局配置類
springmvc有一個可用作用于做全局配置的接口,這個接口是WebMvcConfigurer,在這個接口中有很多默認方法,每一個默認方法都可以進行一項全局配置,這些配置可以和我們配置文件的配置一一對應:這些配置在全局的xml中也可以進行配置。
🍀列舉幾個xml的配置
<!--處理靜態資源--> <mvc:resources mapping="/js/**" location="/static/js/"/> <mvc:resources mapping="/css/**" location="/static/css/"/> <mvc:resources mapping="/./image/**" location="/static/./image/"/><!--配置頁面跳轉--> <mvc:view-controller path="/toGoods" view-name="goods"/> <mvc:view-controller path="/toUpload" view-name="upload"/> <mvc:view-controller path="/websocket" view-name="websocket"/><mvc:cors><mvc:mapping path="/goods/**" allowed-methods="*"/> </mvc:cors>🍀列舉幾個常用的WebMvcConfigurer的配置
@Configuration @EnableWebMvc public class MvcConfiguration implements WebMvcConfigurer {// 攔截器進行配置@Overridepublic void addInterceptors(InterceptorRegistry registry) {registry.addInterceptor(new LoginInterceptor()).addPathPatterns("/**").excludePathPatterns(List.of("/toLogin","/login")).order(1);}// 資源的配置@Overridepublic void addResourceHandlers(ResourceHandlerRegistry registry) {registry.addResourceHandler("/js/**").addResourceLocations("/static/js/");registry.addResourceHandler("/css/**").addResourceLocations("/static/css/");}// 跨域的全局配置@Overridepublic void addCorsMappings(CorsRegistry registry) {registry.addMapping("/api/**").allowedOrigins("*").allowedMethods("GET","POST","PUT","DELETE").maxAge(3600);}// 頁面跳轉的配置@Overridepublic void addViewControllers(ViewControllerRegistry registry) {registry.addViewController("/index").setViewName("index");}}后記
👉Java全棧學習路線可參考:【Java全棧學習路線】最全的Java學習路線及知識清單,Java自學方向指引,內含最全Java全棧學習技術清單~
👉算法刷題路線可參考:算法刷題路線總結與相關資料分享,內含最詳盡的算法刷題路線指南及相關資料分享~
總結
以上是生活随笔為你收集整理的最全面的SpringMVC教程(二)——SpringMVC核心技术篇的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 关于已上发布app,升级admob后,激
- 下一篇: DOS命令:cls