iT邦幫忙

2026 iThome 鐵人賽

0
AI Engineering

Java × AI:30天打造智慧健康助理系列 第 11 篇

使用 DTO 與 @Valid 驗證使用者輸入

  • 分享至 

  • xImage
  •  

Day 11|使用 DTO 與 @Valid 驗證使用者輸入

昨天的 Day 10,我們學會了將 Controller 和 Service 分開,讓程式架構變得更清楚。

目前的智慧健康助理已經可以接收 JSON、計算 BMI,並回傳結果。

不過今天我突然想到一個問題:

如果使用者輸入的資料不完整,甚至輸入一些奇怪的數字,程式應該怎麼辦?

例如:

{
  "height": -162,
  "weight": 50
}

或是:

{
  "weight": 50
}

這些資料顯然不適合直接拿來計算 BMI。

雖然之前已經使用 if 判斷資料是否合理,但如果未來有十幾個健康資料欄位,難道每個都要自己寫判斷嗎?

所以今天決定學習 Spring Boot 的資料驗證功能!

今天的目標是:

使用 DTO、@Valid 和驗證註解,讓 Spring Boot 自動檢查使用者輸入的資料。


一、為什麼需要資料驗證?

目前我們的 BMI API 可以接收:

{
  "height": 162,
  "weight": 50
}

但使用者不一定每次都會輸入正確資料。

例如:

輸入情況 可能問題
身高為 0 無法正常計算 BMI
體重為負數 資料不合理
沒有輸入身高 缺少必要資料
身高輸入文字 資料型別錯誤
身高超出合理範圍 可能輸入錯誤

如果沒有適當的驗證,程式可能產生錯誤結果,甚至無法正常處理請求。

因此我們希望在進行 BMI 計算之前,先確認資料是否符合基本規則。

整個流程應該是:

使用者傳送 JSON
        ↓
Spring Boot 接收資料
        ↓
檢查資料是否符合規則
        ↓
    是否有效?
     /     \
   是       否
   ↓        ↓
計算 BMI   回傳錯誤
   ↓
回傳結果

這就是今天要完成的功能。


二、再認識一次 DTO

昨天整理專案架構時,我們已經提到 DTO。

DTO 的全名是:

Data Transfer Object

中文可以理解成「資料傳輸物件」。

它主要用來在不同層之間傳遞資料。

例如我們的:

HealthRequest

就是一個 DTO。

它負責接收使用者傳入的健康資料。

原本的程式:

public class HealthRequest {

    private double height;
    private double weight;

    // Getter 和 Setter
}

不過這樣只能儲存資料,還沒有設定資料的驗證規則。

所以今天要幫 DTO 加上一些限制。


三、先加入 Validation 套件

要使用 Spring Boot 的資料驗證功能,通常需要加入:

Spring Boot Starter Validation

如果使用 Maven,可以在 pom.xml 的 <dependencies> 裡加入:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-validation</artifactId>
</dependency>

加入之後,重新載入 Maven 專案,讓相關套件下載完成。

這個 Starter 提供 Jakarta Bean Validation 的相關支援。

接下來就可以使用:

@NotNull

以及:

@Positive

等驗證註解。


四、認識 @NotNull

第一個要認識的是:

@NotNull

它的用途是:

要求欄位不能是 null。

例如:

@NotNull
private Double height;

代表使用者必須提供身高資料。

如果傳送:

{
  "weight": 50
}

因為缺少 height,驗證就不會通過。

這裡有一個需要注意的地方。

原本我們使用:

private double height;

今天改成:

private Double height;

為什麼呢?

因為 double 是 Java 的基本資料型別,預設值是 0.0。

但 Double 是包裝類別,可以表示 null。

如果使用者沒有提供資料,我們就能利用 @NotNull 判斷欄位是否缺少。


五、認識 @Positive

第二個註解是:

@Positive

它代表:

數值必須大於 0。

例如:

@Positive
private Double weight;

如果使用者輸入:

{
  "weight": -50
}

就無法通過驗證。

對 BMI 計算來說,身高和體重都必須大於 0。

因此我們可以同時使用:

@NotNull
@Positive
private Double height;

以及:

@NotNull
@Positive
private Double weight;

這樣就不需要在 Controller 裡面重複寫所有基本判斷。

不過要注意,@Positive 只檢查是否大於 0,並不代表數值一定符合真實人體範圍。


六、修改 HealthRequest

接下來正式修改昨天建立的:

HealthRequest.java

完整程式如下:

package com.example.healthassistant.dto;

import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Positive;

public class HealthRequest {

    @NotNull(message = "Height is required.")
    @Positive(message = "Height must be greater than 0.")
    private Double height;

    @NotNull(message = "Weight is required.")
    @Positive(message = "Weight must be greater than 0.")
    private Double weight;

