引數校驗與國際化:提高程式碼穩定性和可維護性的重要方法

努力的小雨發表於2023-12-15

在我們日常的工作中,引數校驗是一項非常重要的任務。它能夠幫助我們確保程式碼的穩定性和可靠性,避免因為引數錯誤而導致的意外情況發生。引數校驗通常包括對輸入引數的合法性、格式的檢查等。而為了提高程式碼的可維護性和可重用性,我們可以使用一些常見的引數校驗方法。

另外,國際化也是一項非常重要的功能,尤其是在跨國專案中。透過國際化的配置,我們可以根據使用者的語言環境,自動切換顯示的語言,提供更好的使用者體驗。在引數校驗中,我們也可以使用國際化配置來返回相應的提示資訊,使得提示資訊能夠適應不同的語言環境。

  • RequestParam引數校驗
  • NotEmpty等註解引數校驗
  • 實體類增加引數校驗
  • CustomException,自定義異常對引數異常進行國際化定製
  • NotEmptyField,自定義註解實現對引數異常進行國際化定製

好的,讓我們一步一步來介紹引數校驗的方法。除了上述提到的方法,如果還有一些其他的優秀方法可以用來進行引數校驗,也可以提醒我一下!

RequestParam引數校驗

RequestParam本身就有對引數進行的基礎校驗,比如required表示是否引數必填。可以透過在方法引數上使用@RequestParam註解來實現引數校驗。以下是示例程式碼:

@GetMapping("/hello-not-required")
public String sayHelloByNotRequired(@RequestParam(required = false, name = "who") String who) {
    if (StrUtil.isBlank(who)) {
        who = "World";
    }
    return StrUtil.format("Hello, {}!", who);
}

如果不傳遞引數,也不會有任何問題,這裡不做演示就可以了,非常簡單。

NotEmpty等註解引數校驗

預設message

然而,雖然RequestParam只能指示引數是否被攜帶,但它並不負責檢驗引數值是否為空。因此,我們可以透過與NotEmpty註解結合使用來進行判斷。

@GetMapping("/hello-required")
  public String sayHelloByRequired(@RequestParam @NotEmpty String who) {
    return StrUtil.format("Hello, {}!", who);
  }

當訪問http://localhost:8080/demo/hello-required?who=時,我們的程式是報錯的,因為who欄位不能為空

image

自定義message

如果你仔細閱讀了NotEmpty註解的文件,你會發現它允許自定義報錯提示資訊。因此,我們可以嘗試自定義提示資訊,以滿足我們的需求。

@GetMapping("/hello-required-message")
public String sayHelloByRequiredWithMessage(@RequestParam @NotEmpty(message = "who must be not null") String who) {
  return StrUtil.format("Hello, {}!", who);
}

我再來為你演示一下,除了NotEmpty註解,還有許多其他已經內建的註解可供使用。我就不一一列舉了,你可以自行探索更多的註解。

image

實體類增加引數校驗

除了上面提到的簡單校驗方式,我們在實際開發中更常使用的是透過JSON傳遞引數,並使用實體類接收引數。下面是相關程式碼示例:

@PostMapping("/hello-required-message-object")
public String sayHelloByRequiredWithMessageForObject(@RequestBody @Valid MyTest test) {
  return StrUtil.format("Hello, {}!", test);
}
public class MyTest {

  @NotEmpty(message = "NotEmpty.message")
  private String test;

  public String getTest() {
    return test;
  }

  public void setTest(String test) {
    this.test = test;
  }
}

加長最佳化語句:為了方便演示,我特意更換了一些提示資訊,以便更清晰地展示給大家。接下來,我們再仔細觀察一下。

image

自定義異常

其實在工作中,校驗引數是一個常見的任務。我們通常會使用程式碼進行各種校驗,因為我們對引數的要求遠遠超出了簡單的非空檢查。除了非空檢查,我們還需要進行其他等值校驗等。因此,如果在程式碼中進行校驗,我們可以選擇丟擲異常或直接返回錯誤資訊。舉個例子,我可以使用自定義異常來說明這一點:

@PostMapping("/hello-required-message-i18n")
public String sayHelloByRequiredWithMessageFori18n() {
  throw new CustomException("notBlank.message");
}

程式碼經過最佳化,已經去除了多餘的邏輯,直接透過丟擲異常來處理。讓我來演示一下。

image

