iT邦幫忙

2026 iThome 鐵人賽

0
AI Engineering

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

認識 Controller 與 Service 分層

  • 分享至 

  • xImage
  •  

Day 10|認識 Controller 與 Service 分層

昨天的 Day 9,我們學會了使用 @PostMapping 和 @RequestBody,讓 Spring Boot 可以直接接收 JSON 格式的資料。

例如使用者傳送:

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

Spring Boot 就能將 JSON 轉換成 Java 物件,再進行 BMI 計算。

不過在整理昨天的程式時,我發現一個問題:

目前接收資料、檢查資料、計算 BMI 和回傳結果,全部都寫在 Controller 裡面。

現在只有 BMI 一個功能,程式看起來還算簡單。

但如果之後還要加入睡眠紀錄、飲水量、運動時間,甚至 AI 功能,Controller 可能會變得越來越長。

所以今天決定先不增加新的健康功能,而是整理目前的程式架構!

今天的目標是:

認識 Controller 與 Service 的分工,並將 BMI 計算邏輯移到 Service。


一、為什麼程式需要分層?

回顧昨天的程式:

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

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

    double heightInMeters = height / 100;

    double bmi = weight / (heightInMeters * heightInMeters);

    // 整理並回傳結果
}

這樣寫其實可以正常運作。

但是仔細觀察,Controller 同時負責了很多事情:

  1. 接收 HTTP Request
  2. 取得使用者資料
  3. 執行 BMI 計算
  4. 整理回傳結果

如果未來功能越來越多,就可能讓 Controller 變得很難維護。

因此我們可以把不同工作交給不同的 Class。

例如:

Controller
    ↓
Service
    ↓
計算結果
    ↓
Controller
    ↓
Response

簡單來說:

Controller 負責接收請求與回傳結果,Service 負責處理主要的業務邏輯。


二、Controller 是什麼?

前幾天已經使用過:

@RestController

Controller 是 Spring Boot Web 應用程式中,負責處理 HTTP Request 的元件。

例如:

@PostMapping("/health/bmi")

當使用者發送:

POST /health/bmi

Spring Boot 就會找到對應的 Controller Method。

Controller 主要負責:

  • 接收 HTTP Request
  • 取得使用者輸入
  • 呼叫 Service
  • 回傳 HTTP Response

可以把 Controller 想成系統的入口。

它知道使用者想做什麼,但不一定需要自己完成所有工作。


三、Service 是什麼?

Service 通常用來放置系統的業務邏輯。

例如我們的智慧健康助理,未來可能需要:

  • 計算 BMI
  • 整理健康紀錄
  • 計算平均睡眠時間
  • 統計每日飲水量
  • 處理 AI 健康資訊摘要

這些工作就可以依照功能放進不同的 Service。

例如:

BmiService
SleepService
WaterService
AiService

這樣每個 Service 都有自己的工作。

對目前的專案來說,我們先建立:

BmiService.java

讓它專門負責 BMI 計算。


四、認識 @Service

在 Spring Boot 中,可以使用:

@Service

標記 Service 類別。

例如:

import org.springframework.stereotype.Service;

@Service
public class BmiService {

}

@Service 是 Spring 的一種 Annotation。

它可以讓 Spring 在元件掃描時辨識這個類別,並將它註冊成由 Spring 管理的 Bean。

這樣其他 Spring 元件就可以透過依賴注入使用它。

目前可以先理解成:

@Service 告訴 Spring,這是一個負責處理業務邏輯的元件。


五、建立 BmiService

接下來正式開始整理程式。

首先建立:

BmiService.java

程式如下:

import org.springframework.stereotype.Service;

@Service
public class BmiService {

    public double calculateBMI(double height, double weight) {

        if (!Double.isFinite(height) ||
            !Double.isFinite(weight) ||
            height <= 0 || weight <= 0) {

            throw new IllegalArgumentException(
                "Height and weight must be positive numbers."
            );
        }

        double heightInMeters = height / 100;

        double bmi = weight / (heightInMeters * heightInMeters);

        if (!Double.isFinite(bmi)) {
            throw new IllegalArgumentException(
                "Unable to calculate BMI."
            );
        }

        return Math.round(bmi * 100.0) / 100.0;
    }
}

