Chuyển đến nội dung chính

第 5 課:REST API 基礎 — @RestController 與請求映射

HTTP 方法、@RestController、@RequestMapping、@GetMapping、@PostMapping。路徑變數、查詢參數、請求標頭。響應實體和狀態代碼。

💻 程式設計 — 第 4 課 第 5 課:REST API 基礎 — @RestController & 請求映射

Spring Boot 4:從基礎到高級

第 2 部分:建立 REST API

亞洲開發網

簡介

REST API 是現代後端應用程式中最常見的通訊方法。 Spring Boot 提供了 Spring Web MVC——一個用於建立 RESTful API 的強大且成熟的框架。本文將指導使用控制器時從基礎到進階的技術。


1. HTTP 方法與 REST 約定

1.1 RESTful 資源映射

Resource: /api/users

GET    /api/users          → Lấy danh sách users
GET    /api/users/{id}     → Lấy user theo ID
POST   /api/users          → Tạo user mới
PUT    /api/users/{id}     → Cập nhật toàn bộ user
PATCH  /api/users/{id}     → Cập nhật một phần user
DELETE /api/users/{id}     → Xóa user

1.2 重要的 HTTP 狀態碼

程式碼意義何時使用
200200好的取得、放置、修補成功
201201建立POST 建立資源成功
204204沒有內容刪除成功
400錯誤的請求輸入無效
401401未經授權未經驗證
403403禁止沒有權限
404404未找到資源不存在
409409衝突重複資料(重複電子郵件)
500500內部伺服器錯誤伺服器錯誤

2. @RestController & 請求映射

2.1 基本控制器

@RestController
@RequestMapping("/api/v1/products")
public class ProductController {

    private final ProductService productService;

    public ProductController(ProductService productService) {
        this.productService = productService;
    }

    // GET /api/v1/products
    @GetMapping
    public List<Product> getAllProducts() {
        return productService.findAll();
    }

    // GET /api/v1/products/123
    @GetMapping("/{id}")
    public Product getProduct(@PathVariable Long id) {
        return productService.findById(id);
    }

    // POST /api/v1/products
    @PostMapping
    @ResponseStatus(HttpStatus.CREATED)
    public Product createProduct(@RequestBody Product product) {
        return productService.create(product);
    }

    // PUT /api/v1/products/123
    @PutMapping("/{id}")
    public Product updateProduct(@PathVariable Long id,
                                  @RequestBody Product product) {
        return productService.update(id, product);
    }

    // DELETE /api/v1/products/123
    @DeleteMapping("/{id}")
    @ResponseStatus(HttpStatus.NO_CONTENT)
    public void deleteProduct(@PathVariable Long id) {
        productService.delete(id);
    }
}

2.2 @RestController 與 @Controller

// @RestController = @Controller + @ResponseBody
// Mọi method tự động serialize return value thành JSON

@RestController // Dùng cho REST API
public class ApiController {
    @GetMapping("/api/data")
    public Map<String, String> getData() {
        return Map.of("key", "value"); // → JSON response
    }
}

@Controller // Dùng cho server-side rendering (Thymeleaf)
public class WebController {
    @GetMapping("/home")
    public String home(Model model) {
        model.addAttribute("title", "Home");
        return "home"; // → Render template home.html
    }
}

3. 接收來自Request的數據

3.1 路徑變數

@GetMapping("/users/{userId}/orders/{orderId}")
public Order getOrder(
        @PathVariable Long userId,
        @PathVariable("orderId") Long orderId) { // Custom name mapping
    return orderService.findByUserAndId(userId, orderId);
}

// Optional path variable
@GetMapping({"/files", "/files/{filename}"})
public String getFile(
        @PathVariable(required = false) String filename) {
    return filename != null ? filename : "index.html";
}

3.2 查詢參數

// GET /api/products?category=electronics&minPrice=100&maxPrice=500
@GetMapping
public List<Product> searchProducts(
        @RequestParam String category,
        @RequestParam(defaultValue = "0") double minPrice,
        @RequestParam(defaultValue = "999999") double maxPrice,
        @RequestParam(required = false) String brand) {
    return productService.search(category, minPrice, maxPrice, brand);
}

// Nhận tất cả params dưới dạng Map
@GetMapping("/search")
public List<Product> search(@RequestParam Map<String, String> params) {
    return productService.searchByParams(params);
}

3.3 請求頭

@GetMapping("/api/profile")
public UserProfile getProfile(
        @RequestHeader("Authorization") String authHeader,
        @RequestHeader(value = "Accept-Language", defaultValue = "vi") String lang) {
    String token = authHeader.replace("Bearer ", "");
    return userService.getProfile(token, lang);
}

3.4 請求正文

// POST /api/users
// Content-Type: application/json
// Body: {"name": "Duy", "email": "[email protected]"}
@PostMapping
public User createUser(@RequestBody CreateUserRequest request) {
    return userService.create(request);
}

// Record class cho request
public record CreateUserRequest(
    String name,
    String email,
    String password
) {}

4. ResponseEntity — 回應控制

4.1 基礎知識

@GetMapping("/{id}")
public ResponseEntity<Product> getProduct(@PathVariable Long id) {
    return productService.findById(id)
        .map(ResponseEntity::ok)
        .orElse(ResponseEntity.notFound().build());
}

