Spring基礎知識(18)- Spring MVC (八)
攔截器(Interceptor)、REST 風格
1. 攔截器(Interceptor)
在系統中,經常需要在處理使用者請求之前和之後執行一些行為,例如檢測使用者的許可權,或者將請求的資訊記錄到日誌中,即平時所說的 “許可權檢測” 及 “日誌記錄”。當然不僅僅這些,所以需要一種機制,攔截使用者的請求,在請求的前後新增處理邏輯。
Spring MVC 提供了 Interceptor 攔截器機制,用於請求的預處理和後處理。
Spring MVC 的攔截器(Interceptor)與 Java Servlet 的過濾器(Filter)類似,它主要用於攔截使用者的請求並做相應的處理,通常應用在許可權驗證、記錄請求資訊的日誌、判斷使用者是否登入等功能上。
1) 攔截器的定義
在 Spring MVC 框架中定義一個攔截器需要對攔截器進行定義和配置,主要有以下 2 種方式。
(1) 通過實現 HandlerInterceptor 介面或繼承 HandlerInterceptor 介面的實現類(例如 HandlerInterceptorAdapter)來定義;
(2) 通過實現 WebRequestInterceptor 介面或繼承 WebRequestInterceptor 介面的實現類來定義。
本節以實現 HandlerInterceptor 介面的定義方式為例講解自定義攔截器的使用方法。示例程式碼如下。
1 package com.example.interceptor; 2 3 import javax.servlet.http.HttpServletRequest; 4 import javax.servlet.http.HttpServletResponse; 5 import org.springframework.web.servlet.HandlerInterceptor; 6 import org.springframework.web.servlet.ModelAndView;7 8 public class TestInterceptor implements HandlerInterceptor { 9 10 @Override 11 public boolean preHandle(HttpServletRequest request, 12 HttpServletResponse response, Object handler) throws Exception { 13 // preHandle() 方法在控制器的處理請求方法呼叫之前執行 14 System.out.println("TestInterceptor -> preHandle()"); 15 return false; 16 } 17 18 @Override 19 public void postHandle(HttpServletRequest request, 20 HttpServletResponse response, Object handler, 21 ModelAndView modelAndView) throws Exception { 22 // postHandle() 方法在控制器的處理請求方法呼叫之後,解析檢視之前執行 23 System.out.println("TestInterceptor -> postHandle()"); 24 } 25 26 @Override 27 public void afterCompletion(HttpServletRequest request, 28 HttpServletResponse response, Object handler, Exception e) 29 throws Exception { 30 // afterCompletion() 方法在控制器的處理請求方法執行完成後執行,即檢視渲染結束之後執行 31 System.out.println("TestInterceptor -> afterCompletion()"); 32 } 33 }
上述攔截器的定義中實現了 HandlerInterceptor 介面,並實現了介面中的 3 個方法,說明如下。
(1) preHandle( ):該方法在控制器的處理請求方法前執行,其返回值表示是否中斷後續操作,返回 true 表示繼續向下執行,返回 false 表示中斷後續操作;
(2) postHandle( ):該方法在控制器的處理請求方法呼叫之後、解析檢視之前執行,可以通過此方法對請求域中的模型和檢視做進一步的修改;
(3) afterCompletion( ):該方法在控制器的處理請求方法執行完成後執行,即檢視渲染結束後執行,可以通過此方法實現一些資源清理、記錄日誌資訊等工作;
2) 攔截器的配置
讓自定義的攔截器生效需要在 Spring MVC 的配置檔案中進行配置,配置示例程式碼如下:
1 <!-- 配置攔截器 --> 2 <mvc:interceptors> 3 <!-- 配置一個全域性攔截器,攔截所有請求 --> 4 <bean class="com.example.interceptor.TestInterceptor" /> 5 6 <mvc:interceptor> 7 <!-- 配置攔截器作用的路徑 --> 8 <mvc:mapping path="/**" /> 9 <!-- 配置不需要攔截作用的路徑 --> 10 <mvc:exclude-mapping path="" /> 11 12 <!-- 定義 <mvc:interceptor> 元素中,表示匹配指定路徑的請求才進行攔截 --> 13 <bean class="com.example.interceptor.Interceptor1" /> 14 </mvc:interceptor> 15 16 <mvc:interceptor> 17 <!-- 配置攔截器作用的路徑 --> 18 <mvc:mapping path="/test/interceptor" /> 19 20 <!-- 定義在 <mvc:interceptor> 元素中,表示匹配指定路徑的請求才進行攔截 --> 21 <bean class="com.example.interceptor.Interceptor2" /> 22 </mvc:interceptor> 23 </mvc:interceptors>
在上述示例程式碼中,元素說明如下。
<mvc:interceptors>:該元素用於配置一組攔截器。
<bean>:該元素是 <mvc:interceptors> 的子元素,用於定義全域性攔截器,即攔截所有的請求。
<mvc:interceptor>:該元素用於定義指定路徑的攔截器。
<mvc:mapping>:該元素是 <mvc:interceptor> 的子元素,用於配置攔截器作用的路徑,該路徑在其屬性 path 中定義。path 的屬性值為/**時,表示攔截所有路徑,值為/gotoTest時,表示攔截所有以/gotoTest結尾的路徑。如果在請求路徑中包含不需要攔截的內容,可以通過 <mvc:exclude-mapping> 子元素進行配置。
需要注意的是,<mvc:interceptor> 元素的子元素必須按照 <mvc:mapping.../>、<mvc:exclude-mapping.../>、<bean.../> 的順序配置。
示例
下面通過攔截器來完成一個使用者登入許可權驗證的 Web 應用。
在 “Spring基礎知識(12)- Spring MVC (二)” 的示例裡,更新過 springmvc-beans.xml 的 SpringmvcBasic 專案基礎上,修改如下。
(1) 建立 src/main/java/com/example/entity/User.java 檔案
1 package com.example.entity; 2 3 public class User { 4 private int id; 5 private String username; 6 private String password; 7 8 public User() { 9 10 } 11 12 public int getId() { 13 return id; 14 } 15 16 public void setId(int id) { 17 this.id = id; 18 } 19 20 public String getUsername() { 21 return this.username; 22 } 23 24 public void setUsername(String username) { 25 this.username = username; 26 } 27 28 public String getPassword() { 29 return password; 30 } 31 32 public void setPassword(String password) { 33 this.password = password; 34 } 35 }
(2) View
建立 src/main/webapp/WEB-INF/jsp/auth.jsp 檔案
1 <%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%> 2 <html> 3 <head> 4 <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"> 5 <title>Auth</title> 6 </head> 7 <body> 8 <h4>Auth Page</h4> 9 <p>Message: ${message}</p> 10 <p> </p> 11 12 <form action="${pageContext.request.contextPath }/user/auth/post" method="POST"> 13 <p>Username: <input type="text" name="username" /></p> 14 <p>Password: <input type="password" name="password" /></p> 15 <p><input type="submit" value="Submit" /></p> 16 </form> 17 </body> 18 </html>
建立 src/main/webapp/WEB-INF/jsp/main.jsp 檔案
1 <%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%> 2 <html> 3 <head> 4 <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"> 5 <title>Main</title> 6 </head> 7 <body> 8 <p>Welcome ${user.username }</p> 9 <p><a href="${pageContext.request.contextPath }/user/logout">Exit</a></p> 10 </body> 11 </html>
(3) 建立 src/main/java/com/example/interceptor/LoginInterceptor.java
1 package com.example.interceptor; 2 3 import javax.servlet.http.HttpServletRequest; 4 import javax.servlet.http.HttpServletResponse; 5 import javax.servlet.http.HttpSession; 6 import org.springframework.web.servlet.HandlerInterceptor; 7 import org.springframework.web.servlet.ModelAndView; 8 9 public class LoginInterceptor implements HandlerInterceptor { 10 11 @Override 12 public boolean preHandle(HttpServletRequest request, HttpServletResponse response, 13 Object handler) throws Exception { 14 System.out.println("TestInterceptor -> preHandle()"); 15 16 // URL 17 String url = request.getRequestURI(); 18 if (url.indexOf("/user/auth") >= 0 || url.indexOf("/user/auth/post") >= 0) { 19 return true; 20 } 21 22 // Session 23 HttpSession session = request.getSession(); 24 Object obj = session.getAttribute("user"); 25 if (obj != null) 26 return true; 27 28 // Check auth 29 request.setAttribute("message", "Not authorized"); 30 request.getRequestDispatcher("/user/auth").forward(request, response); 31 32 return false; 33 } 34 35 @Override 36 public void postHandle(HttpServletRequest request, 37 HttpServletResponse response, Object handler, 38 ModelAndView modelAndView) throws Exception { 39 System.out.println("TestInterceptor -> postHandle()"); 40 } 41 42 @Override 43 public void afterCompletion(HttpServletRequest request, HttpServletResponse response, 44 Object object, Exception e) throws Exception { 45 System.out.println("TestInterceptor -> afterCompletion()"); 46 } 47 48 }
(4) 修改 springmvc-beans.xml 檔案,新增如下配置
1 <!-- 配置攔截器 --> 2 <mvc:interceptors> 3 <mvc:interceptor> 4 <!-- 配置攔截器作用的路徑 --> 5 <mvc:mapping path="/**" /> 6 <bean class="com.example.interceptor.LoginInterceptor" /> 7 </mvc:interceptor> 8 </mvc:interceptors>
(5) 建立 src/main/java/com/example/controller/UserController.java 檔案
1 package com.example.controller; 2 3 import javax.servlet.http.HttpSession; 4 import org.springframework.stereotype.Controller; 5 import org.springframework.ui.Model; 6 import org.springframework.web.bind.annotation.RequestMapping; 7 import com.example.entity.User; 8 9 @Controller 10 @RequestMapping("/user") 11 public class UserController { 12 13 @RequestMapping("/auth") 14 public String auth() { 15 return "auth"; 16 } 17 18 @RequestMapping("/auth/post") 19 public String authPost(User user, Model model, HttpSession session) { 20 if ("admin".equals(user.getUsername()) && "123456".equals(user.getPassword())) { 21 session.setAttribute("user", user); 22 return "redirect:/user/main"; 23 } 24 model.addAttribute("message", "Invalid username or password"); 25 return "auth"; 26 } 27 28 @RequestMapping("/main") 29 public String main() { 30 return "main"; 31 } 32 33 @RequestMapping("/logout") 34 public String logout(HttpSession session) { 35 session.invalidate(); 36 return "auth"; 37 } 38 }
訪問:http://localhost:9090/user/main
2. REST 風格
REST(Representational State Transfer)即表述性轉移,是目前最流行的一種軟體架構風格。它結構清晰、易於理解、有較好的擴充套件性。
Spring REST 風格可以簡單理解為:使用 URL 表示資源時,每個資源都用一個獨一無二的 URL 來表示,並使用 HTTP 方法表示操作,即準確描述伺服器對資源的處理動作(GET、POST、PUT、DELETE),實現資源的增刪改查。
GET:表示獲取資源
POST:表示新建資源
PUT:表示更新資源
DELETE:表示刪除資源
下面舉例說明 REST 風格的 URL 與傳統 URL 的區別。
/user/view.jsp?id=3 VS /user/view/3
/user/delete.jsp?id=3 VS /user/delete/3
/user/modify.jsp?id=3 VS /user/modify/3
REST 風格的 URL 中最明顯的就是引數不再使用 “?” 傳遞。這種風格的 URL 可讀性更好,使得專案架構清晰,最關鍵的是 Spring MVC 也提供對這種風格的支援。
REST 風格在開發 API(不需要view)返回 JSON 或 XML 的介面中,一般使用 @RestController (或 &Controller + @ResponseBody),@RestController (或 &Controller + @ResponseBody) 支援 GET、POST、PUT、DELETE 等動作。
在處理 Form 表單方式的開發中,由於 Form 的 method 預設不支援 PUT 和 DELETE 請求,所以需要將 DELETE 和 PUT 請求轉換成 POST 請求,在 web.xml 中配置過濾器 HiddenHttpMethodFilter。
本文只討論 Form 表單方式的 REST 風格, @RestController (或 &Controller + @ResponseBody) 可以參考 “Spring基礎知識(17)- Spring MVC (七)” 的 “JSON 資料互動”。
示例
在 “Spring基礎知識(12)- Spring MVC (二)” 的示例裡,更新過 springmvc-beans.xml 的 SpringmvcBasic 專案基礎上,修改如下。
(1) 建立 src/main/webapp/WEB-INF/jsp/rest.jsp 檔案
1 <%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%> 2 <html> 3 <head> 4 <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"> 5 <title> REST Style</title> 6 </head> 7 <body> 8 9 <h4>傳送 GET 請求</h4> 10 <p><a href="/user/rest/1">GET</a></p> 11 12 <hr /> 13 <h4>傳送 POST 請求</h4> 14 <form action="/user/rest/1" method="POST"> 15 <p><input type="submit" value="POST" /></p> 16 </form> 17 18 <hr /> 19 <h4>傳送 PUT 請求</h4> 20 <form action="/user/rest/1" method="POST"> 21 <input type="hidden" name="_method" value="PUT" /> 22 <p><input type="submit" value="PUT" /></p> 23 </form> 24 25 <hr /> 26 <h4>傳送 DELETE 請求</h4> 27 <form action="/user/rest/1" method="POST"> 28 <input type="hidden" name="_method" value="DELETE" /> 29 </p><input type="submit" value="DELETE" /></p> 30 </form> 31 32 </body> 33 </html>
(2) 在 web.xml 檔案中新增以下程式碼
1 <!-- 將 POST 請求轉化為 PUT 請求和 DELETE 請求 --> 2 <filter> 3 <filter-name>hiddenHttpMethodFilter</filter-name> 4 <filter-class>org.springframework.web.filter.HiddenHttpMethodFilter</filter-class> 5 </filter> 6 <filter-mapping> 7 <filter-name>hiddenHttpMethodFilter</filter-name> 8 <url-pattern>/*</url-pattern> 9 </filter-mapping>
(3) 建立 src/main/java/com/example/controller/UserController.java 檔案
1 package com.example.controller; 2 3 import org.springframework.ui.Model; 4 import org.springframework.stereotype.Controller; 5 import org.springframework.web.bind.annotation.PathVariable; 6 import org.springframework.web.bind.annotation.RequestMapping; 7 import org.springframework.web.bind.annotation.RequestMethod; 8 9 @Controller 10 @RequestMapping("/user") 11 public class UserController { 12 @RequestMapping("/rest") 13 public String rest() { 14 return "rest"; 15 } 16 17 @RequestMapping(value = "/rest/{id}", method = RequestMethod.GET) 18 public String restGet(@PathVariable Integer id, Model model) { 19 System.out.println("UserController -> restGet(): id = " + id); 20 model.addAttribute("message", "GET " + id); 21 return "success"; 22 } 23 24 @RequestMapping(value = "/rest/{id}", method = RequestMethod.POST) 25 public String restPost(@PathVariable Integer id, Model model) { 26 System.out.println("UserController -> restPost(): id = " + id); 27 model.addAttribute("message", "POST " + id); 28 return "success"; 29 } 30 31 @RequestMapping(value = "/user/{id}", method = RequestMethod.PUT) 32 public String restPut(@PathVariable Integer id, Model model) { 33 System.out.println("UserController -> restPut(): id = " + id); 34 model.addAttribute("message", "PUT " + id); 35 return "success"; 36 } 37 38 @RequestMapping(value = "/rest/{id}", method = RequestMethod.DELETE) 39 public String restDelete(@PathVariable Integer id, Model model) { 40 System.out.println("UserController -> restDelete(): id = " + id); 41 model.addAttribute("message", "DELETE " + id); 42 return "success"; 43 } 44 45 }
訪問:http://localhost:9090/user/rest