Chương 15. Tối ưu hiệu năng và bảo mật

📚 Mục lục sách

Một module chạy đúng chức năng vẫn có thể thất bại nếu chậm dần theo thời gian hoặc để lộ dữ liệu. Chương này giới thiệu những kỹ thuật tối ưu hiệu năng và nguyên tắc bảo mật cơ bản nên áp dụng ngay khi phát triển module cho EzyPlatform, thay vì đợi đến khi gặp sự cố mới xử lý.

Tối ưu hiệu năng khi phát triển giao diện người dùng

Nếu bạn mở mã nguồn dự án tiệm bánh sweet-home-bakery đã xây dựng ở các chương trước, bạn sẽ thấy mỗi trang HTML thường tự mang theo cả một khối <script> xử lý logic (ví dụ đoạn script phát hiện theme sáng/tối ngay khi tải trang) lẫn một khối <style> định kiểu riêng, nhúng thẳng trong <head> của chính trang đó thay vì để trong một tập tin riêng.

Phần đầu trang HTML của dự án sweet-home-bakery chứa cả khối script và style nhúng trực tiếp trong thẻ head
Trước khi tối ưu: mỗi trang tự mang theo khối <script><style> riêng ngay trong <head>.

Vấn đề là các trang này không phải HTML tĩnh — EzyPlatform phải dựng (render) lại toàn bộ nội dung trang qua template engine ở phía máy chủ cho mỗi request, nên phần script/style nhúng thẳng trong trang cũng bị đọc và gửi kèm lại từ đầu mỗi lần, không tách rời và không cache riêng được. Nếu nhiều trang dùng chung gần như y hệt một đoạn CSS/JS (ví dụ đoạn script theme, các biến màu, các class dùng chung...), đoạn mã đó sẽ bị lặp lại nguyên vẹn trong từng trang, vừa tốn thêm bộ nhớ để máy chủ xử lý mỗi khi có nhiều người truy cập cùng lúc, vừa khiến mỗi trang nặng hơn cần thiết.

Cách khắc phục là nhờ AI gom các đoạn CSS/JS dùng chung này ra khỏi từng trang, hợp nhất vào một vài tập tin riêng rồi lưu dưới dạng media, include lại vào các trang bằng thẻ <link>/<script src> thay vì nhúng trực tiếp. Nhờ vậy, các tập tin media này được EzyPlatform phục vụ qua cơ chế tải tĩnh đã tối ưu sẵn (không phải dựng lại qua template engine ở mỗi request như trang HTML), đồng thời trình duyệt cũng cache lại được — chỉ cần tải một lần, những lần sau (kể cả khi chuyển qua trang khác) sẽ không phải tải lại. Bạn hãy gõ prompt:

📋 Prompt mẫu — Gom CSS dùng chung
Gom các mã nguồn css của các trang thuộc dự án sweet-home-bakery vào chung một tập tin sweet-bakery.css, include lại vào các trang để giảm tải bộ nhớ khi render các trang.

Sau đó bạn gõ tiếp prompt:

📋 Prompt mẫu — Gom JavaScript dùng chung
Tiếp tục gom các mã nguồn javascript có thể gom của các trang thuộc dự án sweet-home-bakery vào chung một tập tin sweet-bakery.js, include lại vào các trang để giảm tải bộ nhớ khi render các trang.

AI sẽ tạo 2 tập tin trong thư mục api/v1/media cho bạn:

api/v1/media/
├── sweet-bakery.css
└── sweet-bakery.js

Hai tập tin này tự động được lưu lên máy chủ ngay khi AI tạo mới, nhưng từ lần kế tiếp trở đi, mỗi khi bạn (hoặc AI) sửa lại nội dung của chúng, bạn sẽ cần chuột phải vào tập tin và chọn Publish Content To Server để đẩy thay đổi lên máy chủ — đúng cơ chế xuất bản thủ công đã dùng cho các trang ở những chương trước, chỉ khác là lần này áp dụng cho tập tin media.

Chuột phải vào tập tin sweet-bakery.js trong VS Code, chọn Publish Content To Server từ menu ngữ cảnh
Chuột phải vào tập tin media vừa sửa, chọn Publish Content To Server để xuất bản lên máy chủ.

Cuối cùng, bạn hãy lưu và xuất bản lại các trang đã được AI sửa (giờ trỏ tới sweet-bakery.css/sweet-bakery.js thay vì nhúng trực tiếp) lên máy chủ. Mở lại trên trình duyệt để xác nhận các trang vẫn hiển thị bình thường như trước — chỉ khác là phần CSS/JS dùng chung đã được tách khỏi luồng dựng trang và được trình duyệt cache lại cho những lần truy cập sau.

Tối ưu hiệu năng khi phát triển module