這段程式其實就是把昨天寫在 Controller 裡面的 BMI 計算搬過來。

最大的差別是:

現在 BmiService 只需要負責計算 BMI,不需要知道 HTTP Request 是怎麼傳進來的。

例如:

calculateBMI(162, 50);

就可以得到:

19.05

這樣未來即使不是透過網頁呼叫,也可以重複使用這個計算方法。


六、修改 HealthController

接下來回到:

HealthController.java

原本 Controller 自己計算 BMI。

今天要改成呼叫 BmiService。

完整程式如下:

import org.springframework.http.HttpStatus;
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(
            @RequestBody HealthRequest request) {

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

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

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

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

            return ResponseEntity.ok(result);

        } catch (IllegalArgumentException e) {

            result.put("error", e.getMessage());

            return ResponseEntity
                    .status(HttpStatus.BAD_REQUEST)
                    .body(result);
        }
    }
}

這次的 Controller 比昨天更有條理。

它不需要自己寫 BMI 公式,而是透過:

bmiService.calculateBMI(height, weight);

呼叫 Service。

整個流程變成:

使用者傳送 JSON
        ↓
HealthController
        ↓
取得身高與體重
        ↓
呼叫 BmiService
        ↓
BmiService 計算 BMI
        ↓
回傳計算結果
        ↓
HealthController
        ↓
回傳 JSON

七、什麼是依賴注入?

剛剛的 Controller 出現了一段新的程式:

private final BmiService bmiService;

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

這裡使用的是 Dependency Injection(依賴注入)。

簡單來說,就是讓 Spring 幫我們提供需要使用的物件。

因為 BmiService 已經加上:

@Service

所以 Spring 可以管理這個 Service。

當 Spring 建立 HealthController 時,就可以將 BmiService 傳入建構子。

這樣我們就不需要自己寫:

BmiService bmiService = new BmiService();

而是交給 Spring 管理物件之間的依賴關係。

這種方式稱為 Constructor Injection(建構子注入)。

它也是 Spring 開發中很常見的做法。


八、認識 ResponseEntity

今天還多了一個新的東西:

ResponseEntity

前幾天我們直接回傳:

Map<String, Object>

Spring Boot 就會將它轉換成 JSON。

但今天改成:

ResponseEntity<Map<String, Object>>

原因是我們希望除了回傳資料,也能控制 HTTP Status Code。

例如成功時:

return ResponseEntity.ok(result);

代表回傳:

HTTP 200 OK

如果資料不合理:

return ResponseEntity
        .status(HttpStatus.BAD_REQUEST)
        .body(result);

就會回傳:

HTTP 400 Bad Request

這樣 API 不只可以告訴使用者結果,也能透過 HTTP 狀態碼表示請求是否成功。


九、測試新的 BMI API

接下來重新啟動 Spring Boot。

使用 Postman 發送:

POST http://localhost:8080/health/bmi

Body 選擇:

raw → JSON

輸入:

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

預期回傳:

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

HTTP Status:

200 OK

接著測試錯誤資料:

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

預期回傳:

{
  "error": "Height and weight must be positive numbers."
}

HTTP Status:

400 Bad Request

這樣就代表 Controller 和 Service 已經可以互相合作。


十、整理目前的專案結構

經過今天的調整,專案可以整理成以下結構:

health-assistant
│
├── src
│   └── main
│       ├── java
│       │   └── com.example.healthassistant
│       │       │
│       │       ├── HealthAssistantApplication.java
│       │       │
│       │       ├── controller
│       │       │   └── HealthController.java
│       │       │
│       │       ├── service
│       │       │   └── BmiService.java
│       │       │
│       │       └── dto
│       │           └── HealthRequest.java
│       │
│       └── resources
│           └── application.properties
│
└── pom.xml

