Spring REST API & Data JPA/Request Binding — @PathVariable, @RequestParam, @RequestBody và cơ chế bên dưới
8/46
Bài 8 / 46~12 phútRequest & ResponseMiễn phí lượt xem

Request Binding — @PathVariable, @RequestParam, @RequestBody và cơ chế bên dưới

Bài này bóc đúng một thứ: 6 nguồn dữ liệu trong HTTP request, 6 annotation Spring tương ứng, và cơ chế HttpMessageConverter + HandlerMethodArgumentResolver biến chúng thành object Java. Tại sao mỗi annotation tồn tại, tại sao @RequestBody cần Jackson, và tại sao quên @Valid là lỗi thầm lặng.

TL;DR: Mỗi HTTP request mang dữ liệu theo 6 con đường — path, query string, body, header, cookie, request attribute. Spring ánh xạ 6 con đường đó thành 6 annotation: @PathVariable, @RequestParam, @RequestBody, @RequestHeader, @CookieValue, @RequestAttribute. Mỗi annotation có 1 HandlerMethodArgumentResolver chuyên biệt bên dưới. Riêng @RequestBody thêm một bước: chuyển byte thô sang object Java qua HttpMessageConverter (mặc định Jackson). Pitfall lớn nhất: không có @Valid trước @RequestBody — Spring vẫn bind được, nhưng constraint annotation trên DTO hoàn toàn im lặng, dữ liệu sai vẫn chui thẳng vào service.

Bài 02 — @RestController & mapping đã giải thích DispatcherServlet định tuyến request đến đúng method controller. Câu hỏi atomic của bài này hẹp hơn: sau khi Spring chọn được method, nó lấy dữ liệu từ HTTP request và đổ vào parameter Java bằng cách nào?

1. Sáu nguồn dữ liệu trong một HTTP request

Một HTTP request không chỉ có body. Dữ liệu tồn tại ở nhiều vị trí khác nhau trong cấu trúc của request:

GET /orders/42?status=active HTTP/1.1
Host: api.olhub.vn
Authorization: Bearer eyJ...
X-Request-Id: req-abc-123
Cookie: theme=dark; JSESSIONID=xyz
flowchart LR
  Req["HTTP Request"]
  P["Path segment<br/>/orders/42"]
  Q["Query string<br/>?status=active"]
  B["Body<br/>JSON / form"]
  H["Headers<br/>Authorization"]
  C["Cookies<br/>theme=dark"]
  A["Request attributes<br/>(set boi Filter)"]

  Req --> P & Q & B & H & C & A

  P --> PV["@PathVariable"]
  Q --> RP["@RequestParam"]
  B --> RB["@RequestBody"]
  H --> RH["@RequestHeader"]
  C --> CV["@CookieValue"]
  A --> RA["@RequestAttribute"]
AnnotationNguồn dữ liệuVí dụ cụ thể
@PathVariableURL path segment/orders/{id}id = 42
@RequestParamQuery string hoặc form body?status=active
@RequestBodyRequest body (JSON / XML){"customer":"Alice","total":99.99}
@RequestHeaderHTTP headerAuthorization: Bearer eyJ...
@CookieValueCookietheme=dark
@RequestAttributeAttribute set bởi Filter/Interceptorreq.setAttribute("user", ...)

Mỗi annotation tồn tại vì mỗi vị trí trong request có format và cách đọc khác nhau. Spring không thể đoán bạn muốn đọc từ đâu — phải khai tường minh.

2. @PathVariable — bind từ URL path segment

@GetMapping("/orders/{id}")
public OrderDto get(@PathVariable Long id) { ... }

Spring lấy phần URL khớp placeholder {id}, tự chuyển String "42" sang Long 42. Khi conversion thất bại (URL chứa "abc" cho kiểu Long), Spring throw MethodArgumentTypeMismatchException và trả 400 Bad Request — không crash app, không exception chưa handle.

Tên không trùng giữa placeholder và parameter

Khi đặt tên khác nhau (naming convention URL và Java thường khác):

@GetMapping("/orders/{order_id}")
public OrderDto get(@PathVariable("order_id") Long orderId) { ... }

Phần trong ngoặc "order_id" là tên trong URL template. Parameter orderId là tên Java.

Nhiều path variable

