@RestControllerAdvice — tập trung xử lý exception cho toàn REST API
Mỗi controller tự catch exception = DRY violation và 5xx ẩn. Bài này giải thích 3 layer exception handling, cơ chế HandlerExceptionResolver, cách @RestControllerAdvice tập trung mapping domain exception → HTTP status, và khi nào nên dùng ResponseStatusException shortcut.
TL;DR: Spring có 3 layer exception handling theo thứ tự ưu tiên: @ExceptionHandler cục bộ trong controller → @RestControllerAdvice global → HandlerExceptionResolver default của framework. Không tập trung exception handling, mỗi controller tự try-catch — duplicate code và khó audit. @RestControllerAdvice là một class duy nhất chứa toàn bộ mapping domain exception → HTTP status, sử dụng HandlerExceptionResolverComposite để dispatch tới đúng handler. Domain exception giữ nguyên POJO thuần — không import gì từ org.springframework.http. ResponseStatusException là shortcut cho prototype hoặc endpoint one-off.
Bài Response handling — ResponseEntity & status giải thích HTTP status semantics. Bài này giải thích câu hỏi tiếp theo: khi service throw exception, làm sao biến nó thành response đúng status code một cách không lặp code?
1. Bài toán — exception rải rác trong controller
Hãy bắt đầu từ vấn đề cụ thể. App TaskFlow có 5 controller, mỗi endpoint cần trả 404 khi không tìm thấy resource, 409 khi conflict, 500 với message generic (không leak internal). Nếu mỗi controller tự xử lý:
@RestController
public class ProjectController {
@GetMapping("/projects/{id}")
public ProjectDto get(@PathVariable Long id) {
try {
return projectService.findById(id);
} catch (ProjectNotFoundException ex) {
// Copy-paste logic nay o 5 controller
return ResponseEntity.notFound().build(); // compile error -- wrong return type
} catch (Exception ex) {
log.error("Error", ex);
return ResponseEntity.status(500)...; // lap lai
}
}
}
Vấn đề ngay lập tức:
- Duplicate logic: 5 controller, mỗi cái copy đoạn try-catch này — muốn thay đổi format response phải sửa 5 chỗ.
- Inconsistent: dev A trả
{"error": "not found"}, dev B trả{"message": "not found"}— client không thể parse đồng nhất. - Audit khó: muốn biết "exception nào được handle?" phải đọc từng method trong từng controller.
- Type mismatch: method return
ProjectDtonhưng catch trảResponseEntity— compile error, phải đổi return type toàn bộ.
@RestControllerAdvice giải quyết tất cả bằng cách tách exception handling ra khỏi controller và tập trung vào một class.
2. 3 layer exception handling
Spring iterate qua 3 layer theo thứ tự ưu tiên khi một exception chưa được handle trong handler method:
flowchart TB EX["Exception thrown trong handler method"] L1["Layer 1: @ExceptionHandler trong controller<br/>(scope: chi controller do, priority cao nhat)"] L2["Layer 2: @RestControllerAdvice global<br/>(scope: toan app, priority trung binh)"] L3["Layer 3: HandlerExceptionResolver default<br/>(scope: Spring framework, priority thap nhat)"] RESP["HTTP Response 4xx / 5xx"] EX --> L1 L1 -->|"khong match"| L2 L2 -->|"khong match"| L3 L3 --> RESP L1 -->|"da handle"| RESP L2 -->|"da handle"| RESP
| Layer | Scope | Khi nào dùng |
|---|---|---|
@ExceptionHandler trong controller | Chỉ controller đó | Rất hiếm — exception đặc thù 1 endpoint không áp dụng nơi nào khác |
@RestControllerAdvice global | Toàn app | Pattern chuẩn 2026 — mapping mọi domain exception |
HandlerExceptionResolver default | Spring framework | Fallback cuối — handle exception built-in MVC |
Khi exception không khớp layer nào, Spring trả 500 với body Problem Details generic. Vì vậy layer 2 luôn nên có @ExceptionHandler(Exception.class) làm catch-all.
3. Cơ chế bên dưới — HandlerExceptionResolver
Câu hỏi cơ chế: khi Spring dispatch exception tới đúng layer, cụ thể nó làm gì?
DispatcherServlet.processHandlerException() gọi HandlerExceptionResolverComposite, là một danh sách HandlerExceptionResolver được iterate theo thứ tự Order:
flowchart TB DS["DispatcherServlet.processHandlerException()"] COMP["HandlerExceptionResolverComposite<br/>(iterate theo Order)"] EH["1. ExceptionHandlerExceptionResolver<br/>(Order=0) -- tim @ExceptionHandler match"] RS["2. ResponseStatusExceptionResolver<br/>(Order=1) -- doc @ResponseStatus tren exception class"] DH["3. DefaultHandlerExceptionResolver<br/>(Order=2) -- handle exception built-in Spring MVC"] NULL["khong resolver nao handle<br/>-> re-throw -> Tomcat error page"] DS --> COMP COMP --> EH EH -->|"khong match"| RS RS -->|"khong match"| DH DH -->|"khong match"| NULL EH -->|"da resolve"| RESP["ModelAndView + status set"] RS -->|"da resolve"| RESP DH -->|"da resolve"| RESP
Ba resolver cụ thể:
ExceptionHandlerExceptionResolver (Order 0): Tìm method @ExceptionHandler — trước tiên trong controller hiện tại (Layer 1), sau đó trong các class @ControllerAdvice/@RestControllerAdvice được scan (Layer 2). Khi tìm thấy match, gọi method đó và serialize return value thành response body. Đây là resolver xử lý cả custom handler của bạn.
ResponseStatusExceptionResolver (Order 1): Đọc annotation @ResponseStatus trên class exception. Nếu exception annotated @ResponseStatus(HttpStatus.NOT_FOUND), resolver set status 404 và trả response. Không cần @ExceptionHandler — annotation trên class exception là đủ.
DefaultHandlerExceptionResolver (Order 2): Handle exception built-in Spring MVC như MethodArgumentNotValidException (400), HttpRequestMethodNotSupportedException (405), HttpMediaTypeNotSupportedException (415), MissingServletRequestParameterException (400). Boot 3+ resolver này trả body Problem Details JSON thay vì HTML.
Vì sao tách làm 3 resolver thay vì một? Mỗi resolver có chiến lược resolve khác nhau — đọc annotation, tìm method, handle hard-coded. Composite pattern cho phép extend bằng cách add resolver mới vào danh sách mà không sửa code hiện tại.
4. @RestControllerAdvice — tập trung mapping
@RestControllerAdvice = @ControllerAdvice + @ResponseBody. Giống như @RestController = @Controller + @ResponseBody, annotation này đảm bảo mọi method return value đều được serialize thành response body JSON tự động.
Cấu trúc class chuẩn:
@RestControllerAdvice
@Slf4j
public class GlobalExceptionHandler {
// Domain exception → 404
@ExceptionHandler(TaskNotFoundException.class)
public ProblemDetail handleTaskNotFound(TaskNotFoundException ex) {
ProblemDetail pd = ProblemDetail.forStatus(HttpStatus.NOT_FOUND);
pd.setTitle("Task not found");
pd.setDetail(ex.getMessage());
pd.setProperty("taskId", ex.getTaskId());
return pd;
}
// Domain exception → 409 Conflict
@ExceptionHandler(DuplicateProjectException.class)
public ProblemDetail handleDuplicateProject(DuplicateProjectException ex) {
ProblemDetail pd = ProblemDetail.forStatus(HttpStatus.CONFLICT);
pd.setTitle("Duplicate project");
pd.setDetail(ex.getMessage());
pd.setProperty("projectKey", ex.getProjectKey());
return pd;
}
// Validation fail → 400 voi danh sach violations
@ExceptionHandler(MethodArgumentNotValidException.class)
public ProblemDetail handleValidation(MethodArgumentNotValidException ex) {
ProblemDetail pd = ProblemDetail.forStatus(HttpStatus.BAD_REQUEST);
pd.setTitle("Validation failed");
pd.setDetail("Request has " + ex.getBindingResult().getErrorCount() + " invalid field(s)");
List<Map<String, String>> violations = ex.getBindingResult().getFieldErrors().stream()
.map(err -> Map.of(
"field", err.getField(),
"message", err.getDefaultMessage()
))
.toList();
pd.setProperty("violations", violations);
return pd;
}
// Catch-all — 500 generic, KHONG leak stack trace
@ExceptionHandler(Exception.class)
public ProblemDetail handleAll(Exception ex, HttpServletRequest req) {
String requestId = MDC.get("requestId");
log.error("Unhandled exception, path={}, requestId={}", req.getRequestURI(), requestId, ex);
ProblemDetail pd = ProblemDetail.forStatus(HttpStatus.INTERNAL_SERVER_ERROR);
pd.setTitle("Internal server error");
pd.setDetail("An unexpected error occurred. Please contact support with the request ID.");
pd.setProperty("requestId", requestId);
return pd;
}
}
Vì @RestControllerAdvice = @ResponseBody ngầm, method return ProblemDetail được Spring tự động serialize thành JSON với Content-Type: application/problem+json. Controller method không cần try-catch — chỉ throw exception, advice lo phần còn lại.
4.1 Thứ tự match exception type
Khi một exception throw, ExceptionHandlerExceptionResolver tìm method @ExceptionHandler có type gần nhất trong cây kế thừa:
@ExceptionHandler(RuntimeException.class) // match catch-all RuntimeException
public ProblemDetail handleRuntime(RuntimeException ex) { ... }
@ExceptionHandler(TaskNotFoundException.class) // match specific
public ProblemDetail handleNotFound(TaskNotFoundException ex) { ... }
Nếu TaskNotFoundException extends RuntimeException được throw: Spring chọn handler TaskNotFoundException vì nó gần hơn trong cây kế thừa. Handler RuntimeException chỉ được gọi khi không tìm thấy handler specific hơn.
Pitfall: nếu đặt @ExceptionHandler(Exception.class) trước các handler specific trong class, Spring vẫn chọn đúng — thứ tự trong class không quan trọng, Spring luôn chọn closest match. Nhưng nếu có hai advice class có handler cùng type, thứ tự @Order của advice class quyết định cái nào thắng.
5. Domain exception → HTTP status mapping
Pattern sạch nhất cho production: domain exception không biết gì về HTTP. Exception class là POJO thuần, advice class là nơi duy nhất biết HTTP status.
// Domain layer -- KHONG import spring.http
public class TaskNotFoundException extends RuntimeException {
private final Long taskId;
public TaskNotFoundException(Long taskId) {
super("Task " + taskId + " not found");
this.taskId = taskId;
}
public Long getTaskId() { return taskId; }
}
public class ProjectKeyConflictException extends RuntimeException {
private final String projectKey;
public ProjectKeyConflictException(String projectKey) {
super("Project key '" + projectKey + "' already exists");
this.projectKey = projectKey;
}
public String getProjectKey() { return projectKey; }
}
public class InvalidTaskStateException extends RuntimeException {
private final String currentState;
private final String attemptedTransition;
public InvalidTaskStateException(String currentState, String attemptedTransition) {
super("Cannot transition from " + currentState + " via " + attemptedTransition);
this.currentState = currentState;
this.attemptedTransition = attemptedTransition;
}
// getters...
}
// Service layer -- throw domain exception
@Service
public class TaskService {
public TaskDto findById(Long id) {
return taskRepo.findById(id)
.map(taskMapper::toDto)
.orElseThrow(() -> new TaskNotFoundException(id));
}
public TaskDto complete(Long id) {
Task task = taskRepo.findById(id)
.orElseThrow(() -> new TaskNotFoundException(id));
if (!task.canComplete()) {
throw new InvalidTaskStateException(task.getStatus().name(), "COMPLETE");
}
task.complete();
return taskMapper.toDto(taskRepo.save(task));
}
}
// Web layer -- advice la noi duy nhat biet HTTP status
@RestControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(TaskNotFoundException.class)
public ProblemDetail handleTaskNotFound(TaskNotFoundException ex) {
ProblemDetail pd = ProblemDetail.forStatus(HttpStatus.NOT_FOUND);
pd.setTitle("Task not found");
pd.setDetail(ex.getMessage());
pd.setProperty("taskId", ex.getTaskId());
return pd;
}
@ExceptionHandler(ProjectKeyConflictException.class)
public ProblemDetail handleProjectConflict(ProjectKeyConflictException ex) {
ProblemDetail pd = ProblemDetail.forStatus(HttpStatus.CONFLICT);
pd.setTitle("Project key conflict");
pd.setDetail(ex.getMessage());
pd.setProperty("projectKey", ex.getProjectKey());
return pd;
}
@ExceptionHandler(InvalidTaskStateException.class)
public ProblemDetail handleInvalidState(InvalidTaskStateException ex) {
ProblemDetail pd = ProblemDetail.forStatus(HttpStatus.UNPROCESSABLE_ENTITY);
pd.setTitle("Invalid task state transition");
pd.setDetail(ex.getMessage());
return pd;
}
}
Lợi ích của việc tách này:
- Domain layer test ngoài Spring:
assertThrows(TaskNotFoundException.class, () -> taskService.findById(999L))— không cần@SpringBootTest. - Đổi HTTP status không chạm service: muốn đổi "task not found" từ 404 sang 422? Sửa 1 dòng trong advice, không đụng service.
- Audit exception mapping: mở
GlobalExceptionHandler.java→ thấy toàn bộ domain exception nào map sang status nào.
6. ResponseStatusException — shortcut
ResponseStatusException là exception built-in Spring cho phép throw trực tiếp trong handler method mà không cần domain exception class riêng:
@GetMapping("/tasks/{id}")
public TaskDto get(@PathVariable Long id) {
return taskRepo.findById(id)
.map(taskMapper::toDto)
.orElseThrow(() -> new ResponseStatusException(
HttpStatus.NOT_FOUND,
"Task " + id + " not found"
));
}
ResponseStatusExceptionResolver (Order 1) handle nó: đọc status code từ exception, set response status, trả Problem Details body tự động với Boot 3+.
Khi nào phù hợp:
- Prototype / internal tool — code nhanh, ít class.
- Endpoint one-off — exception chỉ throw duy nhất một chỗ, không tái dùng.
- App nhỏ dưới 5 controller — domain layer chưa cần tách phức tạp.
Khi nào không phù hợp:
- Service layer throw exception nhưng cần test ngoài Spring —
ResponseStatusExceptioncoupling service với HTTP. - Cần custom field trong response (như
taskId,projectKey) —ResponseStatusExceptionchỉ cómessage. - Cùng exception throw từ nhiều nơi — phải copy status code mỗi nơi, brittle.
- API public cần error format stable và documented — domain exception + advice rõ ràng hơn.
// SAI -- method tra ProblemDetail nhung khong serialize
@ControllerAdvice
public class GlobalHandler {
@ExceptionHandler(TaskNotFoundException.class)
public ProblemDetail handle(TaskNotFoundException ex) { ... }
}
@ControllerAdvice không có @ResponseBody ngầm như @RestControllerAdvice. Method return ProblemDetail sẽ được Spring hiểu như tên view để render, không phải response body JSON. Kết quả: client nhận HTML error page hoặc TemplateInputException.
// DUNG
@RestControllerAdvice // = @ControllerAdvice + @ResponseBody
public class GlobalHandler { ... }
// SAI -- Spring khong biet map status nao
throw new RuntimeException("Task " + id + " not found");
Không có @ExceptionHandler(RuntimeException.class) trong advice, exception rơi xuống DefaultHandlerExceptionResolver; resolver này cũng không match nên Spring wrap thành 500 với message ngẫu nhiên.
Ngay cả khi có @ExceptionHandler(Exception.class) catch-all, nó vẫn map thành 500, trong khi đây là 404 client error. Client tưởng server bug, thực ra là resource không tồn tại.
// DUNG -- domain exception specific + advice map sang 404
throw new TaskNotFoundException(id);
7. Liên hệ các bài khác
- ResponseEntity & status: hiểu HTTP status semantics trước khi đọc bài này — advice map exception sang status code nào là câu hỏi bài đó trả lời cho domain exception → HTTP.
- Problem Details (RFC 9457):
@RestControllerAdvicetrảProblemDetailobject — bài tiếp theo đào sâu format RFC 9457, 5 field standard,typeURI, custom extension, vàspring.mvc.problemdetails.enabled.
Tóm tắt
- Spring có 3 layer exception handling, iterate theo thứ tự: controller local →
@RestControllerAdviceglobal →HandlerExceptionResolverdefault. HandlerExceptionResolverCompositedispatch tới 3 resolver theoOrder:ExceptionHandlerExceptionResolver(tìm@ExceptionHandler) →ResponseStatusExceptionResolver(đọc@ResponseStatus) →DefaultHandlerExceptionResolver(exception built-in MVC).@RestControllerAdvice=@ControllerAdvice+@ResponseBody— mọi method return value tự serialize thành JSON response body.- Spring chọn handler
@ExceptionHandlervới type gần nhất trong cây kế thừa — đặt specific trước generic không bắt buộc nhưng tốt cho readability. - Domain exception là POJO thuần, không import HTTP. Advice class là nơi duy nhất biết HTTP status → domain testable ngoài Spring, mapping tập trung một chỗ.
ResponseStatusExceptionlà shortcut cho prototype / endpoint one-off — tránh ở service layer và app enterprise.- Catch-all
@ExceptionHandler(Exception.class)là bắt buộc — log server, response generic +requestId, không bao giờ expose stack trace.
Tự kiểm tra
Q1Vì sao nên đặt toàn bộ @ExceptionHandler vào một class @RestControllerAdvice thay vì rải vào từng controller? Liệt kê ít nhất 3 lý do cụ thể.▸
@ExceptionHandler vào một class @RestControllerAdvice thay vì rải vào từng controller? Liệt kê ít nhất 3 lý do cụ thể.1. Tránh duplicate code (DRY): Logic format response (tạo ProblemDetail, set field, trả status) phải lặp ở mỗi controller nếu không tập trung. Đổi format — sửa N controller. Với advice — sửa 1 class.
2. Đảm bảo consistent format: Nhiều dev làm nhiều controller, mỗi người tự format exception response khác nhau. Client không parse đồng nhất được. Advice tập trung → một format duy nhất cho toàn app.
3. Audit dễ dàng: "Domain exception nào được handle? Map sang status nào?" → mở GlobalExceptionHandler.java thấy toàn bộ. Rải trong controller → phải đọc từng method của từng controller.
4. Controller method clean hơn: Controller chỉ lo happy path — throw exception, advice handle. Không try-catch làm nhiễu business logic. Và return type không bị ép đổi thành ResponseEntity chỉ để trả error.
Q2Giải thích cơ chế HandlerExceptionResolverComposite: nó gồm mấy resolver, thứ tự nào, và mỗi resolver handle loại exception gì?▸
HandlerExceptionResolverComposite: nó gồm mấy resolver, thứ tự nào, và mỗi resolver handle loại exception gì?HandlerExceptionResolverComposite là một danh sách HandlerExceptionResolver được iterate theo Order từ thấp đến cao (ưu tiên cao nhất trước). Ba resolver mặc định:
1. ExceptionHandlerExceptionResolver (Order 0): Tìm method @ExceptionHandler phù hợp — trước trong controller hiện tại, sau đó trong tất cả class @ControllerAdvice/@RestControllerAdvice. Nếu tìm được, gọi method đó và serialize return value thành response. Đây là cơ chế xử lý handler custom của bạn.
2. ResponseStatusExceptionResolver (Order 1): Đọc annotation @ResponseStatus trên class exception. Nếu exception được đánh dấu @ResponseStatus(HttpStatus.NOT_FOUND), resolver set status 404 và trả response. Cũng handle ResponseStatusException thrown trực tiếp trong code.
3. DefaultHandlerExceptionResolver (Order 2): Handle exception built-in Spring MVC: MethodArgumentNotValidException (400), HttpRequestMethodNotSupportedException (405), HttpMediaTypeNotSupportedException (415), MissingServletRequestParameterException (400), và nhiều loại khác. Boot 3+ resolver này trả Problem Details JSON.
Nếu không resolver nào handle được, exception được re-throw và Tomcat/Undertow render error page mặc định.
Q3Vì sao domain exception (như TaskNotFoundException) không nên import org.springframework.http.HttpStatus? Lợi ích cụ thể là gì khi test service layer?▸
TaskNotFoundException) không nên import org.springframework.http.HttpStatus? Lợi ích cụ thể là gì khi test service layer?Vì sao không import HTTP: Domain exception là business concept — "task không tồn tại" là sự thật của domain, không phải HTTP. Nếu exception biết HttpStatus.NOT_FOUND, nó bị coupling với transport layer. Ngày mai muốn expose cùng service qua gRPC (dùng status code khác) hoặc message queue — domain exception phải sửa.
Lợi ích khi test service layer:
Khi service throw TaskNotFoundException (POJO thuần), test không cần Spring context:
@ExtendWith(MockitoExtension.class)
class TaskServiceTest {
@Mock TaskRepository taskRepo;
@InjectMocks TaskService taskService;
@Test
void findById_notFound_throwsDomainException() {
when(taskRepo.findById(999L)).thenReturn(Optional.empty());
assertThrows(TaskNotFoundException.class,
() -> taskService.findById(999L));
}
}Test chạy bằng Mockito thuần, không cần @SpringBootTest (load toàn bộ Spring context tốn 3-5 giây). Service logic có thể assert hoàn toàn bằng unit test nhanh. Nếu exception chứa HttpStatus, test service phải assert HTTP field — trách nhiệm sai lớp.
Q4App có @ExceptionHandler(RuntimeException.class) và @ExceptionHandler(TaskNotFoundException.class) trong cùng một @RestControllerAdvice. TaskNotFoundException extends RuntimeException. Spring chọn handler nào khi throw TaskNotFoundException? Thứ tự khai báo trong class có ảnh hưởng không?▸
@ExceptionHandler(RuntimeException.class) và @ExceptionHandler(TaskNotFoundException.class) trong cùng một @RestControllerAdvice. TaskNotFoundException extends RuntimeException. Spring chọn handler nào khi throw TaskNotFoundException? Thứ tự khai báo trong class có ảnh hưởng không?Spring chọn handler TaskNotFoundException — luôn luôn, bất kể thứ tự khai báo trong class.
Cơ chế: ExceptionHandlerExceptionResolver duyệt tất cả method @ExceptionHandler trong advice, tính "khoảng cách" từ exception thực tế (TaskNotFoundException) lên cây kế thừa tới mỗi handler type. TaskNotFoundException.class có khoảng cách 0 (exact match). RuntimeException.class có khoảng cách 1 (superclass). Spring chọn khoảng cách nhỏ nhất.
Thứ tự khai báo trong class: không ảnh hưởng. Spring build một map từ exception type → handler method khi khởi tạo advice, không phụ thuộc thứ tự trong source file.
Trường hợp thứ tự CÓ ảnh hưởng: khi hai class advice khác nhau đều có handler cho cùng exception type. Khi đó @Order của advice class quyết định cái nào được ưu tiên. Đây là lý do nên có 1 advice global thay vì nhiều advice cùng scope.
Q5Catch-all handler @ExceptionHandler(Exception.class) trả response 500. Vì sao không bao giờ include ex.getMessage() hay stack trace trong response body production? Attacker khai thác thông tin đó thế nào?▸
@ExceptionHandler(Exception.class) trả response 500. Vì sao không bao giờ include ex.getMessage() hay stack trace trong response body production? Attacker khai thác thông tin đó thế nào?Vấn đề với ex.getMessage(): Exception message thường chứa thông tin nhạy cảm mà không ai để ý:
SQLException:Duplicate entry '[email protected]' for key 'users.email_unique'— leak email user + tên constraint → attacker biết schema DB.FileNotFoundException:/etc/secrets/stripe-api-key.txt (No such file)— leak đường dẫn file hệ thống.NullPointerException:Cannot invoke TaskRepository.findById on null— leak tên class nội bộ.
Vấn đề với stack trace: Stack trace expose toàn bộ kiến trúc nội bộ:
- Tên package và class (
com.taskflow.infra.db.TaskRepositoryImpl) → attacker reverse engineer cấu trúc. - Tên thư viện + version (
org.hibernate.engine.jdbc... at Hibernate 6.6.3) → attacker tra CVE database tìm lỗ hổng của đúng version đó. - Query SQL qua Hibernate trace → leak schema table và column name.
Pattern an toàn: log đầy đủ server-side với requestId, trả response generic:
log.error("Unhandled exception, requestId={}", requestId, ex); // day du SERVER
ProblemDetail pd = ProblemDetail.forStatus(500);
pd.setDetail("Unexpected error. Contact support with request ID.");
pd.setProperty("requestId", requestId); // client lookup log qua support
return pd;Client nhận requestId, liên hệ support, support query log → debug. Attacker chỉ thấy generic 500 + UUID vô nghĩa.
Bài tiếp theo: Problem Details (RFC 9457)
Bài này có giúp bạn hiểu bản chất không?
Hỏi đáp về bài này
Chưa có câu hỏi
Có gì chưa rõ trong bài? Đặt câu hỏi đầu tiên — câu trả lời từ cộng đồng giúp bạn (và người sau).
Đặt câu hỏi đầu tiên