雖然在出錯時會有報錯資訊返回,但是這些錯誤資訊並不完全符合業務返回資料的格式。為了解決這個問題,我們需要定義一個全域性異常處理類來統一處理異常情況。

@RestControllerAdvice
public class GlobalExceptionHandler {

  @Autowired
  private MessageSource messageSource;
  
    @ExceptionHandler(Exception.class)
    @ResponseStatus(HttpStatus.BAD_REQUEST)
    public ResponseEntity<ErrorResponse> handleCustomException(CustomException ex, Locale locale) {
      String errorMessage = messageSource.getMessage(ex.getMessage(), null, locale);
      ErrorResponse errorResponse = new ErrorResponse(500, errorMessage);
      return new ResponseEntity<>(errorResponse, HttpStatus.INTERNAL_SERVER_ERROR);
    }
}

ErrorResponse是我單獨定義的錯誤描述類,我就不寫了,因為每個專案都會有自己的返回類。

國際化資訊

讓我們進一步探討一下關於國際化資訊的定製。首先,為了實現這一目標,你需要在應用程式中進行相關的配置,具體來說,就是在application.yaml檔案中進行必要的設定。

server:
  port: 8080
  servlet:
    context-path: /demo
spring:
  messages:
    baseName: i18n/messages
    encoding: UTF-8
  mvc:
    locale: zh_CN

請注意,baseName在這裡表示路徑,而最後的messages並不代表包的含義,而是檔案的字首。如果無法找到messages_zh_CN檔案,則會嘗試查詢messages_zh檔案,如果還找不到,則會查詢messages檔案。因此,請不要再建立一個名為messages的包。

image

當所有準備工作完成後,我們可以檢查演示結果是否已經改變了欄位資訊。

image

自定義註解

現在基本上已經存在了全域性異常處理機制,不過我們還需要進一步最佳化。現在讓我們來討論一下自定義註解的使用。首先,我們注意到錯誤資訊無法給出具體的欄位值。顯然,我們不能每次都手動寫上每個欄位的名稱並提示不能為空。為了解決這個問題,我們需要自定義一個註解來實現自動化的校驗。

@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface NotEmptyField {
    String message() default "";
}

有了註解後,下一步就是透過切面來獲取並處理這個註解。接下來,我們來編寫相應的程式碼來實現這個功能:


@Aspect
@Component
public class NotEmptyFieldAspect {
  @Autowired
  private MessageSource messageSource;

    @Around("within(com.xkcoding..*)")
    public Object validateNotEmptyField(ProceedingJoinPoint joinPoint) throws Throwable {
      // 獲取目標方法的引數列表
      Object[] args = joinPoint.getArgs();

      // 遍歷引數列表
      for (Object arg : args) {
        // 獲取引數實體類的屬性
        Field[] fields = arg.getClass().getDeclaredFields();

        // 遍歷屬性列表
        for (Field field : fields) {
          // 判斷屬性是否被 @NotEmptyField 註解修飾
          if (field.isAnnotationPresent(NotEmptyField.class)) {
            // 獲取註解資訊
            NotEmptyField annotation = field.getAnnotation(NotEmptyField.class);
            field.setAccessible(true);
            // TODO: 進一步處理邏輯
            if (ObjectUtil.isEmpty(field.get(arg))) {
              String message = messageSource.getMessage(annotation.message(), null, Locale.getDefault());
              throw new CustomException(field.getName() + message);
            }
            // 輸出屬性和註解資訊
            System.out.println("屬性:" + field.getName());
            System.out.println("註解資訊:" + annotation.message());
            System.out.println("值:" + field.get(arg));
          }
        }
      }

      // 呼叫目標方法
      Object result = joinPoint.proceed();

      return result;
    }
}

我只是簡單地寫了一下實現邏輯,並沒有進行最佳化操作,所以以上程式碼僅供參考。現在讓我們來看一下外層程式碼:

  @PostMapping("/hello-required-message-i18n-object")
  public String sayHelloByRequiredWithMessageFori18nObject(@RequestBody @Valid MyTestForAspect myTestForAspect) {
    return "";
  }

當我們完成所有的準備工作之後,讓我們來觀察一下演示情況:

image

總結

在我們的工作中,引數校驗是一項不可或缺的重要任務。因此,本文只是初步探討了可以進行最佳化的方面,而並未詳細闡述如何完美地進行最佳化。如果我遺漏了一些解決方案,也歡迎大家提供寶貴的建議和提醒。我的目的只是提供一些思路和引發討論,以期能夠共同進步。

相關文章