@GetMapping("/users/{userId}/orders/{orderId}")
public OrderDto get(
    @PathVariable Long userId,
    @PathVariable Long orderId
) { ... }

Mỗi placeholder trong URL template ánh xạ 1:1 với một parameter @PathVariable. Tên phải khớp hoặc khai explicit trong ngoặc.

Tại sao @PathVariable chứ không phải đọc HttpServletRequest thủ công?

Không có annotation, bạn phải viết:

@GetMapping("/orders/{id}")
public OrderDto get(HttpServletRequest req) {
    String idStr = (String) req.getAttribute(HandlerMapping.URI_TEMPLATE_VARIABLES_ATTRIBUTE);
    // ... extract "id" từ map, parse sang Long, handle exception
}

Annotation encapsulate toàn bộ đoạn đó — type conversion, error handling, null check. Controller method chỉ nhận giá trị đã sẵn sàng.

3. @RequestParam — bind từ query string

@GetMapping("/orders")
public List<OrderDto> list(
    @RequestParam(defaultValue = "ACTIVE") String status,
    @RequestParam(required = false) Long minTotal,
    @RequestParam(defaultValue = "0") int page,
    @RequestParam(defaultValue = "20") int size
) { ... }

Request: GET /orders?status=PENDING&minTotal=100&page=2&size=50

Spring bind từng query param theo tên, auto-convert sang kiểu Java. defaultValue là String — Spring tự chuyển sang kiểu target. Phải chọn giá trị parse được, không thì fail lúc startup hoặc runtime:

// SAI - "abc" khong parse sang int:
@RequestParam(defaultValue = "abc") int page

// DUNG:
@RequestParam(defaultValue = "0") int page

required vs defaultValue

@RequestParam Long userId                           // required, missing -> 400
@RequestParam(required = false) Long userId         // optional, null neu missing
@RequestParam(defaultValue = "0") Long userId       // optional voi default

required = false trả null khi param vắng mặt — chỉ dùng được với kiểu reference (Long, String), không phải primitive (long, int). Primitive không nhận null.

Multi-value

// URL: ?tags=java&tags=spring&tags=rest
@RequestParam List<String> tags   // -> ["java", "spring", "rest"]

Spring detect collection type, bind tất cả value của cùng tên param.

Tại sao phân biệt @PathVariable@RequestParam?

Path variable là định danh resource — thay đổi thì chỉ một resource khác (/orders/42 vs /orders/43). Query param là modifier — filter, sort, page, không xác định resource. REST convention: dùng path cho ID, query cho filter/pagination.

4. @RequestBody — bind từ request body qua HttpMessageConverter

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

public record OrderRequest(
    String customer,
    BigDecimal total,
    List<OrderItem> items
) {}

Cơ chế bên dưới — HttpMessageConverter

@RequestBody không đơn giản như đọc String từ query param. Body là stream byte thô — Spring cần biết format (JSON? XML? form?) và cần công cụ parse format đó sang object Java.

flowchart TB
  Req["HTTP POST /orders<br/>Content-Type: application/json<br/>Body: {customer:Alice, total:99.99}"]
  HMA["RequestMappingHandlerAdapter<br/>phat hien @RequestBody"]
  Pick["Chon HttpMessageConverter<br/>khop Content-Type: application/json"]
  Jackson["MappingJackson2HttpMessageConverter<br/>(Jackson ObjectMapper)"]
  Obj["OrderRequest record<br/>customer=Alice, total=99.99"]

  Req --> HMA --> Pick --> Jackson --> Obj

Luồng chính xác:

  1. RequestMappingHandlerAdapter phát hiện parameter có @RequestBody.
  2. Đọc header Content-Type của request — ví dụ application/json.
  3. Duyệt danh sách HttpMessageConverter đã đăng ký, hỏi: converter nào canRead(OrderRequest.class, application/json)?
  4. MappingJackson2HttpMessageConverter trả true — đây là converter cho JSON, sử dụng Jackson ObjectMapper bên dưới.
  5. Jackson đọc byte stream từ body, build object OrderRequest bằng canonical constructor của record.
  6. Object pass vào method controller.

Khi body JSON sai cú pháp, hoặc kiểu field không khớp DTO (ví dụ gửi "total": "not-a-number"), Jackson throw JsonParseException hoặc HttpMessageNotReadableException, Spring map về 400 Bad Request.