Khi module còn ít dữ liệu, mọi truy vấn đều chạy nhanh dù viết chưa tối ưu. Vấn đề chỉ lộ ra khi dữ liệu tăng lên hàng chục nghìn, hàng triệu bản ghi — lúc đó một trang Admin tưởng chừng đơn giản có thể mất vài giây để tải, hoặc một API bị timeout. Dưới đây là 6 kỹ thuật phổ biến, bạn có thể nhờ AI áp dụng ngay từ đầu.

1. Đánh chỉ mục (index) cho các truy vấn còn thiếu

Mọi cột dùng để lọc (WHERE), sắp xếp (ORDER BY) hay join giữa các bảng đều nên có index tương ứng — thiếu index khiến MySQL phải quét toàn bộ bảng (full table scan) mỗi lần truy vấn, càng nhiều dữ liệu càng chậm. Nhờ AI rà lại các truy vấn trong module, đối chiếu với cấu trúc index hiện có, và bổ sung index còn thiếu.

📋 Prompt mẫu — Rà soát và bổ sung index
Hãy rà lại các truy vấn (query) trong module {tên-module-của-bạn}, đặc biệt các câu SELECT có điều kiện WHERE, ORDER BY hoặc JOIN.
Với mỗi truy vấn, kiểm tra bảng tương ứng đã có index phù hợp chưa (dùng EXPLAIN để xác nhận), liệt kê những truy vấn đang phải quét toàn bảng (full table scan), và đề xuất index cần bổ sung kèm câu lệnh ALTER TABLE tương ứng.

Với bảng đã tồn tại (đã có dữ liệu thật), không sửa lại file .sql tạo bảng gốc trong src/main/resources/scripts của module admin-plugin — thay vào đó tạo thêm một file riêng có chữ "alter" trong tên (ví dụ alter_add_index_xxx.sql), chứa câu lệnh ALTER TABLE ... ADD INDEX .... EzyPlatform tự quét và chạy các file này khi khởi động, bỏ qua lỗi nếu index đã được thêm từ trước, nên bạn có thể yên tâm cài đặt module này lên nhiều môi trường (dev, staging, production) mà không lo chạy trùng. Nếu có nhiều file alter và cần đảm bảo chạy đúng thứ tự (ví dụ alter sau phải phụ thuộc vào alter trước), thêm số thứ tự vào cuối tên file dạng _{số thứ tự}.sql, ví dụ alter_add_index_1.sql, alter_add_index_2.sql — EzyPlatform sẽ chạy các file theo đúng thứ tự số tăng dần đó. Lưu ý tên file (kể cả phần alter và số thứ tự) chỉ được chứa ký tự tiếng Anh, chữ số, dấu gạch dưới _ và dấu gạch ngang -, tối đa 120 ký tự.

📋 Prompt mẫu — Tạo file alter bổ sung index
Bảng {tên-bảng} trong module {tên-module-của-bạn} đã có dữ liệu thật, tôi cần bổ sung index cho cột {tên-cột} mà không được sửa file SQL tạo bảng gốc.
Hãy tạo giúp tôi một file alter_add_index_{tên-cột}.sql trong src/main/resources/scripts của module admin-plugin, chứa câu lệnh ALTER TABLE {tên-bảng} ADD INDEX cho cột trên, đúng theo cơ chế EzyPlatform tự quét và chạy file "alter" khi khởi động.
Nếu tôi cần thêm nhiều file alter khác phụ thuộc thứ tự lẫn nhau, hãy đặt tên kèm số thứ tự dạng _{số thứ tự}.sql để đảm bảo chạy đúng trình tự. Đặt tên file chỉ dùng ký tự tiếng Anh, chữ số, dấu gạch dưới _ và dấu gạch ngang -, tối đa 120 ký tự.

2. Dùng DataIndex thay vì toán tử LIKE

Toán tử LIKE '%từ khoá%' (có dấu % ở đầu) không thể tận dụng index, nên với bảng lớn, tìm kiếm kiểu này sẽ ngày càng chậm dần theo thời gian. EzyPlatform giải quyết vấn đề này bằng một bảng dùng chung tên DataIndex (dataType, dataId, keyword, priority) — thay vì mỗi module tự thêm cột rồi LIKE trên bảng dữ liệu gốc của mình, mọi module đều lưu các từ khoá tìm kiếm của bản ghi vào chung bảng này, gắn với dataType riêng của module (ví dụ media, admin_account) và dataId là khoá của bản ghi gốc. Vì cột keyword được so khớp chính xác hoặc theo tiền tố (không phải LIKE '%...%'), index trên cột này vẫn phát huy tác dụng dù dữ liệu lớn tới đâu.

