Spring Boot bài 07: Biểu thức Thymeleaf, vòng lặp và hiển thị an toàn
- 18-10-2026
- Toanngo92
- 0 Comments
Giáo trình Spring Boot MVC và REST API · Chương 4/12: Tổ chức giao diện Thymeleaf · Bài 07/28 · Biên soạn tháng 10/2026.
Mục lục
Mục tiêu bài học
Hiển thị danh sách và trạng thái rỗng, dùng điều kiện và hiểu ranh giới giữa văn bản với HTML.
Danh sách dữ liệu và một dòng mẫu
Template không cần biết trước có bao nhiêu khóa học. th:each lặp qua result.content, gán từng phần tử vào biến course rồi render một dòng tương ứng. Nếu danh sách rỗng, th:if hiển thị thông báo rõ ràng thay vì để người xem đoán trang đang lỗi hay chưa có nội dung.
Nên để Service chuẩn bị thứ tự, phân trang và nội dung cần thiết. Template chỉ phụ trách biểu diễn: không vừa lặp vừa truy vấn database cho từng dòng, không tính lại quy tắc giảm giá hoặc phân quyền ở đây. Điều này giữ giao diện dễ đọc và tránh vô tình tạo hàng chục truy vấn.
<tbody>
<tr th:each="course : ${result.content}">
<td><a th:href="@{/courses/{id}(id=${course.id})}"
th:text="${course.title}"></a></td>
<td th:text="${course.categoryName}"></td>
<td th:text="${course.price}"></td>
</tr>
<tr th:if="${result.empty}">
<td colspan="3">Chưa có khóa học phù hợp</td>
</tr>
</tbody>
Các loại biểu thức thường gặp
${…} lấy dữ liệu trong context; @{…} xây URL; *{…} đọc thuộc tính của đối tượng đang được chọn bởi th:object trong biểu mẫu. Fragment dùng ~{…}; message quốc tế hóa dùng #{…}. Không cần học thuộc mọi cú pháp ngay, nhưng nên phân biệt biểu thức dữ liệu và biểu thức URL.
Với điều kiện đơn giản, có thể dùng th:if hoặc toán tử ba ngôi. Ví dụ giá 0 hiển thị “Miễn phí”, ngược lại hiển thị giá. Khi định dạng tiền cho hệ thống thực tế, phải thống nhất đơn vị tiền, locale và cách làm tròn; một con số không tự mang thông tin VND hay USD.
<span th:if="${course.price.signum() == 0}">Miễn phí</span>
<span th:unless="${course.price.signum() == 0}"
th:text="${course.price}"></span>
Escape giúp tránh biến dữ liệu thành mã HTML
Nếu người dùng nhập tên khóa học là <script>alert(1)</script>, ta muốn trình duyệt coi đó là tên dạng văn bản, không thực thi script. th:text escape ký tự đặc biệt để thực hiện điều này. th:utext dành cho HTML đã được kiểm soát và làm sạch; không dùng nó để sửa vấn đề “chữ không in đậm được” với dữ liệu người dùng nhập.
Escape theo ngữ cảnh không thay thế mọi biện pháp chống XSS. Không nhét dữ liệu tùy ý vào đoạn JavaScript, URL nguy hiểm hoặc style. Với nội dung bài học có HTML phong phú, cần quy trình sanitize riêng, không chỉ chuyển từ th:text sang th:utext.
Giữ lại trạng thái bộ lọc
Ô tìm kiếm dùng th:value=${q}, lựa chọn sort dùng th:selected để người xem thấy bộ lọc hiện tại. Việc hiển thị đúng trạng thái giúp hiểu vì sao chỉ có vài khóa học. Ở bài phân trang, các liên kết tiếp theo còn phải mang theo những tham số đó.
Khi debug template, xem response HTML cuối cùng trong trình duyệt và so sánh với Model. Không tìm các thuộc tính th:* trong HTML đã render như bằng chứng Thymeleaf chưa chạy: chúng thường đã được xử lý và thay thế. Nếu mở tệp HTML trực tiếp từ ổ đĩa, bạn chỉ thấy bản mẫu tĩnh, không có dữ liệu từ Spring.
Lỗi thường gặp và lưu ý
- Tên biến vòng lặp chỉ có ý nghĩa trong phạm vi phần tử tương ứng.
- Không dùng unescaped HTML cho dữ liệu nhập chỉ vì muốn giữ xuống dòng; hãy xử lý cách trình bày phù hợp.
Ghi nhớ
Vòng lặp, điều kiện và escape là ba nền tảng để render danh sách đúng dữ liệu và an toàn.