Tại sao @RequestBody cần HttpMessageConverter riêng còn @RequestParam không cần?

Query param luôn là String trong URL — type conversion đơn giản (String → Long, String → Enum). Body là byte stream có thể ở bất kỳ format nào — JSON, XML, Protobuf. HttpMessageConverter là abstraction cho phép Spring support nhiều format mà không hardcode logic parse vào framework. Bạn có thể thêm converter custom cho format bất kỳ.

Record DTO là pattern chuẩn cho @RequestBody

// Modern (Java 17+, Boot 3):
public record OrderRequest(
    String customer,
    BigDecimal total,
    List<OrderItem> items
) {}

// Legacy (verbose, still works):
public class OrderRequest {
    private String customer;
    private BigDecimal total;
    private List<OrderItem> items;
    // getters, setters, equals, hashCode, toString...
}

Record ngắn hơn 10x. Immutable — field final, không setter, thread-safe. Jackson 2.12+ (Spring Boot 3.x mặc định) hỗ trợ record native qua jackson-module-parameter-names có sẵn trong Boot starter — không cần @JsonCreator hay @JsonProperty.

@Valid — pitfall lớn nhất của @RequestBody

public record OrderRequest(
    @NotBlank String customer,     // constraint khai bao...
    @NotNull @Positive BigDecimal total
) {}

// SAI - constraint KHONG trigger:
@PostMapping("/orders")
public OrderDto create(@RequestBody OrderRequest req) { ... }

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

Không có @Valid, Spring bind body thành công nhưng Bean Validation không chạy. customer = "" vẫn chui vào service. Không có exception, không có log cảnh báo — lỗi thầm lặng. Khi có @Valid, constraint fail → Spring throw MethodArgumentNotValidException → 400 Problem Details với danh sách field vi phạm.

Luôn đặt @Valid trước @RequestBody

Đây là lỗi phổ biến nhất khi viết controller đầu tiên. Thứ tự annotation không quan trọng (@Valid @RequestBody hay @RequestBody @Valid đều được), nhưng thiếu @Valid là lỗi hoàn toàn im lặng.

5. @RequestHeader — bind từ HTTP header

@GetMapping("/orders/{id}")
public OrderDto get(
    @PathVariable Long id,
    @RequestHeader("Authorization") String authToken,
    @RequestHeader(value = "X-Request-Id", required = false) String requestId,
    @RequestHeader(defaultValue = "en") String acceptLanguage
) { ... }

Pattern hay gặp:

  • Authorization cho JWT Bearer token.
  • X-Request-Id hoặc X-Correlation-Id cho distributed tracing.
  • Accept-Language cho i18n.

Bind toàn bộ header vào HttpHeaders (Spring wrapper) để có method tiện ích:

@RequestHeader HttpHeaders headers
// headers.getContentType(), headers.getAccept(), headers.getFirst("X-Custom")
@GetMapping("/dashboard")
public DashboardDto get(
    @CookieValue(value = "theme", defaultValue = "light") String theme
) { ... }

Ít dùng trong REST API thuần JSON — cookie gắn với session-based auth. JWT API thường không cần. Pattern này hay gặp trong app web truyền thống hoặc khi cần đọc cookie preference của user (theme, locale).

7. @RequestAttribute — bind từ request attribute

Filter hoặc Interceptor chạy trước controller, có thể gắn object vào request attribute:

@Component
public class JwtAuthFilter extends OncePerRequestFilter {
    protected void doFilterInternal(HttpServletRequest req, HttpServletResponse res, FilterChain chain)
            throws ServletException, IOException {
        String token = req.getHeader("Authorization");
        User user = jwtService.validate(token);
        req.setAttribute("currentUser", user);   // <-- ghi vao attribute
        chain.doFilter(req, res);
    }
}

@GetMapping("/profile")
public UserDto profile(@RequestAttribute("currentUser") User user) { ... }

Filter set attribute → controller bind. Tránh phải gọi jwtService.validate() lặp lại trong mỗi controller.

Pattern này ổn, nhưng Spring Security có @AuthenticationPrincipal chuẩn hơn cho trường hợp inject user từ auth context. @RequestAttribute dùng khi cần pass data tùy biến không liên quan security.

