首頁 > 軟體

SpringBoot中controller深層詳細講解

2023-09-12 18:01:41

在基於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內容請搜尋it145.com以前的文章或繼續瀏覽下面的相關文章希望大家以後多多支援it145.com!


IT145.com E-mail:sddin#qq.com