@PostMapping
public ResponseEntity<Product> createProduct(@RequestBody Product product) {
    Product created = productService.create(product);
    URI location = URI.create("/api/v1/products/" + created.getId());

    return ResponseEntity
        .created(location)  // 201 Created + Location header
        .body(created);
}

@DeleteMapping("/{id}")
public ResponseEntity<Void> deleteProduct(@PathVariable Long id) {
    productService.delete(id);
    return ResponseEntity.noContent().build(); // 204 No Content
}

4.2 自訂標頭

@GetMapping("/download/{fileId}")
public ResponseEntity<byte[]> downloadFile(@PathVariable String fileId) {
    FileData file = fileService.getFile(fileId);

    return ResponseEntity.ok()
        .header(HttpHeaders.CONTENT_DISPOSITION,
                "attachment; filename=\"" + file.name() + "\"")
        .header(HttpHeaders.CONTENT_TYPE, file.contentType())
        .header("X-File-Size", String.valueOf(file.size()))
        .body(file.data());
}

5. 內容協商

5.1 生產與消費

@RestController
@RequestMapping("/api/v1/reports")
public class ReportController {

    // Chỉ chấp nhận JSON input, trả về JSON
    @PostMapping(
        consumes = MediaType.APPLICATION_JSON_VALUE,
        produces = MediaType.APPLICATION_JSON_VALUE
    )
    public Report createJsonReport(@RequestBody ReportRequest request) {
        return reportService.generate(request);
    }

    // Trả về CSV
    @GetMapping(value = "/{id}/csv", produces = "text/csv")
    public String exportCsv(@PathVariable Long id) {
        return reportService.exportCsv(id);
    }
}

6. API 版本控制策略

6.1 URL 路徑版本控制(建議)

@RestController
@RequestMapping("/api/v1/users")
public class UserControllerV1 {
    @GetMapping("/{id}")
    public UserV1Response getUser(@PathVariable Long id) { }
}

@RestController
@RequestMapping("/api/v2/users")
public class UserControllerV2 {
    @GetMapping("/{id}")
    public UserV2Response getUser(@PathVariable Long id) { }
}

6.2 標頭版本控制

@GetMapping(value = "/users/{id}", headers = "X-API-Version=1")
public UserV1Response getUserV1(@PathVariable Long id) { }

@GetMapping(value = "/users/{id}", headers = "X-API-Version=2")
public UserV2Response getUserV2(@PathVariable Long id) { }

7. 真實範例:完整的 CRUD API

@RestController
@RequestMapping("/api/v1/tasks")
public class TaskController {

    private final TaskService taskService;

    public TaskController(TaskService taskService) {
        this.taskService = taskService;
    }

    @GetMapping
    public ResponseEntity<List<TaskResponse>> getAllTasks(
            @RequestParam(defaultValue = "all") String status) {
        List<TaskResponse> tasks = taskService.findByStatus(status);
        return ResponseEntity.ok(tasks);
    }

    @GetMapping("/{id}")
    public ResponseEntity<TaskResponse> getTask(@PathVariable Long id) {
        return taskService.findById(id)
            .map(ResponseEntity::ok)
            .orElse(ResponseEntity.notFound().build());
    }

    @PostMapping
    public ResponseEntity<TaskResponse> createTask(
            @RequestBody CreateTaskRequest request) {
        TaskResponse task = taskService.create(request);
        URI location = ServletUriComponentsBuilder
            .fromCurrentRequest()
            .path("/{id}")
            .buildAndExpand(task.id())
            .toUri();
        return ResponseEntity.created(location).body(task);
    }

    @PatchMapping("/{id}")
    public ResponseEntity<TaskResponse> updateTask(
            @PathVariable Long id,
            @RequestBody UpdateTaskRequest request) {
        return taskService.update(id, request)
            .map(ResponseEntity::ok)
            .orElse(ResponseEntity.notFound().build());
    }

    @DeleteMapping("/{id}")
    public ResponseEntity<Void> deleteTask(@PathVariable Long id) {
        taskService.delete(id);
        return ResponseEntity.noContent().build();
    }
}

// Request/Response records
public record CreateTaskRequest(String title, String description) {}
public record UpdateTaskRequest(String title, String description, String status) {}
public record TaskResponse(Long id, String title, String description,
                           String status, LocalDateTime createdAt) {}

總結

  • @RestController 結合@Controller + @ResponseBody,自動將反應序列化為JSON
  • 使用@GetMapping、@PostMapping、@PutMapping、@PatchMapping、@DeleteMapping進行CRUD操作
  • ResponseEntity 允許完全控制 HTTP 狀態碼、標頭和正文
  • 為了簡單和清晰起見,API 版本控制應使用 URL 路徑 (/api/v1/)

練習

1.為實體建立CRUD REST API Book (id、標題、作者、isbn、價格、publishedYear)具有完整的 HTTP 方法 2. 實作搜尋端點: GET /api/books?author=X&minPrice=Y&maxPrice=Z 帶有所有可選參數 3. 使用 ResponseEntity 傳回正確的狀態碼(201 Created with Location header,204 No Content 為 DELETE,404 為資源不存在)