Bí quyết công nghệ của EzyPlatform - Ánh xạ URI
Back To Blogs/api/users/user1
./api/users/user2
.
Lại có thể trả về thông tin của 2 người dùng khác nhau? Có lẽ nào mã nguồn của controller là kiểu này?
@Controller('/api/v1') public class UserController { @DoGet("/users/user1") public ResponseEntity usersUser1Get() {} @DoGet("/users/user1") public ResponseEntity usersUser1Get() {} }
Trên thực tế đây không phải là một câu hỏi ngây ngô, mà là một câu hỏi chính mình đã từng đặt ra từ hồi mới viết ezyhttp. Lúc đó mình chưa biết đến khái niệm path variable, nên mình phải cài đặt API dạng /users?username=abc
, tức là tên người dùng sẽ được truyền qua tham số của request để lấy được thông tin người dùng khác nhau. Sau đó mình thấy cách này bất tiện vô cùng vì sẽ có thêm các điều kiện lọc khác nhau nữa nên mã nguồn của hàm lấy thông tin người dùng cứ bị dài mãi ra.
Path variable
URI là một phần của giao thức HTTP, ví dụ bạn có một đường dẫn đầy đủ http://localhost:8080/api/v1/users/user1
thì /api/v1/users/user1
là URI.
Tuy nhiên khi chúng ta cài đặt mã nguồn xử lý ở máy chủ thì không thể nào cứ một người dùng chúng ta lại tạo ra một hàm xử lý được, ví có cả hàng ngàn, hàng triệu người dùng và không biết trước được username của họ là gì. Chính vì vậy một khái niệm mới được sinh ra đó là là path variable. Một URI sẽ ở dạng động (dynamic) kiểu này /api/v1/users/{username}
thì username
chính là path variable, tương ứng với mỗi yêu cầu mà biến username
này sẽ có giá trị khác nhau, ví dụ user1
, user2
, ... Mã nguồn của hàm xử lý yêu cầu cũng sẽ chỉ cần duy nhất như sau:
@Controller('/api/v1') public class UserController { @DoGet("/users/{username}") public ResponseEntity usersUsernameGet() {} }
Nguyên lý ánh xạ
Nghe thì dễ hiểu, tuy nhiên để ánh xạ được từ một URI cụ thể đến được hàm xử lý lại là cả một quá trình phức tạp, sẽ thế nào nếu có hai hàm thế này:
@Controller('/api/v1') public class UserController { @DoGet("/users/{username}") public ResponseEntity usersUsernameGet() {} @DoGet("/users/me") public ResponseEntity usersAddGet() {} }
Một hàm lấy thông tin người dùng tuỳ biến, một hàm lấy thông tin của người dùng là chính mình, thì me
có bị hiểu là một username
không?
Ngày nay có lẽ nhiều bạn đã đọc nhiều tài liệu và hiểu được nguyên lý cơ bản của việc ánh xạ URI này là dùng cấu trúc dữ liệu cây, tuy nhiên dùng cây thế nào cũng là cả một vấn đề. EzyPlatform được thừa hưởng giải pháp từ EzyHTTP như sau:
Ví dụ chúng ta có 3 URI:
/api/v1/users/{username}
./api/v1/users/me
./api/v2/admin/{username}
.
Chúng ta sẽ có một cây kiểu này:

- Gốc của cây sẽ là
api
. - Hai nhánh lớp là
v1
vàv2
. - Nhánh
v1
có một nhánh làusers
. - Nhánh
v2
có một nhánh làadmins
. - Nhánh
user
có hai nhành làme
và{}
. - Nhánh
admins
có một nhánh là{}
.
Điều bí ẩn ở đấy chính là tại sao path variable username
mà chính ta lại sử dụng {}
mà không phải {username}
? Câu trả lời đó là giả sử client yêu cầu URI là /api/v1/users/user1
, thì URI này sẽ được cắt nhỏ ra thành 1 mảng gồm các phần tử api,v1,users,user1
và sẽ trải qua các bước so sánh với cây như sau:
- So sánh
api
, tìm thấy cây. - So sánh với
v1
, tìm thấy cây theo nhánh v1. - So sánh với
users
, tìm thấy cây. - So sánh
user
vớime
thấy không bằng, mặc định lấy nhánh{}
. - Không còn nhánh nào nữa, kết thúc.
Như vậy chúng ta sẽ tìm thấy nhánh api - v1 - users - {}
. Và trong mã nguồn quản lý chúng ta cũng lưu /api/v1/users/{}
ánh xạ với /api/v1/users/{username}
, như vậy kết quả chúng ta sẽ tìm thấy /api/v1/users/{username}
tương ứng với hàm hàm xử lý usersUsernameGet
.
Mã nguồn cài đặt
Các bước có thể phức tạp, nhưng mã nguồn cài đặt khá đơn giản như sau:
public class URITree { protected String uri; protected Map<String, URITree> children; public void addURI(String uri) { URITree lastChild = this; String[] paths = uri.split("/"); for (String s : paths) { if (lastChild.children == null) { lastChild.children = new HashMap<>(); } String path = s; if (PathVariables.isPathVariable(path)) { path = "{}"; } URITree child = lastChild.children.get(path); if (child == null) { child = new URITree(); lastChild.children.put(path, child); } lastChild = child; } lastChild.uri = uri; } public String getMatchedURI(String uri) { URITree lastChild = this; String[] paths = uri.split("/"); for (String path : paths) { if (lastChild.children == null) { return null; } URITree child = lastChild.children.get(path); if (child == null) { child = lastChild.children.get("{}"); } if (child == null) { child = lastChild.children.get("*"); return child == null ? null : child.uri; } lastChild = child; } return lastChild.uri; } }
Bạn có thể tham khảo mã nguồn đầy đủ tại đây.
Tổng kết
Tưởng chừng như URI là một kiến thức gì đó quá cơ bản mà hàng ngày hàng giờ chúng ta vẫn sử dụng, tuy nhiên khi phải làm xuống đến mức thư viện framework mới thấy nó là cả một bí quyết. Mình hy vọng rằng các bạn có thể tham khảo cách làm của EzyPlatform để sáng tạo ra những cách làm khác thông minh hơn nhé.