Backend

99 Ngày Spring — Ngày 11: REST controller đầu tiên

SSite Admin
7 tháng 08, 2026 5 phút đọc 54 lượt xem
99 Ngày Spring — Ngày 11: REST controller đầu tiên

Mười ngày đầu ta mài giũa "nội công": container, bean, DI, cấu hình. Hôm nay bắt đầu Giai đoạn 2 — xây REST API: làm thứ mà trình duyệt, app di động hay service khác thật sự gọi được. Nhân vật chính là @RestController — và bạn sẽ thấy chỉ cần vài dòng: không XML, không servlet tay, không cấu hình server — mọi nền móng Ngày 02–10 giờ gặt quả.

Sketchnote Ngày 11: REST controller đầu tiên — @RestController, @GetMapping, JSON tự động và hành trình request

REST 30 giây: tài nguyên & động từ

  • REST nói gọn: mọi thứ là tài nguyên định danh bằng URL (/api/posts, /api/posts/1), thao tác bằng động từ HTTP: GET đọc, POST tạo, PUT sửa, DELETE xóa.

  • Dữ liệu trao đổi mặc định là JSON — nhẹ, mọi ngôn ngữ đọc được; server và client nhờ đó tách rời hoàn toàn (web, mobile, service khác đều xài chung một API).

  • Hôm nay ta tập trung GET — các động từ còn lại lần lượt xuất hiện trong tuần này.

Controller đầu tiên: 5 dòng là chạy

@RestController                          // = @Controller + @ResponseBodypublic class HelloController {

    @GetMapping("/hello")                // GET /hello → chạy method này
    public String hello() {
        return "Xin chào từ Spring Boot!";   // chuỗi trả thẳng vào response body
    }
}

// Chạy app (Ngày 02) rồi mở trình duyệt / curl:// GET http://localhost:8080/hello// → 200 OK, body: Xin chào từ Spring Boot!
  • @RestController là stereotype (họ hàng @Component — Ngày 05): component scan nhặt nó thành bean, kiêm lời hứa "giá trị method trả về đi thẳng vào response body".

  • @GetMapping("/hello") khắc biển chỉ đường: GET tới /hello thì gọi method này. Nó là dạng gọn của @RequestMapping(method = GET).

  • Không cần main nào khác, không đăng ký gì thêm — chạy app là endpoint sống. So với thời Java EE phải viết servlet + web.xml, đây chính là lý do Spring Boot thắng cuộc (Ngày 01).

Trả JSON: Jackson lo phần nặng nhọc

Chuỗi thô mới là khởi động — API thật trả dữ liệu có cấu trúc. Điều tuyệt nhất: bạn cứ trả object Java, phần còn lại đã có người lo:

public record Post(Long id, String title, boolean published) { }

@RestControllerpublic class PostController {

    @GetMapping("/api/posts")
    public List<Post> list() {           // trả OBJECT — không phải String!
        return List.of(
            new Post(1L, "Ngày 11: REST controller", true),
            new Post(2L, "Bản nháp ngày mai", false)
        );
    }
}

// GET /api/posts → Jackson TỰ serialize thành JSON:// [//   { "id": 1, "title": "Ngày 11: REST controller", "published": true },//   { "id": 2, "title": "Bản nháp ngày mai", "published": false }// ]// Content-Type: application/json — không viết một dòng chuyển đổi nào!
  • Jackson — thư viện JSON đi kèm starter web — tự serialize mọi object/record/list thành JSON, đặt luôn Content-Type: application/json.

  • Dùng record (Java 21) làm kiểu trả về rất hợp: gọn, bất biến, tên field thành tên key JSON. Java thuần sẽ mổ xẻ record kỹ ở Ngày 20 của series Java.

  • Vì sao Jackson "tự nhiên" có mặt? Auto-configuration Ngày 10: classpath có Jackson → @ConditionalOnClass đậu → HttpMessageConverter JSON được dựng sẵn.

Hành trình một request

// Hành trình một request GET /api/posts://// Trình duyệt/khách hàng//   → Tomcat (server nhúng — starter web mang theo, Ngày 02)//     → DispatcherServlet (bưu điện trung tâm của Spring MVC)//       → HandlerMapping: "/api/posts" thuộc về PostController.list()//         → gọi method — controller là BEAN (Ngày 03!), được DI đầy đủ//       → giá trị trả về → Jackson (HttpMessageConverter) → JSON//   ← 200 OK + application/json//// DispatcherServlet & bạn bè đều do AUTO-CONFIGURATION dựng (Ngày 10!)// — thấy spring-boot-starter-web trên classpath là cả dây chuyền mọc ra

Nắm sơ đồ này là nắm xương sống Spring MVC: DispatcherServlet nhận mọi request rồi phân phát cho controller khớp đường dẫn — vì thế gọi là mô hình front controller. Từng mắt xích (mapping, converter, xử lý lỗi) đều là bean thay được — nhưng mặc định của Boot đã đủ tốt để ta chỉ viết controller.

Controller mỏng, service dày

Thói quen vàng nên tập từ ngày đầu tiên: controller chỉ nhận request và trả response — nghiệp vụ đẩy xuống tầng @Service (Ngày 05), nối nhau bằng constructor injection (Ngày 04):

@RestControllerpublic class PostController {
    private final PostService postService;      // controller MỎNG: nhận & trả

    PostController(PostService postService) {   // constructor injection — Ngày 04!
        this.postService = postService;
    }

    @GetMapping("/api/posts")
    public List<Post> list() {
        return postService.findAll();           // nghiệp vụ ở tầng service
    }
}

@Servicepublic class PostService {
    public List<Post> findAll() {               // mai mốt: gọi repository, cache…
        return List.of(new Post(1L, "Bài đầu tiên", true));
    }
}
  • Controller mỏng dễ đọc, dễ test; service không dính gì tới HTTP nên tái sử dụng được ở scheduler, listener, CLI…

  • Chuỗi phân tầng đầy đủ controller → service → repository sẽ khép kín khi gặp Spring Data JPA (Ngày 21+).

Bài tập nhỏ

  • Tạo HelloController với GET /hello trả tên bạn — chạy và gọi bằng trình duyệt lẫn curl.

  • Tạo record Book(Long id, String title, double price) và endpoint GET /api/books trả 3 cuốn — ngắm JSON tự sinh.

  • Tách BookService rồi tiêm vào controller bằng constructor — xác nhận kết quả không đổi.

  • Gọi một đường dẫn không tồn tại (GET /nope) — xem JSON lỗi 404 mặc định của Boot; Ngày 16 ta sẽ tự may trang lỗi đẹp hơn.

Kết luận

Ứng dụng của bạn đã nói chuyện được với thế giới: @RestController + @GetMapping mở endpoint trong vài dòng, Jackson tự đổi object thành JSON, DispatcherServlet điều phối sau cánh gà, và kiến trúc controller-mỏng-service-dày đặt nền cho mọi ngày sau. Nhưng API mới chỉ biết trả một danh sách cứngNgày 12 ta học nhận tham số: @PathVariable cho /api/posts/1, @RequestParam cho ?page=2, và nghệ thuật thiết kế URL tử tế. 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