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
| Concept | Khi nào dùng | Pitfall thường gặp |
|---|---|---|
@RestController | REST API trả JSON/XML | Dùng @Controller → method trả DTO bị lookup ViewResolver như tên view |
@Controller | Server-side render (Thymeleaf/JSP) | Quên @ResponseBody mỗi method → JSON không serialize |
@EnableWebMvc | Tự config MVC từ đầu (hiếm) | Disable toàn bộ Boot WebMvcAutoConfiguration — mất Jackson, static resource, content negotiation |
WebMvcConfigurer | Thêm interceptor, converter, CORS | Khô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 @PostMapping | Filter request theo Content-Type | Mismatch → 415 sớm với log rõ |
produces trong @GetMapping | Filter response theo Accept | Mismatch → 406 Not Acceptable |
@PathVariable Long id | Bind URL segment /orders/{id} | Khai String thay Long → type mismatch, convert manual |
@RequestParam(defaultValue = "0") | Query string với fallback | defaultValue = "abc" cho int → 400 runtime |
@Valid @RequestBody | Trigger Bean Validation trên DTO | Quên @Valid → constraint không trigger, validation bị bỏ qua |
ResponseEntity.created(uri) | POST tạo resource mới | Trả 200 thay 201 → client không biết URL resource mới |
@ResponseStatus(NO_CONTENT) + void | DELETE không trả data | Trả null + 200 → HTTP contract sai |
@RestControllerAdvice | Global exception handler toàn app | @ControllerAdvice cho REST thêm @ResponseBody thủ công |
ProblemDetail.forStatus(400) | Error response chuẩn RFC 9457 | Pattern cũ Map<String, Object> ad-hoc — không chuẩn, thiếu type/instance |
pd.setProperty("violations", ...) | Extension field trong ProblemDetail | Field tên trùng reserved (type, status) → override metadata |
spring-boot-starter-validation | Kích hoạt Bean Validation | Quên add → annotation @NotBlank có nhưng không trigger |
@NotBlank vs @NotEmpty vs @NotNull | String empty/whitespace check | @NotEmpty cho phép " " (chỉ whitespace) → dùng @NotBlank cho string user input |
@Valid cascade nested | Validate object lồng trong DTO | Quên @Valid trên field nested → nested constraint không chạy |
springdoc-openapi-starter-webmvc-ui | Auto-generate Swagger UI | Expose /swagger-ui.html production mà không auth → leak API surface |
@Operation(summary = "...") | Mô tả endpoint trong Swagger UI | Không điền → Swagger UI hiển thị method name tự động generate, không đọc được |
ShallowEtagHeaderFilter | ETag auto từ response body hash | Vẫn compute full body — chỉ save bandwidth, không save DB query |
StreamingResponseBody | Export large file không OOM | Quên close Stream<T> trong try-with-resources → connection/cursor leak |
Glossary module
| Thuật ngữ | Định nghĩa 1 câu | Nguồn |
|---|---|---|
| REST (Representational State Transfer) | Architectural style 6 constraint do Roy Fielding đề xuất năm 2000 — không phải protocol hay framework | Bài 01 |
| Richardson Maturity Model | Thang đ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 |
| Idempotent | Gọi N lần cho cùng kết quả như gọi 1 lần — PUT/DELETE retry-safe, POST không | Bài 01 |
| Front Controller pattern | 1 servlet duy nhất nhận mọi request, dispatch đến handler — pattern của Fowler 2002, Spring implement qua DispatcherServlet | Bài 01 |
DispatcherServlet | Servlet trung tâm Spring MVC, bind /*, delegate 9 bước từ route đến serialize | Bài 01 |
HandlerMapping | Bean map URL pattern + HTTP method → handler method (RequestMappingHandlerMapping mặc định) | Bài 01 |
HandlerAdapter | Bean invoke handler đúng cách — RequestMappingHandlerAdapter bind argument + invoke + convert return | Bài 01 |
HttpMessageConverter | Convert giữa HTTP body và Java object — MappingJackson2HttpMessageConverter cho JSON (default Boot) | Bài 01 |
HandlerInterceptor | Spring MVC-level cross-cutting chạy sau routing — biết handler method, khác Filter (servlet-level) | Bài 01 |
@RestController | Meta-annotation = @Controller + @ResponseBody — áp serialize body cho mọi method trong class | Bài 02 |
| Content negotiation | Cơ chế Spring chọn format response theo Accept header (produces) hoặc reject request theo Content-Type (consumes) | Bài 02 |
HandlerMethodArgumentResolver | Interface resolve 1 method parameter từ request — mỗi annotation binding có 1 implementation | Bài 03 |
ResponseEntity | Wrapper cho HTTP response gồm status, headers, body — cho phép custom status code và header tùy ý | Bài 04 |
| 201 Created | HTTP status cho POST tạo resource mới thành công — kèm Location header trỏ đến URL resource vừa tạo | Bài 04 |
| 204 No Content | HTTP status cho DELETE thành công hoặc PUT/PATCH không trả body | Bài 04 |
ProblemDetail | Class Boot 3 implement RFC 9457 Problem Details — fields type, title, status, detail, instance + extension | Bài 05 |
| RFC 9457 | "Problem Details for HTTP APIs" — chuẩn IETF 2023 định nghĩa format JSON/XML cho error response REST API | Bài 05 |
@RestControllerAdvice | @ControllerAdvice + @ResponseBody — global exception handler cho REST, apply toàn app | Bài 05 |
| Jakarta Bean Validation 3.0 | Chuẩn Java (JSR 380) cho declarative validation — implementation tham chiếu: Hibernate Validator | Bài 06 |
@Valid | Kích hoạt Bean Validation trên method parameter hoặc cascade vào nested object | Bà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.0 | Bài 07 |
| springdoc-openapi | Library auto-generate OpenAPI spec từ Spring annotation, expose Swagger UI tại /swagger-ui.html | Bà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
@RestControllerCRUD đầy đủ với đúng HTTP method, status code (201/204/404), vàLocationheader.- 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ế
HandlerMethodArgumentResolvercho 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+ProblemDetailRFC 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
GlobalExceptionHandlerhandle 3 loại exception với ProblemDetail — không dùngMap<String, Object>.
- Nếu chưa: re-read bài 05 section 4-6 ("ProblemDetail Boot 3 native", "domain exception mapping", "security"). Viết 1
- 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
@Constraintcho 1 business rule cụ thể (vd@ValidPhoneNumber), test với invalid input.
- Nếu chưa: re-read bài 06 section 2-6 + SelfCheck Q3. Viết custom
- 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-openapivào TaskFlow v1. Mở Swagger UI, verify mọi endpoint xuất hiện với description. Thêm JWT security scheme.
- Nếu chưa: re-read bài 07 section 2-5 + thêm
What's next — Module 04: Spring Data JPA
Module 03 đã build REST API layer với in-memory storage — Map<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:
- RFC 9110 — HTTP Semantics — HTTP method semantics, status code class chính thức. Bookmark cho reference.
- RFC 9457 — Problem Details for HTTP APIs — chuẩn IETF 2023 cho error response format. 8 trang — đọc 1 lần.
- Roy Fielding Thesis — Chapter 5: REST — nguồn gốc REST constraints. Đọc để hiểu "pragmatic REST" là trade-off gì.
- Jakarta Bean Validation 3.0 Spec — full constraint list + lifecycle.
Video / Talk:
- Spring Tips: Spring MVC — Josh Long demo Spring MVC patterns live.
- Stéphane Nicoll — Spring Boot REST with Problem Details — Boot maintainer giải thích RFC 9457 integration.
Source code để đọc:
DispatcherServlet.doDispatch()— 80 dòng cô đọng toàn bộ 9 bước flow.ProblemDetail— class RFC 9457 trong Spring Framework.RequestMappingHandlerAdapter— invoke handler + bind argument.
Blog / Guide:
- Baeldung — Spring MVC — 100+ bài cover từng annotation/pattern cụ thể.
- springdoc-openapi Getting Started — official guide nhanh cho
springdoc-openapi. - Microsoft REST API Guidelines — URL naming, versioning, error format convention industry.
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?