Spring Boot ‐ Spring Web MVC - CCH0124/spring-sandbox GitHub Wiki

Spring Web model-view-controller (MVC) 框架是圍繞 DispatcherServlet 設計,它將請求分派給處理程序,具有可配置的處理程序映射、view 解析以及對上傳文件的支援等。可用於開發靈活且鬆散耦合的 Web 應用程式。 MVC 模式導致分離應用程式的不同方面(輸入邏輯、業務邏輯和 UI 邏輯),同時在這些元素之間提供鬆散耦合。

  • Model 封裝了應用程式數據,通常它們由 POJO 組成
  • View 負責呈現模型數據,通常它會產生客戶端瀏覽器可以解釋的 HTML。
  • Controller 負責處理使用者請求並建立適當的模型並將其傳遞給 View 進行渲染

"Context hierarchy in Spring Web MVC" From https://docs.spring.io/spring-framework/docs/3.2.x/spring-framework-reference/html/mvc.html

DispatcherServlet

Spring 的 DispatcherServlet 負責將 HttpRequest 正確協調到正確的處理程序。

基本上,DispatcherServlet 處理傳入的 HttpRequest,將請求委派出去,並根據在 Spring 應用中實現的 HandlerAdapter 介面處理該請求。這些介面在 Spring 應用中已被實現,並伴隨著指定處理程序、控制器端點和響應對象的註釋。

  • 在關鍵字 DispatcherServlet.WEB_APPLICATION_CONTEXT_ATTRIBUTE 下,與 DispatcherServlet 關聯的 WebApplicationContext 被搜索並提供給處理過程中的所有元素。
  • DispatcherServlet 使用 getHandler() 方法查找為其配置的所有 HandlerAdapter 接口實現——每個找到並配置的實現通過 handle() 方法在處理過程中處理請求。
  • LocaleResolver 可選綁定到請求,以便在處理過程中的各個元素解析語言環境。
  • ThemeResolver 可選綁定到請求,以便元素(例如視圖)確定使用哪個主題。
  • 如果指定了 MultipartResolver,則會檢查請求中的 MultipartFiles 找到的所有文件都會包裝在 MultipartHttpServletRequest 中以進行進一步處理。
  • 在 WebApplicationContext 中聲明的 HandlerExceptionResolver 實現會捕獲在請求處理過程中拋出的異常。

Request flow

FROM https://medium.com/@lakshyachampion/the-dispatcherservlet-the-engine-of-request-handling-in-spring-boot-3a85c2bdbe6b

  1. 用戶端發送 HTTP 請求至 Spring MVC 應用程式
  2. DispatcherServlet(Front Controller)接收並捕獲請求。並將請求委託給 Handler Mapping
  3. DispatcherServlet 的一項關鍵責任是查找 HandlerMappingHandlerMapping 負責根據請求 URL、請求方法或其他參數來確定應該由哪個控制器(handler)來處理傳入的請求。當 HandlerMapping 將請求映射到特定的控制器之後,它會返回這個控制器(habdler)的訊息給 DispatcherServlet。接著,DispatcherServlet 會接收這個控制器的 Bean ID,然後獲取相應的控制器對象並調用該對象的方法,從而將控制權移交給控制器。
  4. Handler class / Controller class 負責創建 command class 物件並執行請求封裝。即,它接收 form data 並將其儲存(也稱為請求封裝)到 command class 物件中。 command class 實際上是一個 Java Bean,其物件保存接收到的 form data。
  5. Handler class 執行自己的業務邏輯或與 Service 或 DAO 類別交互,透過處理請求取得輸出。
  6. Handler class 將結果傳回給 DispatcherServlet 並調用 ViewResolver。這檢視對於生成適當的回應格式,例如 HTML、JSON 或其他內容類型。以 application/json 類型來說 Spring Boot 利用 HttpMessageConverters 中 MappingJackson2HttpMessageConverter 將返回的物件轉換成 JSON 格式。
  7. DispatcherServlet 會向客戶端發送回應。表示請求處理過程完成,客戶端將接收後端生成的內容,無論是渲染的網頁還是 JSON 數據,具體取決於控制器執行的操作。

HandlerAdapter 介面在 DispatcherServlet 請求處理工作流程的各個階段都扮演著重要角色。HandlerAdapter 被許多介面實作更簡化了控制器的使用。

以下是 HandlerAdapter 實作關係:

image

以下是 HandlerAdapter 的工作流程:

  1. 加入鏈中 HandlerExecutionChain:首先,每個 HandlerAdapter 的實作類別都會從 DispatcherServletgetHandler() 方法中被加入到 HandlerExecutionChain中。
  2. 逐一處理請求 handle(): 然後,隨著處理鏈的執行,每個 HandlerAdapter 實作類別的 handle() 方法都會處理 HttpServletRequest 物件。

SimpleControllerHandlerAdapter 允許在沒有 @Controller 註解的情況下明確實作控制器。 RequestMappingHandlerAdapter 支援使用 @RequestMapping 註解進行註解的方法。這裡重點關注 @Controller 註釋。@RequestMapping 註解設定處理程序在其關聯的 WebApplicationContext 中可用的特定端點。

@RestController
@RequestMapping("/api")
@Tag(name = "Tutorial", description = "Tutorial management APIs")
public class TutorialController {

  @Autowired TutorialService tutorialService;

