iT邦幫忙

2024 iThome 鐵人賽

DAY 23
0
Software Development

從零開始構建能理解語義的 Linebot 架構系列 第 23

使用 Spring Boot 開發 Backend Bot Server: Serialize 及 Deserialize

  • 分享至 

  • xImage
  •  

概述

  • 本篇說明我們在資料庫存取,或使用API和外界溝通時,經常需要使用的兩個重要概念:序列化 (Serialize) 和反序列化 (Deserialize)。
  • 在本專案中,Kafka Consumer取得資料後的第一件事,就是將讀取到的資料進行反序列化 (Deserialize),並將其映射到相應的 Java Class,才能對資料進行後續的邏輯處理。

簡單定義Serialize / Deserialize

  • 序列化和反序列化是將物件(Object)狀態持久保存的重要過程,Java早在1.1以來就提供了Serializable 介面以實現物件的序列化機制。

  • 序列化是將記憶體中的實體物件(Object)轉換為位元流(byte stream)、JSON String,或其他格式,以便能夠儲存於永久媒體(如硬碟),或與外部系統進行資料交換的過程。

  • 這樣的處理,允許我們在未來可以透過反序列化(Deserialize)從中讀取該物件,並在記憶體中重建,從而恢復其先前的狀態。

Serialize

  • 一般我們在Java的開發過程中提到的序列化,多半是指將物件轉成byte stream。

  • 實現物件序列化非常簡單,只需讓類別(Class)實作 java.io.Serializable 介面,再透過OutputStream輸出即可。

  • 例如以下程式碼,可將Class Person的實例: person 轉成byte string輸出到外部檔案:person.serial

  • Class Person

public class Person implements Serializable {
    private static final long serialVersionUID = 1L; // 明確版本控制
    private String name;
    private int age;

    public Person(String name, int age) {
        this.name = name;
        this.age = age;
    }

  • 將byte string輸出到外部檔案: person.serial
...
Person person = new Person("John", 30);
// 將物件序列化並輸出到檔案
try (FileOutputStream fileOut = new FileOutputStream("person.serial");
    ObjectOutputStream objectOut = new ObjectOutputStream(fileOut)) {
        // 寫入物件到檔案中
        objectOut.writeObject(person);
...
  • 需要注意的是,Serializable介面本身並不定義任何方法,因為它是一種標記介面(Marker Interface),主要功能是告訴Java該物件是可以被序列化的。

transient

  • Java 提供了一個關鍵的保留字: transient,用於告訴Java哪些純值型態(primitive type)或類別變數不應該被序列化。

Deserialize

  • 反序列化則是將byte stream、字串或其他格式(如 JSON)轉換回 Java 物件的過程。
  • 例如我們要將person.serial這個檔案轉回Person型態的物過時:
...
try (FileInputStream fileIn = new FileInputStream("person.ser");
    ObjectInputStream objectIn = new ObjectInputStream(fileIn)) {
    // 讀取並轉換回 Person 物件
    Person person = (Person) objectIn.readObject();
    System.out.println("反序列化的物件: " + person);
    ...

總結來說,序列化和反序列化使得Java物件能夠在不同的儲存格式和傳輸環境中持久保存,並且能夠在需要時恢復原本的物件狀態,這在資料存取與系統間溝通時尤為重要。

使用Jackson做Deserialize

  • 在本專案中,由於Webhook傳輸的資料皆為JSON格式,因此我們需要將JSON String進行反序列化。
  • 為了方便處理外部資料並映射到程式中的Java Class,我們使用了老牌的Jackson套件作為反序列化工具,將JSON資料轉換為Java 物件。
  • 簡單說,我們可以透過Jackson的com.fasterxml.jackson.databind.ObjectMapper 來進行Deserialize,例如以下片段:
OpenAIResponse openAIResponse = objectMapper.readValue(jsonString, OpenAIResponse.class);
  • 其中,jsonString 是 API 回傳的JSON String,我們透過readValue(),指定要Maping的 Java POJO Class為OpenAIResponse,如下:
public class OpenAIResponse {
    public List<MessageContent> messageContent;
}
  • 這個Class 只包含了一個List,並且看到角括號<>中指定的型態是MessageContent,這代表OpenAI回應的內容中,包含了JSON陣列(JSON Array),而Array中每個元素的結構則是在MessageContent這個class中定義:

  • Class MessageContent

package com.cancerpio.nextpagelinebotserver.model;
...
@JsonTypeInfo(
        use = JsonTypeInfo.Id.NAME,
        include = JsonTypeInfo.As.PROPERTY,
        property = "about")
@JsonSubTypes({
        @Type(value = MessageContentTrainingLog.class, name = RecordType.TRAININGRECORDS),
        @Type(value = MessageContentDiet.class, name = RecordType.DIET)

public abstract class MessageContent {
}
  • 由於我們設定OpenAI會有兩種型態,一個是飲食紀錄(DIET),一個是訓練記錄(TRAININGRECORDS),上面程式碼分別針對這兩種型態設置了不同的POJO Class映射。
  • 簡單來說,兩種JSON String 會 Deserialize 的 Class如下:

將飲食紀錄的API Response Deserialize成MessageContentDiet.class

  • 飲食紀錄 API Response
 {
  "messageContent": [
    {
      "about": "Diet",
      "calories": 250,
      "protein": 35,
      "fat": 10
    }
  ]
}
  • MessageContentDiet.class
package com.cancerpio.nextpagelinebotserver.model;

public class MessageContentDiet extends MessageContent {
    public String about;
    public Integer calories;
    public Integer protein;
    public Integer fat;

}

將訓練紀錄的API Response Deserialize成 MessageContentTrainingLog.class

  • 訓練紀錄 API Response
 {
  "messageContent": [
    {
      "about": "TrainingRecords",
      "action": "Deadlift",
      "actionType": "Weight lifting",
      "weight": 140,
      "repetition": 1,
      "set": 5,
      "percentagOfRepetitionMaximum": null,
      "duration": 300,
      "feeling": "超累",
      "advice": "適當休息,注意保護腰部",
      "date": "today"
    }
  ]
}
  • MessageContentTrainingLog.class
package com.cancerpio.nextpagelinebotserver.model;

import org.springframework.data.mongodb.core.mapping.Document;

@Document("action_record")
public class MessageContentTrainingLog extends MessageContent {
    public String userId;
    public String action;
    public String actionType;
    public Integer weight;
    public Integer repetition;
    public Integer set;
    public Integer percentagOfRepetitionMaximum;
    public String duration;
    public String feeling;
    public String advice;
    public String date;

}

總結

以上就是序列化和反序列化的原理,在我們的程式中,有些地方是直接使用ObjectMapper進行反序列化,而在其他地方,則透過指定序列化方式和自行定義的Class,讓Spring Boot在背後自動執行這項任務。這部分的細節會在Kafka Consumer的實作見到。


上一篇
使用 Spring Boot 開發 Backend Bot Server: 程式結構及Spring Boot Starter
下一篇
使用 Spring Boot 開發 Backend Bot Server: Kafka Consumer 客製化配置與訊息接收
系列文
從零開始構建能理解語義的 Linebot 架構30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言