    public HealthRequest() {
    }

    public Double getHeight() {
        return height;
    }

    public void setHeight(Double height) {
        this.height = height;
    }

    public Double getWeight() {
        return weight;
    }

    public void setWeight(Double weight) {
        this.weight = weight;
    }
}

這次最重要的改變,就是在欄位上加入驗證註解。

例如:

@NotNull(message = "Height is required.")

如果沒有提供身高,就可以產生對應的錯誤訊息。

而:

@Positive(message = "Height must be greater than 0.")

則可以避免輸入 0 或負數。

這樣就能把資料驗證規則直接寫在 DTO 裡面。


七、認識 @Valid

DTO 已經加入驗證規則,但 Spring Boot 要怎麼知道我們希望它執行驗證?

這時候就需要:

@Valid

它可以告訴 Spring:

在將 Request Body 轉換成 Java 物件後,依照 DTO 上的驗證註解檢查資料。

例如:

@PostMapping("/health/bmi")
public ResponseEntity<Map<String, Object>> calculateBMI(
        @Valid @RequestBody HealthRequest request) {

和昨天相比,只多了一個:

@Valid

但它就可以啟動 DTO 的驗證機制。

如果資料不符合規則,Spring MVC 通常會在進入 Controller Method 之前回傳 HTTP 400 錯誤。


八、修改 HealthController

接下來修改 Controller。

今天的 Controller 不需要自己處理所有驗證工作,而是讓 @Valid 負責檢查輸入資料。

package com.example.healthassistant.controller;

import com.example.healthassistant.dto.HealthRequest;
import com.example.healthassistant.service.BmiService;

import jakarta.validation.Valid;

import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;

import java.util.HashMap;
import java.util.Map;

@RestController
public class HealthController {

    private final BmiService bmiService;

    public HealthController(BmiService bmiService) {
        this.bmiService = bmiService;
    }

    @PostMapping("/health/bmi")
    public ResponseEntity<Map<String, Object>> calculateBMI(
            @Valid @RequestBody HealthRequest request) {

        double height = request.getHeight();
        double weight = request.getWeight();

        double bmi = bmiService.calculateBMI(height, weight);

        Map<String, Object> result = new HashMap<>();

        result.put("height", height);
        result.put("weight", weight);
        result.put("bmi", bmi);

        return ResponseEntity.ok(result);
    }
}

這樣 Controller 就變得更簡潔了。

現在主要負責:

  1. 接收 JSON
  2. 啟動資料驗證
  3. 呼叫 BmiService
  4. 回傳計算結果

而身高和體重是否大於 0,就交給 DTO 的驗證註解處理。

昨天 BmiService 裡面的基本防呆可以繼續保留,避免其他程式直接呼叫 Service 時傳入不合理的資料。


九、測試正確資料

接下來使用 Postman 測試。

HTTP Method:

POST

URL:

http://localhost:8080/health/bmi

Body:

{
  "height": 162,
  "weight": 50
}

如果資料符合規則,預期回傳:

{
  "height": 162.0,
  "weight": 50.0,
  "bmi": 19.05
}

HTTP Status:

200 OK

代表資料通過驗證,並成功完成 BMI 計算。


十、測試錯誤資料

接下來故意輸入錯誤資料,看看驗證是否有作用。

測試一:身高為負數

{
  "height": -162,
  "weight": 50
}

因為身高不符合:

@Positive

所以驗證不會通過。

預期 HTTP Status:

400 Bad Request

測試二:缺少身高

{
  "weight": 50
}

因為 height 沒有提供,所以會是 null。

這時候:

@NotNull

就會發揮作用。

預期 HTTP Status:

400 Bad Request

測試三:體重為 0

{
  "height": 162,
  "weight": 0
}

因為:

@Positive

要求數值必須大於 0,因此同樣無法通過驗證。

預期 HTTP Status:

400 Bad Request

目前我們還沒有自訂驗證失敗時的 JSON 格式,所以實際回傳的錯誤內容,可能會因為 Spring Boot 的版本與設定而有所不同。


十一、讓錯誤訊息更清楚

雖然現在可以自動驗證資料,但我希望使用者能清楚知道是哪個欄位出現問題。

例如:

{
  "errors": {
    "height": "Height must be greater than 0."
  }
}

所以我們可以進一步使用:

@RestControllerAdvice

來統一處理 Controller 發生的例外。

先建立:

GlobalExceptionHandler.java

程式如下:

package com.example.healthassistant.exception;

import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;

import java.util.HashMap;
import java.util.Map;

@RestControllerAdvice
public class GlobalExceptionHandler

上一篇
認識 Controller 與 Service 分層
下一篇
使用 Java 加入 BMI 分類功能
系列文
Java × AI:30天打造智慧健康助理 共 30 篇
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言