8. Cơ chế bên dưới — HandlerMethodArgumentResolver

Sáu annotation trong bài không hoạt động bằng reflection magic. Mỗi annotation có 1 class HandlerMethodArgumentResolver cụ thể chịu trách nhiệm:

AnnotationResolver
@PathVariablePathVariableMethodArgumentResolver
@RequestParamRequestParamMethodArgumentResolver
@RequestBodyRequestResponseBodyMethodProcessor
@RequestHeaderRequestHeaderMethodArgumentResolver
@CookieValueServletCookieValueMethodArgumentResolver
@RequestAttributeRequestAttributeMethodArgumentResolver

Interface của resolver:

public interface HandlerMethodArgumentResolver {
    boolean supportsParameter(MethodParameter parameter);
    Object resolveArgument(MethodParameter parameter,
                           ModelAndViewContainer mavContainer,
                           NativeWebRequest webRequest,
                           WebDataBinderFactory binderFactory) throws Exception;
}

RequestMappingHandlerAdapter giữ danh sách resolver. Khi cần resolve một parameter:

flowchart TB
  P["Parameter: @PathVariable Long id"]
  Loop["Iterate resolver list"]
  Q{"supportsParameter()?"}
  R["resolveArgument() -> 42L"]
  Pass["Pass 42L vao method"]

  P --> Loop --> Q
  Q -->|"PathVariableMethodArgumentResolver: true"| R --> Pass
  Q -->|"Other resolvers: false"| Loop
  1. Duyệt danh sách resolver theo thứ tự đăng ký.
  2. Hỏi supportsParameter() — resolver kiểm tra parameter có annotation phù hợp không.
  3. Resolver đầu tiên trả true được chọn — gọi resolveArgument() để lấy giá trị thực.
  4. Giá trị pass vào method khi Spring invoke handler.

Đây là lý do bạn có thể viết custom resolver: implement interface, đăng ký vào WebMvcConfigurer.addArgumentResolvers(), Spring tự thêm vào danh sách.

Xem chi tiết cơ chế resolver tại HandlerMethodArgumentResolver & type conversion.

Pitfall tổng hợp

Pitfall 1: Quên @Valid trước @RequestBody — đã giải thích ở mục 4. Constraint không trigger, dữ liệu sai vào service im lặng.

Pitfall 2: defaultValue không parse được sang kiểu target:

// SAI - "abc" khong parse sang int -> 400 moi request:
@RequestParam(defaultValue = "abc") int page

// DUNG:
@RequestParam(defaultValue = "0") int page

Pitfall 3: @PathVariable trên primitive, URL truyền sai kiểu:

@GetMapping("/orders/{id}")
public OrderDto get(@PathVariable long id) { ... }
// URL: /orders/abc -> MethodArgumentTypeMismatchException -> 400 (dung)
// URL: /orders/   -> 404 (khong match pattern, cung dung)

Primitive và wrapper đều Spring xử lý tốt, nhưng nên dùng wrapper (Long) khi path variable có thể vắng mặt (optional pattern).

Pitfall 4: Dùng @RequestBody cho GET request:

// GET khong co body theo HTTP spec - client co the khong gui:
@GetMapping("/search")
public List<OrderDto> search(@RequestBody SearchCriteria criteria) { ... }

HTTP cho phép GET có body nhưng semantics undefined — proxy và server có thể bỏ qua. Tìm kiếm phức tạp dùng POST /search với body, hoặc flatten criteria thành query param cho GET.

Pitfall 5: Tên parameter Java không khớp query param name:

// Query: ?minTotal=100 nhung parameter ten la "min":
@RequestParam Long min   // -> null (100 bi bo qua)

Mặc định Spring bind theo tên parameter Java. Nếu khác: @RequestParam("minTotal") Long min.

Liên hệ các bài khác

  • @RestController & mapping: DispatcherServlet chọn controller method trước, rồi mới đến bước bind argument bài này giải thích. Hiểu routing trước khi hiểu binding.
  • HandlerMethodArgumentResolver & type conversion: bài tiếp theo đào sâu cơ chế resolver — cách Spring convert String sang Enum/Date/custom type, và cách viết custom resolver cho annotation riêng (@CurrentUser, @TenantId).
  • Bài validation (module 03): @Valid kích hoạt Bean Validation chạy trên DTO — bài đó giải thích constraint nào dùng khi nào và cách handle MethodArgumentNotValidException trả về 400 Problem Details chuẩn.

