Spring Boot/Module 03 — Tổng kết & cheat sheet
~15 phútREST API với Spring MVCMiễn phí lượt xem

Module 03 — Tổng kết & cheat sheet

Recap, cheat sheet 1 trang, glossary, pitfall tổng hợp, self-assessment outcomes. 1 trang để bookmark.

TL;DR: Module 03 đã xây đủ Web Layer production-grade: DispatcherServlet front-controller nhận mọi request qua 9 bước, @RestController annotation-driven với content negotiation, 6 nguồn binding từ HTTP request vào Java method, ResponseEntity với status code đúng ngữ nghĩa, @RestControllerAdvice + ProblemDetail RFC 9457 tập trung error handling, Jakarta Bean Validation 23 constraint declarative, và springdoc-openapi generate Swagger UI không cần viết doc. Capstone TaskFlow v1 tổng hợp 7 layer thành 8 endpoint CRUD — baseline cho Module 04 (JPA) extend lên. Đây là 1 trang để bookmark — quay lại khi debug 404 unexplained, validation không trigger, hay Problem Details không format đúng.

Đã đi qua những gì

Hành trình bắt đầu từ câu hỏi: khi client gửi GET /api/orders/42, Spring làm gì? Bài 01 trả lời qua DispatcherServlet — 1 servlet duy nhất (Front Controller pattern, Fowler 2002) nhận mọi request, rồi delegate qua 9 bước đến đúng method Java. 9 bean infrastructure — HandlerMapping route URL, HandlerAdapter invoke method, HttpMessageConverter serialize JSON, HandlerInterceptor cross-cutting, HandlerExceptionResolver xử lý exception — tất cả WebMvcAutoConfiguration register tự động theo pattern @ConditionalOnMissingBean đã học ở Module 02.

Bài 02 bóc @RestController — meta-annotation gộp @Controller + @ResponseBody, 3 generation evolution từ XML servlet (2003) đến annotation shortcut (2026). @RequestMapping 7 attribute + 5 shortcut (@GetMapping đến @PatchMapping). PathPatternParser Spring 6 support exact/wildcard/regex/capture. Richardson Maturity Model giải thích vì sao 99% "REST API" production là Level 2 (URL + HTTP method + status code), không phải Level 3 HATEOAS strict.

Bài 03-04 là cặp đôi binding/response: request đi vào 6 nguồn (@PathVariable/@RequestParam/@RequestBody/@RequestHeader/@CookieValue/@RequestAttribute) — mỗi nguồn 1 HandlerMethodArgumentResolver. Response đi ra 3 cách (return DTO, ResponseEntity, @ResponseStatus) — với 201 Created + Location header cho POST, 204 No Content cho DELETE, 202 Accepted cho async. StreamingResponseBody cho export 1M record không OOM.

Bài 05 là quan trọng nhất module cho production: @RestControllerAdvice global thay @ExceptionHandler rải rác. ProblemDetail Boot 3 native implement RFC 9457 — type/title/status/detail/instance + extension field tùy ý. 1 GlobalExceptionHandler map mọi domain exception thành response chuẩn. Security rule: không leak stack trace — attacker đọc class path, library version, SQL query để khai thác.

Bài 06 bóc Jakarta Bean Validation: 23 built-in constraint, @Valid cascade vào nested object, custom @Constraint + ConstraintValidator, cross-field validation qua @AssertTrue method. spring-boot-starter-validation phải add explicit — Boot 3 tách khỏi starter-web.

Bài 07 chốt documentation: springdoc-openapi 1 dependency scan controller annotation → OpenAPI 3.x spec → Swagger UI tại /swagger-ui.html. Customize qua @Operation/@ApiResponse/@Schema. Security scheme JWT Bearer. Pattern API-first: spec trước, frontend/backend implement song song.

Bài 08 mini-challenge — bạn build TaskFlow REST API v1: 8 endpoint CRUD (Project + Task), validation, Problem Details, OpenAPI doc, in-memory storage. Baseline sẽ extend Module 04 (JPA + Postgres), 05 (JWT auth), 06 (test pyramid).

Cheat sheet

