1. 程式人生 > >Spring Boot 2.0 讀書筆記_04:MVC 下

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 將丟擲異常。
    • 自定義校驗
      • 關於自定義校驗,說白了就是自定義註解去構建一套驗證邏輯
      • 關於自定義註解,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;
              	}
              }
            
  • WebMvcConfigurer:實現 WebMvcConfigurer 介面,即可配置應用的MVC全域性特性

    • 實現 WebMvcConfigurer 介面,會看到有以下方法可以實現
      在這裡插入圖片描述

    • 攔截器:通過 addInterceptors 方法可以設定多個攔截器,實現對URL攔截檢查使用者登入狀態等操作

        public void addInterceptors(InterceptorRegistry registry) {
        	// 新增一個攔截器,檢查會話,URL以user開頭的都是用此攔截器
          registry.addInterceptor(new SessionHandlerInterceptor()).addPathPatterns("/user/**");
        }
      

      SessionHandlerInterceptor 會話處理攔截器,實現了 HandlerInterceptor 介面
      需要注意的是:攔截器有以下三個方法需要覆蓋實現

      1. preHandle,在呼叫 Controller 方法前會呼叫此方法

      2. postHandle,在呼叫 Controller 方法結束後、頁面渲染之前呼叫此方法

      3. 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"
  • 通用錯誤處理

    • 在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 {
        	...
        }