Tóm tắt

  • HTTP request mang dữ liệu theo 6 con đường — path, query, body, header, cookie, attribute. Spring có 6 annotation tương ứng.
  • @PathVariable cho URL segment định danh resource. @RequestParam cho query string filter/page.
  • @RequestBody cho JSON body — dùng HttpMessageConverter (Jackson mặc định) để chuyển byte stream thành object Java. Record DTO là pattern chuẩn: immutable, concise, Jackson native support.
  • @RequestHeader cho HTTP header. @CookieValue cho cookie. @RequestAttribute cho data filter đã gắn vào request.
  • Bên dưới: mỗi annotation có 1 HandlerMethodArgumentResolver. RequestMappingHandlerAdapter duyệt danh sách resolver, chọn cái đầu tiên supportsParameter() = true, gọi resolveArgument().
  • Pitfall duy nhất cần nhớ: thiếu @Valid trước @RequestBody — binding thành công nhưng validation im lặng.

Tự kiểm tra

Tự kiểm tra
Q1
Cho method controller sau, xác định annotation nào bind từ nguồn nào, và request mẫu trông ra sao:
@GetMapping("/users/{userId}/orders")
public Page<OrderDto> list(
  @PathVariable Long userId,
  @RequestParam(defaultValue = "ACTIVE") String status,
  @RequestHeader(value = "X-Tenant-Id", required = false) String tenantId,
  Pageable pageable
) { ... }

Nguồn từng argument:

  • userId: URL path segment {userId}@PathVariable. String "42" được tự convert sang Long 42.
  • status: query string — @RequestParam. Vắng mặt thì dùng default "ACTIVE".
  • tenantId: HTTP header X-Tenant-Id@RequestHeader. required=false nên vắng header vẫn OK, giá trị null.
  • pageable: Spring Data tự bind từ query param page, size, sort — không cần annotation.

Request mẫu:

GET /users/42/orders?status=PENDING&page=0&size=20&sort=createdAt,desc HTTP/1.1
X-Tenant-Id: tenant-vn

Spring resolve 4 argument độc lập — mỗi cái do 1 HandlerMethodArgumentResolver riêng handle rồi Spring inject vào method khi invoke.

Q2
Giải thích tại sao @RequestBody cần HttpMessageConverter (Jackson) để hoạt động, trong khi @RequestParam không cần. Cơ chế bên dưới khác nhau thế nào?

@RequestParam đọc từ query string — dữ liệu luôn là String thuần trong URL. Conversion String sang Long, Enum, int là type conversion đơn giản, framework tự làm bằng Converter SPI.

@RequestBody đọc từ request body — đây là byte stream có thể ở bất kỳ format nào: JSON, XML, Protobuf, plain text. Cần biết format trước khi parse. Quy trình:

  1. Đọc header Content-Type của request (vd application/json).
  2. Tìm HttpMessageConverter canRead(TargetClass, MediaType) = true.
  3. Converter đó đọc byte stream và build object (Jackson với JSON, JAXB với XML...).

HttpMessageConverter là abstraction cho phép thêm format mới mà không sửa framework. Nếu body là Protobuf, bạn chỉ cần thêm converter Protobuf — không cần thay đổi annotation hay controller.

Q3
Đoạn code sau có lỗi gì? Tại sao lỗi này nguy hiểm hơn lỗi compile error?
public record CreateUserRequest(
  @NotBlank String username,
  @Email String email,
  @Size(min = 8) String password
) {}

@PostMapping("/users")
public UserDto create(@RequestBody CreateUserRequest req) {
  return userService.create(req);
}

Lỗi: thiếu @Valid trước @RequestBody.

Hậu quả cụ thể: Spring bind body JSON thành CreateUserRequest thành công — không có exception. Nhưng Bean Validation (@NotBlank, @Email, @Size) không chạy. Request {"username":"","email":"notEmail","password":"123"} đi thẳng vào userService.create().