  @Operation(
      summary = "Retrieve all Tutorial by title.",
      description = "Get all Tutorial object by specifying title.",
      tags = {"tutorials", "get"})
  @ApiResponses({
    @ApiResponse(
        responseCode = "200",
        content = {
          @Content(
              schema = @Schema(implementation = TutorialResponsePagingDto.class),
              mediaType = "application/json")
        }),
    @ApiResponse(
        responseCode = "500",
        description = "System Error.",
        content = {@Content(schema = @Schema())})
  })
  @GetMapping("/tutorials")
  public ResponseEntity<TutorialResponsePagingDto> getAllTutorials(
      @Parameter(description = "Search by title.") @RequestParam(required = false, name = "title")
          final String title,
      @Parameter(description = "Page number, starting from 0", required = true)
          @RequestParam(defaultValue = "0", name = "page")
          int page,
      @Parameter(description = "Number of items per page", required = true)
          @RequestParam(defaultValue = "3", name = "size")
          int size) {
    var allTutorials = tutorialService.getAllTutorials(title, page, size);
    return ResponseEntity.ok(allTutorials);
  }
}

由 @RequestMapping 註解指定的路徑透過 HandlerMapping 介面在內部進行管理。URL 結構自然相對於 DispatcherServlet 本身,由 servlet 映射決定。

DispatcherServlet 的核心職責是將傳入的 HttpRequest 分派到使用 @Controller@RestController 註解指定的正確處理程序。

常用註解

  1. @RequestMapping

基於 @Controller 類別中請求處理方法。用於應設於哪個 URL 路徑,其還包含以下設定

  • method: HTTP methods
  • params: 根據 HTTP 參數的存不存在或值過濾請求
  • headers: 根據 HTTP 表頭參數的存不存在或值過濾請求
  • consumes: 此 HTTP method 可以在 HTTP 請求載體中使用哪些 Media Type
  • produces: 此 HTTP method 可以在 HTTP 回應載體中產生哪些 Media Type

@GetMapping@PostMapping@PutMapping@DeleteMapping@PatchMapping@RequestMapping 的不同變體,分別對應不同的 HTTP Method。

  1. @RequestBody

它將 HTTP 請求載體對應到一個物件。過程反序列化是自動的,並且取決於請求的內容類型。

  1. @PathVariable

會將參數綁定到 URI 模板變數,可以使用 @RequestMapping 註解設計 URI 模板,並使用 @PathVariable 將方法參數綁定到模板部分之一。

  1. @RequestParam

用來存取 HTTP 請求參數(HTTP Parameters)。當 Spring 在請求中發現該參數沒有值或空值時,可以使用 @RequestParam 來指定預設注入的值,可透果設定 defaultValue 參數設置。

對於存取其他 HTTP 請求部分,像是 cookie 和 header。可以分別使用註解 @CookieValue@RequestHeader 來存取它們。

  1. @ResponseBody

如果使用 @ResponseBody Spring 會將該方法的最後結果視為 HTTP 回應載體。會跳過 view resolve 過程。

  1. @ExceptionHandler

可以聲明自訂錯誤處理方法,當 Controller 下的請求處理過程拋出任何指定的異常時,Spring 呼叫此方法。

  1. @ResponseStatus

可以指定所需的回應 HTTP 狀態。也可以將它與 @ExceptionHandler 一起使用。

  1. @Controller

可以使用 @Controller 定義一個 Spring MVC 控制器。

  1. @RestController

@RestController 結合了 @Controller@ResponseBody

  1. @CrossOrigin

請求處理程序啟用跨域通訊。標記在一個類,它將應用於其中的所有請求處理方法。

Interceptors 攔截器

HandlerMapping 的目的是將處理(Handle)方法對應到 URL。這樣,DispatcherServlet 才能夠在處理請求時呼叫它。攔截器攔截請求並處理它們,有助於避免重複的處理器流程,例如日誌和授權檢查。

Handler Interceptor

Spring 攔截器是一個擴展 HandlerInterceptorAdapter 或實作 HandlerInterceptor 介面的類別。

HandlerInterceptor 主要三個方法:

  • prehandle() 在處理器(Controller)方法執行之前被調用。通常用於進行請求預處理,例如身份驗證、授權、日誌記錄等。如果該方法返回 false,則請求將被攔截,不會繼續執行後續的處理器方法。
  • postHandle() 在處理器方法執行之後,但渲染 view 之前被調用。通常用於處理模型數據、處理異常等。
  • afterCompletion() 在整個請求完成後含渲染 VIEW。通常用於清理資源,例如關閉資料庫連接、釋放其他資源等。

preHandle() 實作方法,該方法傳回一個布林值,false 則不做後續處理。

         default boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler)
			throws Exception {

		return true;
	}

postHandle() 實作方法,攔截器在處理請求後但在生成 VIEW 之前立即呼叫此方法。

	default void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler,
			@Nullable ModelAndView modelAndView) throws Exception {
	}

afterCompletion(),此方法允許在請求處理完成後執行自訂邏輯。

	default void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler,
			@Nullable Exception ex) throws Exception {
	}

Custom Interceptor

@Component
public class LoggerInterceptor implements HandlerInterceptor {
    private static Logger log = LoggerFactory.getLogger(LoggerInterceptor.class);
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        log.info("[preHandle][" + request + "]" + "[" + request.getMethod() + "]" + request.getRequestURI());
        return true;
    }

    @Override
    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
                log.info("[postHandle][" + request + "]");
	}

    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
        if (ex != null)
            ex.printStackTrace();
        log.info("[afterCompletion][" + request + "][exception: " + ex + "]");
        log.info("Request and Response is completed");
    }
}

實作完之後需要進行配置讓其生效。

@Configuration
public class InterceptorConfig implements WebMvcConfigurer {
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(new LoggerInterceptor());
    }
}

啟用此配置後,該定義攔截器會處於活動狀態,並且應用程式中的所有請求都將被正確捕獲。如果配置了多個 Spring 攔截器,則 preHandle() 方法將按配置順序執行,而 postHandle()afterCompletion() 方法將依相反順序呼叫。