DEV Community

Cyrus Tse
Cyrus Tse

Posted on

Spring Boot 4 入門教學 - Part 5 (HelloWorld REST API)

第五章:建立 HelloWorld REST API

5.1 前言:構建你的第一個 REST API

在這一章節中,我們將學習如何使用 Spring Boot 創建 REST API。REST(Representational State Transfer)是一種軟體架構風格,用於設計網絡應用程式的接口。Spring Boot 提供了強大的支援,讓你可以輕鬆地構建符合 REST 原則的 API。

我們將從最簡單的「Hello World」API 開始,逐步擴展到包含不同類型端點的完整 API。這個過程中,你將學習到 Spring Boot Web 開發的核心概念和最佳實踐。

5.2 @RestController 和 @GetMapping 詳解

@RestController 是 Spring Web 開發中最常用的註解之一。它是 @Controller@ResponseBody 的組合,表示這個類是一個 REST 控制器,其方法的返回值會直接作為 HTTP 響應的內容。

package com.example.myfirstapp.controller;

import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class HelloController {

    /**
     * 最簡單的 GET API
     * 訪問: GET /hello
     */
    @GetMapping("/hello")
    public String hello() {
        return "Hello, Spring Boot!";
    }

    /**
     * 帶查詢參數的 API
     * 訪問: GET /hello?name=John
     */
    @GetMapping("/greet")
    public String greet(
            @RequestParam(name = "name", defaultValue = "World") String name) {
        return String.format("Hello, %s! Welcome to Spring Boot!", name);
    }

    /**
     * 帶路徑參數的 API
     * 訪問: GET /hello/John
     */
    @GetMapping("/hello/{name}")
    public String helloPath(
            @PathVariable(name = "name") String name) {
        return String.format("Hello, %s!", name);
    }
}
Enter fullscreen mode Exit fullscreen mode

讓我們詳細分析這個範例:

@RestController 註解標記 HelloController 類為 REST 控制器。當 Spring 處理 HTTP 請求時,它會找到對應的控制器方法並執行。

@GetMapping("/hello") 指定這個方法處理 /hello 的 GET 請求。Spring MVC 會將方法的返回值轉換為 HTTP 響應體。在這個例子中,返回的是 String,Spring 會直接將其寫入響應體。

@RequestParam 用於提取查詢參數。name 參數對應 URL 中的 ?name=John 部分。defaultValue = "World" 指定當沒有提供 name 參數時使用 "World" 作為預設值。

@PathVariable 用於提取路徑參數。在 /hello/{name} 中,{name} 是一個路徑變數,可以通過 @PathVariable 獲取其值。

5.3 HTTP 請求方法對照表

Spring Boot 支援所有 HTTP 請求方法。以下是常見的請求方法對照:

@RestController
@RequestMapping("/api/users")
public class UserController {

    /**
     * GET - 獲取資源
     * 查詢所有用戶: GET /api/users
     * 查詢指定用戶: GET /api/users/{id}
     */
    @GetMapping
    public List<User> getAllUsers() {
        return userService.findAll();
    }

    @GetMapping("/{id}")
    public User getUser(@PathVariable Long id) {
        return userService.findById(id);
    }

    /**
     * POST - 創建新資源
     * 創建用戶: POST /api/users
     * 請求體包含用戶數據
     */
    @PostMapping
    public User createUser(@RequestBody User user) {
        return userService.save(user);
    }

    /**
     * PUT - 更新資源(完整更新)
     * 更新用戶: PUT /api/users/{id}
     */
    @PutMapping("/{id}")
    public User updateUser(
            @PathVariable Long id,
            @RequestBody User user) {
        user.setId(id);
        return userService.save(user);
    }

    /**
     * PATCH - 部分更新資源
     * 部分更新用戶: PATCH /api/users/{id
     */
    @PatchMapping("/{id}")
    public User partialUpdateUser(
            @PathVariable Long id,
            @RequestBody Map<String, Object> updates) {
        return userService.partialUpdate(id, updates);
    }

    /**
     * DELETE - 刪除資源
     * 刪除用戶: DELETE /api/users/{id}
     */
    @DeleteMapping("/{id}")
    public void deleteUser(@PathVariable Long id) {
        userService.deleteById(id);
    }
}
Enter fullscreen mode Exit fullscreen mode

