Response Nâng Cao: Streaming, Advice, HATEOAS và Security
Làm chủ kỹ thuật streaming dữ liệu chống OOM, ResponseBodyAdvice bọc envelope đồng nhất, HATEOAS và tối ưu bảo mật response trong Spring MVC.
TL;DR: Khi response vượt ra khỏi mô hình trả JSON tĩnh thông thường, Spring MVC cung cấp 4 cơ chế nâng cao: StreamingResponseBody ghi trực tiếp từng phần dữ liệu vào servlet output stream giúp giải quyết triệt để nguy cơ tràn bộ nhớ (OOM) khi export hàng triệu bản ghi; SseEmitter duy trì kết nối HTTP để đẩy sự kiện thời gian thực (one-way realtime); ResponseBodyAdvice chặn bắt toàn bộ response trước khi tuần tự hóa để bọc cấu trúc envelope đồng nhất; HATEOAS nhúng các liên kết điều hướng (_links) vào body giúp client tự khám phá hành động tiếp theo. Ngoài ra, việc thiết lập ETag, nén gzip và các Security Headers là tiêu chuẩn bắt buộc khi đưa API lên môi trường production.
1. Khi nào nên dùng StreamingResponseBody và Server-Sent Events?
Xử lý xuất dữ liệu dung lượng lớn với StreamingResponseBody
Phương thức trả về danh sách truyền thống gom toàn bộ dữ liệu vào Java Heap trước khi serialize:
// Anti-pattern: Load toan bo 1 trieu ban ghi gay OutOfMemoryError
@GetMapping("/orders/export")
public ResponseEntity<List<OrderDto>> exportAll() {
List<OrderDto> all = orderService.findAll(); // Gay qua tai bo nho heap
return ResponseEntity.ok(all);
}
Để tránh cạn kiệt RAM, StreamingResponseBody cho phép ghi dữ liệu từng phần trực tiếp vào socket output stream:
@GetMapping("/orders/export")
public ResponseEntity<StreamingResponseBody> exportOrdersCsv() {
StreamingResponseBody stream = outputStream -> {
try (PrintWriter writer = new PrintWriter(outputStream);
Stream<Order> orders = orderService.streamAll()) {
writer.println("id,customer,total,createdAt");
orders.forEach(order -> {
writer.println(order.getId() + "," + order.getCustomer() + "," + order.getTotal() + "," + order.getCreatedAt());
});
writer.flush();
}
};
return ResponseEntity.ok()
.contentType(MediaType.parseMediaType("text/csv"))
.header(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=\"orders.csv\"")
.body(stream);
}

