Spring Boot 2.0 讀書筆記_04:MVC 下
2. MVC 下
-
驗證框架
關於驗證框架,之前很少用到, 在前端傳遞的引數中,前端框架已經存在一些驗證策略。比如:型別監測、長度監測、日期正則判斷等。因此在後端Controller層中的校驗就很少用到。但實際情況也可能存在有些惡意程式碼繞過前端驗證,直接向後端傳送請求這樣的事情發生,因此後端的驗證框架的存在也是做了二次驗證,防止惡意的請求產生。- JSR-303
- JSR-303是Java標準的驗證框架,已有的實現有 Hibernate Validator
- JSR-303定義一系列的註解來驗證Bean的屬性,如:
-
空檢查
- @Null,驗證物件是否為空
- @NotNull,驗證物件不為空
- @NotBlank,驗證字串不為空或不是空字串,即:"" 和 " " 都會驗證失敗
- @NotEmpty,驗證物件不為null,或者集合不為空
-
長度檢查
- @Size(min= , max= ),驗證物件長度,可支援字串、集合
- @Length,字串長度
-
數值監測
- @Min,驗證數字是否大於等於指定的值
- @Max,驗證數字是否小於等於指定的值
- @Digits,驗證數字是否符合指定格式,如:@Digits(integer=9, fraction=2)
- @Range,驗證數字是否在指定的範圍內,如:@Range(min=1, max=1000)
-
其他
- @Email,驗證是否為郵件格式,為null則不做校驗,已過期
- @Pattern,驗證String物件是否符合正則表示式規則
-
舉個栗子
public class UserInfo { @NotNull Long id; @Size(min=3, max=20) String name; }
-
- Group
-
通常,不同的業務邏輯會有不同的驗證邏輯。比如上述例子,當UserInfo更新的時候,id欄位不能為null;當UserInfo新增的時候,id欄位必須為null;
-
JSR-303中定義了group概念,每個校驗註解都必須支援。校驗註解作用在欄位上的時候,可以指定一個或多個group,當 Spring Boot 校驗物件的時候,也可以指定校驗的上下文屬於某一個group。這樣,只有group匹配的時候,校驗註解的作用才能生效。 改寫上述例子:
public class UserInfo { // 更新校驗組 public interface Update{} // 新增校驗組 public interface Add{} @NotNull(groups={Update.class}) @Null(groups={Add.class}) Long id; }
上述程式碼表示:
當校驗上下文為 Add.class 的時候,@Null 生效,id需為空才能校驗通過;
當校驗上下文為 Update.class 的時候,@NotNull 生效,id不能為空;
-
- @Validated
-
在Controller中,只需要給方法引數新增 @Validated 即可觸發引數校驗,比如:
@PostMapping("/addUserInfo") @ResponseBody public void addUserInfo(@Validated({UserInfo.Add.class}) UserInfo userInfo, BindingResult result) { ... }
此方法可以接受HTTP引數並對映到UserInfo物件,該引數使用了 @Validated 註解,將觸發 Spring 的校驗,並將驗證結果存放到 BindingResult 物件中。
@Validated 註解使用了分組後的新增校驗組 UserInfo.Add.class ,因此,整個校驗按照 Add.class 來校驗。 -
BindingResult
- BindingResult 包含驗證結果,並提供以下方法:
- hasErrors,判斷驗證是否通過;
- getAllErrors,獲取所有的錯誤資訊,通常返回的是 FieldError 列表
- 如果Controller引數未提供BindingResult物件,則Spring MVC 將丟擲異常。
- BindingResult 包含驗證結果,並提供以下方法:
-
- 自定義校驗
- 關於自定義校驗,說白了就是自定義註解去構建一套驗證邏輯。
- 關於自定義註解,AOP這篇文章中介紹到了。詳情請移步:AOP
- 這裡簡單分析下案例中的自定義註解:@WorkOverTime
-
註解介面:關鍵是@Constraint註解,宣告註解實現類
@Constraint(validatedBy = { WorkOverTimeValidator.class }) @Documented @Target({ ElementType.ANNOTATION_TYPE, ElementType.METHOD, ElementType.FIELD }) @Retention(RetentionPolicy.RUNTIME) public @interface WorkOverTime { String message() default "加班時間過長,不能超過{max}"; int max() default 4; Class<?>[] groups() default {}; Class<? extends Payload>[] payload() default {}; }
@Documented,宣告需要加入JavaDoc
@Target,描述註解使用範圍,這裡是分別描述了:類、介面或列舉;方法;域
@Retention,描述註解宣告週期,這裡是描述了:執行時註解有效
引數方面:- 常規引數:message(String)、max(int)
- 其他引數:
- groups:驗證規則分組
- payload:驗證有效負荷
-
註解實現類
-
註解實現類必須實現 ConstraintValidator 介面 initialize 方法及驗證方法 isValid
public class WorkOverTimeValidator implements ConstraintValidator<WorkOverTime, Integer> { WorkOverTime work; int max; public void initialize(WorkOverTime work) { // 獲取註解定義 this.work = work; max = work.max(); } public boolean isValid(Integer value, ConstraintValidatorContext context) { // 校驗邏輯 if (value == null) { return true; } return value < max; } }
-
-
- JSR-303
-
WebMvcConfigurer:實現 WebMvcConfigurer 介面,即可配置應用的MVC全域性特性
-
實現 WebMvcConfigurer 介面,會看到有以下方法可以實現
-
攔截器:通過 addInterceptors 方法可以設定多個攔截器,實現對URL攔截檢查使用者登入狀態等操作
public void addInterceptors(InterceptorRegistry registry) { // 新增一個攔截器,檢查會話,URL以user開頭的都是用此攔截器 registry.addInterceptor(new SessionHandlerInterceptor()).addPathPatterns("/user/**"); }
SessionHandlerInterceptor 會話處理攔截器,實現了 HandlerInterceptor 介面
需要注意的是:攔截器有以下三個方法需要覆蓋實現-
preHandle,在呼叫 Controller 方法前會呼叫此方法
-
postHandle,在呼叫 Controller 方法結束後、頁面渲染之前呼叫此方法
-
afterCompletion,在頁面渲染完畢後呼叫此方法
具體程式碼實現/** * 檢查使用者是否已經登入,如果未登入,重定向到登入頁面 */ class SessionHandlerInterceptor implements HandlerInterceptor { // 呼叫Controller方法前會進行呼叫 public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { User user = (User) request.getSession().getAttribute("user"); if (user == null) { response.sendRedirect("/login.html"); return false; } return true; } // 呼叫Controller方法結束後,頁面渲染之前呼叫此方法 @Override public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception { } // 頁面渲染完畢後呼叫此方法 @Override public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception { } }
-
-
跨域訪問
-
Spring Boot 提供對 CORS 的支援,可以通過實現 addCorsMappings 介面來新增特定配置
// 配置跨域訪問 public void addCorsMappings(CorsRegistry registry) { // 僅允許來自domain2.com的跨域訪問,路徑限定為/api,方法限定為:POST、GET registry.addMapping("/api/**") .allowedOrigins("http://domain2.com") .allowedMethods("POST", "GET"); }
allowedOrigins的作用:跨域請求發起的時候,瀏覽器會對請求與返回的響應資訊檢查 HTTP 頭,如果 Access-Control-Allow-Origin 包含了自身域,則表示允許訪問。反之報錯。
-
-
格式化
-
當HTTP請求對映到Controller方法上的引數後,Spring會自動的進行型別轉換。針對日期型別的引數,Spring預設並沒有配置如何將字串轉換為日期型別,為了支援可按照指定格式轉換為日期型別,需要新增一個DateFormatter類。
public void addFormatters(FormatterRegistry registry) { registry.addFormatter(new DateFormatter("yyyy-MM-dd HH:mm:ss")); }
-
-
檢視對映
-
有些時候沒有必要為一個URL指定一個Controller方法,可以直接將URL請求轉到對應的模板渲染上。
public void addViewControllers(ViewControllerRegistry registry) { registry.addViewController("/index.html").setViewName("/index.btl"); registry.addRedirectViewController("/**/*.do", "/index.html"); }
對於 index.html 的請求,設定返回的檢視為 index.btl
所有以 .do 結尾的請求重定向到 /index.html 請求
-
-
-
內建檢視技術
- FreeMarker
- Groovy
- Thymeleaf
- Mustache
-
Redirect 和 Forward
- Controller 中重定向可以返回以 “redirect:” 為字首的URL
- return "redirect:/other/page"
- ModelAndView view = new ModelAndView(“redirect:/other/page”)
- RedirectView view = new RedirectView("/other/page")
- Controller 中轉發可以返回以 “forward:” 為字首的URL
- return "forward:/next/page"
- Controller 中重定向可以返回以 “redirect:” 為字首的URL
-
通用錯誤處理
-
在Spring Boot 中,Controller 中丟擲的異常預設交給了 “/error” 處理,應用程式可以將 /error 對映到一個特定的 Controller 中處理來替代的 Spring Boot 的預設實現,應用可以繼承 AbstractErrorController 來統一處理各種系統異常。
@Controller public class ErrorController extends AbstractErrorController { private static final String ERROR_PATH = "/error"; public ErrorController() { super(new DefaultErrorAttributes()); } @RequestMapping(ERROR_PATH) public ModelAndView getErrorPath(HttpServletRequest request, HttpServletResponse response) { ... }
-
AbstractErrorController 提供多個方法可以從 request 中獲取錯誤相關資訊。
錯誤資訊 說明 timestamp 錯誤發生時間 status HTTP Status error 錯誤資訊,如Bad Request、Not Found message 詳細錯誤資訊 exception 丟擲異常類名 path 請求的URI errors @Validated 引數校驗錯誤的結果資訊 - 錯誤處理優化:
- 異常資訊直接顯示給使用者並不合適,尤其是 RuntimeException。
- 頁面渲染、JSON請求的錯誤處理應分類處理。前者返回錯誤頁面,後者返回JSON結果。
getErrorPath 方法完善:-
獲取錯誤資訊
// getErrorAttributes 提供用於獲取錯誤資訊的方法,返回Map Map<String, Object> model = Collections.unmodifiableMap( getErrorAttributes(request, false));
-
獲取異常
// 獲取異常(存在空情況) Throwable cause = getCause(request);
getCause() 方法
protected Throwable getCause(HttpServletRequest request) { Throwable error = (Throwable) request.getAttribute("javax.servlet.error.exception"); if (error != null) { // MVC有可能會封裝異常成ServletException,需要呼叫getCause獲取真正的異常 while (error instanceof ServletException && error.getCause() != null) { error = ((ServletException) error).getCause(); } } return error; }
-
資訊獲取
int status = (Integer) model.get("status"); //錯誤資訊 String message = (String) model.get("message"); String requestPath = (String) model.get("path"); //友好提示 String errorMessage = getErrorMessage(cause);
-
日誌資訊列印
//後臺列印日誌資訊方方便查錯 log.info(message, cause);
-
區分客戶端發起的是頁面渲染請求還是JSON請求
protected boolean isJsonRequest(HttpServletRequest request) { String requestUri = request.getRequestURI(); if (requestUri.endsWith(".json")) { return true; } else { return (request.getHeader("accept").contains("application/json") || (request.getHeader("X-Requested-With") != null && request.getHeader("X-Requested-With").contains("XMLHttpRequest"))); } }
-
針對請求型別判斷,進行相應的錯誤處理
if (!isJsonRequest(request)) { ModelAndView view = new ModelAndView("/error.btl"); view.addAllObjects(model); view.addObject("status", status); view.addObject("errorMessage", errorMessage); view.addObject("cause", cause); return view; } else { Map error = new HashMap(); error.put("success", false); error.put("errorMessage", getErrorMessage(cause)); error.put("message", message); // Json 資料寫入 writeJson(response, error); return null; }
writeJson() 方法
protected void writeJson(HttpServletResponse response, Map error) { response.setContentType("application/json;charset=utf-8"); try { response.getWriter().write(objectMapper.writeValueAsString(error)); } catch (IOException e) { // ignore } }
-
友好提示:getErrorMessage() 方法
protected String getErrorMessage(Throwable ex) { /*不給前端顯示詳細錯誤*/ if (ex instanceof YourApplicationException) { // 如果YourApplicationException的資訊可以顯示給使用者 return ((YourApplicationException)ex).getMessage(); } return "伺服器錯誤,請聯絡管理員"; }
-
-
-
Transitional
-
在業務邏輯層Service中,常規採用 “介面 + 實現類” 的方式進行專案構建。
-
通常情況下,業務介面實現類中要新增 @Service 註解,同時搭配上 @Transactional 進行事務增強。
@Service @Transactional public class UserServiceImpl implements UserService { ... }
-