理解 HTTP 請求方法的語義很重要:

  • GET:用於獲取資源,應該是冪等的(多次調用結果相同)
  • POST:用於創建資源,不一定是冪等的
  • PUT:用於完整更新資源,是冪等的
  • PATCH:用於部分更新資源,是冪等的
  • DELETE:用於刪除資源,是冪等的

5.4 @RequestBody 和 @ResponseBody 詳解

@RequestBody@ResponseBody 是處理 HTTP 請求和響應體的核心註解。

@RequestBody 將 HTTP 請求體自動轉換為 Java 物件:

@PostMapping("/users")
public User createUser(@RequestBody User user) {
    // Spring 會自動將 JSON 請求體解析為 User 物件
    return userService.save(user);
}
Enter fullscreen mode Exit fullscreen mode

@ResponseBody 將 Java 物件自動轉換為 HTTP 響應體(@RestController 已包含此功能):

@PostMapping("/users")
public ResponseEntity<User> createUser(@RequestBody User user) {
    User saved = userService.save(user);
    return ResponseEntity.ok(saved);
}
Enter fullscreen mode Exit fullscreen mode

讓我們創建一個完整的 DTO 類來演示複雜的請求/響應處理:

package com.example.myfirstapp.dto;

import jakarta.validation.constraints.Email;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.Size;
import java.time.LocalDateTime;

// 創建用戶請求 DTO
public class CreateUserRequest {

    @NotBlank(message = "用戶名不能為空")
    @Size(min = 3, max = 20, message = "用戶名長度必須在 3-20 之間")
    private String username;

    @NotBlank(message = "電子郵件不能為空")
    @Email(message = "電子郵件格式不正確")
    private String email;

    @NotBlank(message = "密碼不能為空")
    @Size(min = 8, message = "密碼長度至少為 8 個字元")
    private String password;

    private String fullName;

    // getters and setters
    public String getUsername() { return username; }
    public void setUsername(String username) { this.username = username; }
    public String getEmail() { return email; }
    public void setEmail(String email) { this.email = email; }
    public String getPassword() { return password; }
    public void setPassword(String password) { this.password = password; }
    public String getFullName() { return fullName; }
    public void setFullName(String fullName) { this.fullName = fullName; }
}

// 用戶響應 DTO
public class UserResponse {

    private Long id;
    private String username;
    private String email;
    private String fullName;
    private LocalDateTime createdAt;
    private String status;

    // getters and setters
    public Long getId() { return id; }
    public void setId(Long id) { this.id = id; }
    public String getUsername() { return username; }
    public void setUsername(String username) { this.username = username; }
    public String getEmail() { return email; }
    public void setEmail(String email) { this.email = email; }
    public String getFullName() { return fullName; }
    public void setFullName(String fullName) { this.fullName = fullName; }
    public LocalDateTime getCreatedAt() { return createdAt; }
    public void setCreatedAt(LocalDateTime createdAt) { this.createdAt = createdAt; }
    public String getStatus() { return status; }
    public void setStatus(String status) { this.status = status; }

    // 靜態工廠方法
    public static UserResponse fromEntity(User user) {
        UserResponse response = new UserResponse();
        response.setId(user.getId());
        response.setUsername(user.getUsername());
        response.setEmail(user.getEmail());
        response.setFullName(user.getFullName());
        response.setCreatedAt(user.getCreatedAt());
        response.setStatus(user.isActive() ? "ACTIVE" : "INACTIVE");
        return response;
    }
}

// API 統一響應格式
public class ApiResponse<T> {
    private boolean success;
    private String message;
    private T data;
    private LocalDateTime timestamp;
    private String traceId;

    public static <T> ApiResponse<T> success(T data) {
        ApiResponse<T> response = new ApiResponse<>();
        response.setSuccess(true);
        response.setMessage("操作成功");
        response.setData(data);
        response.setTimestamp(LocalDateTime.now());
        response.setTraceId(UUID.randomUUID().toString().substring(0, 8));
        return response;
    }

    public static <T> ApiResponse<T> error(String message) {
        ApiResponse<T> response = new ApiResponse<>();
        response.setSuccess(false);
        response.setMessage(message);
        response.setData(null);
        response.setTimestamp(LocalDateTime.now());
        response.setTraceId(UUID.randomUUID().toString().substring(0, 8));
        return response;
    }

    // getters and setters
    public boolean isSuccess() { return success; }
    public void setSuccess(boolean success) { this.success = success; }
    public String getMessage() { return message; }
    public void setMessage(String message) { this.message = message; }
    public T getData() { return data; }
    public void setData(T data) { this.data = data; }
    public LocalDateTime getTimestamp() { return timestamp; }
    public void setTimestamp(LocalDateTime timestamp) { this.timestamp = timestamp; }
    public String getTraceId() { return traceId; }
    public void setTraceId(String traceId) { this.traceId = traceId; }
}
Enter fullscreen mode Exit fullscreen mode

5.5 完整的 User API 實作

讓我們創建一個完整的 User API,包含 CRUD 操作:

package com.example.myfirstapp.controller;

import com.example.myfirstapp.dto.ApiResponse;
import com.example.myfirstapp.dto.CreateUserRequest;
import com.example.myfirstapp.dto.UserResponse;
import com.example.myfirstapp.entity.User;
import com.example.myfirstapp.service.UserService;
import jakarta.validation.Valid;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;

import java.util.List;
import java.util.stream.Collectors;

@RestController
@RequestMapping("/api/users")
public class UserController {

    private final UserService userService;

    public UserController(UserService userService) {
        this.userService = userService;
    }

    /**
     * 獲取所有用戶
     * GET /api/users
     */
    @GetMapping
    public ResponseEntity<ApiResponse<List<UserResponse>>> getAllUsers() {
        List<User> users = userService.findAll();
        List<UserResponse> responses = users.stream()
            .map(UserResponse::fromEntity)
            .collect(Collectors.toList());
        return ResponseEntity.ok(ApiResponse.success(responses));
    }

    /**
     * 獲取指定用戶
     * GET /api/users/{id}
     */
    @GetMapping("/{id}")
    public ResponseEntity<ApiResponse<UserResponse>> getUser(@PathVariable Long id) {
        User user = userService.findById(id);
        return ResponseEntity.ok(ApiResponse.success(UserResponse.fromEntity(user)));
    }

    /**
     * 創建用戶
     * POST /api/users
     */
    @PostMapping
    public ResponseEntity<ApiResponse<UserResponse>> createUser(
            @Valid @RequestBody CreateUserRequest request) {

        // 檢查用戶名是否已存在
        if (userService.existsByUsername(request.getUsername())) {
            return ResponseEntity
                .status(HttpStatus.BAD_REQUEST)
                .body(ApiResponse.error("用戶名已存在"));
        }

        // 檢查電子郵件是否已存在
        if (userService.existsByEmail(request.getEmail())) {
            return ResponseEntity
                .status(HttpStatus.BAD_REQUEST)
                .body(ApiResponse.error("電子郵件已存在"));
        }

        // 創建用戶
        User user = new User();
        user.setUsername(request.getUsername());
        user.setEmail(request.getEmail());
        user.setPassword(request.getPassword()); // 注意:實際應用中應該加密
        user.setFullName(request.getFullName());

        User saved = userService.save(user);
        return ResponseEntity
            .status(HttpStatus.CREATED)
            .body(ApiResponse.success(UserResponse.fromEntity(saved)));
    }

    /**
     * 更新用戶
     * PUT /api/users/{id}
     */
    @PutMapping("/{id}")
    public ResponseEntity<ApiResponse<UserResponse>> updateUser(
            @PathVariable Long id,
            @Valid @RequestBody CreateUserRequest request) {

        User existing = userService.findById(id);

        // 更新用戶信息
        existing.setUsername(request.getUsername());
        existing.setEmail(request.getEmail());
        existing.setPassword(request.getPassword());
        existing.setFullName(request.getFullName());

        User updated = userService.save(existing);
        return ResponseEntity.ok(ApiResponse.success(UserResponse.fromEntity(updated)));
    }

    /**
     * 刪除用戶
     * DELETE /api/users/{id}
     */
    @DeleteMapping("/{id}")
    public ResponseEntity<ApiResponse<Void>> deleteUser(@PathVariable Long id) {
        userService.deleteById(id);
        return ResponseEntity.ok(ApiResponse.success(null));
    }

