JavaRush /Blog Java /Random-VI /Đăng nhập vào Java: cái gì, như thế nào, ở đâu và bằng cá...
Roman Beekeeper
Mức độ

Đăng nhập vào Java: cái gì, như thế nào, ở đâu và bằng cái gì?

Xuất bản trong nhóm
Xin chào mọi người, cộng đồng JavaRush! Hôm nay chúng ta sẽ nói về ghi nhật ký Java:
  1. Đây là cái gì, tại sao vậy. Sử dụng trong trường hợp nào thì tốt hơn, trong trường hợp nào thì không?
  2. Các cách triển khai ghi nhật ký khác nhau trong Java là gì và chúng ta nên làm gì với sự đa dạng này?
  3. Mức độ ghi nhật ký. Hãy cùng thảo luận về ứng dụng bổ sung là gì và cách cấu hình nó một cách chính xác.
  4. Ghi nhật ký các nút và cách định cấu hình chúng chính xác để mọi thứ hoạt động theo cách chúng ta muốn.
Tài liệu này dành cho nhiều đối tượng. Nó sẽ rõ ràng đối với cả những người mới làm quen với Java và những người đã đang làm việc nhưng chỉ tìm hiểu nó với logger.info(“log something”); Let's Go!

Tại sao cần đăng nhập?

Hãy xem xét các trường hợp thực tế trong đó việc ghi nhật ký sẽ giải quyết được vấn đề. Đây là một ví dụ từ công việc của tôi. Có những điểm ứng dụng tích hợp với các dịch vụ khác. Tôi sử dụng việc ghi lại những điểm này như một “bằng chứng ngoại phạm” : nếu quá trình tích hợp không hoạt động, sẽ dễ dàng tìm ra vấn đề bắt nguồn từ phía nào. Bạn cũng nên ghi lại những thông tin quan trọng được lưu vào cơ sở dữ liệu. Ví dụ: tạo người dùng quản trị viên. Đây chính xác là những gì sẽ tốt để đăng nhập.

Công cụ ghi nhật ký Java

Ghi nhật ký: cái gì, như thế nào, ở đâu và với cái gì?  - 2Các giải pháp nổi tiếng để đăng nhập Java bao gồm:
  • log4j
  • Tháng 7 - java.util.logging
  • JCL - ghi nhật ký chung của jakarta
  • Đăng lại
  • SLF4J - mặt tiền ghi nhật ký đơn giản cho java
Chúng ta hãy xem nhanh từng phần và trong phần thực tế của tài liệu, chúng ta sẽ lấy kết nối Slf4j - log4j làm cơ sở . Bây giờ điều này có vẻ lạ, nhưng đừng lo lắng: đến cuối bài viết, mọi thứ sẽ rõ ràng.

System.err.println

Tất nhiên, ban đầu có System.err.println (đầu ra bản ghi ra bảng điều khiển). Nó vẫn được sử dụng để nhanh chóng lấy được nhật ký trong quá trình gỡ lỗi. Tất nhiên, không cần phải nói về bất kỳ cài đặt nào ở đây, vì vậy chúng ta chỉ cần ghi nhớ nó và tiếp tục.

Log4j

Đây đã là một giải pháp hoàn chỉnh, được tạo ra từ nhu cầu của các nhà phát triển. Nó hóa ra là một công cụ thực sự thú vị để sử dụng. Do nhiều hoàn cảnh khác nhau, giải pháp này chưa bao giờ được đưa vào JDK, điều này khiến toàn bộ cộng đồng rất khó chịu. log4j có các tùy chọn cấu hình để có thể bật tính năng ghi nhật ký trong gói com.example.typevà tắt trong gói con com.example.type.generic. Điều này giúp có thể nhanh chóng tách những gì cần ghi ra khỏi những gì không cần thiết. Điều quan trọng cần lưu ý ở đây là có hai phiên bản log4j: 1.2.x và 2.x.x, không tương thích với nhau . log4j đã thêm một khái niệm như ứng dụng bổ sung , tức là một công cụ ghi lại nhật ký và bố cục - định dạng nhật ký. Điều này cho phép bạn chỉ ghi lại những gì bạn cần và cách thức bạn cần. Chúng ta sẽ nói nhiều hơn về Apper sau.

Tháng 7 - java.util.logging

