《優化介面設計的思路》系列:第一篇—介面引數的一些彎彎繞繞

2023-09-14 18:00:33

大家好!我是sum墨,一個一線的底層碼農,平時喜歡研究和思考一些技術相關的問題並整理成文,限於本人水平,如果文章和程式碼有表述不當之處,還請不吝賜教。

作為一名從業已達六年的老碼農,我的工作主要是開發後端Java業務系統,包括各種管理後臺和小程式等。在這些專案中,我設計過單/多租戶體系系統,對接過許多開放平臺,也搞過訊息中心這類較為複雜的應用,但幸運的是,我至今還沒有遇到過線上系統由於程式碼崩潰導致資損的情況。這其中的原因有三點:一是業務系統本身並不複雜;二是我一直遵循某大廠程式碼規約,在開發過程中儘可能按規約編寫程式碼;三是經過多年的開發經驗積累,我成為了一名熟練工,掌握了一些實用的技巧。

介面引數是導致很多BUG產生的始作俑者,原因在於介面引數有3多:介面引數的取值地方多,如查詢引數(Query Parameters)、路徑引數(Path Parameters)、請求體(Request Body)等;資料型別多,如數位、字元、日期、檔案等;判斷情況多,如空值判斷、格式判斷、大小判斷等;

一、介面引數的取值

1. 放在查詢引數和請求體裡

a、方法引數

範例程式碼如下:

@GetMapping("/testParams1")
public ResponseEntity<String> testParams1(String param1, Integer param2) {
  return ResponseEntity.ok(MessageFormat.format("param1:[{0}];param2:[{1}]", param1, param2));
}

呼叫請求:http://localhost:8080/testParams1?param1=111¶m2=222
返回如下:

沒啥坑。

b、請求物件

GET請求

範例程式碼如下

@GetMapping("/testParams2")
public ResponseEntity<String> testParams2(ParamsReq paramsReq) {
  return ResponseEntity.ok(MessageFormat.format("param1:[{0}];param2:[{1}]", paramsReq.getParam1(), paramsReq.getParam2()));
}

ParamsReq.java

public class ParamsReq {

    private String param1;

    private String param2;

    public ParamsReq() {
    }

    public ParamsReq(String param1, String param2) {
        this.param1 = param1;
        this.param2 = param2;
    }

    public String getParam1() {
        return param1;
    }

    public void setParam1(String param1) {
        this.param1 = param1;
    }

    public String getParam2() {
        return param2;
    }

    public void setParam2(String param2) {
        this.param2 = param2;
    }

    @Override
    public String toString() {
        return "ParamsReq{" +
            "param1='" + param1 + '\'' +
            ", param2='" + param2 + '\'' +
            '}';
    }
}

呼叫請求:http://localhost:8080/testParams2?param1=111¶m2=222
返回如下:

這種有一個坑,Spring預設使用無參建構函式來範例化物件,所以ParamsReq不能是介面、抽象類等特殊類。

POST請求

範例程式碼如下:

@PostMapping("/testParams3")
public ResponseEntity<String> testParams3(ParamsReq paramsReq) {
  return ResponseEntity.ok(MessageFormat.format("param1:[{0}];param2:[{1}]", paramsReq.getParam1(), paramsReq.getParam2()));
}

ParamsReq類程式碼同上

  • 呼叫方式1:引數放在連結上

和GET請求類似,沒啥坑。

  • 呼叫方式2:放在Form表單中,content-type為application/x-www-form-urlencoded

沒啥坑。

  • 呼叫方式3:放在body引數中,content-type為application/json

這裡有坑了,當content-type為application/json時,介面引數取值為空。這時就需要在引數前加上一個註解:@RequestBody,原因是通過@RequestBody註解,Spring Boot可以自動地將請求體中的JSON資料轉換為Java物件,從而方便地進行資料的處理和轉換。如果不加@RequestBody註解,Spring Boot預設會將請求體中的JSON資料作為普通的表單資料來處理,而不會自動轉換為Java物件。:

改成這樣就行:public ResponseEntity<String> testParams3(@RequestBody ParamsReq paramsReq)

2. 放在路徑引數上

範例程式碼如下:

@GetMapping("/testParams4/{pathParam}")
public ResponseEntity<String> testParams4(@PathVariable("pathParam") String pathParam) {
  return ResponseEntity.ok(MessageFormat.format("pathParam:[{0}];", pathParam));
}

引數使用{}框起來,然後使用@PathVariable即可獲取到值,坑不多。

3. 放在請求頭和Cookie

這兩種情況裡面的引數主要是標識類的引數如userToken,一般都是不變的,業務中很少使用到。

二、介面引數的型別

1. 數位、字串

沒啥坑。

2. 日期

範例程式碼如下:

@GetMapping("/testParams5")
public ResponseEntity<String> testParams5(Date date) {
  return ResponseEntity.ok(MessageFormat.format("pathParam:[{0}];", date));
}

這裡有個問題,這樣的介面前端怎麼傳這個date值,字串?時間戳?我已經替大家試過了,都不行,介面直接報400。