ConceptKhi nào dùngPitfall thường gặp
@RestControllerREST API trả JSON/XMLDùng @Controller → method trả DTO bị lookup ViewResolver như tên view
@ControllerServer-side render (Thymeleaf/JSP)Quên @ResponseBody mỗi method → JSON không serialize
@EnableWebMvcTự config MVC từ đầu (hiếm)Disable toàn bộ Boot WebMvcAutoConfiguration — mất Jackson, static resource, content negotiation
WebMvcConfigurerThêm interceptor, converter, CORSKhông replace defaults — contribute thêm vào Boot defaults
@GetMapping/@PostMapping...Shortcut cho HTTP method cụ thểMix @GetMapping(method=POST) → compile error
consumes trong @PostMappingFilter request theo Content-TypeMismatch → 415 sớm với log rõ
produces trong @GetMappingFilter response theo AcceptMismatch → 406 Not Acceptable
@PathVariable Long idBind URL segment /orders/{id}Khai String thay Long → type mismatch, convert manual
@RequestParam(defaultValue = "0")Query string với fallbackdefaultValue = "abc" cho int → 400 runtime
@Valid @RequestBodyTrigger Bean Validation trên DTOQuên @Valid → constraint không trigger, validation bị bỏ qua
ResponseEntity.created(uri)POST tạo resource mớiTrả 200 thay 201 → client không biết URL resource mới
@ResponseStatus(NO_CONTENT) + voidDELETE không trả dataTrả null + 200 → HTTP contract sai
@RestControllerAdviceGlobal exception handler toàn app@ControllerAdvice cho REST thêm @ResponseBody thủ công
ProblemDetail.forStatus(400)Error response chuẩn RFC 9457Pattern cũ Map<String, Object> ad-hoc — không chuẩn, thiếu type/instance
pd.setProperty("violations", ...)Extension field trong ProblemDetailField tên trùng reserved (type, status) → override metadata
spring-boot-starter-validationKích hoạt Bean ValidationQuên add → annotation @NotBlank có nhưng không trigger
@NotBlank vs @NotEmpty vs @NotNullString empty/whitespace check@NotEmpty cho phép " " (chỉ whitespace) → dùng @NotBlank cho string user input
@Valid cascade nestedValidate object lồng trong DTOQuên @Valid trên field nested → nested constraint không chạy
springdoc-openapi-starter-webmvc-uiAuto-generate Swagger UIExpose /swagger-ui.html production mà không auth → leak API surface
@Operation(summary = "...")Mô tả endpoint trong Swagger UIKhông điền → Swagger UI hiển thị method name tự động generate, không đọc được
ShallowEtagHeaderFilterETag auto từ response body hashVẫn compute full body — chỉ save bandwidth, không save DB query
StreamingResponseBodyExport large file không OOMQuên close Stream<T> trong try-with-resources → connection/cursor leak

Glossary module