Một trong những ưu điểm chính là giải pháp - JUL được bao gồm trong JDK (bộ công cụ phát triển Java). Thật không may, trong quá trình phát triển của nó, nó không phải là log4j phổ biến được lấy làm cơ sở mà là một giải pháp của IBM, điều này đã ảnh hưởng đến sự phát triển của nó. Thực tế hiện tại đã có JUL nhưng không có ai sử dụng. Từ “tương tự”: trong JUL, mức ghi nhật ký khác với mức ghi trong Logback, Log4j, Slf4j và điều này làm xấu đi sự hiểu biết giữa chúng. Việc tạo một trình ghi nhật ký ít nhiều giống nhau. Để làm điều này, bạn cần nhập:
java.util.logging.Logger log = java.util.logging.Logger.getLogger(LoggingJul.class.getName());
Tên lớp được truyền cụ thể để biết việc ghi nhật ký đến từ đâu. Kể từ Java 8, có thể chuyển Supplier<String>. Điều này giúp đếm và tạo chuỗi chỉ vào thời điểm nó thực sự cần thiết chứ không phải mọi lúc như trước đây. Chỉ với việc phát hành Java 8, các nhà phát triển mới giải quyết được các vấn đề quan trọng, sau đó JUL mới thực sự có thể sử dụng được. Cụ thể là các phương thức có đối số Supplier<String> msgSuppliernhư dưới đây:
public void info(Supplier<String> msgSupplier) {
   log(Level.INFO, msgSupplier);
}

JCL - ghi nhật ký chung của jakarta

Do thực tế là trong một thời gian dài không có tiêu chuẩn ngành nào về ghi nhật ký và đã có thời kỳ nhiều người tạo trình ghi nhật ký tùy chỉnh của riêng mình, họ đã quyết định phát hành JCL - một trình bao bọc phổ biến sẽ được sử dụng thay cho những trình khác. Tại sao? Khi một số phần phụ thuộc được thêm vào dự án, chúng có thể sử dụng một trình ghi nhật ký khác với trình ghi nhật ký trong dự án. Vì lý do này, chúng được thêm vào dự án một cách tạm thời, điều này tạo ra những vấn đề thực sự khi cố gắng kết hợp tất cả lại với nhau. Thật không may, trình bao bọc rất kém về chức năng và không đưa ra bất kỳ bổ sung nào. Có lẽ sẽ thuận tiện hơn nếu mọi người sử dụng JCL để thực hiện công việc của mình. Nhưng trên thực tế mọi chuyện lại không diễn ra như vậy nên việc sử dụng JCL không phải là một ý tưởng hay vào lúc này.

Đăng lại

Con đường của nguồn mở thật chông gai biết bao... Logback được viết bởi cùng một nhà phát triển với log4j để tạo ra một phiên bản kế thừa cho nó. Ý tưởng này giống như log4j. Sự khác biệt là ở chỗ đăng nhập lại:
  • hiệu suất được cải thiện;
  • thêm hỗ trợ riêng cho slf4j;
  • Tùy chọn lọc đã được mở rộng.
Theo mặc định, đăng nhập lại không yêu cầu bất kỳ cài đặt nào và ghi lại tất cả nhật ký từ cấp DEBUG trở lên. Nếu cần cấu hình, nó có thể được thực hiện thông qua cấu hình xml:
<configuration>
    <appender name="FILE" class="ch.qos.logback.core.FileAppender">
        <file>app.log</file>
        <encoder>
            <pattern>%d{HH:mm:ss,SSS} %-5p [%c] - %m%n</pattern>
        </encoder>
    </appender>
    <logger name="org.hibernate.SQL" level="DEBUG" />
    <logger name="org.hibernate.type.descriptor.sql" level="TRACE" />
    <root level="info">
        <appender-ref ref="FILE" />
    </root>
</configuration>

SLF4J - mặt tiền ghi nhật ký đơn giản cho java

