Như đã giới thiệu trong bài viết trước về khả năng xử lý form của Javalin, hôm nay mình sẽ tiếp tục giới thiệu đến các bạn cách phân nhóm xử lý cho phần Backend, uy nhiên một điều khác biệt trong cách tổ chức package. Chúng ta sẽ phân nhóm package theo chức năng mà không phải là theo tầng (layer) như cách chúng ta hay làm với các framework khác như Spring hay Struts.
Ví dụ hôm nay cũng sẽ bao gồm chức năng authentication Login, Logout một cách cơ bản và cả xử lý lỗi (error handling).
Mình sẽ cấu trúc Project như sau:
Thư viện Maven:
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="https://maven.apache.org/POM/4.0.0"
xmlns:xsi="https://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="https://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>thecoderoot</groupId>
<artifactId>javalin-library-website</artifactId>
<version>1.0-SNAPSHOT</version>
<dependencies>
<dependency>
<groupId>io.javalin</groupId>
<artifactId>javalin</artifactId>
<version>2.8.0</version>
</dependency>
<!--Template Engine for MVC Web-->
<dependency>
<groupId>org.apache.velocity</groupId>
<artifactId>velocity-engine-core</artifactId>
<version>2.0</version>
</dependency>
<!--Logging Library-->
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-simple</artifactId>
<version>1.7.26</version>
</dependency>
<!--Java Collection Utils by Google-->
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>19.0</version>
</dependency>
<!--an implementation the OpenBSD Blowfish password hashing algorithm-->
<dependency>
<groupId>org.mindrot</groupId>
<artifactId>jbcrypt</artifactId>
<version>0.3m</version>
</dependency>
</dependencies>
</project>
Giải thích công dụng của thư viện của các bên thứ 3:
- Apache Velocity: là 1 template engine để tạo các file HTML
- Google Guava: các thư viện mở rộng tiện ích khi làm việc với các Java Collection.
- Mindrot jbcrypt: thư viện hỗ trợ thuật toán hashing. Chúng ta sẽ dùng thư viện này để mã hóa password của người dùng.
Application.java
public class Main {
// Declare dependencies
public static BookDao bookDao;
public static UserDao userDao;
public static void main(String[] args) {
// Instantiate your dependencies
bookDao = new BookDao();
userDao = new UserDao();
Javalin app = Javalin.create()
.port(7000)
.enableStaticFiles("/public")
.enableRouteOverview("/routes")
.start();
app.routes(() -> {
before(LoginController.ensureLoginBeforeViewingBooks);
get(Path.Web.INDEX, IndexController.serveIndexPage);
get(Path.Web.BOOKS, BookController.fetchAllBooks);
get(Path.Web.ONE_BOOK, BookController.fetchOneBook);
get(Path.Web.LOGIN, LoginController.serveLoginPage);
post(Path.Web.LOGIN, LoginController.handleLoginPost);
post(Path.Web.LOGOUT, LoginController.handleLogoutPost);
});
app.error(404, ViewUtil.notFound);
}
}
Before-handlers
Trong phần cấu hình routing cho app, chúng ta khai báo method before, để filter toàn bộ request của người dùng, và method LoginController.ensureLoginBeforeViewingBooks() đảm bảo rằng nguời dùng phải đăng nhập bằng username và password trước khi xem được các đầu sách của thư viện.
Redirect Routing khi có lỗi
app.error(404, ViewUtil.notFound);
Với khai báo này khi người dùng gặp lỗi 404 (như truy cập link web không tồn tại chẳng hạn) website sẽ tự động redirect người dùng về method ViewUtil.notFound, chúng ta xem qua file ViewUtil.java
package thecoderoot.javalin.library.web.util;
import io.javalin.Context;
import io.javalin.ErrorHandler;
import java.util.HashMap;
import java.util.Map;
import static thecoderoot.javalin.library.web.util.RequestUtil.getSessionCurrentUser;
public class ViewUtil {
public static Map<String, Object> baseModel(Context ctx) {
Map<String, Object> model = new HashMap<>();
model.put("currentUser", getSessionCurrentUser(ctx));
return model;
}
public static ErrorHandler notFound = ctx -> {
ctx.render(Path.Template.NOT_FOUND, baseModel(ctx));
};
}
Khai báo các Entity:
Entity User
package thecoderoot.javalin.library.web.user;
public class User {
public final String username;
public final String salt;
public final String hashedPassword;
public User(String username, String salt, String hashedPassword) {
this.username = username;
this.salt = salt;
this.hashedPassword = hashedPassword;
}
}
Entity Book:
package thecoderoot.javalin.library.web.book;
public class Book {
public final String isbn;
public final String title;
public final String author;
public String getMediumCover() {
return "https://covers.openlibrary.org/b/isbn/" + this.isbn + "-M.jpg";
}
public String getLargeCover() {
return "https://covers.openlibrary.org/b/isbn/" + this.isbn + "-L.jpg";
}
public Book(String title, String author, String isbn) {
this.title = title;
this.author = author;
this.isbn = isbn;
}
public String getIsbn() {
return isbn;
}
public String getTitle() {
return title;
}
public String getAuthor() {
return author;
}
}
Khai báo các Controller:
LoginController
package thecoderoot.javalin.library.web.user;
import org.mindrot.jbcrypt.BCrypt;
import static thecoderoot.javalin.library.web.Main.*;
public class UserController {
// Authenticate the user by hashing the inputted password using the stored salt,
// then comparing the generated hashed password to the stored hashed password
public static boolean authenticate(String username, String password) {
if (username == null || password == null) {
return false;
}
User user = userDao.getUserByUsername(username);
if (user == null) {
return false;
}
String hashedPassword = BCrypt.hashpw(password, user.salt);
return hashedPassword.equals(user.hashedPassword);
}
}
UserController chỉ làm một nhiệm vụ duy nhất là authenticate người dùng bằng username và password, bằng cách gọi method userDao.getUserByUsername(username) của class UserDao, trong ví dụ này chúng ta đơn giản hóa logic authentication bằng các dữ liệu user và password được set cứng. Công việc cũng tương tự nếu bạn làm authentication bằng Database.
BookController:
package thecoderoot.javalin.library.web.book;
import java.util.Map;
import io.javalin.Handler;
import thecoderoot.javalin.library.web.util.Path;
import thecoderoot.javalin.library.web.util.ViewUtil;
import static thecoderoot.javalin.library.web.Main.bookDao;
import static thecoderoot.javalin.library.web.util.RequestUtil.getParamIsbn;
public class BookController {
public static Handler fetchAllBooks = ctx -> {
Map<String, Object> model = ViewUtil.baseModel(ctx);
model.put("books", bookDao.getAllBooks());
ctx.render(Path.Template.BOOKS_ALL, model);
};
public static Handler fetchOneBook = ctx -> {
Map<String, Object> model = ViewUtil.baseModel(ctx);
model.put("book", bookDao.getBookByIsbn(getParamIsbn(ctx)));
ctx.render(Path.Template.BOOKS_ONE, model);
};
}
Như tên gọi BookController sẽ handle các request liên quan đến Entity Book, cụ thể trong ví dụ này BookController handle 2 method: trả về list tất cả các sách có trong thư viện, hoặc tìm và trả về 1 đối tượng sách theo trường Isbn - một kiểu id định danh cho từng đối tượng sách.
Setup folder Velocity
Velocity là một template engine được dùng để tích hợp với các nền tảng Java Web khác nhau trong đó có Javalin. Nếu chưa từng làm quen với velocity các bạn có thể hiểu đơn giản Velocity engine giống như JSP hay Thymeleaf - các công cụ có công dụng chính là xử lý và sắp xếp dữ liệu từ Server thành giao diện HTML cho người dùng.
Trong ví dụ này mình sẽ không đề cập quá chi tiết về các syntax của Velocity Engine, mà chỉ nói những điểm chính về master layout và cách bind data từ model của Backend. Những chi tiết khác các bạn có thể tham khảo tại Apache Velocity Engine.
Chúng ta sẽ tạo folder /resources/velocity và đặt tất cả file velocity của project vào trong folder này.
Cấu trúc folder Velocity bao gồm:
- layout.vm: file master layout chính cho website, định nghĩa một maste layout cho toàn bộ các trang bao gồm header, body và phần footer.
- notfound.vm: hiển thị message not found khi user truy cập vào các resource không tồn tại trên website
- Folder book: chứa các file velocity hiển thị danh sách các quyển sách của thư viện và thông tin của một quyển sách bất kỳ.
- Folder login - login.vm: hiển thị form Login.
- Folder velocityconfig - velocity_implicit.vm: Setup các mapping định danh khi gọi hàm của các class của phần Backend.
layout.vm
#macro(mainLayout)
<html>
<head>
<title>Javalin Library</title>
<link rel="stylesheet" href="/main.css">
<link rel="icon" href="/favicon.png">
<meta name="viewport" content="width=device-width, initial-scale=1">
</head>
<body>
<header>
<nav>
<a href="/index"><img id="logo" src="/img/logo.png" alt="Javalin Library"></a>
<ul id="menu">
<li><a href="/books">View all books</a></li>
#if($currentUser)
<li>
<form method="post" action="/logout">
<button id="logout">Log out</button>
</form>
</li>
#else
<li><a href="/login">Log in</a></li>
#end
</ul>
</nav>
</header>
<main>
<div id="content">
$bodyContent
</div>
</main>
<footer>
This Application uses <a href="https://openlibrary.org/" target="_blank">OpenLibrary</a> for images.
</footer>
</body>
</html>
#end
#macro: dùng để định danh cho master layout, ở đây là mainLayout.
Trong master layout này chúng ta define header và footer cho toàn bộ các trang của website, nghĩa là các trang chỉ khác nhau phần nội dung của từng trang nằm giữa cặp tag <main>.
login.vm
#parse("/velocity/layout.vm")
#@mainLayout()
<form id="loginForm" method="post">
#if($authenticationFailed)
<p class="bad notification">The login information you supplied was incorrect.</p>
#elseif($authenticationSucceeded)
<p class="good notification">You''re logged in as '$currentUser'</p>
#elseif($loggedOut)
<p class="notification">You have been logged out.</p>
#end
<h1>Login</h1>
<p>Please enter your username and password. <small><br>(See the <a href="/index">index page</a> if you need a hint)</small></p>
<label>Username</label>
<input type="text" name="username" placeholder="Username" value="" required>
<label>Password</label>
<input type="password" name="password" placeholder="Password" value="" required>
#if($loginRedirect)
<input type="hidden" name="loginRedirect" value="$loginRedirect">
#end
<input type="submit" value="Log in">
</form>
#end
Các bạn lưu ý ở đầu file login.vm khai báo tên master layout mà file login.vm sử dụng.
#parse("/velocity/layout.vm")
#@mainLayout()
Nghĩa là trang login của chúng ta sẽ có header và footer được định nghĩa ở trang mainLayout.vm
Tiếp theo trước khi vào chi tiết các file vm hiển thị thông tin về sách mình muốn lưu ý trước cho các bạn về các cú pháp cơ bản.
book/all.vm
Hiển thị thông tin của tất cả sách có trong thư viện:
#parse("/velocity/layout.vm")
#@mainLayout()
<h1>All books</h1>
<div class="row row-3">
#foreach($book in $books)
<div class="col">
<a class="book" href="/books/$book.isbn">
<div class="bookCover">
<img src="$book.mediumCover" alt="$book.title">
</div>
<strong>$book.title</strong>
<br>
$book.author
</a>
</div>
#end
</div>
#end
book/one.vm
Hiển thị thông tin của 1 quyển sách theo trường isbn
#parse("/velocity/layout.vm")
#@mainLayout()
#if($book)
<h1>$book.title</h1>
<h2>$book.author</h2>
<div class="book">
<div class="bookCover">
<img src="$book.largeCover" alt="$book.title">
</div>
</div>
#else
<h1>Book not found</h1>
#end
#end
login.vm
Trang đăng nhập
#parse("/velocity/layout.vm")
#@mainLayout()
<form id="loginForm" method="post">
#if($authenticationFailed)
<p class="bad notification">The login information you supplied was incorrect.</p>
#elseif($authenticationSucceeded)
<p class="good notification">You''re logged in as '$currentUser'</p>
#elseif($loggedOut)
<p class="notification">You have been logged out.</p>
#end
<h1>Login</h1>
<p>Please enter your username and password. <small><br>(See the <a href="/index">index page</a> if you need a hint)</small></p>
<label>Username</label>
<input type="text" name="username" placeholder="Username" value="" required>
<label>Password</label>
<input type="password" name="password" placeholder="Password" value="" required>
#if($loginRedirect)
<input type="hidden" name="loginRedirect" value="$loginRedirect">
#end
<input type="submit" value="Log in">
</form>
#end
Các bạn có thể download source code tại đây
Chúc các bạn thành công.
TheCodeRoot via Javalin.io