Dữ liệu DataIndex không được ghi ngay lúc lưu bản ghi, mà do một Appender riêng (kế thừa AdminLastUpdatedAtDataIndexAppender ở module admin-plugin) quét định kỳ các bản ghi mới/thay đổi theo updatedAt, tách nội dung cần tìm kiếm thành từng từ khoá qua một class "KeywordsExtractor" riêng (ví dụ AdminMediaKeywordsExtractor tách tiêu đề, mô tả... của Media thành các từ khoá), rồi lưu qua DataIndexService.saveKeyword()/saveKeywords(). Đây cũng chính là một ví dụ thực tế cho kỹ thuật ở mục 4 bên dưới: việc đánh chỉ mục là nghiệp vụ nặng, nên EzyPlatform tách hẳn ra một Appender chạy nền thay vì làm ngay trong lúc lưu bản ghi.

Khi tìm kiếm, cách EzyPlatform thực sự dùng ở các module lõi không phải gọi DataIndexService để lấy danh sách dataId rồi truy vấn bảng gốc thêm một lần nữa, mà viết thẳng một câu truy vấn JOIN giữa bảng của bạn với bảng DataIndex ngay trong Repository — lấy dữ liệu về trong một lượt duy nhất. Ví dụ module quản lý tài khoản Admin có truy vấn (khai báo bằng annotation @EzyQuery trên phương thức Repository):

💻 Ví dụ truy vấn JOIN với DataIndex (trích từ AdminAdminRepository)
SELECT e.id, e.username, e.displayName FROM Admin e
INNER JOIN DataIndex a ON e.id = a.dataId
WHERE a.dataType = 'ezy_admins' AND a.keyword LIKE CONCAT(?0,'%')

Chú ý câu lệnh vẫn dùng LIKE, nhưng là LIKE CONCAT(?0,'%') — chỉ có dấu % ở cuối (khớp tiền tố), không phải % ở đầu, nên vẫn tận dụng được index trên cột keyword. Khi bản ghi bị xoá hoặc nội dung thay đổi, nhớ gọi DataIndexService.deleteKeywordsByDataTypeAndDataId() để dọn từ khoá cũ trong bảng DataIndex, tránh kết quả tìm kiếm bị rác.

Vì EzyPlatform là mã nguồn đóng, bạn không có sẵn source code để đọc — nhưng khi đã cài EzyPlatform và EzyPlatform SDK (xem lại Chương 14), các lớp trên nằm sẵn trong file .jar ở thư mục lib (ví dụ ezyplatform-common-sdk-*.jar, ezyplatform-admin-*.jar) hoặc trong kho Maven cục bộ (~/.m2). Nhờ AI dùng lệnh javap (có sẵn trong JDK) để xem cấu trúc class ngay trong các file .jar đó, hoặc mở trực tiếp trong IntelliJ IDEA — IDE sẽ tự decompile khi bạn nhấn vào một class đến từ thư viện — thay vì tìm mã nguồn không tồn tại.

📋 Prompt mẫu — Áp dụng DataIndex cho tìm kiếm
Module {tên-module-của-bạn} của tôi đang tìm kiếm bằng LIKE '%từ khoá%' trên cột {tên-cột} của bảng {tên-bảng}, tôi lo về hiệu năng khi dữ liệu lớn dần. Tôi không có source code của EzyPlatform, chỉ có các file jar sau khi cài EzyPlatform và EzyPlatform SDK.
Hãy dùng lệnh javap để xem cấu trúc các lớp DataIndexService, SaveDataKeywordModel (trong file ezyplatform-common-sdk-*.jar ở thư mục lib của EzyPlatform hoặc trong ~/.m2), các Appender như AdminDataIndexAppender/AdminLastUpdatedAtDataIndexAppender và class KeywordsExtractor tương ứng, cùng các truy vấn @EzyQuery có JOIN DataIndex trong các Repository lõi như AdminAdminRepository (trong ezyplatform-admin-*.jar / ezyplatform-admin-sdk — dùng nhóm Web tương ứng nếu tôi đang ở module web-plugin), rồi áp dụng cơ chế tương tự cho module của tôi:
1. Tạo class tách nội dung của {tên-bảng} thành các từ khoá tìm kiếm.
2. Tạo Appender quét bản ghi mới/thay đổi theo updatedAt và lưu từ khoá qua DataIndexService, dùng dataType riêng cho module tôi.
3. Viết một phương thức Repository dùng @EzyQuery, JOIN bảng {tên-bảng} với DataIndex theo dataId, lọc dataType đúng của module tôi và keyword LIKE CONCAT(?0,'%') (chỉ % ở cuối), thay cho câu LIKE '%từ khoá%' hiện tại.
4. Đảm bảo có gọi deleteKeywordsByDataTypeAndDataId khi bản ghi bị xoá hoặc nội dung thay đổi.

3. Cache các setting nặng, hay được sử dụng

