Controller và routing
Nội dung bài · 5 mục
- 1.Khái niệm
- 2.Ví dụ
- 3.Thử ngay
- 4.Lỗi hay gặp
- 5.Tóm tắt
Project mới chưa trả lời URL nào, còn cửa hàng cần API sản phẩm. Bài này hướng dẫn tự viết controller đầu tiên và xem Spring chọn method nào của controller để chạy khi có request tới.
Khái niệm
🎮 Controller: class đánh dấu @RestController, gom các method xử lý request cho cùng một tài nguyên.
⚡ Handler method: method trong controller gắn @GetMapping, @PostMapping…, mỗi method ứng với một cặp HTTP method và URL.
🗺️ Routing: cơ chế Spring dựa vào HTTP method và URL của request để chọn handler method sẽ chạy.
🏷️ Annotation: nhãn bắt đầu bằng @ đặt phía trên class, method hay tham số để compiler hoặc framework đọc và xử lý theo, ví dụ @Override, @GetMapping.
Ví dụ
Tạo hai file ProductsController.java và Product.java cạnh
ShopApiApplication.java:
// ProductsController.java
package com.shop.api;
import java.util.List;
import org.springframework.web.bind.annotation.*;
@RestController
@RequestMapping("/api/products")
class ProductsController {
private final List<Product> products = List.of(
new Product(1, "Pen", 5000),
new Product(2, "Notebook", 12000));
@GetMapping
public List<Product> getAll() {
return products;
}
@GetMapping("/{id}")
public Product getById(@PathVariable int id) {
return products.stream()
.filter(p -> p.getId() == id)
.findFirst()
.orElse(null);
}
}
// Product.java
package com.shop.api;
class Product {
private int id;
private String name;
private long price;
public Product(int id, String name, long price) {
this.id = id;
this.name = name;
this.price = price;
}
public int getId() { return id; }
public String getName() { return name; }
public long getPrice() { return price; }
}import org.springframework.web.bind.annotation.*;mở package chứa các annotation của controller, thiếu thì compiler báo không tìm thấy tên.@RequestMapping("/api/products")đặt đường dẫn chung cho cả controller.@GetMappingnốiGET /api/productstớigetAll.@GetMapping("/{id}")nốiGET /api/products/2tớigetById. Phần{id}trong URL được gán vào tham số có@PathVariablecùng tên.@RestControllerbáo cho Spring đây là controller, và giá trị trả về được gửi thẳng cho client dưới dạng JSON.- Spring chỉ tạo một object controller dùng cho mọi request, nên field
productslà dữ liệu chung của cả server. - Danh sách sản phẩm để tạm trong bộ nhớ. Chương 4 sẽ chuyển sang database.
Routing ghép đường dẫn của controller với đường dẫn của method để chọn handler method:
Thử ngay
Bấm Ctrl+C rồi chạy lại server, sau đó gọi lần lượt:
curl -i http://localhost:5000/api/products
curl -i http://localhost:5000/api/products/2
curl -i http://localhost:5000/api/products/99Đoán trước khi chạy: sản phẩm số 99 không có. Lần gọi cuối trả status code nào?
Xem kết quả
HTTP/1.1 200
[{"id":1,"name":"Pen","price":5000},{"id":2,"name":"Notebook","price":12000}]
HTTP/1.1 200
{"id":2,"name":"Notebook","price":12000}
HTTP/1.1 200
Content-Length: 0Không phải 404 mà là 200 với body rỗng. Method trả về null, Spring coi đó
là "thành công nhưng không có gì để gửi". Bài Trả về kết quả sẽ hướng dẫn
trả 404 đúng cách.
Lỗi hay gặp
Controller thiếu @RequestMapping. Method không còn đường dẫn chung phía
trước, nên @GetMapping nối vào GET /. Gọi GET /api/orders nhận 404.
// SAI — getAll nằm ở GET /, /api/orders trả 404
// OrdersController.java
package com.shop.api;
import org.springframework.web.bind.annotation.*;
@RestController
class OrdersController {
@GetMapping
public String getAll() {
return "orders";
}
}// ĐÚNG
// OrdersController.java
package com.shop.api;
import org.springframework.web.bind.annotation.*;
@RestController
@RequestMapping("/api/orders")
class OrdersController {
@GetMapping
public String getAll() {
return "orders";
}
}Hai method cùng HTTP method và cùng URL. Spring không biết chọn cái nào,
nên ứng dụng dừng ngay lúc khởi động với lỗi Ambiguous mapping.
// SAI — hai method đều là GET /api/customers
// CustomersController.java
package com.shop.api;
import org.springframework.web.bind.annotation.*;
@RestController
@RequestMapping("/api/customers")
class CustomersController {
@GetMapping
public String getAll() {
return "all";
}
@GetMapping
public String getActive() {
return "active";
}
}Sửa bằng cách cho method thứ hai một URL riêng.
// ĐÚNG — GET /api/customers/active
// CustomersController.java
package com.shop.api;
import org.springframework.web.bind.annotation.*;
@RestController
@RequestMapping("/api/customers")
class CustomersController {
@GetMapping
public String getAll() {
return "all";
}
@GetMapping("/active")
public String getActive() {
return "active";
}
}Tóm tắt
- Controller đánh dấu
@RestControllervà@RequestMapping. - Mỗi handler method có
@GetMapping,@PostMapping… kèm phần URL riêng nếu cần. {id}trong URL được gán vào tham số@PathVariablecùng tên.- Handler method trả về
nullthì response là 200 với body rỗng, không phải
Tự kiểm tra
0/3 câuController có @RequestMapping("/api/orders"), method có @GetMapping("/{id}/lines"). URL nào gọi tới method này?
Method getById(@PathVariable int id) trả về null khi không tìm thấy. Client nhận gì?
OrdersController có @RequestMapping("/api/orders") và method getAll() gắn @GetMapping, nhưng quên @RestController. Gọi GET /api/orders nhận gì?