昨天把 Redis Pub/Sub 的功能微調後,測試已經可以在分布式的環境下透過 WebSocket Session 收到通知了,基本上我們要完成的業務邏輯差不多就這樣,接下來第三階段會來把這些內容優化、測試跟部署,算是填一下前面的坑,今天先把前幾天的功能做一點改善,把使用頻繁的 ObjectMapper 用工具類包裝起來,作為一個單例以降低系統的開銷。
今天要來把在我們系統中使用頻率很高的 ObjectMapper 單例化,在諸如 Kafka Consumer、Redis Listener 等等類別都需要 ObjectMapper 來序列化或反序列化 Java 物件,這個實例的使用率很高,每次在一個類別都要重複創建的話,對效能、JVM 記憶體空間會有一定程度的開銷。
特別是 ObjectMapper 這東西創建的成本還頗高,需要掃描和緩存類別的反射信息,又要初始化各種序列化跟反序列化器、建立一些配置等等。既然在各個類別的使用邏輯都相同的話,就統一在第一次類加載時,就創建 ObjectMapper 就好。
單例模式確保一個類別在整個應用程式中就只有一個實例,並提供全域的訪問點。簡單實作如下:
public class JsonUtils {
private static final ObjectMapper OM;
static {
OM = new ObjectMapper();
OM.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
OM.configure(SerializationFeature.FAIL_ON_EMPTY_BEANS, false);
OM.registerModule(new JavaTimeModule());
}
public static String toJson(Object object) {
try {
return OM.writeValueAsString(object);
} catch (JsonProcessingException e) {
throw new BaseException(StatusCode.UNKNOW_ERR, "Json parse failed: " + e.getMessage());
}
}
public static <T> T fromJson(String json, Class<T> clazz) {
try {
return OM.readValue(json, clazz);
} catch (JsonProcessingException e) {
throw new BaseException(StatusCode.UNKNOW_ERR, "Json parse failed " + e.getMessage());
}
}
}
如此一來我們就可以在系統中的任何一個地方,在不用創建實例的情況下調用這個工具類別:
@KafkaListener(topics = Topic.NOTIFICATION)
public void processMailNotification(String content) {
log.info("Receive topic [{}] and message=[{}]", Topic.NOTIFICATION, content);
try {
// 直接調用包裝好的方法,將字串轉換成 Json 物件
var notification = JsonUtils.fromJson(content, NotificationTO.class);
notificationService.pushToUsers(notification);
} catch (Exception e) {
log.warn("Failed to process mail task", e);
}
}
趁著這個機會來複習一下 Spring 的生命週期跟 Java 的基本常識,寫到這可能會出現一個疑問:為何 JsonUtils 不用注入呢?
這是因為在這類別裡用 static 來初始化方法、屬性跟配置,它的生命週期由 JVM 管理,不需要經過 Spring 容器,當某個類別第一次在裡面調用 JsonUtils 方法時,JVM 才會第一次進行類加載(Lazy loading),把這些東東初始化出來,如果沒有任何地方用到 JsonUtils,它的 static 初始化塊永遠不會執行,所以 JsonUtils 上不需要添加 @Component。
但像是那些由 Spring 容器管理的 Bean 也是一種單例的類別,在容器啟動時,Spring 會去掃描 class 上有任何 @Component、@Service 等註解的類別,將他們放到容器內,等到有其他地方要用時就直接注入即可,不用再創建新的實例。
今天比較開心的是可以藉由一些簡單的實作來複習、釐清一些以前不太熟悉的概念,包含 Spring 生命週期、JVM 實例的生命週期等等,這也是我覺得自己在鐵人賽一個蠻大的收穫,繼續努力吧。