Nếu module của bạn đọc một setting (cấu hình) ở gần như mọi request — ví dụ một cờ bật/tắt tính năng, hay một giá trị dùng để tính toán — mà mỗi lần đọc lại truy vấn thẳng xuống database, đó là một điểm nghẽn hiệu năng dễ bị bỏ qua. SettingService (interface trong ezyplatform-common-sdk mà module nào cũng dùng được) đã có sẵn cơ chế cache trong bộ nhớ (một Map nội bộ), bạn chỉ cần đăng ký đúng cách thay vì tự viết cache riêng.

Có 2 cách đăng ký cache, gọi một lần lúc khởi động (thường trong constructor của lớp implement SettingService, hoặc trong EzyBeanConfig của module):

  • scheduleCacheValue(tên-setting, số-giây) — cứ sau mỗi khoảng thời gian chỉ định lại tự đọc và cache lại giá trị mới, bất kể setting có thay đổi hay không. Ví dụ module Admin lõi tự cache ezyplatform_media_up_down_loader_name mỗi 15 giây: scheduleCacheValue("ezyplatform_media_up_down_loader_name", 15).
  • watchLastUpdatedTime(tên-setting, số-giây, callback) (hoặc watchLastUpdatedTimeAndCache(tên-setting, số-giây) nếu chỉ cần cache mà không cần xử lý gì thêm) — định kỳ kiểm tra thời điểm cập nhật của setting trong database, chỉ đọc và cache lại khi có thay đổi thật sự, tránh truy vấn thừa. Mặc định chu kỳ kiểm tra là SettingService.DEFAULT_CACHE_PERIOD_IN_SECOND (5 giây) nếu không truyền số giây riêng.

Sau khi đã đăng ký, mọi nơi trong code chỉ cần gọi settingService.getCachedValue(tên-setting, giá-trị-mặc-định) để lấy giá trị từ bộ nhớ, không còn truy vấn database ở mỗi lần gọi.

📋 Prompt mẫu — Cache setting hay dùng
Module {tên-module-của-bạn} của tôi đang đọc setting {tên-setting} trực tiếp từ database ở mỗi request, tôi muốn cache lại để giảm tải. Tôi không có source code của EzyPlatform, chỉ có file jar sau khi cài EzyPlatform SDK.
Hãy dùng javap kiểm tra interface SettingService (trong ezyplatform-common-sdk-*.jar) và cách AdminSettingService/WebSettingService đăng ký cache trong constructor (trong ezyplatform-admin-*.jar hoặc ezyplatform-web-*.jar) để xác nhận đúng chữ ký các phương thức scheduleCacheValue, watchLastUpdatedTime, watchLastUpdatedTimeAndCache, getCachedValue.
Sau đó giúp tôi:
1. Đăng ký cache cho setting {tên-setting} lúc khởi động module, chọn scheduleCacheValue nếu setting ít khi đổi và chấp nhận độ trễ vài giây, hoặc watchLastUpdatedTimeAndCache nếu cần cache được làm mới ngay khi setting thay đổi.
2. Thay mọi chỗ đang đọc {tên-setting} trực tiếp từ database bằng settingService.getCachedValue.

4. Dùng Appender để tách nghiệp vụ nặng khỏi luồng chính

Appender là tiến trình chạy nền của EzyPlatform — thay vì xử lý một tác vụ nặng hoặc không cần phản hồi ngay (gửi email, gửi thông báo, tính toán thống kê...) ngay trong luồng nghiệp vụ chính, bạn tách nó ra thành một Appender chạy nền. Ví dụ: khi tạo đơn hàng, thay vì gửi thông báo cho khách ngay trong service tạo đơn (khiến request tạo đơn phải chờ gửi thông báo xong mới trả kết quả), bạn chỉ cần ghi nhận sự kiện, để một Appender riêng quét và gửi thông báo sau.

Ở module admin-plugin, mọi Appender đều kế thừa (trực tiếp hoặc gián tiếp) từ lớp AdminDataAppender<V, D, P> trong ezyplatform-admin-sdk — 3 tham số kiểu lần lượt là: V kiểu dữ liệu bạn quét (ví dụ Order), D kiểu "bản ghi" cần xử lý cho mỗi V quét được (có thể trùng với V, hoặc là một model khác), P kiểu page token để nhớ đã quét tới đâu, tránh quét lại từ đầu mỗi lần chạy. Bạn cần cài đặt tối thiểu:

  • getValueList(P pageToken) — lấy tiếp một trang dữ liệu (ví dụ đơn hàng có updatedAt mới hơn token hiện tại).
  • extractNewLastPageToken(...), defaultPageToken(), pageTokenType() — quản lý vị trí đã quét tới.
  • addDataRecord(D) (hoặc addDataRecords(List<D>)) — nơi thực sự xử lý side-effect, ví dụ gọi service gửi thông báo.
  • getAppenderNamePrefix() — đặt tên để nhận diện Appender trong Admin.