Khoảng năm 2006, một trong những người sáng lập log4j đã rời dự án và tạo ra slf4j - Simple Logging Facade for Java - một trình bao bọc xung quanh log4j, JUL, common-loggins và logback. Như bạn có thể thấy, tiến trình đã đạt đến mức họ đã tạo một trình bao bọc bên trên trình bao bọc... Hơn nữa, nó được chia thành hai phần: API, được sử dụng trong ứng dụng và phần triển khai, được thêm vào dưới dạng các phụ thuộc riêng biệt cho từng loại ghi nhật ký. Ví dụ, slf4j-log4j12.jar, slf4j-jdk14.jar. Chỉ cần kết nối việc triển khai chính xác là đủ và thế là xong: toàn bộ dự án sẽ hoạt động với nó. Slf4j hỗ trợ tất cả các tính năng mới như định dạng chuỗi để ghi nhật ký. Trước đây đã có một vấn đề như vậy. Giả sử có một mục nhật ký:
log.debug("User " + user + " connected from " + request.getRemoteAddr());
userCó một chuyển đổi tiềm ẩn trong đối tượng user.toString()do nối chuỗi và việc này tốn thời gian, làm chậm hệ thống. Và mọi thứ đều ổn nếu chúng tôi gỡ lỗi ứng dụng. Sự cố bắt đầu xảy ra nếu mức ghi nhật ký cho lớp này là INFO trở lên. Nghĩa là, nhật ký này không được ghi lại và việc nối chuỗi cũng không được thực hiện. Về lý thuyết, điều này lẽ ra phải do chính thư viện ghi nhật ký quyết định. Hơn nữa, đây hóa ra lại là vấn đề lớn nhất của phiên bản đầu tiên của log4j. Họ không đưa ra giải pháp thông thường mà đề xuất thực hiện như thế này:
if (log.isDebugEnabled()) {
    log.debug("User " + user + " connected from " + request.getRemoteAddr());
}
Nghĩa là, thay vì viết một dòng ghi nhật ký, họ đề xuất viết 3(!). Việc ghi nhật ký sẽ giảm thiểu những thay đổi đối với mã và ba dòng rõ ràng mâu thuẫn với cách tiếp cận chung. slf4j không gặp vấn đề tương thích với JDK và API, vì vậy một giải pháp hay ngay lập tức xuất hiện:
log.debug("User {} connected from {}", user, request.getRemoteAddr());
trong đó {}biểu thị việc chèn các đối số được truyền vào phương thức. Nghĩa là, cái đầu tiên {}tương ứng với user, cái thứ hai {}- request.getRemoteAddr(). Do đó, chỉ khi mức ghi nhật ký cho phép ghi nhật ký thì thông báo này mới có thể được nối thành một thông báo duy nhất. Sau đó, SJF4J nhanh chóng trở nên phổ biến và hiện là giải pháp tốt nhất. Do đó, chúng tôi sẽ xem xét việc ghi nhật ký bằng ví dụ về gói slf4j-log4j12.

Những gì cần phải được ghi lại

Tất nhiên, bạn không nên ghi lại mọi thứ. Đôi khi điều này là không cần thiết và thậm chí còn nguy hiểm. Ví dụ: nếu bạn cầm cố dữ liệu cá nhân của ai đó và bằng cách nào đó nó bị lộ ra ánh sáng, thì sẽ có vấn đề thực sự, đặc biệt là đối với các dự án hướng tới phương Tây. Nhưng cũng có một số thứ bắt buộc phải đăng nhập :
  1. Bắt đầu/kết thúc ứng dụng. Chúng ta cần biết rằng ứng dụng thực sự đã khởi chạy như chúng ta mong đợi và kết thúc như mong đợi.
  2. Câu hỏi bảo mật. Ở đây sẽ rất tốt nếu ghi lại các lần thử đoán mật khẩu, đăng nhập thông tin đăng nhập của những người dùng quan trọng, v.v.
  3. Một số trạng thái ứng dụng Ví dụ, sự chuyển đổi từ trạng thái này sang trạng thái khác trong một quy trình kinh doanh.
  4. Một số thông tin để gỡ lỗi , với mức ghi nhật ký thích hợp.
  5. Một số tập lệnh SQL. Có những trường hợp thực tế khi điều này là cần thiết. Một lần nữa, bằng cách điều chỉnh mức độ một cách khéo léo, bạn có thể đạt được kết quả xuất sắc.
  6. Các luồng đã thực hiện (Thread) có thể được ghi lại trong trường hợp kiểm tra hoạt động chính xác.

Lỗi ghi nhật ký phổ biến

