SpringBoot中controller深層詳細講解

在基於spring框架的項目開發中,必然會遇到controller層,它可以很方便的對外提供數據接口服務,也是非常關鍵的出口,所以非常有必要進行規范統一,使其既簡潔又優雅。

controller層的職責為負責接收和響應請求,一般不負責具體的邏輯業務的實現。controller主要工作如下:

  • 接收請求並解析參數;
  • 調用service層執行具體的業務邏輯(可能包含參數校驗);
  • 捕獲業務異常做出反饋;
  • 業務邏輯執行成功做出響應;

目前controller層代碼會存在的問題:

  • 參數校驗過多地耦合瞭業務代碼,違背瞭單一職責原則;
  • 可能在多個業務邏輯中拋出同一個異常,導致代碼重復;
  • 各種異常反饋和成功響應格式不統一,接口對接不友好;

優雅寫法一:統一返回結構

統一返回值類型,無論項目前後端是否分離都是非常必要的,方便對接接口的前端開發人員更加清晰地知道這個接口的調用是否成功,不能僅僅簡單地看返回值是否為 null 就判斷成功與否,因為有些接口的設計就是如此。

統一返回結構,通過狀態碼就能清楚的知道接口的調用情況:

@Data
public class ResponseData<T> {
    private Boolean status = true;
    private int code = 200;
    private String message;
    private T data;
    public static ResponseData ok(Object data) {
        return new ResponseData(data);
    }
    public static ResponseData ok(Object data,String message) {
        return new ResponseData(data,message);
    }
    public static ResponseData fail(String message,int code) {
        ResponseData responseData= new ResponseData();
        responseData.setCode(code);
        responseData.setMessage(message);
        responseData.setStatus(false);
        responseData.setData(null);
        return responseData;
    }
    public ResponseData() {
        super();
    }
    public ResponseData(T data) {
        super();
        this.data = data;
    }
    public ResponseData(T data,String message) {
        super();
        this.data = data;
        this.message=message;
    }
}
@AllArgsConstructor
@Data
public enum ResponseCode {
    SYS_FAIL(1, "操作失敗"),
    SYS_SUCESS(200, "操作成功"),
    SYSTEM_ERROR_CODE_403(403, "權限不足"),
    SYSTEM_ERROR_CODE_404(404, "未找到請求資源"),
	;
	private int code;
    private String msg;
}

統一返回結構後,就可以在controller中使用瞭,但是每個controller都這麼寫,都是很重復的工作,所以還可以繼續想辦法處理統一返回結構。

優雅寫法二:統一包裝處理

Spring 中提供瞭一個類 ResponseBodyAdvice ,能幫助我們實現上述需求:

ResponseBodyAdvice 是對 Controller 返回的內容在 HttpMessageConverter 進行類型轉換之前攔截,進行相應的處理操作後,再將結果返回給客戶端。這樣就可以把統一包裝處理的工作放到這個類裡面,其中supports判斷是否要交給beforeBodyWrite 方法執行,true為需要,false為不需要,beforeBodyWrite 是對response的具體處理。

@RestControllerAdvice(basePackages = "com.example.demo")
public class ResponseAdvice implements ResponseBodyAdvice<Object> {
    @Override
    public boolean supports(MethodParameter returnType, Class<? extends HttpMessageConverter<?>> converterType) {
        // 如果不需要進行封裝的,可以添加一些校驗手段,比如添加標記排除的註解
        return true;
    }
    @Override
    public Object beforeBodyWrite(Object body, MethodParameter returnType, MediaType selectedContentType, Class<? extends HttpMessageConverter<?>> selectedConverterType, ServerHttpRequest request, ServerHttpResponse response) {
        // 提供一定的靈活度,如果body已經被包裝瞭,就不進行包裝
        if (body instanceof Result) {
            return body;
        }
        return Result.success(body);
    }
}

這樣即能實現對controller返回的數據進行統一,又不需要對原有代碼進行大量的改動瞭。

優雅寫法三:參數校驗

Java API 的規范 JSR303 定義瞭校驗的標準 validation-api ,其中一個比較出名的實現是 hibernate validation。

@PathVariable 和 @RequestParam 參數校驗:get請求的參數接收一般依賴這兩個註解,但是處於 url 有長度限制和代碼的可維護性,超過 5 個參數盡量用實體來傳參;

對 @PathVariable 和 @RequestParam 參數進行校驗需要在入參處聲明約束的註解,如果校驗失敗,會拋出 MethodArgumentNotValidException 異常。

@RestController
@RequestMapping("/test")
public class TestController {
    private TestService testService;
	@Autowired
    public void setTestService(TestService prettyTestService) {
        this.testService = prettyTestService;
    }
    @GetMapping("/{num}")
    public Integer num(@PathVariable("num") @Min(1) @Max(20) Integer num) {
        return num * num;
    }
    @GetMapping("/email")
    public String email(@RequestParam @NotBlank @Email String email) {
        return email;
    }
}