Không cần viết từ đầu nếu nghiệp vụ chỉ đơn giản là "quét bản ghi thay đổi theo updatedAt": mã nguồn lõi đã có 2 lớp trung gian làm sẵn phần quét theo updatedAt mà bạn có thể tham khảo và mô phỏng lại — AdminLastUpdatedAtDataIndexAppender (dùng cho đánh chỉ mục, đã nhắc ở mục 2) và AdminLastUpdatedAtLinkAppender (dùng để liên kết/đồng bộ dữ liệu, ví dụ AdminAuthorLinkAppender đồng bộ thông tin tác giả). Cả hai chỉ còn bắt bạn cài đặt extractLastUpdatedAt(V)getValueList(pageToken), phần quản lý page token đã được xử lý sẵn — vì không có source code, hãy dùng javap để xem cấu trúc 2 lớp này rồi mô phỏng lại tương tự cho D là dữ liệu thông báo của bạn.

Một điểm quan trọng: bạn không cần tự đăng ký Appender vào đâu cả — chỉ cần khai báo class kế thừa AdminDataAppender thành một bean bình thường (theo cách khai báo bean của EzyPlatform), AdminDataAppenderManager sẽ tự quét và nhận diện tất cả bean thuộc kiểu này, hiển thị ngay trong danh sách Appender ở Admin > Cài đặt (đã nhắc ở Chương 5) để bạn bật/tắt/restart/reload như mọi Appender lõi khác.

📋 Prompt mẫu — Tách nghiệp vụ nặng ra Appender
Trong module {tên-module-của-bạn}, nghiệp vụ {mô tả nghiệp vụ, ví dụ "tạo đơn hàng"} đang gửi thông báo/email trực tiếp trong cùng luồng xử lý, khiến request phải chờ lâu hơn cần thiết. Tôi không có source code của EzyPlatform, chỉ có file jar sau khi cài EzyPlatform SDK.
Hãy dùng javap kiểm tra cấu trúc lớp AdminDataAppender, và cách AdminLastUpdatedAtLinkAppender/AdminLastUpdatedAtDataIndexAppender cài đặt phần quét theo updatedAt (trong ezyplatform-admin-*.jar / ezyplatform-admin-sdk — dùng nhóm Web tương ứng nếu tôi đang ở module web-plugin), rồi giúp tôi:
1. Tạo class {TênAppenderCủaBạn} kế thừa AdminDataAppender, quét các bản ghi {mô tả nghiệp vụ} có updatedAt mới hơn lần quét trước.
2. Cài đặt addDataRecord để gọi service gửi thông báo/email cho mỗi bản ghi quét được.
3. Khai báo class này thành bean đúng chuẩn để AdminDataAppenderManager tự nhận diện và hiển thị trong Admin > Cài đặt.
4. Sửa lại nghiệp vụ {mô tả nghiệp vụ} hiện tại: bỏ phần gửi thông báo trực tiếp, chỉ còn lưu dữ liệu cần thiết để Appender quét sau.

5. Xử lý song song cho các tác vụ IO độc lập

Khi một nghiệp vụ cần gọi nhiều tác vụ IO độc lập với nhau (gọi nhiều API bên ngoài, đọc nhiều bảng không phụ thuộc nhau...), chạy tuần tự sẽ cộng dồn thời gian chờ của từng tác vụ. EzyPlatform có sẵn một bộ tiện ích tên Reactive (package org.youngmonkeys.ezyplatform.rx trong ezyplatform-common-sdk, đã được nhiều service lõi như PaginationResultService dùng) để kết hợp nhiều tác vụ bất đồng bộ mà không cần thêm thư viện ngoài:

  • Reactive.single(giá-trị).mapItem(fn)...blockingGet() — chạy một chuỗi biến đổi bất đồng bộ trên một giá trị, chờ và lấy kết quả cuối cùng.
  • Reactive.multiple() — gộp nhiều tác vụ độc lập (mỗi tác vụ ứng với một RxFunction riêng) để chạy song song, rồi blockingGet()/blockingConsume() khi cần chờ tất cả hoàn tất và lấy kết quả.

Điểm đáng chú ý: các tác vụ chạy trên một ExecutorService riêng của Reactive (kích thước mặc định bằng số nhân CPU), tách biệt hoàn toàn với common ForkJoinPoolparallelStream dùng. Đây là lý do nên cân nhắc kỹ trước khi dùng parallelStream cho tác vụ IO: parallelStream ở bất kỳ đâu trong cùng một JVM đều dùng chung một pool luồng giới hạn (mặc định bằng số nhân CPU trừ 1) — nếu bạn dùng nó để gọi API/IO (vốn mất thời gian chờ chứ không tốn CPU), các luồng bị "giữ chỗ" lâu có thể làm nghẽn cả những chỗ khác trong hệ thống đang dùng parallelStream cho việc tính toán thuần CPU. CompletableFuture vẫn là lựa chọn tốt nếu bạn tự truyền vào một Executor riêng (không dùng pool mặc định) cho các tác vụ IO.

📋 Prompt mẫu — Chạy song song các tác vụ IO
Nghiệp vụ {mô tả nghiệp vụ} trong module {tên-module-của-bạn} đang gọi tuần tự {liệt kê các tác vụ IO độc lập, ví dụ "3 API bên ngoài"}, khiến tổng thời gian xử lý cộng dồn khá lâu. Tôi không có source code của EzyPlatform, chỉ có file jar sau khi cài EzyPlatform SDK.
Hãy dùng javap kiểm tra cấu trúc các lớp Reactive, RxSingle, RxMultiple, RxOperation trong ezyplatform-common-sdk-*.jar để xác nhận đúng API, rồi đánh giá xem nên dùng Reactive.multiple() có sẵn của EzyPlatform, CompletableFuture với Executor riêng, hay parallelStream để chạy các tác vụ này song song — giải thích rõ lý do chọn (đặc biệt lưu ý parallelStream dùng chung common ForkJoinPool, không phù hợp cho tác vụ IO) — rồi sửa code giúp tôi.

6. Giảm số lượng bản ghi được đếm trong truy vấn phân trang

Mỗi trang danh sách có phân trang thường hiển thị kèm tổng số bản ghi thoả điều kiện — nhưng để tính con số đó, database phải đếm (COUNT) qua toàn bộ bản ghi thoả điều kiện, càng nhiều dữ liệu càng chậm, kể cả khi đã có index. EzyPlatform xử lý đánh đổi này bằng setting ezyplatform_pagination_count_limit: giới hạn số bản ghi tối đa mà một truy vấn đếm sẽ quét qua, mặc định là 1.000.000. Khi số bản ghi thoả điều kiện vượt quá giới hạn này, EzyPlatform sẽ không đếm tiếp mà dừng lại ở đúng giới hạn — nghĩa là tổng số hiển thị có thể không còn chính xác tuyệt đối, đổi lại truy vấn không bị chậm dần vô hạn khi dữ liệu lớn lên.

Đây cũng chính là một setting được đọc qua SettingService.getPaginationCountLimit() — đúng cơ chế cache setting đã nhắc ở mục 3. Nếu module của bạn có bảng dữ liệu rất lớn và đang thấy trang phân trang chậm vì bước đếm tổng số bản ghi, bạn có thể giảm giới hạn này xuống thấp hơn 1.000.000 để đổi lấy tốc độ, chấp nhận việc không hiển thị chính xác tổng số bản ghi khi dữ liệu vượt ngưỡng. Vào Admin > Cài đặt > Thông thường (hoặc truy cập trực tiếp {đường-dẫn-admin-của-bạn}/settings/regular), tìm đến trường Số bản ghi được đếm tối đa trong truy vấn phân trang và chỉnh lại giá trị.

Trường 'Số bản ghi được đếm tối đa trong truy vấn phân trang' trong Admin > Cài đặt > Thông thường, giá trị mặc định 1000000
Setting ezyplatform_pagination_count_limit tại Admin > Cài đặt > Thông thường, mặc định 1.000.000 bản ghi.
📋 Prompt mẫu — Đánh giá giới hạn đếm phân trang
Trang danh sách của module {tên-module-của-bạn} đang chậm khi tải, tôi nghi ngờ do bước đếm tổng số bản ghi trong truy vấn phân trang (setting ezyplatform_pagination_count_limit, mặc định 1.000.000).
Hãy kiểm tra giúp tôi bảng {tên-bảng} hiện có khoảng bao nhiêu bản ghi thoả các điều kiện lọc thường dùng, và tư vấn xem có nên giảm ezyplatform_pagination_count_limit xuống hay không, đánh đổi giữa tốc độ tải trang và độ chính xác của tổng số bản ghi hiển thị.

Đây là 6 kỹ thuật phổ biến nhất, nhưng không phải danh sách đầy đủ. Sau khi module đã chạy ổn định, bạn có thể nhờ AI rà lại toàn bộ mã nguồn để tự phát hiện thêm điểm cần tối ưu.

📋 Prompt mẫu — Rà soát hiệu năng tổng thể
Hãy đọc toàn bộ mã nguồn module {tên-module-của-bạn}, đối chiếu với các nguyên tắc tối ưu hiệu năng đã áp dụng (index, DataIndex, cache setting, Appender, xử lý song song, giới hạn đếm phân trang), và chỉ ra thêm những điểm có thể tối ưu mà tôi chưa đề cập — kèm giải thích vì sao và mức độ ưu tiên nên xử lý trước.

Bảo mật cho module bạn phát triển

Hiệu năng tốt mà lộ dữ liệu hay bị chiếm quyền truy cập thì thiệt hại còn lớn hơn nhiều so với việc chạy chậm. Dưới đây là những nguyên tắc bảo mật cơ bản nên áp dụng cho bất kỳ module nào bạn phát triển.

Permission: cấp quyền tối thiểu cần thiết

Mỗi chức năng trong module (xem, thêm, sửa, xoá...) nên gắn với một permission riêng, và mặc định không cấp quyền nào cho role mới — chỉ cấp đúng những gì role đó thực sự cần dùng. Tránh dùng chung một permission cho nhiều chức năng khác nhau vì sẽ khó kiểm soát khi cần phân quyền chi tiết hơn về sau.

Màn hình Quản trị viên > Các quyền trong Admin EzyPlatform, liệt kê từng tính năng kèm URI và phương thức GET/POST/PUT/DELETE có thể tick chọn riêng lẻ
EzyPlatform phân quyền chi tiết tới từng URI và phương thức (GET/POST/PUT/DELETE) — nền tảng để áp dụng nguyên tắc cấp quyền tối thiểu cho module bạn phát triển.

Màn hình Quản trị viên > Các quyền ở trên cho thấy mức độ chi tiết EzyPlatform hỗ trợ sẵn: mỗi "tính năng" (feature) gộp nhiều URI, và mỗi URI lại tách riêng theo từng phương thức HTTP — bạn có thể chỉ cấp quyền GET (xem) mà không cấp POST/PUT/DELETE (thêm/sửa/xoá) cho cùng một tính năng. Khi khai báo permission cho module riêng, hãy tận dụng đúng mức độ chi tiết này thay vì gộp chung nhiều thao tác vào một permission duy nhất.

📋 Prompt mẫu — Rà soát permission của module
Hãy liệt kê toàn bộ permission đang được khai báo trong module {tên-module-của-bạn}, kèm chức năng tương ứng của từng permission.
Kiểm tra xem có chức năng nào đang thiếu permission riêng (dùng chung permission với chức năng khác) hoặc có endpoint nào không kiểm tra permission trước khi xử lý không, rồi đề xuất cách khắc phục.

Token: bảo vệ như bảo vệ mật khẩu

Access token, API key hay bất kỳ chuỗi dùng để xác thực nào đều cần được xem như mật khẩu: không hardcode trong mã nguồn, không log ra file log, không hiển thị trực tiếp trên giao diện hay commit lên kho mã nguồn công khai. Nên lưu ở nơi được mã hoá, hoặc dùng biến môi trường/file cấu hình không được commit.

📋 Prompt mẫu — Rà soát rò rỉ token
Hãy rà lại toàn bộ mã nguồn module {tên-module-của-bạn}, tìm những chỗ token/API key/mật khẩu đang bị hardcode trực tiếp trong code, bị ghi ra log, hoặc bị trả về nguyên vẹn trong response API.
Với mỗi chỗ tìm thấy, đề xuất cách khắc phục phù hợp (biến môi trường, mã hoá, che bớt khi log...).

Logging và Monitoring: phát hiện và điều tra sự cố kịp thời

Log không chỉ để debug lúc phát triển — đây còn là dữ liệu quan trọng nhất khi cần điều tra một sự cố bảo mật hoặc lỗi nghiệp vụ đã xảy ra trong quá khứ. Đảm bảo module ghi log đủ (ai làm gì, khi nào, với dữ liệu nào) cho các thao tác nhạy cảm, nhưng không log dữ liệu nhạy cảm (mật khẩu, token, thông tin cá nhân) dưới dạng rõ.

📋 Prompt mẫu — Bổ sung logging cho thao tác nhạy cảm
Hãy rà lại module {tên-module-của-bạn}, liệt kê các thao tác nhạy cảm (đăng nhập, đổi quyền, thao tác dữ liệu tài chính...) hiện chưa được ghi log đầy đủ.
Bổ sung log cho các thao tác đó (ai thực hiện, thời điểm, dữ liệu liên quan), đồng thời kiểm tra và che/loại bỏ các thông tin nhạy cảm (mật khẩu, token) nếu đang bị log dưới dạng rõ.

Bảo mật tải tập tin lên EzyPlatform

Nếu module của bạn cho phép người dùng tải tập tin lên, đây là một trong những cửa ngõ dễ bị khai thác nhất: chỉ tin vào phần đuôi file (.jpg, .pdf...) hoặc header Content-Type do trình duyệt gửi lên là không đủ, vì cả hai đều do phía client cung cấp và có thể giả mạo — kẻ tấn công hoàn toàn có thể đổi tên một file thực thi thành ảnh.jpg. Thay vì tự viết lại việc kiểm tra này, hãy dùng lớp MediaValidator có sẵn (trong ezyplatform-http-sdk, module admin-plugin/web-plugin có sẵn lớp con tương ứng AdminMediaValidator/WebMediaValidator) — cụ thể là phương thức validateFilePart(Part, boolean). Cách kiểm tra thực sự của lớp này:

  • Kiểm tra kích thước file (part.getSize()) so với giới hạn lấy từ SettingService.getMaxUploadFileSize() — báo lỗi ngay nếu vượt quá, trước khi đọc nội dung file.
  • Dùng Apache Tika (TikaConfig.getDetector().detect(...)) để đọc trực tiếp nội dung (byte) đầu file và xác định mimetype thật — không dựa vào đuôi file hay Content-Type client gửi lên.
  • So khớp mimetype vừa phát hiện với danh sách cho phép từ SettingService.getAcceptedMediaMimeTypes() (một Set<String>, có thể chứa ký tự đại diện * nếu cho phép mọi loại).

Cả 3 giới hạn trên đều là setting, và tách riêng cho từng phía: admin_max_upload_file_size/admin_accepted_media_mime_types cho admin-plugin, web_max_upload_file_size/web_accepted_media_mime_types cho web-plugin (mặc định kích thước tối đa 5MB, riêng ảnh là 1MB) — chỉnh trực tiếp trong Admin thay vì hardcode trong code, và đã được cache theo đúng cơ chế SettingService ở mục 3.

Màn hình cấu hình tải lên tập tin cho Website trong Admin EzyPlatform: dung lượng tải lên tối đa 5MB, dung lượng ảnh tối đa 1MB, các tuỳ chọn cho phép thay thế/giảm kích thước/xoá vĩnh viễn media, và danh sách các loại media được chấp nhận
Cấu hình giới hạn tải tập tin lên phía Website (tab Website trong Cài đặt) — đúng các giá trị mà MediaValidator đọc qua SettingService để kiểm tra kích thước và mimetype.

Trường Các loại media được chấp nhận trong ảnh trên chính là giá trị của getAcceptedMediaMimeTypes() — khai báo dạng danh sách mimetype thật (ví dụ image/png, image/jpeg), không phải đuôi file. Nếu module của bạn cần nhận thêm loại tập tin khác (ví dụ application/pdf), bổ sung trực tiếp vào đây thay vì tự nới lỏng việc kiểm tra trong code.

📋 Prompt mẫu — Kiểm tra file tải lên bằng MediaValidator
Module {tên-module-của-bạn} của tôi có chức năng cho người dùng tải tập tin lên ({mô tả loại file, ví dụ "ảnh đại diện sản phẩm"}), hiện đang tự kiểm tra bằng đuôi file/Content-Type nên tôi lo bị giả mạo. Tôi không có source code của EzyPlatform, chỉ có file jar sau khi cài EzyPlatform SDK.
Hãy dùng javap kiểm tra cấu trúc lớp MediaValidator (trong ezyplatform-http-sdk-*.jar) và lớp con AdminMediaValidator/WebMediaValidator tương ứng với loại module tôi đang làm, xác nhận đúng chữ ký phương thức validateFilePart.
Sau đó giúp tôi thay đoạn code tự kiểm tra đuôi file/Content-Type hiện tại bằng cách tiêm (inject) MediaValidator và gọi validateFilePart cho Part người dùng tải lên, đảm bảo lỗi kích thước/mimetype không hợp lệ được trả về đúng thông báo cho người dùng.
Nếu chưa chắc một đoạn code có an toàn hay không, cứ hỏi thẳng AI: "đoạn code này có lỗ hổng bảo mật nào không, ví dụ SQL injection, XSS, hay lộ thông tin nhạy cảm?" — thói quen hỏi lại này quan trọng hơn việc nhớ hết mọi loại lỗ hổng.

📌 Tóm tắt chương

  • Đánh chỉ mục đầy đủ và dùng DataIndex thay vì LIKE giúp truy vấn không chậm dần khi dữ liệu lớn lên.
  • Cache các setting hay dùng, và dùng Appender để tách nghiệp vụ nặng khỏi luồng xử lý chính.
  • Chạy song song bằng Reactive có sẵn của EzyPlatform hoặc CompletableFuture với Executor riêng cho các tác vụ IO độc lập, tránh dùng parallelStream vì dùng chung pool luồng với cả hệ thống.
  • Giảm giới hạn đếm bản ghi trong phân trang (ezyplatform_pagination_count_limit) nếu bảng dữ liệu quá lớn.
  • Permission nên tuân theo nguyên tắc cấp quyền tối thiểu cần thiết; token cần được bảo vệ như mật khẩu.
  • Logging và Monitoring giúp phát hiện và điều tra sự cố kịp thời.
  • Dùng MediaValidator có sẵn (kiểm tra mimetype thật bằng Apache Tika, không dựa vào đuôi file) khi cho phép tải tập tin lên.
Chương trước: tự phát triển module/plugin riêng bằng AI để mở rộng EzyPlatform. ← Đọc Chương 14
Chương tiếp theo: áp dụng tất cả kiến thức đã học để xây dựng các sản phẩm thực tế hoàn chỉnh. Đọc Chương 16 →