Có nhiều sắc thái, nhưng đây là một vài lỗi phổ biến:
  1. Khai thác quá mức. Bạn không nên ghi lại mọi bước mà về mặt lý thuyết có thể quan trọng. Có một quy tắc: nhật ký có thể tải hiệu suất không quá 10%. Nếu không sẽ có vấn đề về hiệu suất.
  2. Ghi lại tất cả dữ liệu vào một tập tin. Điều này sẽ khiến việc đọc/ghi vào nó trở nên rất khó khăn ở một thời điểm nhất định, chưa kể có giới hạn kích thước tệp trên một số hệ thống nhất định.
  3. Sử dụng mức ghi nhật ký không chính xác. Mỗi cấp độ ghi nhật ký đều có ranh giới rõ ràng và cần được tôn trọng. Nếu ranh giới không rõ ràng, bạn có thể thống nhất mức độ sử dụng.

Mức độ ghi nhật ký

x: Hiển thị
GÂY TỬ VONG LỖI CẢNH BÁO THÔNG TIN GỠ LỖI DẤU VẾT TẤT CẢ
TẮT
GÂY TỬ VONG x
LỖI x x
CẢNH BÁO x x x
THÔNG TIN x x x x
GỠ LỖI x x x x x
DẤU VẾT x x x x x x
TẤT CẢ x x x x x x x
Mức độ ghi nhật ký là gì? Để bằng cách nào đó xếp hạng các bản ghi, cần phải đưa ra một số chỉ định và phân biệt nhất định. Với mục đích này, các cấp độ ghi nhật ký đã được giới thiệu. Mức độ được thiết lập trong ứng dụng. Nếu một mục thuộc một cấp độ dưới mức được chỉ định, nó sẽ không được nhập vào nhật ký. Ví dụ: chúng tôi có nhật ký được sử dụng để gỡ lỗi ứng dụng. Trong công việc sản xuất thông thường (khi ứng dụng được sử dụng đúng mục đích đã định), những nhật ký đó là không cần thiết. Vì vậy, mức độ ghi nhật ký sẽ cao hơn mức độ gỡ lỗi. Hãy xem các cấp độ sử dụng log4j làm ví dụ. Các giải pháp khác, ngoại trừ JUL, sử dụng các cấp độ tương tự. Đây là thứ tự giảm dần:
  • TẮT: không có nhật ký nào được ghi, tất cả sẽ bị bỏ qua;
  • FATAL: một lỗi sau đó ứng dụng sẽ không thể hoạt động được nữa và sẽ bị dừng, ví dụ: lỗi JVM hết bộ nhớ;
  • ERROR: Tỷ lệ lỗi khi có vấn đề cần giải quyết. Lỗi không dừng toàn bộ ứng dụng. Các truy vấn khác có thể hoạt động chính xác;
  • CẢNH BÁO: Cho biết nhật ký có chứa cảnh báo. Một hành động bất ngờ đã xảy ra, mặc dù vậy hệ thống đã chống lại và hoàn thành yêu cầu;
  • THÔNG TIN: nhật ký ghi lại các hành động quan trọng trong ứng dụng. Đây không phải là lỗi, không phải cảnh báo, đây là những hành động được mong đợi của hệ thống;
  • DEBUG: nhật ký cần thiết để gỡ lỗi ứng dụng. Để đảm bảo rằng hệ thống thực hiện chính xác những gì được mong đợi hoặc để mô tả hành động của hệ thống: “phương thức1 đã bắt đầu hoạt động”;
  • TRACE: nhật ký có mức độ ưu tiên thấp hơn để gỡ lỗi, với mức ghi nhật ký thấp nhất;
  • TẤT CẢ: mức độ mà tất cả nhật ký từ hệ thống sẽ được ghi lại.
Hóa ra là nếu cấp độ ghi nhật ký INFO được bật ở một nơi nào đó trong ứng dụng thì tất cả các cấp độ sẽ được ghi lại, bắt đầu từ INFO và cho đến FATAL. Nếu mức ghi nhật ký là FATAL thì chỉ những nhật ký có mức này mới được ghi lại.

Ghi và gửi nhật ký: Appender

