Best Practices của tôi – Java
1. Dùng Pattern.compile() thay cho String.replaceAll() trong vòng lặp
public String replaceAll(String regex, String replacement) {
return Pattern.compile(regex).matcher(this).replaceAll(replacement);
}
Pattern luôn phải được biên dịch trước. Nếu cùng một Pattern được chạy nhiều lần với các chuỗi ngắn, hiệu năng sẽ tốt hơn rõ rệt khi tái sử dụng Pattern
đã biên dịch.
2. Dùng StringBuilder thay cho + trong vòng lặp
Trong vòng lặp, new StringBuilder() cho hiệu năng tốt hơn +.
3. Dùng equals() và hashCode() cho đúng
Đôi khi tất cả các field đều được đưa vào equals() hoặc hashCode(). Cách này tiện để kiểm tra một entity đã bị thay đổi hay chưa, nhưng là sai. Mục đích của
equals() không phải là kiểm tra entity đã bị sửa đổi hay chưa, mà là xác định xem, dù không phải cùng một instance của đối tượng, đó có vẫn là cùng một entity
hay không. Nói cách khác: Chỉ dùng primary key!
Điều này cần thiết cho mọi loại collection. Khi gọi .contains() trên một List, Java sẽ dùng phương thức equals. HashMap còn cần thêm hashCode().
Hai phương thức này nên được override cùng nhau.
Một cái bẫy phổ biến là các entity chưa được lưu và chưa có ID. Hãy cẩn thận ở đây và persist sớm để tránh rắc rối.
Tham khảo:
Hợp đồng (contract) của các phương thức này phải luôn được tuân thủ:
4. Bẫy DateTime khi dùng lẫn nhiều hệ điều hành
Một số hệ điều hành có đồng hồ nội bộ chính xác đến nano giây, số khác chỉ đến mili giây. Vì vậy ZonedDateTime.now() không chạy giống nhau trên cả hai nền tảng và làm hỏng
equals(). Thay vào đó, hãy LUÔN LUÔN điều chỉnh độ chính xác!
/**
* truncating the precision, because it relies on the internal clock of linux/macOs/windows.
* windows and older macOS versions use only milliseconds, whereas linux and new macOS versions use microsecond precision.
*/
public static ZonedDateTime getNow() {
return ZonedDateTime.now().truncatedTo(DateTimeUtil.PRECISION);
}
Các phương thức JPA sau đây cũng dựa vào ZonedDateTime.now() và không bao giờ nên được dùng nguyên trạng:
@CreationTimestamp@UpdateTimestamp@PastOrPresent
5. Đừng khai báo kiểu dữ liệu phức tạp ngay tại chỗ
Luôn tạo class riêng cho các kiểu dữ liệu phức tạp, thay vì lồng Pair, List v.v. vào nhau.
Tệ:
// unlesbar
public Triple<List<Pair<List<List<ChatMessage>>, ZonedDateTime>>, Language, ChatRoom> getChatMessagesGroupedByDay();
Tốt hơn:
public class FTOChatMessageCollection {
private List<FTOChatMessageCollectionPerDay> messagesPerDay;
private Language language;
private ChatRoom chatRoom;
// constructor, getter & setter ...
}
public class FTOChatMessageCollectionPerDay {
private List<FTOChatMessage> messages;
private ZonedDateTime day;
// constructor, getter & setter ...
}
Ưu điểm:
- Tên gọi tốt hơn
getFirst()vàgetSecond() - Cách sử dụng được định nghĩa rõ ràng qua getter và setter (class có immutable không? List nào được phép thêm phần tử? Đối tượng phải được khởi tạo thế nào?) → Tính đóng gói tự nó đã ghi lại cách dùng code
- Dễ đọc hơn hẳn
Ngoại lệ:
Với các quan hệ đơn giản, Pair rất tuyệt! Ví dụ:
Pair<User, Training> pair = this.addCurrentUserToTraining(training);
6. Caching – nhưng phải làm cho đúng (hướng dẫn thiết lập tối thiểu)
Cache (phát âm /kæʃ/, giống “cash”) về cơ bản chỉ là một Map chứa các mục dữ liệu. Ở đây không dùng đến những thứ phức tạp hơn như vô hiệu hóa theo thời gian hay tương tự.
Không phải lúc nào cũng cần đến thư viện khi một Map đơn giản là đủ! Nhiều lập trình viên không hiểu điều này và theo thói quen cứ dùng thư viện vì sợ. Khía cạnh quan trọng nhất khi cache là phải vô hiệu hóa (invalidate) đúng thời điểm.
Bẫy số 1: Memory Leak
Memory Leak rất dễ xảy ra khi cache. Chỉ cần quên override equals() và hashCode() cho đúng, là bạn đã có ngay một HashMap
bị rò rỉ bộ nhớ. Một cách luôn an toàn là tự tay tạo key cho Map. Tốt nhất là chỉ dùng ID của entity, nếu không thì Map đã bị rò rỉ ngay khi bạn chỉ
thay đổi một field. Điều đó cũng hợp lý, vì chỉ đổi tên của một user thì đó vẫn là user đó. Tham khảo: 3. Dùng equals() và hashCode()
cho đúng
Bẫy số 2: Thread Safety
Tốt nhất cache nên dùng ConcurrentHashMap, vốn cung cấp các thao tác atomic, thread-safe. Map chỉ được sửa đổi qua một lời gọi duy nhất – computeIfAbsent() – nhờ đó đạt được thread safety:
private ConcurrentMap<String, T> cache = new ConcurrentHashMap<>();
protected T getOrCompute(final String key, final Supplier<T> computeFunction) {
// thread safe, because `computeIfAbsent` is atomic, so no need for `synchronized` here.
// if you extend this to more than 1 map operation, you should synchronize it
return this.getCacheMap().computeIfAbsent(key, k -> computeFunction.get());
}
Nếu code dài hơn và có nhiều lời gọi, cả phương thức sẽ phải được synchronized.
7. Serialization – còn cần nữa không?
Câu hỏi của tôi: Có một quy tắc của Sonar tên là Fields in a “Serializable” class should either be transient or serializable (field trong class “Serializable” phải là transient hoặc serializable). Quy tắc này xuất hiện khi bạn thêm một field không serializable vào class. Quy tắc phát biểu:
Chẳng hạn, khi tải cao, hầu hết các framework ứng dụng J2EE sẽ flush đối tượng xuống đĩa, và một đối tượng được cho là Serializable nhưng có các thành phần dữ liệu không transient, không serializable có thể khiến chương trình bị sập, và mở cửa cho kẻ tấn công.
Với tôi, điều này nghe có vẻ lỗi thời. Trong Effective Java 3rd Edition có nói rằng Serialization không còn quan trọng khi phát triển phần mềm mới. Ngoài ra, Removing Serialization from Java Is a 'Long-Term Goal' at Oracle. Chính kiến trúc sư trưởng của Java, Mark Reinhold, đã tuyên bố
“Serialization là một sai lầm khủng khiếp vào năm 1997.”
Câu hỏi liệu Spring framework có thể flush đối tượng hay không đã từng được hỏi ở đây:
Vì vậy tôi đặt câu hỏi:
Có application server, framework hiện đại nào, hay chính bản triển khai J2EE (đặc biệt là các thành phần Java tương tác với CDI, EJB, JPA), flush đối tượng xuống đĩa khi tải cao không?
Hay phát biểu này đã lỗi thời?
Câu trả lời:
TL;DR Nếu bạn không xây dựng một ứng dụng web stateful có thể phân tán (distributable), không bật session persistence và chỉ dùng cache trong bộ nhớ, thì có thể bạn sẽ không cần Serializable ở bất cứ đâu.
Giải thích:
Để sao chép session (session replication), Java Serialization vẫn là bắt buộc theo Servlet Specification mới nhất: https://jakarta.ee/specifications/servlet/5.0/jakarta-servlet-spec-5.0.html#distributed-environments
Một trường hợp phổ biến là khi bạn có nhiều server phía sau một load balancer, bạn đăng nhập trên một server và nhận được cookie JSESSIONID. Nếu load balancer chuyển bạn sang một server
khác ở request tiếp theo, các thuộc tính đăng nhập và những thuộc tính khác trong phạm vi session sẽ được serialize và sao chép sang server mới, để JSESSIONID của bạn vẫn hợp lệ ở đó.
Một trường hợp khác cần Serialization cho các thuộc tính trong phạm vi session là khi application server quyết định chuyển session xuống đĩa. Ví dụ với Tomcat 8, có thể cấu hình để việc này kích hoạt khi có quá nhiều session không giữ hết được trong bộ nhớ, hoặc khi server khởi động lại mà bạn muốn giữ các session. Xem https://tomcat.apache.org/tomcat-9.0-doc/config/manager.html
Mọi application server tương thích Servlet 5.0 đều phải mặc định hỗ trợ đối tượng Serializable trong HTTP session cho những trường hợp như thế này.
Với một số server, có thể tinh chỉnh hoặc thay thế cơ chế này, ví dụ bằng serialization JSON của Jackson hoặc tương tự. Xem ví dụ https://stackoverflow.com/questions/46459707/how-to-use-jackson-instead-of-jdkserializationredisserializer-in-spring
Một trường hợp phổ biến khác là caching. Caching dùng để giữ đối tượng ở nơi truy cập nhanh nhất có thể, lý tưởng là trong bộ nhớ, nhưng cũng có thể tạm thời được chuyển xuống đĩa hoặc sang một cache server bên ngoài.
Có một API tiêu chuẩn cho việc này (JSR-107) đã chủ động chọn không yêu cầu đối tượng Serializable, nhưng hầu hết các bản triển khai vẫn mặc định dùng Java Serialization cho phần chuyển dữ liệu ra ngoài.
Bạn cũng có thể tinh chỉnh chúng để hỗ trợ các cơ chế serialization khác, ví dụ với ehcache: https://www.ehcache.org/documentation/3.8/serializers-copiers.html
8. Hibernate: Chiến lược fetch
Xét đến bài EAGER fetching is a code smell, được viết bởi một tay ra vẻ ta đây cực kỳ cuốn hút, đồng thời là contributor của Hibernate,
nên ưu tiên LAZY làm FetchType. Dù một số kiểu dữ liệu không có ý nghĩa gì nếu thiếu khóa ngoại, các kiểu này vẫn hoàn toàn có thể được
query riêng lẻ (getCount(), data table, v.v.).
Dưới đây là tất cả các kịch bản, sắp xếp theo mức độ thỏa đáng của giải pháp, bắt đầu từ cái kém thỏa đáng nhất:
1. Trường hợp tệ nhất: LazyInitializationException
Không cần nói nhiều. Ai trong chúng ta cũng từng nếm nỗi đau này.
2. Sửa tạm cho nhanh
public void updateChatRoom(ChatMessage message) {
// reload here or LazyInitializationException would be thrown
message = this.chatMessageService.reload(message); // not optimal: second roundtrip to the database - could easily take 100ms!
// we missed the chance to unproxy without a performance hit.
ChatRoom chatRoom = message.getChatRoom().getUpdatedAt(DateTimeUtil.getNow());
}
3. Unproxy ngay trong service
public ChatMessage getChatMessage(final Long id, boolean withRoom) {
// assume this is the same service + the same transaction.
// so we are in the same persistence context + everything will be performed together at the transaction flush.
// perfect: no additional roundtrip like in example 2.!
ChatMessage message = this.chatMessageService.getById(id);
if (withRoom) {
ChatRoom chatRoom = this.chatMessageService.unproxy(message.getChatRoom());
}
// we should not call message.setChatRoom() -> this getter should not even exist as Hibernate would
// map this relation for us! setting it again would perform an update on our foreign key!!
// so we have no option to set the ChatRoom back to the message and have to pass it everywhere in a Tuple or something similar
return ???;
}
4. Fetch ngay trong service (hãy làm thế này!)
public ChatMessage getChatMessage(final Long id, boolean withRoom) {
// all aforementioned problems solved
return this.executeSingleResultQuery((query, cb, root) -> {
if (withRoom) {
root.fetch(ChatRoomEntity_.partition).fetch(ChatPartitionEntity_.chatModule);
}
return cb.equal(root.get(ChatRoomEntity_.id), id);
});
}
9. Hibernate Search
Hibernate Search kết nối liền mạch với Hibernate JPA. Nó nhận biết khi entity thay đổi và có thể cấu hình dễ dàng:
@Entity
@Indexed(interceptor = SearchableChatMessageInterceptor.class) // hier können Einträge von der Indexierung ausgeschlossen werden
@FullTextFilterDef(name = "SearchableChatMessageFilter", impl = SearchableChatMessageFilter.class) // hier wird der Key festgelegt
public class ChatMessageEntity implements Searchable { // das Searchable dient als Marker-Interface
@Column
@Field // dieses Feld wird indexiert
private String message;
@ManyToOne(targetEntity = ChatRoomEntity.class, fetch = FetchType.LAZY, optional = false)
@IndexedEmbedded(includeEmbeddedObjectId = true, targetElement = ChatRoomEntity.class) // indexiert den Fremschlüssel mit.
// Über dieses Entity wird auch die Message gesucht,
// wie in @FullTextFilterDef -> SearchableChatMessageFilter definiert
private ChatRoom chatRoom;
// ...
}
Ở môi trường local, tính năng tìm kiếm được tắt:
// DefaultInstanceInitializer.java
// we don't need the global search everywhere, givin an option to override this setting here
protected boolean isBuildSearchIndexes() {
return !Constants.Mode.isDebug();
}
Tìm kiếm mờ (fuzzy) & hành vi tìm kiếm
if ((needle.split(" ").length > 1 || needle.contains("\"")) && !searchExact) {
/*
* Simple Query String. Provides basic functionality like:
*
* boolean (AND using “+”, OR using “|”, NOT using “-“)
* prefix (prefix*)
* phrase (“some phrase”)
* precedence (using parentheses)
* fuzzy (fuzy~2)
* near operator for phrase queries (“some phrase”~3)
*/
query = qb.simpleQueryString()
.onField(field)
.andFields(moreFields)
.withAndAsDefaultOperator()
.matching(needle)
.createQuery();
} // ...
Lucene
Debug Lucene bằng Luke
Phiên bản Hibernate Search của chúng tôi phải được debug bằng Luke phiên bản 6.6.0. Chỉ phiên bản này mới chạy được!
- Tải về: https://github.com/DmitryKey/luke/releases
- Có thể dùng nó để mở các index trong thư mục
target/. - Một cách hay để xem các entity có thực sự được index đúng hay không.
- Cũng có thể chạy tìm kiếm ngay từ trong Luke.
ElasticSearch
Hibernate Search cũng hỗ trợ ElasticSearch (khi đó nó được dùng thay cho Lucene và có tìm kiếm mờ tốt hơn / cảm giác giống Google hơn).