Ở tầng repository, cấu hình fetch size qua JDBC cursor và giữ transaction mở:
@Repository
public interface OrderRepository extends JpaRepository<Order, Long> {
@QueryHints(@QueryHint(name = AvailableSettings.HINT_FETCH_SIZE, value = "500"))
Stream<Order> streamAll();
}
@Service
public class OrderService {
@Transactional(readOnly = true) // Bat buoc de giu mo DB cursor
public Stream<Order> streamAll() {
return orderRepository.streamAll();
}
}
Đẩy dữ liệu thời gian thực một chiều với Server-Sent Events (SSE)
SSE cho phép server chủ động push dữ liệu về client qua kết nối HTTP/1.1 hoặc HTTP/2 dạng text/event-stream:
@GetMapping(value = "/orders/stream", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
public SseEmitter streamOrderUpdates() {
SseEmitter emitter = new SseEmitter(60_000L); // Timeout 60s
orderEventBus.subscribe(event -> {
try {
emitter.send(SseEmitter.event()
.id(String.valueOf(event.getId()))
.name("order-status")
.data(event, MediaType.APPLICATION_JSON));
} catch (IOException ex) {
emitter.completeWithError(ex);
}
});
emitter.onTimeout(emitter::complete);
emitter.onError(ex -> emitter.complete());
return emitter;
}
Khác với WebSocket hỗ trợ giao tiếp hai chiều (bi-directional), SSE là giao tiếp một chiều từ server tới client, cực kỳ phù hợp cho bảng điều khiển giám sát, cập nhật trạng thái đơn hàng và tiến trình tác vụ nền.
2. ResponseBodyAdvice hoạt động và can thiệp vào response toàn cục ra sao?
Nhiều tổ chức yêu cầu chuẩn hóa mọi response về dạng envelope thống nhất:
{
"success": true,
"data": { "id": 1024, "status": "COMPLETED" },
"requestId": "req-9872",
"timestamp": "2026-04-15T10:00:00Z"
}
ResponseBodyAdvice là cơ chế chặn bắt chính thức của Spring MVC cho phép thực hiện việc đóng gói này tại một điểm duy nhất:
public record ApiResponse<T>(boolean success, T data, String requestId, Instant timestamp) {
public static <T> ApiResponse<T> success(T data) {
return new ApiResponse<>(true, data, MDC.get("requestId"), Instant.now());
}
}
@RestControllerAdvice
public class ApiResponseEnvelopeAdvice implements ResponseBodyAdvice<Object> {
@Override
public boolean supports(MethodParameter returnType, Class<? extends HttpMessageConverter<?>> converterType) {
// Chi ap dung cho JSON converter
if (!converterType.isAssignableFrom(MappingJackson2HttpMessageConverter.class)) {
return false;
}
// Bo qua neu controller tra ve String hoac da duoc wrap
Class<?> paramType = returnType.getParameterType();
return !paramType.isAssignableFrom(String.class)
&& !paramType.isAssignableFrom(ApiResponse.class)
&& !paramType.isAssignableFrom(Resource.class)
&& !paramType.isAssignableFrom(ProblemDetail.class);
}
@Override
public Object beforeBodyWrite(
Object body,
MethodParameter returnType,
MediaType selectedContentType,
Class<? extends HttpMessageConverter<?>> selectedConverterType,
ServerHttpRequest request,
ServerHttpResponse response
) {
return ApiResponse.success(body);
}
}
Nếu controller trả về trực tiếp kiểu String, Spring MVC sẽ ưu tiên chọn StringHttpMessageConverter. Nếu Advice bọc String thành ApiResponse<String>, converter sẽ cố ép kiểu đối tượng này thành chuỗi và gây lỗi ClassCastException. Do đó, luôn kiểm tra và bỏ qua kiểu String trong phương thức supports().
3. HATEOAS giúp client tự khám phá API (Discoverability) như thế nào?
HATEOAS (Hypermedia As The Engine Of Application State) bổ sung khối _links vào dữ liệu JSON, chỉ dẫn cho client biết các thao tác nghiệp vụ khả dụng tiếp theo:
@RestController
@RequestMapping("/api/orders")
public class OrderController {
@GetMapping("/{id}")
public EntityModel<OrderDto> getOrder(@PathVariable Long id) {
OrderDto order = orderService.findById(id);
return EntityModel.of(order,
linkTo(methodOn(OrderController.class).getOrder(id)).withSelfRel(),
linkTo(methodOn(OrderController.class).cancelOrder(id)).withRel("cancel"),
linkTo(methodOn(OrderController.class).listItems(id)).withRel("items")
);
}
@DeleteMapping("/{id}/cancel")
public ResponseEntity<Void> cancelOrder(@PathVariable Long id) {
orderService.cancel(id);
return ResponseEntity.noContent().build();
}
}
Đoạn code trên tự động tạo ra response có cấu trúc siêu liên kết:
{
"id": 1024,
"status": "PENDING",
"_links": {
"self": { "href": "http://localhost:8080/api/orders/1024" },
"cancel": { "href": "http://localhost:8080/api/orders/1024/cancel" },
"items": { "href": "http://localhost:8080/api/orders/1024/items" }
}
}
Client chỉ cần truy vấn theo tên quan hệ (rel) "cancel" thay vì tự lắp ráp URL thủ công, giúp server tự do thay đổi cấu trúc URL mà không làm gãy hợp đồng tích hợp.
4. Tối ưu HTTP Caching với ETag và nén dữ liệu trong Production ra sao?
Tận dụng ETag và HTTP 304 Not Modified
ETag đại diện cho phiên bản trạng thái của bản ghi dữ liệu. Khi client gửi kèm header If-None-Match, nếu phiên bản trên DB không đổi, server phản hồi mã 304 không kèm body:
@GetMapping("/orders/{id}")
public ResponseEntity<OrderDto> getOrder(
@PathVariable Long id,
@RequestHeader(value = "If-None-Match", required = false) String ifNoneMatch
) {
OrderDto order = orderService.findById(id);
String currentEtag = "\"" + order.getVersion() + "\""; // Su dung version optimistic locking
if (currentEtag.equals(ifNoneMatch)) {
return ResponseEntity.status(HttpStatus.NOT_MODIFIED).eTag(currentEtag).build();
}
return ResponseEntity.ok()
.eTag(currentEtag)
.cacheControl(CacheControl.maxAge(Duration.ofMinutes(10)).cachePublic())
.body(order);
}
Kích hoạt nén Gzip trong tệp cấu hình
Nén dữ liệu JSON giúp giảm từ 70% đến 90% dung lượng truyền tải trên mạng:
server:
compression:
enabled: true
mime-types: application/json,text/html,text/csv,application/xml
min-response-size: 1024 # Chi nen khi body tu 1KB tro len
5. Cấu hình Security Headers thế nào để bảo vệ ứng dụng?
Để bảo vệ client chống lại các cuộc tấn công Clickjacking, MIME-sniffing và XSS, API cần gửi kèm các HTTP Security Headers:
@Configuration
public class SecurityWebConfig implements WebMvcConfigurer {
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new HandlerInterceptor() {
@Override
public boolean preHandle(HttpServletRequest req, HttpServletResponse res, Object handler) {
res.setHeader("X-Content-Type-Options", "nosniff");
res.setHeader("X-Frame-Options", "DENY");
res.setHeader("Strict-Transport-Security", "max-age=31536000; includeSubDomains");
res.setHeader("Referrer-Policy", "strict-origin-when-cross-origin");
return true;
}
});
}
}
| Security Header | Tác dụng phòng vệ |
|---|---|
X-Content-Type-Options: nosniff | Ngăn chặn trình duyệt tự ý đoán định dạng file tải về |
X-Frame-Options: DENY | Chống tấn công Clickjacking (không cho nhúng iframe) |
Strict-Transport-Security (HSTS) | Ép buộc toàn bộ kết nối chuyển tiếp qua HTTPS |
Referrer-Policy | Kiểm soát thông tin URL nhạy cảm rò rỉ qua header Referer |
6. Luồng xử lý toàn cảnh response pipeline trong Spring MVC diễn ra ra sao?
Quyết định cách phản hồi dựa trên kiểu dữ liệu trả về của controller method:

Nhánh StreamingResponseBody và SseEmitter đi tắt qua Handler chuyên biệt và ghi thẳng ra socket stream mà không cần qua tầng HttpMessageConverter hay ResponseBodyAdvice.
7. Những cạm bẫy kỹ thuật thường gặp khi làm việc với response nâng cao là gì?
Cạm bẫy 1: Quên đóng Stream trong StreamingResponseBody
Nếu không dùng cấu trúc try-with-resources để đóng Stream<T>, JDBC cursor trên database sẽ bị treo, dẫn đến cạn kiệt connection pool dưới tải cao.
Cạm bẫy 2: Không dọn dẹp listener của SseEmitter gây rò rỉ bộ nhớ
Khi trình duyệt tắt tab, kết nối SSE bị ngắt nhưng subscription trong EventBus vẫn giữ tham chiếu mạnh tới SseEmitter:

Biện pháp xử lý: Luôn đăng ký callback onTimeout và onError để hủy đăng ký listener tương ứng.
8. Các kỹ thuật response nâng cao kết nối với kiến trúc hệ thống ra sao?
- ResponseEntity & HTTP status: Đặt nền tảng quản lý header và status code trước khi bước sang các kỹ thuật streaming và advice.
- Content Negotiation: Định hướng converter serialize dữ liệu tương thích với yêu cầu của client.
- Problem Details RFC 9457: Phối hợp cùng
@ExceptionHandlerđể trả về phản hồi lỗi chuẩn tắc mà không bị ghi đè bởiResponseBodyAdvice.
9. Những điểm then chốt cần ghi nhớ là gì?
- Sử dụng
StreamingResponseBodykết hợp JDBC cursor và@Transactional(readOnly = true)để xử lý xuất dữ liệu dung lượng lớn an toàn bộ nhớ. SseEmitterlà giải pháp lý tưởng cho luồng dữ liệu thời gian thực một chiều trên nền HTTP.- Triển khai
ResponseBodyAdviceđể đóng gói định dạng phản hồi chung cho toàn bộ API JSON, lưu ý loại trừ kiểu trả vềString. - HATEOAS giúp giảm thiểu sự phụ thuộc cứng vào URL giữa client và server thông qua hypermedia links.
- Tối ưu hạ tầng production bằng ETag (304 Not Modified), nén Gzip và thiết lập đầy đủ HTTP Security Headers.
10. Bài tập tự kiểm tra mức độ hiểu sâu
- Q1Tại sao việc trả về trực tiếp List<OrderDto> chứa 1 triệu bản ghi từ Controller dễ gây OutOfMemoryError, trong khi StreamingResponseBody lại an toàn?
- Q2Giao thức Server-Sent Events (SSE) sử dụng Media Type nào sau đây để duy trì kết nối truyền dữ liệu liên tục?
- Q3Khi cài đặt ResponseBodyAdvice, lỗi ClassCastException thường xảy ra nhất trong trường hợp nào nếu không kiểm tra kiểu trả về cẩn thận?
- Q4Lợi ích cốt lõi nhất của việc áp dụng mô hình HATEOAS trong kiến trúc REST API là gì?
- Q5Cơ chế HTTP Caching dựa trên header ETag kết hợp If-None-Match giúp tiết kiệm tài nguyên hệ thống bằng cách nào?
Bài tiếp theo: Tổng kết module
Bài này có giúp bạn hiểu bản chất không?
Bài này đáng gửi cho bạn học cùng?
Copy link đã gắn nguồn — dán group, chat, hoặc LinkedIn.
ResponseEntity và HTTP Status Chuẩn Ngữ Nghĩa REST
Bài tiếpTổng Kết Module 02: Xử Lý Request và Response Spring MVC
Lộ trình Junior Java Backend Developer
Chặng 15/21 · REST API với Spring MVC — bài 11/21 của chặng
Thảo luận & góp ý
Hỏi đáp về bài này