Chúng tôi sẽ xem xét quá trình này bằng cách sử dụng log4j làm ví dụ: nó cung cấp nhiều cơ hội để ghi/gửi nhật ký:
  • để ghi vào một tập tin - giải pháp DailyRollingFileAppender ;
  • để nhận dữ liệu vào bảng điều khiển ứng dụng - ConsoleAppender ;
  • để ghi nhật ký vào cơ sở dữ liệu - JDBCAppender ;
  • để điều khiển việc truyền qua TCP/IP - TelnetAppender ;
  • để đảm bảo rằng việc ghi nhật ký không ảnh hưởng đến hiệu suất - AsyncAppender .
Có một số cách triển khai khác: bạn có thể tìm thấy danh sách đầy đủ tại đây . Nhân tiện, nếu không có ứng dụng bổ sung cần thiết thì đây không phải là vấn đề. Bạn có thể viết ứng dụng của riêng mình bằng cách triển khai giao diện Appender , giao diện này chỉ chấp nhận log4j.

Nút ghi nhật ký

Để trình diễn, chúng tôi sẽ sử dụng giao diện slf4j và triển khai từ log4j. Tạo một trình ghi nhật ký rất đơn giản: bạn cần viết nội dung sau vào một lớp có tên MainDemo, trong đó việc ghi nhật ký sẽ được thực hiện:
org.slf4j.Logger logger = org.slf4j.LoggerFactory.getLogger(MainDemo.class);
Điều này sẽ tạo ra một logger cho chúng tôi. Để tạo một mục nhật ký, bạn có thể sử dụng nhiều phương pháp cho biết các mục sẽ được thực hiện ở cấp độ nào. Ví dụ:
logger.trace("Method 1 started with argument={}", argument);
logger.debug("Database updated with script = {}", script);
logger.info("Application has started on port = {}", port);
logger.warn("Log4j didn't find log4j.properties. Please, provide them");
logger.error("Connection refused to host = {}", host);
Mặc dù chúng ta đang vượt qua lớp nhưng cuối cùng nó vẫn ghi tên đầy đủ của lớp với các gói. Điều này được thực hiện để sau đó bạn có thể chia quá trình đăng nhập thành các nút và định cấu hình mức ghi nhật ký cũng như một ứng dụng bổ sung cho mỗi nút. Ví dụ: tên của lớp: com.github.romankh3.logginglecture.MainDemo- một trình ghi nhật ký đã được tạo trong đó. Và đây là cách nó có thể được chia thành các nút ghi nhật ký. Nút chính là RootLogger null . Đây là nút nhận tất cả nhật ký của toàn bộ ứng dụng. Phần còn lại có thể được mô tả như bên dưới: Ghi nhật ký: cái gì, như thế nào, ở đâu và với cái gì?  - 4Người bổ sung định cấu hình công việc của họ cụ thể trên các nút ghi nhật ký. Bây giờ, sử dụng log4j.properties làm ví dụ , chúng ta sẽ xem cách định cấu hình chúng.

Cấu hình từng bước của Log4j.properties

Bây giờ chúng ta sẽ thiết lập mọi thứ từng bước một và xem những gì có thể làm được:
log4j.appender.CONSOLE=org.apache.log4j.ConsoleAppender
Dòng này cho biết rằng chúng tôi đang đăng ký một ứng dụng CONSOLE sử dụng cách triển khai org.apache.log4j.ConsoleAppender. Ứng dụng này ghi dữ liệu vào bảng điều khiển. Tiếp theo, hãy đăng ký một ứng dụng khác sẽ ghi vào một tệp:
log4j.appender.FILE=org.apache.log4j.RollingFileAppender
Điều quan trọng cần lưu ý là các ứng dụng bổ sung vẫn cần được cấu hình. Sau khi đã đăng ký các ứng dụng bổ sung, chúng tôi có thể xác định mức độ ghi nhật ký trong các nút và ứng dụng bổ sung nào sẽ được sử dụng.

log4j.rootLogger=GỠ LỖI, CONSOLE, TẬP TIN

  • log4j.rootLogger có nghĩa là chúng tôi sẽ định cấu hình nút chính chứa tất cả nhật ký;
  • sau dấu bằng, từ đầu tiên cho biết nhật ký sẽ được ghi ở cấp độ nào trở lên (trong trường hợp của chúng tôi, đây là DEBUG);
  • sau dấu phẩy, tất cả các phần bổ sung sẽ được sử dụng sẽ được chỉ định.
