0
0
Lập trình
Admin Team
Admin Teamtechmely

Hướng Dẫn Tạo Todo App với Spring Boot MVC và Thymeleaf

Đăng vào 3 tuần trước

• 10 phút đọc

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

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

pom.xml sẽ bao gồm những phần quan trọng như sau:

xml Copy
<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 Copy
# 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 Copy
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 Copy
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 Copy
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 / 1234
    • jane / 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 Copy
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 Copy
<!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 Copy
<!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

  1. Chạy ứng dụng với mvn spring-boot:run
  2. Mở http://localhost:8080/login
  3. Đăng nhập với:
    • john / 1234
    • jane / 1234
  4. 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é!

Gợi ý câu hỏi phỏng vấn
Không có dữ liệu

Không có dữ liệu

Bài viết được đề xuất
Bài viết cùng tác giả

Bình luận

Chưa có bình luận nào

Chưa có bình luận nào