    /**
     * 搜索用戶
     * GET /api/users/search?name=john
     */
    @GetMapping("/search")
    public ResponseEntity<ApiResponse<List<UserResponse>>> searchUsers(
            @RequestParam(name = "name", required = false) String name) {
        List<User> users;
        if (name != null && !name.isEmpty()) {
            users = userService.searchByName(name);
        } else {
            users = userService.findAll();
        }
        List<UserResponse> responses = users.stream()
            .map(UserResponse::fromEntity)
            .collect(Collectors.toList());
        return ResponseEntity.ok(ApiResponse.success(responses));
    }
}
Enter fullscreen mode Exit fullscreen mode

5.6 異常處理

一個良好的 API 應該有完善的異常處理機制。讓我們創建一個全局異常處理器:

package com.example.myfirstapp.config;

import com.example.myfirstapp.dto.ApiResponse;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.validation.FieldError;
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 {

    /**
     * 處理參數驗證錯誤
     */
    @ExceptionHandler(MethodArgumentNotValidException.class)
    public ResponseEntity<ApiResponse<Map<String, String>>> handleValidationExceptions(
            MethodArgumentNotValidException ex) {

        Map<String, String> errors = new HashMap<>();
        ex.getBindingResult().getAllErrors().forEach((error) -> {
            String fieldName = ((FieldError) error).getField();
            String errorMessage = error.getDefaultMessage();
            errors.put(fieldName, errorMessage);
        });

        ApiResponse<Map<String, String>> response = new ApiResponse<>();
        response.setSuccess(false);
        response.setMessage("參數驗證失敗");
        response.setData(errors);

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

    /**
     * 處理資源不存在異常
     */
    @ExceptionHandler(ResourceNotFoundException.class)
    public ResponseEntity<ApiResponse<Void>> handleResourceNotFound(
            ResourceNotFoundException ex) {
        return ResponseEntity
            .status(HttpStatus.NOT_FOUND)
            .body(ApiResponse.error(ex.getMessage()));
    }

    /**
     * 處理非法參數異常
     */
    @ExceptionHandler(IllegalArgumentException.class)
    public ResponseEntity<ApiResponse<Void>> handleIllegalArgument(
            IllegalArgumentException ex) {
        return ResponseEntity
            .status(HttpStatus.BAD_REQUEST)
            .body(ApiResponse.error(ex.getMessage()));
    }

    /**
     * 處理通用異常
     */
    @ExceptionHandler(Exception.class)
    public ResponseEntity<ApiResponse<Void>> handleGenericException(Exception ex) {
        return ResponseEntity
            .status(HttpStatus.INTERNAL_SERVER_ERROR)
            .body(ApiResponse.error("系統錯誤,請稍後再試"));
    }
}

// 自定義異常類
class ResourceNotFoundException extends RuntimeException {

    public ResourceNotFoundException(String message) {
        super(message);
    }

    public ResourceNotFoundException(String resourceType, Long id) {
        super(String.format("%s not found with id: %d", resourceType, id));
    }
}
Enter fullscreen mode Exit fullscreen mode

5.7 測試 REST API

讓我們使用 curl 命令測試我們的 API:

# 啟動應用程式後執行以下命令

# 1. 獲取所有用戶
curl -X GET http://localhost:8080/api/users

# 2. 創建新用戶
curl -X POST http://localhost:8080/api/users \
  -H "Content-Type: application/json" \
  -d '{
    "username": "john",
    "email": "john@example.com",
    "password": "password123",
    "fullName": "John Doe"
  }'

# 3. 獲取指定用戶(假設 ID 為 1)
curl -X GET http://localhost:8080/api/users/1

# 4. 更新用戶
curl -X PUT http://localhost:8080/api/users/1 \
  -H "Content-Type: application/json" \
  -d '{
    "username": "johnny",
    "email": "johnny@example.com",
    "password": "newpassword456",
    "fullName": "Johnny Doe"
  }'

# 5. 搜索用戶
curl -X GET "http://localhost:8080/api/users/search?name=john"

# 6. 刪除用戶
curl -X DELETE http://localhost:8080/api/users/1

# 7. 測試參數驗證(空的用戶名)
curl -X POST http://localhost:8080/api/users \
  -H "Content-Type: application/json" \
  -d '{
    "username": "",
    "email": "invalid-email",
    "password": "123"
  }'
Enter fullscreen mode Exit fullscreen mode

Top comments (0)