Backend

99 Ngày Spring — Ngày 12: Request mapping & tham số — @PathVariable, @RequestParam

SSite Admin
8 tháng 08, 2026 5 phút đọc 49 lượt xem
99 Ngày Spring — Ngày 12: Request mapping & tham số — @PathVariable, @RequestParam

Ngày 11, API của ta trả đúng một danh sách cứng — gọi kiểu gì cũng ra bấy nhiêu. API thật phải nghe được ý người gọi: "cho tôi bài viết số 42", "tìm bài chứa spring, trang 2". Hôm nay ta học hai kênh nhận ý đó — @PathVariable trong đường dẫn và @RequestParam sau dấu hỏi — cùng chuẩn mực thiết kế URL mà người phỏng vấn rất hay soi.

Sketchnote Ngày 12: @PathVariable, @RequestParam, quy tắc mapping và thiết kế URL chuẩn REST

@PathVariable — biến trong đường dẫn

Muốn /api/posts/42 thì đường dẫn phải có chỗ trống: khai {id} trong mapping rồi hứng bằng @PathVariable. Tiện thể, gom prefix chung của cả controller lên @RequestMapping cấp class:

@RestController@RequestMapping("/api/posts")            // prefix CHUNG cho cả classpublic class PostController {

    @GetMapping                          // GET /api/posts
    public List<Post> list() { ... }

    @GetMapping("/{id}")                 // GET /api/posts/42
    public Post detail(@PathVariable Long id) {
        return postService.find(id);     // "42" → Long 42 — Spring tự đổi kiểu
    }

    @GetMapping("/{id}/comments")        // GET /api/posts/42/comments
    public List<Comment> comments(@PathVariable Long id) { ... }
}

// Đường dẫn KHÔNG khớp kiểu? GET /api/posts/abc → 400 Bad Request luôn// (Spring không đổi được "abc" thành Long — chặn từ cửa)
  • Tên {id} khớp tên tham số là đủ — khác tên thì chỉ định @PathVariable("id").

  • Khai Long id thay vì String: Spring tự đổi kiểu, và /abc bị chặn 400 trước khi vào method — validation miễn phí tầng đầu.

  • Prefix cấp class là thói quen sạch: đổi /api/posts thành /api/v2/posts chỉ sửa một dòng.

@RequestParam — tham số sau dấu hỏi

Kênh thứ hai: query string — phần ?keyword=java&page=2 — dành cho lọc, phân trang, sắp xếp:

// GET /api/posts?keyword=java&page=2&size=10@GetMappingpublic List<Post> search(
        @RequestParam String keyword,                       // BẮT BUỘC — thiếu là 400
        @RequestParam(defaultValue = "0") int page,         // tùy chọn + mặc định
        @RequestParam(defaultValue = "10") int size,
        @RequestParam(required = false) String author) {    // tùy chọn, có thể null
    return postService.search(keyword, author, page, size);
}

// ?tag=java&tag=spring → gom vào danh sách:@GetMapping("/by-tags")public List<Post> byTags(@RequestParam List<String> tag) { ... }

// Mẹo: khai kiểu đúng (int, boolean, LocalDate...) — Spring convert giúp,// sai định dạng thì 400 ngay, khỏi tự parse chuỗi trong controller
  • Ba mức linh hoạt: bắt buộc (mặc định — thiếu là 400), defaultValue (vắng thì điền sẵn), required = false (vắng thì null — nhớ Ngày 09 mà phòng thân).

  • Bộ ba keyword/page/size chính là hình hài phân trang chuẩn — Ngày 18 nâng cấp lên Pageable của Spring Data.

  • Đừng nhận String rồi tự parseInt — khai đúng kiểu để Spring convert và trả lỗi chuẩn giúp bạn.

Chọn kênh nào? Định danh vs bộ lọc

Câu hỏi kinh điển: khi nào đường dẫn, khi nào query? Quy tắc một câu: định danh tài nguyên vào path, tùy chọn vào query:

// PATH VARIABLE — định danh tài nguyên: "CON NÀO?"
GET /api/posts/42            // bài viết số 42 — thiếu id thì URL vô nghĩa
GET /api/users/nam/posts     // bài của user nam

// REQUEST PARAM — bộ lọc/tùy chọn: "LỌC/SẮP XẾP THẾ NÀO?"
GET /api/posts?page=2&size=10&sort=views     // bỏ hết vẫn có nghĩa
GET /api/posts?keyword=spring&author=nam

// Đặt cạnh nhau cho quen mắt:
GET /api/posts/42/comments?page=0&size=20
//            └─ định danh ─┘└──── tùy chọn ────┘

// Quy ước URL tử tế:
//  ✓ danh từ SỐ NHIỀU: /posts, /users — KHÔNG động từ: /getPosts ✗
//  ✓ phân cấp theo quan hệ: /posts/42/comments
//  ✓ kebab-case: /api/reading-lists — không camelCase trong URL
  • Tự kiểm: bỏ hết query mà URL vẫn trỏ đúng "thứ đó" → thiết kế ổn; còn /api/posts/page/2 là mùi thiết kế — trang 2 không phải một tài nguyên.

  • URL là hợp đồng công khai của API — đổi sau này là phá client; đầu tư đặt tên từ hôm nay rẻ hơn nhiều so với đổi về sau.

Quy tắc mapping cần nhớ

// Một method — một cặp (động từ + đường dẫn). Trùng nhau là Boot từ chối chạy:// Ambiguous mapping. Cannot map 'postController' method ... — lỗi lúc KHỞI ĐỘNG, may!

@GetMapping("/{id}")         // GET  /api/posts/42  → đọc@PostMapping                 // POST /api/posts     → tạo (Ngày 13!)@PutMapping("/{id}")         // PUT  /api/posts/42  → sửa@DeleteMapping("/{id}")      // DELETE /api/posts/42 → xóa

// Cùng đường dẫn, KHÁC động từ → hợp lệ, và chính là bộ mặt REST chuẩn// Path cụ thể thắng path có biến: /posts/latest được thử TRƯỚC /posts/{id}
  • Trùng (động từ + path) là sập lúc khởi động — lỗi ồn ào là lỗi rẻ; đỡ hơn nhiều so với lặng lẽ gọi nhầm method.

  • Cùng path khác động từ là chuẩn REST: GET /posts/42 đọc, DELETE /posts/42 xóa — Ngày 13–15 lấp nốt POST/PUT với body và status code.

  • Boot còn tự xử lý tinh tế: /posts/latest cụ thể hơn nên được ưu tiên trước /posts/{id} — cứ khai, không cần lo thứ tự method.

Bài tập nhỏ

  • Thêm GET /api/books/{id} vào BookController hôm qua — thử /api/books/abc và quan sát 400.

  • Viết GET /api/books?minPrice=&maxPrice= lọc theo khoảng giá — dùng defaultValue cho hai đầu.

  • Thiết kế URL cho: "các bình luận của bài 42, trang 2, sắp theo mới nhất" — viết mapping đầy đủ.

  • Cố ý khai hai @GetMapping("/api/books") — đọc kỹ lỗi Ambiguous mapping để nhận diện sau này.

Kết luận

API đã biết lắng nghe: @PathVariable hứng định danh trong đường dẫn, @RequestParam hứng bộ lọc sau dấu hỏi, Spring convert kiểu và chặn dữ liệu xấu từ cửa, còn URL theo chuẩn danh-từ-số-nhiều phân cấp là bộ mặt chuyên nghiệp của service. Nhưng GET chỉ đọc — muốn tạo mới tài nguyên, dữ liệu phải đi trong request body. Ngày 13: @RequestBody, DTO, và vì sao đừng bao giờ phơi entity ra ngoài. Hẹn gặp lại!

S

Site Admin

Engineer and writer. Building things with TypeScript and distributed systems.

Bình luận (0)

Bạn cần đăng nhập bằng Google để bình luận.

Hãy là người bình luận đầu tiên.

Bài viết liên quan

99 Ngày Java — Ngày 58: Optional

Optional là công cụ cho kiểu trả về — không field, không tham số, không bọc collection; chuỗi map/flatMap/filter thay kim tự tháp if; orElse luôn tính tham số còn orElseGet lười, orElseThrow cho “không có là lỗi” — và các anti-pattern isPresent + get, orElse(null).

23 thg 9, 20269 phút9
99 Ngày Spring — Ngày 58: Spring Boot Actuator

Boot 3 mặc định chỉ lộ health; bản đồ endpoint an toàn và nguy hiểm; HealthIndicator tự viết với liveness tách khỏi readiness cho Kubernetes, info từ build và git, metrics Micrometer với tag hữu hạn — và ba lớp khóa: cổng riêng, SecurityFilterChain với EndpointRequest, exclude env/heapdump/threaddump.

23 thg 9, 202610 phút5
So sánh EasyExcel và Apache Fesod: khác biệt thật nằm ở đâu?

Fesod là hậu duệ của EasyExcel, cùng engine SAX/SXSSF. Khác biệt kiểm chứng được: vòng đời dự án, POI 5.2.5 vs 5.5.1, các API mới (numRows, csv(), headerMergeStrategy) và độ bền với file xấu — không phải hiệu năng gấp nhiều lần.

23 thg 9, 20268 phút1