Để định cấu hình một nút ghi nhật ký cụ thể, bạn cần sử dụng mục sau:
log4j.logger.com.github.romankh3.logginglecture=TRACE, OWN, CONSOLE
nơi log4j.logger.nó được sử dụng để định cấu hình một nút cụ thể, trong trường hợp của chúng tôi là com.github.romankh3.logginglecture. Và bây giờ hãy nói về việc thiết lập ứng dụng CONSOLE:
# CONSOLE appender customisation
log4j.appender.CONSOLE=org.apache.log4j.ConsoleAppender
log4j.appender.CONSOLE.threshold=DEBUG
log4j.appender.CONSOLE.layout=org.apache.log4j.PatternLayout
log4j.appender.CONSOLE.layout.ConversionPattern=[%-5p] : %c:%L : %m%n
Ở đây chúng ta thấy rằng chúng ta có thể đặt mức độ mà ứng dụng sẽ xử lý. Tình huống thực tế: nút ghi nhật ký đã nhận được một thông báo có cấp độ thông tin và chuyển đến ứng dụng được gán cho nó, nhưng ứng dụng có cấp độ cảnh báo trở lên đã chấp nhận nhật ký này nhưng không làm gì với nó. Tiếp theo, bạn cần quyết định mẫu nào sẽ có trong tin nhắn. Tôi đang sử dụng PatternLayout trong ví dụ này nhưng có rất nhiều giải pháp hiện có. Chúng sẽ không được tiết lộ trong bài viết này. Một ví dụ về thiết lập phần bổ sung FILE:
# File appender customisation
log4j.appender.FILE=org.apache.log4j.RollingFileAppender
log4j.appender.FILE.File=./target/logging/logging.log
log4j.appender.FILE.MaxFileSize=1MB
log4j.appender.FILE.threshold=DEBUG
log4j.appender.FILE.MaxBackupIndex=2
log4j.appender.FILE.layout=org.apache.log4j.PatternLayout
log4j.appender.FILE.layout.ConversionPattern=[ %-5p] - %c:%L - %m%n
Tại đây bạn có thể định cấu hình tệp nhật ký sẽ được ghi vào, như có thể thấy từ
log4j.appender.FILE.File=./target/logging/logging.log
Bản ghi sẽ chuyển đến tập tin logging.log. Để tránh các vấn đề về kích thước tệp, bạn có thể đặt mức tối đa: trong trường hợp này là 1MB. MaxBackupIndex - cho biết sẽ có bao nhiêu tệp như vậy. Nếu tạo nhiều hơn số này, tệp đầu tiên sẽ bị xóa. Để xem ví dụ thực tế về việc định cấu hình ghi nhật ký, bạn có thể truy cập kho lưu trữ mở trên GitHub.

Hãy củng cố kết quả

Hãy thử tự mình làm mọi thứ được mô tả:
  • Tạo dự án của riêng bạn tương tự như dự án trong ví dụ trên.
  • Nếu bạn có kiến ​​thức về sử dụng Maven, chúng tôi sẽ sử dụng nó; nếu không, đây là liên kết đến bài viết mô tả cách kết nối thư viện.

Hãy tóm tắt lại

  1. Chúng tôi đã nói về những giải pháp có trong Java.
  2. Hầu như tất cả các thư viện ghi nhật ký đã biết đều được viết dưới sự kiểm soát của một người :D
  3. Chúng tôi đã biết được điều gì cần phải ghi lại và điều gì không.
  4. Chúng tôi đã tìm ra mức độ ghi nhật ký.
  5. Chúng tôi đã làm quen với các nút ghi nhật ký.
  6. Chúng tôi đã xem một ứng dụng là gì và nó dùng để làm gì.
  7. Chúng tôi đã định cấu hình tệp log4j.proterties từng bước.

Tài liệu bổ sung

  1. JavaRush: Ghi nhật ký. Thư giãn một quả bóng stectrace
  2. JavaRush: Bài giảng về trình ghi nhật ký
  3. Habr: Ghi nhật ký Java. Chào thế giới
  4. Habr: Ghi nhật ký Java: câu chuyện về cơn ác mộng
  5. Youtube: Các khóa học Golovach. Ghi nhật ký. Phần 1 , Phần 2 , Phần 3 , Phần 4
  6. Log4j: phần bổ sung
  7. Log4j: bố cục
Xem thêm các bài viết khác của tôi:
Bình luận
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION