第五章:建立 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);
}
}
讓我們詳細分析這個範例:
@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);
}
}
理解 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);
}
@ResponseBody 將 Java 物件自動轉換為 HTTP 響應體(@RestController 已包含此功能):
@PostMapping("/users")
public ResponseEntity<User> createUser(@RequestBody User user) {
User saved = userService.save(user);
return ResponseEntity.ok(saved);
}
讓我們創建一個完整的 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; }
}
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));
}
}
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));
}
}
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"
}'
Top comments (0)