正確的做法是在日期引數前加上@DateTimeFormat註解,改成這樣就行了:public ResponseEntity<String> testParams5(@DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss") Date date)
傳參的話傳字串:2023-09-14 00:00:00 即可

3. 列表

範例程式碼如下:

@GetMapping("/testParams6")
public ResponseEntity<String> testParams6(List<Integer> paramList) {
  return ResponseEntity.ok(MessageFormat.format("paramList:[{0}];", paramList));
}

這串程式碼不用測試,它本身就是錯誤的,前面說過Spring預設使用無參建構函式來範例化物件,但是List是一個介面,沒有無參建構函式。
為了解決這個問題,可以使用Spring的@RequestParam註解來指定引數名,並將多個引數值繫結到一個List物件中。

修改程式碼如下:public String testParams6(@RequestParam("paramList") List<Integer> paramList) 即可
然後,通過使用逗號分隔的引數值來存取介面,如:http://localhost:8080/testParams6?paramList=1,2,3
這樣就可以成功傳遞參數列並存取介面了。

4. 檔案

先寫一個簡單上傳介面
upload.html

<!DOCTYPE html>
<html>
<head>
    <title>File Upload Demo</title>
</head>
<body>
    <h1>File Upload Demo</h1>
    <form action="http://localhost:8080/upload" method="post" enctype="multipart/form-data">
        <input type="file" name="file" />
        <br/><br/>
        <input type="submit" value="Upload" />
    </form>
</body>
</html>

後端上傳程式碼
FileUploadController.java

import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.multipart.MultipartFile;

@Controller
public class FileUploadController {

    @PostMapping("/upload")
    public ResponseEntity<String> uploadFile(@RequestParam("file") MultipartFile file) {
        // Check if file is empty
        if (file.isEmpty()) {
            return new ResponseEntity<>("File is empty", HttpStatus.BAD_REQUEST);
        }

        // Save the file
        try {
            byte[] bytes = file.getBytes();
            // Logic to save the file to a desired location

            return new ResponseEntity<>("File uploaded successfully", HttpStatus.OK);
        } catch (Exception e) {
            return new ResponseEntity<>("Failed to upload file", HttpStatus.INTERNAL_SERVER_ERROR);
        }
    }
}

上傳介面後端其實還好,主要是前端需要處理的內容多一些,由於MultipartFile類也是一個介面,所以這裡也需要加上@RequestParam註解。

三、介面引數的判斷

前面提到的@RequestBody@RequestParam註解都是SpringBoot自帶的,它們主要的功能是將請求引數轉換為我們介面定義的變數或者Java物件,而校驗引數值是否合法通常有下面幾種做法:

  • 自己寫校驗邏輯,一般是配合使用Assert進行引數校驗
  • 使用javax.validation包的校驗註解,如@NotNull@NotBlank

這裡主要講一下javax.validation如何使用!

1. pom.xml引入

<!-- 介面引數校驗 -->
<dependency>
  <groupId>javax.validation</groupId>
  <artifactId>validation-api</artifactId>
  <version>2.0.1.Final</version>
</dependency>

2. 註解分類

a. 空值檢查

註解 說明 使用頻率
@NotNull 不能為null,常用於數位、日期 常用
@NotBlank 不能為null也不能為空,常用於字串 常用
@NotEmpty 集合不能為空,常用於List、Map、Set 常用

b. 數值檢查

註解 說明 使用頻率
@Max 被註釋的元素必須小於等於指定的值 常用
@Min 被註釋的元素必須大於等於指定的值 常用
@Positive 被註釋的元素必須是正數 不常用
@Negative 被註釋的元素必須是負數 不常用

c. Boolean 檢查

註解 說明 使用頻率
@AssertFalse 被註釋的元素必須是false 常用
@AssertTrue 被註釋的元素必須是true 常用

d. 日期檢查

註解 說明 使用頻率
@Future 被註釋的元素必須是將來的日期 不常用
@Past 被註釋的元素必須是過去的日期 不常用

e. 日期檢查

註解 說明 使用頻率
@Email 被註釋的元素必須是電子郵箱地址 常用
@Pattern 被註釋的元素必須是符合正規表示式,我經常使用這個判斷手機號是否合法 常用

3. 使用方法

下面是一個經典的案例

@Data
public class StudentReq {
    @NotBlank(message = "主鍵不能為空")
    private String id;
    @NotBlank(message = "名字不能為空")
    @Size(min = 2, max = 4, message = "名字字元長度必須為 2~4個")
    private String name;
    @Pattern(regexp = "^1[3456789]\\d{9}$", message = "手機號格式錯誤")
    private String phone;
    @Email(message = "郵箱格式錯誤")
    private String email;
    @Past(message = "生日必須早於當前時間")
    private Date birth;
    @Min(value = 0, message = "年齡必須為 0~100")
    @Max(value = 100, message = "年齡必須為 0~100")
    private Integer age;
    @PositiveOrZero
    private Double score;
}

這些東西看看就行了,用的時候翻一下檔案就行,記也記不住。

四、一些可以直接獲取到的引數

  • HttpServletRequest:用於獲取HTTP請求的相關資訊,包括請求頭、請求引數、請求方法等。
  • HttpServletResponse:用於控制HTTP響應,包括設定響應狀態碼、設定響應頭、傳送響應內容等。
  • HttpSession:用於獲取當前對談的資訊,可以用來儲存和獲取對談級別的資料。
  • Principal:用於獲取當前使用者的身份資訊,通常用於認證和授權。
  • Model/ModelMap:用於在請求處理方法中傳遞資料給前端檢視。
  • BindingResult:用於獲取請求引數繫結和驗證的結果,包含了校驗的錯誤資訊。
  • Locale:用於獲取當前請求的語言環境,可以用來進行國際化處理。
  • MultipartFile(或者List):用於處理上傳的檔案,包括檔案的名稱、大小、內容等。
  • RedirectAttributes:用於在重定向時傳遞資料給目標頁面。
  • ServletRequest/ServletResponse:HttpServletRequest/HttpServletResponse的父類別,可以使用其提供的通用方法。
  • @ModelAttribute註解:用於獲取請求引數,並將其繫結到一個物件上。

這些物件可以直接在介面引數上使用,通過框架自動注入的方式獲取其範例。在使用時,需要保證框架已經正確設定和啟用了對應的註解和攔截器。用的最多的就是HttpServletRequest和HttpServletResponse了。