Giới thiệu
Trong bài viết này, chúng ta sẽ cùng nhau xây dựng một ứng dụng Todo đơn giản sử dụng Spring Boot MVC và Thymeleaf. Ứng dụng này sẽ cho phép người dùng tạo, xem, cập nhật và xóa các tác vụ của mình. Spring Boot là một framework mạnh mẽ giúp phát triển ứng dụng Java nhanh chóng, trong khi Thymeleaf là một thư viện template hiện đại cho web.
Mục Lục
- Bước 1: Thiết lập Dự án
- Bước 2: Cấu hình Ứng dụng
- Bước 3: Tạo Mô Hình Todo
- Bước 4: Lớp Repository
- Bước 5: Cấu hình Bảo mật
- Bước 6: Controller
- Bước 7: Giao diện Frontend
- Bước 8: Chạy & Kiểm tra
- Thực tiễn tốt nhất
- Lỗi thường gặp
- Mẹo Hiệu suất
- Kết luận
Bước 1: Thiết lập Dự án
Chúng ta bắt đầu bằng cách tạo một dự án Spring Boot. Bạn có thể sử dụng:
- Spring Initializr (https://start.spring.io/)
- Hoặc trực tiếp trong IntelliJ/Eclipse với tùy chọn "Spring Initializr project"
Chọn các phụ thuộc:
- Spring Web → để xây dựng các controller MVC
- Thymeleaf → để render các template HTML
- Spring Data JPA → để tương tác với cơ sở dữ liệu
- H2 Database → cơ sở dữ liệu trong bộ nhớ cho phát triển
- Spring Security → cho chức năng đăng nhập/đăng xuất
Mã pom.xml sẽ bao gồm những phần quan trọng như sau:
xml
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
</dependencies>
Bước 2: Cấu hình Ứng dụng
Chúng ta muốn sử dụng cơ sở dữ liệu H2 trong bộ nhớ và muốn xem các truy vấn SQL.
Tạo tệp src/main/resources/application.properties với nội dung như sau:
properties
# Kích hoạt bảng điều khiển H2 (hữu ích cho việc gỡ lỗi)
spring.h2.console.enabled=true
spring.h2.console.path=/h2-console
# Cơ sở dữ liệu H2 trong bộ nhớ
spring.datasource.url=jdbc:h2:mem:todoapp
spring.datasource.driverClassName=org.h2.Driver
spring.datasource.username=sa
spring.datasource.password=
# Cấu hình Hibernate (JPA)
spring.jpa.hibernate.ddl-auto=update
spring.jpa.show-sql=true
Bây giờ, ứng dụng của chúng ta sẽ sử dụng cơ sở dữ liệu trong bộ nhớ (todoapp) và tự động tạo bảng từ các thực thể.
Bước 3: Tạo Mô Hình Todo
Chúng ta cần một bảng todo với các cột: id, title, completed, username.
Tạo tệp Todo.java:
java
package com.example.todoapp.model;
import jakarta.persistence.*;
import lombok.Data;
@Entity
@Data
public class Todo {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY) // Tự động tăng ID
private Long id;
private String title;
private boolean completed = false;
// Mỗi todo thuộc về một người dùng cụ thể
private String username;
}
@Entity→ cho JPA biết đây là một bảng.@Id→ khóa chính.username→ đảm bảo mỗi người dùng chỉ thấy danh sách todo của riêng họ.
Bước 4: Lớp Repository
Chúng ta cần một repository để tương tác với cơ sở dữ liệu.
Tạo tệp TodoRepository.java:
java
package com.example.todoapp.repository;
import com.example.todoapp.model.Todo;
import org.springframework.data.jpa.repository.JpaRepository;
import java.util.List;
public interface TodoRepository extends JpaRepository<Todo, Long> {
// Truy vấn tùy chỉnh: lấy chỉ các todo thuộc về người dùng đã đăng nhập
List<Todo> findByUsername(String username);
}
Điều này sẽ cung cấp cho chúng ta các phương thức CRUD như save, findById, deleteById miễn phí.
Bước 5: Cấu hình Bảo mật (Form Đăng nhập)
Chúng ta không muốn truy cập mở. Hãy sử dụng Spring Security với người dùng trong bộ nhớ (đơn giản nhất).
Tạo tệp SecurityConfig.java:
java
package com.example.todoapp.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.password.NoOpPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.provisioning.InMemoryUserDetailsManager;
import org.springframework.security.web.SecurityFilterChain;
@Configuration
public class SecurityConfig {
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.authorizeHttpRequests(auth -> auth
.requestMatchers("/css/**").permitAll() // Cho phép CSS Bootstrap
.anyRequest().authenticated() // mọi yêu cầu khác cần đăng nhập
)
.formLogin(login -> login
.loginPage("/login").permitAll() // trang đăng nhập tùy chỉnh
.defaultSuccessUrl("/todos", true) // chuyển hướng sau khi đăng nhập
)
.logout(logout -> logout.permitAll());
return http.build();
}
// Người dùng trong bộ nhớ (cho demo)
@Bean
public UserDetailsService users() {
UserDetails user1 = User.withUsername("john")
.password("1234")
.roles("USER")
.build();
UserDetails user2 = User.withUsername("jane")
.password("1234")
.roles("USER")
.build();
return new InMemoryUserDetailsManager(user1, user2);
}
@Bean
public PasswordEncoder passwordEncoder() {
// ⚠ Không an toàn cho các ứng dụng thực tế. Chỉ dành cho demo.
return NoOpPasswordEncoder.getInstance();
}
}
Bây giờ ứng dụng có:
- Trang đăng nhập tại
/login - Người dùng:
john / 1234jane / 1234
Bước 6: Controller
Đây là nơi xử lý các hành động của người dùng:
- Hiển thị todos
- Thêm todo mới
- Chuyển đổi trạng thái
- Xóa todo
Tạo tệp TodoController.java:
java
package com.example.todoapp.controller;
import com.example.todoapp.model.Todo;
import com.example.todoapp.repository.TodoRepository;
import org.springframework.security.core.Authentication;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.*;
@Controller
@RequestMapping("/todos")
public class TodoController {
private final TodoRepository repo;
public TodoController(TodoRepository repo) {
this.repo = repo;
}
// Hiển thị tất cả todos của người dùng đã đăng nhập
@GetMapping
public String listTodos(Model model, Authentication auth) {
String username = auth.getName();
model.addAttribute("todos", repo.findByUsername(username));
model.addAttribute("newTodo", new Todo());
return "todos"; // render todos.html
}
// Thêm todo mới
@PostMapping
public String addTodo(@ModelAttribute Todo todo, Authentication auth) {
todo.setUsername(auth.getName());
repo.save(todo);
return "redirect:/todos";
}
// Chuyển đổi hoàn thành
@PostMapping("/{id}/toggle")
public String toggleTodo(@PathVariable Long id) {
Todo todo = repo.findById(id).orElseThrow();
todo.setCompleted(!todo.isCompleted());
repo.save(todo);
return "redirect:/todos";
}
// Xóa todo
@PostMapping("/{id}/delete")
public String deleteTodo(@PathVariable Long id) {
repo.deleteById(id);
return "redirect:/todos";
}
}
Bước 7: Giao diện Frontend (Thymeleaf + Bootstrap)
login.html
html
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<title>Đăng Nhập</title>
<link th:href="@{/css/bootstrap.min.css}" rel="stylesheet"/>
</head>
<body class="container mt-5">
<div class="row justify-content-center">
<div class="col-md-4">
<h2 class="text-center">Đăng Nhập</h2>
<form th:action="@{/login}" method="post">
<div class="mb-3">
<label>Tên người dùng</label>
<input class="form-control" type="text" name="username"/>
</div>
<div class="mb-3">
<label>Mật khẩu</label>
<input class="form-control" type="password" name="password"/>
</div>
<button class="btn btn-primary w-100">Đăng Nhập</button>
</form>
</div>
</div>
</body>
</html>
todos.html
html
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<title>Todos</title>
<link th:href="@{/css/bootstrap.min.css}" rel="stylesheet"/>
</head>
<body class="container mt-5">
<h2>Các Todo của bạn</h2>
<!-- Thêm todo mới -->
<form th:action="@{/todos}" method="post" class="row g-3 mb-4">
<div class="col-md-8">
<input class="form-control" type="text" name="title" placeholder="Todo mới"/>
</div>
<div class="col-md-4">
<button class="btn btn-success w-100">Thêm</button>
</div>
</form>
<!-- Danh sách tất cả todos -->
<table class="table table-bordered">
<thead>
<tr>
<th>Tiêu đề</th>
<th>Trạng thái</th>
<th>Hành động</th>
</tr>
</thead>
<tbody>
<tr th:each="todo : ${todos}">
<td th:text="${todo.title}"></td>
<td>
<span th:text="${todo.completed} ? '✅ Hoàn thành' : '❌ Chưa hoàn thành'"></span>
</td>
<td>
<form th:action="@{/todos/{id}/toggle(id=${todo.id})}" method="post" style="display:inline">
<button class="btn btn-warning btn-sm">Chuyển đổi</button>
</form>
<form th:action="@{/todos/{id}/delete(id=${todo.id})}" method="post" style="display:inline">
<button class="btn btn-danger btn-sm">Xóa</button>
</form>
</td>
</tr>
</tbody>
</table>
<a th:href="@{/logout}" class="btn btn-secondary">Đăng Xuất</a>
</body>
</html>
Bước 8: Chạy & Kiểm tra
- Chạy ứng dụng với
mvn spring-boot:run - Mở http://localhost:8080/login
- Đăng nhập với:
john / 1234jane / 1234
- Thêm todos, chuyển đổi, xóa. Mỗi người dùng sẽ thấy danh sách của riêng họ.
Thực tiễn tốt nhất
- Luôn mã hóa mật khẩu trong ứng dụng thực tế.
- Sử dụng cơ sở dữ liệu ngoài (như MySQL) cho các ứng dụng sản xuất.
- Thêm kiểm tra đầu vào để bảo vệ khỏi các cuộc tấn công SQL Injection.
Lỗi thường gặp
- Lỗi không tìm thấy bean: Kiểm tra xem tất cả các annotation đã được cấu hình đúng chưa.
- Kết nối cơ sở dữ liệu thất bại: Đảm bảo rằng URL cơ sở dữ liệu là chính xác và cơ sở dữ liệu đã được khởi động.
Mẹo Hiệu suất
- Sử dụng caching cho các truy vấn cơ sở dữ liệu thường xuyên.
- Giảm số lượng truy vấn đến cơ sở dữ liệu bằng cách tối ưu hóa phương thức tìm kiếm.
Kết luận
Trong bài viết này, chúng ta đã xây dựng thành công một ứng dụng Todo đơn giản với Spring Boot và Thymeleaf. Bạn có thể mở rộng ứng dụng này bằng cách thêm các tính năng như phân quyền người dùng, thông báo qua email, hoặc tích hợp API bên ngoài. Hãy thử nghiệm và sáng tạo với mã nguồn của bạn nhé!