@RequestBody 參數校驗:post和put 請求的參數推薦使用 @RequestBody 請求體參數;

對 @RequestBody 參數進行校驗需要在 DTO 對象中加入校驗條件後,再搭配 @Validated 即可完成自動校驗。如果校驗失敗,會拋出 ConstraintViolationException 異常。

@Data
public class TestDTO {
    @NotBlank
    private String userName;
    @NotBlank
    @Length(min = 6, max = 20)
    private String password;
    @NotNull
    @Email
    private String email;
}
@RestController
@RequestMapping("/test")
public class TestController {
    private TestService testService;
	@Autowired
    public void setTestService(TestService testService) {
        this.testService = testService;
    }
    @PostMapping("/testValidation")
    public void testValidation(@RequestBody @Validated TestDTO testDTO) {
        this.testService.save(testDTO);
    }
}

自定義校驗規則:有些時候 JSR303 標準中提供的校驗規則不滿足復雜的業務需求,也可以自定義校驗規則;

優雅寫法四:自定義異常與統一攔截異常

原來拋出的異常會有如下問題:

  • 拋出的異常不夠具體,隻是簡單地把錯誤信息放到瞭 Exception 中;
  • 拋出異常後,Controller 不能具體地根據異常做出反饋;
  • 雖然做瞭參數自動校驗,但是異常返回結構和正常返回結構不一致;

自定義異常是為瞭後面統一攔截異常時,對業務中的異常有更加細顆粒度的區分,攔截時針對不同的異常作出不同的響應。

統一攔截異常的是為瞭可以與前面定義下來的統一包裝返回結構能對應上,還有就是希望無論系統發生什麼異常,Http 的狀態碼都要是 200 ,盡可能由業務來區分系統的異常。

//自定義異常
public class ForbiddenException extends RuntimeException {
    public ForbiddenException(String message) {
        super(message);
    }
}
//自定義異常
public class BusinessException extends RuntimeException {
    public BusinessException(String message) {
        super(message);
    }
}
//統一攔截異常
@RestControllerAdvice(basePackages = "com.example.demo")
public class ExceptionAdvice {
    /**
     * 捕獲 {@code BusinessException} 異常
     */
    @ExceptionHandler({BusinessException.class})
    public Result<?> handleBusinessException(BusinessException ex) {
        return Result.failed(ex.getMessage());
    }
    /**
     * 捕獲 {@code ForbiddenException} 異常
     */
    @ExceptionHandler({ForbiddenException.class})
    public Result<?> handleForbiddenException(ForbiddenException ex) {
        return Result.failed(ResultEnum.FORBIDDEN);
    }
    /**
     * {@code @RequestBody} 參數校驗不通過時拋出的異常處理
     */
    @ExceptionHandler({MethodArgumentNotValidException.class})
    public Result<?> handleMethodArgumentNotValidException(MethodArgumentNotValidException ex) {
        BindingResult bindingResult = ex.getBindingResult();
        StringBuilder sb = new StringBuilder("校驗失敗:");
        for (FieldError fieldError : bindingResult.getFieldErrors()) {
            sb.append(fieldError.getField()).append(":").append(fieldError.getDefaultMessage()).append(", ");
        }
        String msg = sb.toString();
        if (StringUtils.hasText(msg)) {
            return Result.failed(ResultEnum.VALIDATE_FAILED.getCode(), msg);
        }
        return Result.failed(ResultEnum.VALIDATE_FAILED);
    }
    /**
     * {@code @PathVariable} 和 {@code @RequestParam} 參數校驗不通過時拋出的異常處理
     */
    @ExceptionHandler({ConstraintViolationException.class})
    public Result<?> handleConstraintViolationException(ConstraintViolationException ex) {
        if (StringUtils.hasText(ex.getMessage())) {
            return Result.failed(ResultEnum.VALIDATE_FAILED.getCode(), ex.getMessage());
        }
        return Result.failed(ResultEnum.VALIDATE_FAILED);
    }
    /**
     * 頂級異常捕獲並統一處理,當其他異常無法處理時候選擇使用
     */
    @ExceptionHandler({Exception.class})
    public Result<?> handle(Exception ex) {
        return Result.failed(ex.getMessage());
    }
}

通過上述寫法,可以發現 Controller 的代碼變得非常簡潔優雅,可以清楚知道每個參數、每個DTO的校驗規則,可以明確返回的結構,包括異常情況。

到此這篇關於SpringBoot中controller深層詳細講解的文章就介紹到這瞭,更多相關SpringBoot controller內容請搜索WalkonNet以前的文章或繼續瀏覽下面的相關文章希望大傢以後多多支持WalkonNet!

推薦閱讀: