[Thực hành] Sử dụng thymeleaf cho ứng dụng Quản lý Dữ liệu Khách hàng

2. Spring MVC

Mục tiêu

Luyện tập sử dụng Thymeleaf

Điều kiện

Có kiến thức căn bản về Thymeleaf

Mô tả

Trong phần này, chúng ta sẽ sử dụng Thymeleaf cho ứng dụng quản lý khách hàng. Đây là một ứng dụng CRUD đơn giản, cho phép người dùng quản lý danh sách khách hàng.

Ứng dụng bao gồm các chức năng:

  • Hiển thị danh sách khách hàng
  • Thêm mới khách hàng
  • Sửa thông tin khách hàng
  • Xóa khách hàng khỏi danh sách
  • Hiển thị thông tin chi tiết của khách hàng

Hướng dẫn

Bước 1: Tạo dự án Spring MVC sử dụng Thymeleaf.

Để tạo mới dự án, các bạn chọn File -> New -> Project.

Sau đó chọn Gradle -> Tích chọn Java, Web -> Chọn Next

Bạn nhập thông tin vào GroupId, và ArtifactId. Sau đó nhấn Next:

Màn hình sau đó bạn tiếp tục nhấn Next:

Nhập vào Project name, Project location. Sau đó nhấn Finish.

Bước 2: Tạo cấu trúc của dự án.

Bạn tạo cấu trúc của dự án như sau. Nhớ xóa file index.jsp trong trong thư mục webapp.

Bước 3:Thêm springframeword và thymeleaf dependencies trong file build.grade


dependencies {
    compile group: 'javax.servlet', name: 'javax.servlet-api', version: '3.1.0'
    compile group: 'org.springframework', name: 'spring-core', version: '4.3.17.RELEASE'
    compile group: 'org.springframework', name: 'spring-context', version: '4.3.17.RELEASE'
    compile group: 'org.springframework', name: 'spring-beans', version: '4.3.17.RELEASE'
    compile group: 'org.springframework', name: 'spring-web', version: '4.3.17.RELEASE'

    compile group: 'org.springframework', name: 'spring-webmvc', version: '4.3.17.RELEASE'
    compile group: 'org.thymeleaf', name: 'thymeleaf-spring4', version: '3.0.9.RELEASE'
    compile group: 'nz.net.ultraq.thymeleaf', name: 'thymeleaf-layout-dialect', version: '1.2'
}

Bước 4: Cấu hình cho ứng dụng

4.1 Tạo file AppConfiguration

@Configuration
@EnableWebMvc
@ComponentScan("com.codegym.controller")
public class AppConfiguration extends WebMvcConfigurerAdapter implements ApplicationContextAware {

    private ApplicationContext applicationContext;

    @Override
    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
        this.applicationContext = applicationContext;
    }

    //Thymeleaf Configuration
    @Bean
    public SpringResourceTemplateResolver templateResolver() {
        SpringResourceTemplateResolver templateResolver = new SpringResourceTemplateResolver();
        templateResolver.setApplicationContext(applicationContext);
        templateResolver.setPrefix("/WEB-INF/views");
        templateResolver.setSuffix(".html");
        templateResolver.setTemplateMode(TemplateMode.HTML);
        templateResolver.setCharacterEncoding("UTF-8");
        return templateResolver;
    }

    @Bean
    public TemplateEngine templateEngine() {
        TemplateEngine templateEngine = new SpringTemplateEngine();
        templateEngine.setTemplateResolver(templateResolver());
        return templateEngine;
    }

    @Bean
    public ThymeleafViewResolver viewResolver() {
        ThymeleafViewResolver viewResolver = new ThymeleafViewResolver();
        viewResolver.setTemplateEngine(templateEngine());
        viewResolver.setCharacterEncoding("UTF-8");
        return viewResolver;
    }

}

4.2 Tạo file AppInitializer

public class AppInitializer extends AbstractAnnotationConfigDispatcherServletInitializer {

    @Override
    protected Class<?>[] getRootConfigClasses() {
        return new Class[]{AppConfiguration.class};
    }

    @Override
    protected Class<?>[] getServletConfigClasses() {
        return new Class[]{};
    }

    @Override
    protected String[] getServletMappings() {
        return new String[]{"/"};
    }
}

Bước 5: Tạo lớp customer trong thư mục model

public class Customer {
    private int id;
    private String name;
    private String email;
    private String address;

    public Customer() {
    }

    public Customer(int id, String name, String email, String address) {
        this.id = id;
        this.name = name;
        this.email = email;
        this.address = address;
    }

    public int getId() {
        return id;
    }

    public void setId(int id) {
        this.id = id;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public String getEmail() {
        return email;
    }

    public void setEmail(String email) {
        this.email = email;
    }

    public String getAddress() {
        return address;
    }

    public void setAddress(String address) {
        this.address = address;
    }
}

Trong lớp Customer, các bạn chú ý phải có các getter/setter để có thể truy cập các thuộc tính trong View.

Bước 6: Xây dựng interface CustomerService

Ta sẽ xây dựng interface CustomerService trong thư mục service. Chứa các phương thức sẽ sử dụng trong controller.

public interface CustomerService {
    List<Customer> findAll();

    void save(Customer customer);

    Customer findById(int id);

    void update(int id, Customer customer);

    void remove(int id);
}

Bước 7: Implement interface CustomerService

Ta sẽ đi xây dựng lớp CustomerServiceImpl thi hành các phương thức đã được khai báo nguyên mẫu trong interface CustomerService:


public class CustomerServiceImpl implements CustomerService {
    private static Map<Integer, Customer> customers;

    static {

        customers = new HashMap<>();
        customers.put(1, new Customer(1, "John", "john@codegym.vn", "Hanoi"));
        customers.put(2, new Customer(2, "Bill", "bill@codegym.vn", "Danang"));
        customers.put(3, new Customer(3, "Alex", "alex@codegym.vn", "Saigon"));
        customers.put(4, new Customer(4, "Adam", "adam@codegym.vn", "Beijin"));
        customers.put(5, new Customer(5, "Sophia", "sophia@codegym.vn", "Miami"));
        customers.put(6, new Customer(6, "Rose", "rose@codegym.vn", "Newyork"));
    }

    @Override
    public List findAll() {
        return new ArrayList<>(customers.values());
    }

    @Override
    public void save(Customer customer) {

        customers.put(customer.getId(), customer);
    }

    @Override
    public Customer findById(int id) {
        return customers.get(id);
    }

    @Override
    public void update(int id, Customer customer) {
        customers.put(id, customer);

    }

    @Override
    public void remove(int id) {
        customers.remove(id);
    }
}

Trong đó:

  • Chúng ta sẽ khởi tạo các khách hàng và lưu vào HashMap (một kiểu tập hợp)
  • Annotation @Override xác định rằng các phương thức ghi đè các phương thức trong interface CustomerService

Bước 8: Xây dựng trang layout

Ta thấy các file index.html, create.html, edit.html, delete.html, view.html đều có phần chung là head (thẻ head) và footer (thẻ footer) nên chúng ta sẽ giữ 3 phần chung này gộp vào 1 file layout.html đặt trong thư mục views. Và đánh dấu mỗi phần sử dụng thuộc tính th:fragment.

File layout.html cũng chứa các khai báo về Style Sheet cho tất cả các trang con.

Lúc này ở các trang index.html, create.html, edit.html, delete.html, view.html, chúng ta sẽ thay thế các phần head và footer như sau:

<head th:replace="/layout :: head">
<footer th:replace="/layout :: footer"></footer>

Trong đó:

  • layout sẽ tham chiếu tới file layout.html
  • head và footer sau dấu :: là các fragment selector, chính là giá trị của thuộc tính th:fragment của các thẻ head, footer trong trang layout.html.

Bước 9: Hiển thị danh sách khách hàng

9.1. Controller

Trước tiên chúng ta sẽ tạo CustomerController trong thư mục controller:


@Controller
public class CustomerController {
    private CustomerService customerService = new CustomerServiceImpl();

    @GetMapping("/")
    public String index(Model model) {

        List customerList = customerService.findAll();
        model.addAttribute("customers", customerList);
        return "/index";
    }
}

Trong đó:

  • Annotation @Controller giúp Spring xác định lớp hiện tại là một Controller.
  • Annotation @GetMapping xác định phương thức Index sẽ đón nhận các request có HTTP method là GET và URI pattern là “/”
  • Phương thức Index được truyền vào một tham số có kiểu dữ liệu là Model. Model có nhiệm vụ là truyền dữ liệu từ Controller tới View. Ở đây, chúng ta sẽ lấy ra danh sách các khách hàng thông qua customerService.findAll(). Sau đó gắn danh sách này vào Model thông qua phương thức addAttribute(). customers chính là tên biến đại diện cho danh sách mà ta sẽ dùng ở View sau này.
  • Phương thức Index sẽ trả về một String, từ String này mà Spring MVC sẽ suy ra View nào sẽ nhận dữ liệu từ Controller (return “index”), vậy View sẽ nhận dữ liệu ở đây là index.html

9.2. View

Chúng ta tạo view index.html như sau:

Trong đó:

Khai báo namespace thymeleaf thông qua thuộc tính th:

<html xmlns:th="http://www.thymeleaf.org">

Gắn phần head và footer đã được khai báo trong file layout.html:

<head th:replace="/layout :: head"></head>
<footer th:replace="/layout :: footer"></footer>

Thuộc tính th:href khai báo đường link trong thẻ a.

Thuộc tính th:if sử dụng để kiểm tra điều kiện.

Thuộc tính th:each tương ứng với câu lệnh for each.

Thuộc tính th:text dùng để đổ dữ liệu dưới dạng text vào thẻ HTML.

${customers} là danh sách khách hàng sẽ được lặp, chính là model customers được controller truyền lên view.

row đại diện cho một khách hàng tại một bước lặp cụ thể.

rowStat là một biến trạng thái, giúp chúng ta theo dõi vòng lặp.

${…} là biểu thức đánh giá các biến , biểu thức hay model.

9.3. Cấu hình Artifact: Web Application: Exploded

9.4. Cấu hình Tomcat

9.5. Chạy ứng dụng.

Ứng dụng sau khi chạy sẽ hiển thị như sau:

Bước 10: Thêm mới khách hàng

10.1. Hiển thị form thêm mới khách hàng:

Chú ý:

Đoạn mã sau đây trên trang index.html sẽ điều hướng đến trang create.html:

<a th:href="@{/customer/create}">
Add new customer
</a>

Trong class CustomerController, ta viết thêm phương thức create để hiển thị trang create.html:

@GetMapping("/customer/create")
public String create(Model model) {
model.addAttribute("customer", new Customer());
return "/create";
}

Trong đó:

  • Giá trị trong @GetMapping tương ứng đường link của button “Add new customer”.
  • Chúng ta sẽ truyền sang view create.html một model Customer có tên là customer. Mỗi thuộc tính của customer sẽ tương ứng với một input trong form.

Trang create.html sẽ có dạng như sau:

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head th:replace="/layout :: head"></head>
<body>
<h3>Add Customer</h3>
<p>
<a th:href="@{/}">
Back to customer list
</a>
</p>
<div id="form">
<form action="#" th:action="@{/customer/save}" th:object="${customer}"
method="POST" novalidate="novalidate">
<input type="hidden" th:field="*{id}"/>
<div>
<label>Name</label>
<input type="text" th:field="*{name}"/>
</div>
<div>
<label>Email</label>
<input type="email" th:field="*{email}"/>
</div>
<div>
<label>Address</label>
<input type="text" th:field="*{address}"/>
</div>
<input type="submit" value="Save"></input>
</form>
</div>
<footer th:replace="/layout :: footer"></footer>
</body>
</html>

Trong đó:

th:action chỉ ra đường dẫn sẽ xử lý submit form, ${customer} ở th:object chính là biến customer.

Như đã nói ở trên, mỗi thuộc tính của customer sẽ tương ứng với một input trong form, nên chúng ta sẽ thêm thuộc tính th:field=”*{fieldname}” vào các input. Do khi submit form chúng ta cần phải chỉ ra entity nào sẽ được gửi lên cho nên chúng ta phải thêm:

<input type="hidden" th:field="*{id}"/>

để chỉ ra ID của entity.

Trong controller, ta sẽ tạo phương thức save() để lưu một customer, như sau:

@PostMapping("/customer/save")
public String save(Customer customer, RedirectAttributes redirect) {
customer.setId((int)(Math.random() * 10000));
customerService.save(customer);
redirect.addFlashAttribute("success", "Saved customer successfully!");
return "redirect:/";
}

Trong đó:

  • Do request gửi lên có HTTP method là POST, nên ta sẽ sử dụng @PostMapping.
  • Đối tượng customer được truyền vào save() chính là đối tượng customer chúng ta đã truyền từ phương thức create() sang view create.html. Đối tượng này sẽ lưu thông tin của người dùng nhập vào.
  • customer.setId() sẽ tạo Id ngẫu nhiên cho đối tượng customer.
  • Sau khi lưu customer, chúng ta sẽ redirect về trang danh sách khách hàng. Chuỗi đằng sau “redirect:” là đường dẫn của trang mà mình muốn redirect. Đồng thời, chúng ta cũng sẽ gửi một Flash message về trang danh sách khách hàng để thông báo lưu thành công, bằng cách sử dụng redirect.addFlashAttribute(messageName, messageContent).

Bạn chú ý tại file index.html, đoạn mã sau để hiển thị thông báo:

<div class="notify">
<div th:if="${success}">
<span th:text="${success}"></span>
</div>
</div>

Bước 11: Sửa thông tin khách hàng

11.1. Hiển thị form sửa thông tin khách hàng:

Đoạn mã sau đây trên trang index.html sẽ điều hướng đến trang edit.html:

<td><a th:href="@{/customer/{id}/edit(id=${row.getId()})}">edit</a></td>

Trong class CustomerController, ta viết thêm phương thức edit để hiển thị trang edit.html:

@GetMapping("/customer/{id}/edit")
public String edit(@PathVariable int id, Model model) {
model.addAttribute("customer", customerService.findById(id));
return "/edit";
}

Trong đó:

  • Tham số @PathVariable int id lấy id của customer từ đường dẫn rồi gán vào biến id.
  • hàm customerService.findById(id) sẽ lấy customer theo id rồi truyền sang view edit.html

Trang edit.html sẽ có dạng như sau:

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head th:replace="/layout :: head"></head>
<body>
<h3>Modify Customer</h3>
<p>
<a th:href="@{/}">
Back to customer list
</a>
</p>
<div id="form">
<form action="#" th:action="@{/customer/update}" th:object="${customer}"
method="POST" novalidate="novalidate">
<input type="hidden" th:field="*{id}"/>
<div>
<label>Name</label>
<input type="text" th:field="*{name}"/>
</div>
<div>
<label>Email</label>
<input type="email" th:field="*{email}"/>
</div>
<div>
<label>Address</label>
<input type="text" th:field="*{address}"/>
</div>
<input type="submit" value="Modify"></input>
</form>
</div>
<footer th:replace="/layout :: footer"></footer>
</body>
</html>

Trong controller, ta sẽ tạo phương thức update() để lưu một sửa đổi, như sau:

@PostMapping("/customer/update")
public String update(Customer customer, RedirectAttributes redirect) {
customerService.update(customer.getId(), customer);
redirect.addFlashAttribute("success", "Modified customer successfully!");
return "redirect:/";
}

Bước 12: Xóa khách hàng

12.1. Hiển thị form xóa khách hàng:

Đoạn mã sau đây trên trang index.html sẽ điều hướng đến trang delete.html:

<td><a th:href="@{/customer/{id}/delete(id=${row.getId()})}">delete</a></td>

Trong class CustomerController, ta viết thêm phương thức  GET delete() để hiển thị trang delete.html:

@GetMapping("/customer/{id}/delete")
public String delete(@PathVariable int id, Model model) {
model.addAttribute("customer", customerService.findById(id));
return "/delete";
}

Trang delete.html sẽ có dạng như sau:

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head th:replace="/layout :: head"></head>
<body>
<h3>Remove Customer</h3>
<p>
<a th:href="@{/}">
Back to customer list
</a>
</p>
<div id="form">
<form action="#" th:action="@{/customer/delete}" th:object="${customer}"
method="POST" novalidate="novalidate">
<input type="hidden" th:field="*{id}"/>
<div>
<label>Name</label>
<input type="text" th:field="*{name}"/>
</div>
<div>
<label>Email</label>
<input type="email" th:field="*{email}"/>
</div>
<div>
<label>Address</label>
<input type="text" th:field="*{address}"/>
</div>
<input type="submit" value="Remove"></input>
</form>
</div>
<footer th:replace="/layout :: footer"></footer>
</body>
</html>

Trong controller, ta sẽ tạo phương thức POST delete() để xóa khách hàng, như sau:

@PostMapping("/customer/delete")
public String delete(Customer customer, RedirectAttributes redirect) {
customerService.remove(customer.getId());
redirect.addFlashAttribute("success", "Removed customer successfully!");
return "redirect:/";
}

Bước 13: View chi tiết khách hàng

Đoạn mã sau đây trên trang index.html sẽ điều hướng đến trang view.html:

<td><a th:href="@{/customer/{id}/view(id=${row.getId()})}">view</a></td>

Trong class CustomerController, ta viết thêm phương thức  GET view() để hiển thị trang view.html:

@GetMapping("/customer/{id}/view")
public String view(@PathVariable int id, Model model) {
model.addAttribute("customer", customerService.findById(id));
return "/view";
}

Trang view.html sẽ có dạng như sau:

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head th:replace="/layout :: head"></head>
<body>
<h3>View Customer</h3>
<p>
<a th:href="@{/}">
Back to customer list
</a>
</p>
<div id="detail">
<div class="cusdetail">
<label>Name:</label>
<label th:text="${customer.getName()}"></label>
</div>
<div class="cusdetail">
<label>Email:</label>
<label th:text="${customer.getEmail()}"></label>
</div>
<div class="cusdetail">
<label>Address:</label>
<label th:text="${customer.getAddress()}"></label>
</div>
</div>
<footer th:replace="/layout :: footer"></footer>
</body>
</html>

Bước 14: Chạy chương trình

Các bạn chạy chương trình rồi quan sát kết quả.

Lúc này dự án của chúng ta sẽ chạy trên cổng 8080: http://localhost:8080

Leave a Reply

Your email address will not be published. Required fields are marked *