Thuật ngữĐịnh nghĩa 1 câuNguồn
REST (Representational State Transfer)Architectural style 6 constraint do Roy Fielding đề xuất năm 2000 — không phải protocol hay frameworkBài 01
Richardson Maturity ModelThang đo 4 mức RESTful: Level 0 (RPC), Level 1 (Resources), Level 2 (HTTP Verbs+Status), Level 3 (HATEOAS)Bài 02
HTTP methodĐộng từ chỉ thao tác: GET (safe+idempotent), POST (không safe+không idempotent), PUT/DELETE (idempotent), PATCH (tuỳ)Bài 01
IdempotentGọi N lần cho cùng kết quả như gọi 1 lần — PUT/DELETE retry-safe, POST khôngBài 01
Front Controller pattern1 servlet duy nhất nhận mọi request, dispatch đến handler — pattern của Fowler 2002, Spring implement qua DispatcherServletBài 01
DispatcherServletServlet trung tâm Spring MVC, bind /*, delegate 9 bước từ route đến serializeBài 01
HandlerMappingBean map URL pattern + HTTP method → handler method (RequestMappingHandlerMapping mặc định)Bài 01
HandlerAdapterBean invoke handler đúng cách — RequestMappingHandlerAdapter bind argument + invoke + convert returnBài 01
HttpMessageConverterConvert giữa HTTP body và Java object — MappingJackson2HttpMessageConverter cho JSON (default Boot)Bài 01
HandlerInterceptorSpring MVC-level cross-cutting chạy sau routing — biết handler method, khác Filter (servlet-level)Bài 01
@RestControllerMeta-annotation = @Controller + @ResponseBody — áp serialize body cho mọi method trong classBài 02
Content negotiationCơ chế Spring chọn format response theo Accept header (produces) hoặc reject request theo Content-Type (consumes)Bài 02
HandlerMethodArgumentResolverInterface resolve 1 method parameter từ request — mỗi annotation binding có 1 implementationBài 03
ResponseEntityWrapper cho HTTP response gồm status, headers, body — cho phép custom status code và header tùy ýBài 04
201 CreatedHTTP status cho POST tạo resource mới thành công — kèm Location header trỏ đến URL resource vừa tạoBài 04
204 No ContentHTTP status cho DELETE thành công hoặc PUT/PATCH không trả bodyBài 04
ProblemDetailClass Boot 3 implement RFC 9457 Problem Details — fields type, title, status, detail, instance + extensionBài 05
RFC 9457"Problem Details for HTTP APIs" — chuẩn IETF 2023 định nghĩa format JSON/XML cho error response REST APIBài 05
@RestControllerAdvice@ControllerAdvice + @ResponseBody — global exception handler cho REST, apply toàn appBài 05
Jakarta Bean Validation 3.0Chuẩn Java (JSR 380) cho declarative validation — implementation tham chiếu: Hibernate ValidatorBài 06
@ValidKích hoạt Bean Validation trên method parameter hoặc cascade vào nested objectBài 06
OpenAPI Specification (OAS)Chuẩn JSON/YAML mô tả REST API contract — phiên bản 3.1 (2021), trước là Swagger 2.0Bài 07
springdoc-openapiLibrary auto-generate OpenAPI spec từ Spring annotation, expose Swagger UI tại /swagger-ui.htmlBài 07

Pitfall tổng hợp

10 pitfall lớn nhất gom từ section "Nhầm" và "Pitfall" của các bài.

1. @EnableWebMvc disable toàn bộ Boot autoconfig

// SAI: mat Jackson, static resource, content negotiation
@Configuration
@EnableWebMvc
public class WebConfig implements WebMvcConfigurer {
    public void addInterceptors(InterceptorRegistry registry) { ... }
}

// DUNG: contribute vao Boot defaults
@Configuration
public class WebConfig implements WebMvcConfigurer {
    public void addInterceptors(InterceptorRegistry registry) { ... }
}

Lý do: @EnableWebMvc disable WebMvcAutoConfiguration. Mất Jackson auto-register, static resource handler, content negotiation. App start OK nhưng JSON endpoint fail, static 404. Dùng WebMvcConfigurer interface để customize — merge với defaults, không replace.

2. @Controller thay @RestController cho REST API

// SAI: Spring lookup ViewResolver voi ten "OrderDto"
@Controller
public class OrderController {
    @GetMapping("/orders/{id}")
    public OrderDto get(@PathVariable Long id) { return service.findById(id); }
}

// DUNG:
@RestController
public class OrderController {
    @GetMapping("/orders/{id}")
    public OrderDto get(@PathVariable Long id) { return service.findById(id); }
}

Lý do: @Controller không có @ResponseBody — Spring xem return value là tên view. Thymeleaf lookup templates/OrderDto.html → 500 "Could not resolve view". Dùng @RestController cho REST API.

3. Quên @Valid trên @RequestBody

// SAI: constraint @NotBlank khong trigger
public record OrderRequest(@NotBlank String customer, @NotNull BigDecimal total) {}

@PostMapping("/orders")
public OrderDto create(@RequestBody OrderRequest req) {
    return service.create(req);   // customer co the null!
}

// DUNG:
@PostMapping("/orders")
public OrderDto create(@Valid @RequestBody OrderRequest req) { ... }

Lý do: @RequestBody deserialize JSON → Java object. Nhưng validation chỉ chạy khi @Valid explicit. Quên → NPE runtime khi service dùng field null.

4. POST trả 200 thay 201 Created

// SAI: 200 OK - client khong biet URL resource moi
@PostMapping("/orders")
public OrderDto create(@Valid @RequestBody OrderRequest req) {
    return service.create(req);
}

// DUNG: 201 Created + Location header
@PostMapping("/orders")
public ResponseEntity<OrderDto> create(@Valid @RequestBody OrderRequest req) {
    OrderDto created = service.create(req);
    URI location = ServletUriComponentsBuilder.fromCurrentRequest()
        .path("/{id}").buildAndExpand(created.id()).toUri();
    return ResponseEntity.created(location).body(created);
}

Lý do: HTTP 201 Created là contract — client (mobile, frontend, integration) biết resource mới tạo và URL để fetch. Location header cho phép follow-up GET mà không parse response body.

5. Trả 200 với error body thay dùng 4xx

// SAI: status 200 nhung body la error -> client phai parse body
@GetMapping("/orders/{id}")
public Map<String, Object> get(@PathVariable Long id) {
    OrderDto order = service.findById(id);
    if (order == null) {
        return Map.of("error", "not found", "code", 404);
    }
    return Map.of("data", order);
}

// DUNG: throw exception, ControllerAdvice map thanh 404
@GetMapping("/orders/{id}")
public OrderDto get(@PathVariable Long id) {
    return service.findById(id);   // throw OrderNotFoundException if not found
}

Lý do: 200 với error body phá HTTP contract. Client library (Axios, Retrofit, fetch) detect error qua status code. 200 → client xử lý như success → bug UI.

6. Leak stack trace trong error response

// SAI: attacker doc class path, lib version, SQL query
@ExceptionHandler(Exception.class)
public ResponseEntity<Map<String, Object>> handle(Exception ex) {
    return ResponseEntity.status(500).body(Map.of(
        "error", ex.getMessage(),
        "trace", Arrays.toString(ex.getStackTrace())    // NGUY HIEM
    ));
}

// DUNG: ProblemDetail khong co stacktrace
@ExceptionHandler(Exception.class)
public ProblemDetail handle(Exception ex, HttpServletRequest req) {
    log.error("Unhandled exception", ex);              // log day du trong server
    ProblemDetail pd = ProblemDetail.forStatus(500);
    pd.setDetail("Internal server error");             // message chung, khong detail
    pd.setInstance(URI.create(req.getRequestURI()));
    return pd;
}

Lý do: Stack trace expose: class path (directory structure), library version (exploit CVE), SQL fragment (schema info). Log server-side đầy đủ, response chỉ có message generic.

7. Quên spring-boot-starter-validation

<!-- SAI: chi co starter-web, validation khong co -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>

<!-- DUNG: add validation explicit -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-validation</artifactId>
</dependency>

Lý do: Boot 3 tách validation khỏi starter-web. @NotBlank compile OK nhưng không trigger — Hibernate Validator không có classpath. Không error message, validation im lặng bỏ qua. Bug ngầm phát hiện khi invalid data vào DB.

8. Custom exception throw 500 thay 4xx

// SAI: RuntimeException generic -> ControllerAdvice map 500
public class OrderNotFoundException extends RuntimeException {
    public OrderNotFoundException(Long id) {
        super("Order " + id + " not found");
    }
}

// DUNG: annotation @ResponseStatus hoac ControllerAdvice explicit
@ResponseStatus(HttpStatus.NOT_FOUND)
public class OrderNotFoundException extends RuntimeException { ... }

// HOAC trong GlobalExceptionHandler:
@ExceptionHandler(OrderNotFoundException.class)
public ProblemDetail handleNotFound(OrderNotFoundException ex, HttpServletRequest req) {
    ProblemDetail pd = ProblemDetail.forStatus(404);
    pd.setDetail(ex.getMessage());
    pd.setInstance(URI.create(req.getRequestURI()));
    return pd;
}

Lý do: Spring map Exception không annotated thành 500. 404 resource không tồn tại là client error (4xx) — log ở level WARN, không ERROR. Sai status code làm alert production noise tăng vô ích.

9. Expose Swagger UI production không bảo vệ

# SAI: bat tat ca, expose production
springdoc:
  swagger-ui:
    enabled: true

# DUNG: tat production, chi bat dev/staging
springdoc:
  swagger-ui:
    enabled: ${SWAGGER_UI_ENABLED:false}   # default false, set true qua env dev

Lý do: Swagger UI expose toàn bộ API surface: endpoint list, parameter schema, example payload. Attacker dùng để fuzzing tự động. Production nên tắt hoặc bảo vệ bằng auth (Spring Security basic auth cho /swagger-ui.html).

10. @Valid không cascade vào nested object

// SAI: @Valid tren OrderRequest nhung nested OrderItem khong validate
public record OrderRequest(
    @NotBlank String customer,
    @NotEmpty List<OrderItem> items   // thieu @Valid
) {}

public record OrderItem(
    @NotBlank String sku,
    @Min(1) int quantity
) {}

// DUNG: @Valid cascade
public record OrderRequest(
    @NotBlank String customer,
    @NotEmpty @Valid List<OrderItem> items   // @Valid cascade vao tung item
) {}

Lý do: @Valid không tự cascade. @NotEmpty chỉ check list không rỗng — không validate item bên trong. Phải add @Valid trên field nested để Spring cascade vào từng element.

Self-assessment outcomes

Tick được hết các ô sau, bạn sẵn sàng Module 04 (Spring Data JPA). Nếu chưa: re-read bài tương ứng trước khi tiếp tục.

  • Trace vòng đời 1 HTTP request qua 9 bước DispatcherServlet — từ Tomcat nhận request đến MessageConverter write JSON response.
    • Nếu chưa: re-read bài 01 section 2-3 ("DispatcherServlet làm gì", "9 bean infrastructure") + SelfCheck Q6. Vẽ sơ đồ sequence 9 bước từ trí nhớ — không nhìn tài liệu.
  • Implement @RestController CRUD đầy đủ với đúng HTTP method, status code (201/204/404), và Location header.
    • Nếu chưa: re-read bài 02 section 2-4 + bài 04 section 2.1 + 8.1. Viết 5 endpoint CRUD từ scratch cho domain mới (không TaskFlow) — chỉ nhớ pattern, không copy.
  • Explain cơ chế HandlerMethodArgumentResolver cho 6 nguồn binding (path, query, body, header, cookie, attribute) và type conversion.
    • Nếu chưa: re-read bài 03 section 1-8 + SelfCheck Q1. Tự khai báo 1 method với đủ 4 nguồn binding khác nhau và verify request mẫu.
  • Design exception handling tập trung qua @RestControllerAdvice + ProblemDetail RFC 9457 — không leak stack trace production.
    • Nếu chưa: re-read bài 05 section 4-6 ("ProblemDetail Boot 3 native", "domain exception mapping", "security"). Viết 1 GlobalExceptionHandler handle 3 loại exception với ProblemDetail — không dùng Map<String, Object>.
  • Implement Jakarta Bean Validation với 23 built-in constraint, custom @Constraint, cross-field validation, và validation groups.
    • Nếu chưa: re-read bài 06 section 2-6 + SelfCheck Q3. Viết custom @Constraint cho 1 business rule cụ thể (vd @ValidPhoneNumber), test với invalid input.
  • Design và expose OpenAPI 3.x doc qua springdoc-openapi: customize @Operation/@Schema, security scheme, và workflow API-first cho TaskFlow API.
    • Nếu chưa: re-read bài 07 section 2-5 + thêm springdoc-openapi vào TaskFlow v1. Mở Swagger UI, verify mọi endpoint xuất hiện với description. Thêm JWT security scheme.

What's next — Module 04: Spring Data JPA

Module 03 đã build REST API layer với in-memory storageMap<Long, Order> thay thế DB. Module 04 thêm persistence thực sự: Spring Data JPA + Hibernate + PostgreSQL. TaskFlow v1 sẽ được migrate từ HashMap sang JPA Repository — cùng interface controller, khác layer persistence bên dưới.

Sau Module 04 bạn sẽ hiểu: @Entity lifecycle qua Hibernate session, N+1 query problem và cách detect bằng Hibernate statistics, transaction propagation REQUIRED/REQUIRES_NEW, và tại sao @Transactional(readOnly = true) quan trọng cho read-heavy endpoint.

→ Đi tới Module 04: Spring Data JPA

Tài liệu mở rộng

Sách:

  • Spring in Action — Craig Walls (Manning, 7th ed. 2022). Chương 7-9 cover REST API với Spring MVC, validation, exception handling.
  • Designing RESTful APIs — Brian Mulloy (O'Reilly). Ngắn, tập trung API design patterns — đọc song song với module này.

Paper / RFC / Standard:

Video / Talk:

Source code để đọc:

Blog / Guide:

Chúc mừng — bạn đã hoàn thành Module 03. Web Layer đã sẵn sàng: request đến, validate, xử lý, trả response đúng status code, error format chuẩn RFC 9457, doc tự sinh. Nghỉ 1 ngày cho concept lắng xuống, rồi vào Module 04 — thêm persistence thực sự với JPA + PostgreSQL cho TaskFlow.

Bài này có giúp bạn hiểu bản chất không?