Tại sao nguy hiểm hơn compile error: compile error lộ ngay lúc build, không thể deploy. Lỗi này hoàn toàn im lặng — không exception, không log, không test đỏ nếu test không check validation. Dữ liệu sai vào database, hoặc service phía sau crash bất ngờ với NullPointerException hoặc DB constraint violation — rất khó trace nguyên nhân.

Fix:

@PostMapping("/users")
public UserDto create(@Valid @RequestBody CreateUserRequest req) {
  return userService.create(req);
}

Thêm @Valid khiến Spring chạy Bean Validation sau khi bind. Constraint fail → MethodArgumentNotValidException → 400 với danh sách field lỗi. Thứ tự @Valid @RequestBody hay @RequestBody @Valid đều được.

Q4
Bạn cần đọc custom header X-Api-Version ở nhiều controller method. Liệt kê 3 cách tiếp cận và giải thích cách nào clean nhất và vì sao.

Cách 1 — @RequestHeader lặp ở mỗi method:

@GetMapping("/orders")
public List<OrderDto> list(@RequestHeader("X-Api-Version") String version, ...) { ... }

@GetMapping("/users")
public List<UserDto> users(@RequestHeader("X-Api-Version") String version, ...) { ... }

Copy-paste mỗi method. 50 endpoint = 50 lần lặp. Đổi tên header phải sửa 50 chỗ.

Cách 2 — Interceptor / Filter đọc header và set request attribute:

// Filter set attribute:
req.setAttribute("apiVersion", req.getHeader("X-Api-Version"));

// Controller dung @RequestAttribute:
@GetMapping("/orders")
public List<OrderDto> list(@RequestAttribute("apiVersion") String version) { ... }

Đỡ lặp hơn, nhưng vẫn khai báo ở mỗi method, và dùng String key dễ typo.

Cách 3 — Custom HandlerMethodArgumentResolver với annotation riêng:

@Target(ElementType.PARAMETER)
@Retention(RetentionPolicy.RUNTIME)
public @interface ApiVersion {}

@Component
public class ApiVersionResolver implements HandlerMethodArgumentResolver {
  public boolean supportsParameter(MethodParameter p) {
      return p.hasParameterAnnotation(ApiVersion.class);
  }
  public Object resolveArgument(MethodParameter p, ModelAndViewContainer mvc,
          NativeWebRequest req, WebDataBinderFactory binder) {
      return req.getHeader("X-Api-Version");
  }
}

// Controller:
@GetMapping("/orders")
public List<OrderDto> list(@ApiVersion String version) { ... }

Cách 3 clean nhất vì: tên annotation tự document ý định, type-safe, đổi logic lấy version chỉ sửa 1 chỗ resolver, dễ test mock resolver. Tradeoff: setup ban đầu tốn vài phút — nhưng amortize qua toàn bộ codebase.

Q5
Giải thích cơ chế HandlerMethodArgumentResolver theo từng bước khi Spring resolve parameter @PathVariable Long id trong request GET /orders/42.

RequestMappingHandlerAdapter đã chọn được method controller phù hợp. Trước khi invoke, cần resolve từng parameter:

  1. Lấy MethodParameter đại diện cho parameter id — chứa thông tin annotation, kiểu, tên.
  2. Duyệt danh sách HandlerMethodArgumentResolver đã đăng ký theo thứ tự.
  3. Hỏi từng resolver: supportsParameter(MethodParameter). PathVariableMethodArgumentResolver kiểm tra parameter có annotation @PathVariable không — có, trả true.
  4. PathVariableMethodArgumentResolver.resolveArgument() được gọi. Nó đọc URI template variables từ request attribute (HandlerMapping.URI_TEMPLATE_VARIABLES_ATTRIBUTE), lấy value cho key "id" = "42".
  5. Convert "42" (String) sang Long qua ConversionService. Kết quả 42L.
  6. Spring dùng 42L làm argument thứ nhất khi invoke method controller.

Nếu convert thất bại (URL là /orders/abc), bước 5 throw MethodArgumentTypeMismatchExceptionDefaultHandlerExceptionResolver catch và trả 400 Bad Request trước khi method controller được gọi.

Toàn bộ luồng này chạy cho mỗi parameter — 4 parameter thì 4 lần duyệt resolver list, độc lập nhau.

Bài tiếp theo: HandlerMethodArgumentResolver & type conversion

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

Đặt 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