這裡也順便將不同用途的 Class 放進不同的 Package。

例如:

controller

放置處理 HTTP Request 的 Controller。

service

放置處理業務邏輯的 Service。

dto

放置用來傳遞資料的物件。

其中 DTO 是 Data Transfer Object 的縮寫。

昨天建立的 HealthRequest 就可以放在這裡。

實際建立檔案時,記得讓每個 Java 檔案的 package 宣告與所在位置一致,並補上需要的 import。

另外,這些 Package 應該放在 Spring Boot 主程式所在的根 Package 之下,讓 Spring 能夠正常掃描到 Controller 與 Service。


十一、分層之後有什麼好處?

今天雖然沒有增加新的健康功能,但程式結構變得更清楚了。

1. Controller 不需要負責所有事情

以前:

Controller
   ├── 接收資料
   ├── 檢查資料
   ├── 計算 BMI
   └── 回傳結果

現在:

Controller
   ├── 接收資料
   ├── 呼叫 Service
   └── 回傳結果

Service
   ├── 檢查計算資料
   └── 計算 BMI

每個 Class 的工作比較明確。

2. 更容易維護

如果未來 BMI 計算邏輯需要修改,就可以直接修改:

BmiService.java

不一定需要更動 Controller。

3. 更容易測試

因為 BMI 計算已經獨立出來,所以未來可以直接針對:

calculateBMI()

撰寫單元測試。

不需要每次都透過 Postman 才能測試計算邏輯。

4. 方便擴充

未來加入更多功能時,可以建立:

SleepService
WaterService
ExerciseService
AiService

讓不同功能各自管理自己的邏輯。

這樣比較不容易把整個專案寫成一個非常大的 Controller。


十二、今天的程式還可以怎麼改進?

雖然今天已經將 Controller 和 Service 分開,但目前的架構還不是最完整的版本。

例如:

  • HealthRequest 尚未加入完整的欄位驗證
  • Controller 仍然使用 try-catch 處理錯誤
  • API Response 使用 Map,還沒有專門的 Response DTO
  • BMI 計算還沒有單元測試
  • 目前沒有資料庫

這些都可以留到後面的挑戰慢慢改善。

我覺得這也是 Side Project 很有趣的地方。

一開始只求功能可以運作,但隨著學到更多技術,就可以慢慢把原本的程式整理得更好。


Day 10 小結

今天沒有增加新的健康功能,而是把原本的 BMI 計算程式重新整理。

最大的收穫是了解:

Controller 負責處理 HTTP 請求,Service 負責處理業務邏輯。

今天也認識了:

  1. @Service:將類別註冊為 Spring 管理的 Service 元件
  2. Dependency Injection:讓 Spring 提供需要使用的物件
  3. Constructor Injection:透過建構子注入依賴
  4. ResponseEntity:控制 HTTP Response 與狀態碼
  5. DTO:用來傳遞資料的物件

現在我們的智慧健康助理已經從:

Controller
    ↓
BMI 計算
    ↓
Response

進化成:

Controller
    ↓
Service
    ↓
BMI 計算
    ↓
Controller
    ↓
Response

雖然看起來只是多了一個 Service,但對後續開發來說,這是一個很重要的改變。

畢竟這次的目標不是只完成一個 BMI 計算器,而是希望一步一步打造可以擴充的智慧健康助理!


明日預告

Day 11|讓資料更安全!使用 DTO 與 @Valid 驗證使用者輸入

目前雖然已經有基本的錯誤判斷,但如果使用者沒有輸入身高、輸入負數,或是傳送不完整的 JSON,程式應該怎麼處理?

下一篇準備認識:

  • DTO 的用途
  • @Valid
  • @NotNull
  • @Positive
  • Spring Boot 的資料驗證機制

讓智慧健康助理在接收使用者資料時,可以進行更完整的檢查!


上一篇
讓 Spring Boot 接收 JSON
下一篇
使用 DTO 與 @Valid 驗證使用者輸入
系列文
Java × AI:30天打造智慧健康助理 共 30 篇
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言