This is an automated email from the ASF dual-hosted git repository.
jianbin pushed a commit to branch 2.x
in repository https://gitbox.apache.org/repos/asf/incubator-seata.git
The following commit(s) were added to refs/heads/2.x by this push:
new f3aa087b7e optimize: add fastjson support for serialization and
deserialization of PostgreSQL array types (#7711)
f3aa087b7e is described below
commit f3aa087b7eae6e77f1da35d524d3fa13b48dfbe7
Author: maple <[email protected]>
AuthorDate: Wed Oct 22 09:43:06 2025 +0800
optimize: add fastjson support for serialization and deserialization of
PostgreSQL array types (#7711)
---
changes/en-us/2.x.md | 3 +
changes/zh-cn/2.x.md | 2 +
.../undo/parser/FastjsonUndoLogParser.java | 191 ++++++++++++++-
.../undo/parser/FastjsonUndoLogParserTest.java | 260 +++++++++++++++++++++
4 files changed, 454 insertions(+), 2 deletions(-)
diff --git a/changes/en-us/2.x.md b/changes/en-us/2.x.md
index 22c90f66bc..7705bc0bfa 100644
--- a/changes/en-us/2.x.md
+++ b/changes/en-us/2.x.md
@@ -65,6 +65,9 @@ Add changes here for all PR submitted to the 2.x branch.
- [[#7645](https://github.com/seata/seata/pull/7645)] simplifying the relevant
transport.* configuration types
- [[#7673](https://github.com/apache/incubator-seata/pull/7673)] bump
@babel/runtime from ^7.26.10 to ^7.27.0
- [[#7689](https://github.com/apache/incubator-seata/pull/7689)] optimize
source release
+- [[#7711](https://github.com/apache/incubator-seata/pull/7711)] add fastjson
support for serialization and deserialization of PostgreSQL array types
+
+
### security:
diff --git a/changes/zh-cn/2.x.md b/changes/zh-cn/2.x.md
index 8885012973..a34a0e3b19 100644
--- a/changes/zh-cn/2.x.md
+++ b/changes/zh-cn/2.x.md
@@ -64,6 +64,8 @@
- [[#7645](https://github.com/seata/seata/pull/7645)] 简化相关的 transport.* 配置项类型
- [[#7673](https://github.com/apache/incubator-seata/pull/7673)] 升级
@babel/runtime ^7.26.10 到 ^7.27.0
- [[#7689](https://github.com/apache/incubator-seata/pull/7689)] 优化 source
release
+- [[#7711](https://github.com/apache/incubator-seata/pull/7711)] 添加 fastjson 对
PostgreSQL 数组类型的序列化和反序列化的支持
+
### security:
diff --git
a/rm-datasource/src/main/java/org/apache/seata/rm/datasource/undo/parser/FastjsonUndoLogParser.java
b/rm-datasource/src/main/java/org/apache/seata/rm/datasource/undo/parser/FastjsonUndoLogParser.java
index 4c9f8aa3d8..624338be98 100644
---
a/rm-datasource/src/main/java/org/apache/seata/rm/datasource/undo/parser/FastjsonUndoLogParser.java
+++
b/rm-datasource/src/main/java/org/apache/seata/rm/datasource/undo/parser/FastjsonUndoLogParser.java
@@ -17,13 +17,41 @@
package org.apache.seata.rm.datasource.undo.parser;
import com.alibaba.fastjson.JSON;
+import com.alibaba.fastjson.JSONArray;
+import com.alibaba.fastjson.JSONObject;
+import com.alibaba.fastjson.parser.DefaultJSONParser;
+import com.alibaba.fastjson.parser.JSONToken;
+import com.alibaba.fastjson.parser.ParserConfig;
+import com.alibaba.fastjson.parser.deserializer.ObjectDeserializer;
+import com.alibaba.fastjson.serializer.JSONSerializer;
+import com.alibaba.fastjson.serializer.ObjectSerializer;
+import com.alibaba.fastjson.serializer.SerializeConfig;
+import com.alibaba.fastjson.serializer.SerializeWriter;
import com.alibaba.fastjson.serializer.SerializerFeature;
import com.alibaba.fastjson.serializer.SimplePropertyPreFilter;
import org.apache.seata.common.Constants;
import org.apache.seata.common.executor.Initialize;
import org.apache.seata.common.loader.LoadLevel;
+import org.apache.seata.rm.datasource.sql.serial.SerialArray;
import org.apache.seata.rm.datasource.undo.BranchUndoLog;
import org.apache.seata.rm.datasource.undo.UndoLogParser;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.IOException;
+import java.lang.reflect.Type;
+import java.math.BigDecimal;
+import java.sql.SQLException;
+
+import static java.sql.Types.BIGINT;
+import static java.sql.Types.DECIMAL;
+import static java.sql.Types.DOUBLE;
+import static java.sql.Types.FLOAT;
+import static java.sql.Types.INTEGER;
+import static java.sql.Types.NUMERIC;
+import static java.sql.Types.REAL;
+import static java.sql.Types.SMALLINT;
+import static java.sql.Types.TINYINT;
/**
* The type Json based undo log parser.
@@ -34,11 +62,19 @@ public class FastjsonUndoLogParser implements
UndoLogParser, Initialize {
public static final String NAME = "fastjson";
+ private static final Logger LOGGER =
LoggerFactory.getLogger(FastjsonUndoLogParser.class);
+
private final SimplePropertyPreFilter filter = new
SimplePropertyPreFilter();
+ final SerializeConfig serializeConfig = new SerializeConfig();
+ final ParserConfig parserConfig = new ParserConfig();
@Override
public void init() {
filter.getExcludes().add("tableMeta");
+
+ // Register SerialArray serializer and deserializer
+ serializeConfig.put(SerialArray.class, new SerialArraySerializer());
+ parserConfig.putDeserializer(SerialArray.class, new
SerialArrayDeserializer());
}
@Override
@@ -54,13 +90,164 @@ public class FastjsonUndoLogParser implements
UndoLogParser, Initialize {
@Override
public byte[] encode(BranchUndoLog branchUndoLog) {
String json = JSON.toJSONString(
- branchUndoLog, filter, SerializerFeature.WriteClassName,
SerializerFeature.WriteDateUseDateFormat);
+ branchUndoLog,
+ serializeConfig,
+ filter,
+ SerializerFeature.WriteClassName,
+ SerializerFeature.WriteDateUseDateFormat);
return json.getBytes(Constants.DEFAULT_CHARSET);
}
@Override
public BranchUndoLog decode(byte[] bytes) {
String text = new String(bytes, Constants.DEFAULT_CHARSET);
- return JSON.parseObject(text, BranchUndoLog.class);
+ return JSON.parseObject(text, BranchUndoLog.class, parserConfig);
+ }
+
+ /**
+ * Custom Fastjson serializer for SerialArray
+ * Manually construct JSON structure while letting serializer handle
elements properly
+ */
+ private static class SerialArraySerializer implements ObjectSerializer {
+ @Override
+ public void write(JSONSerializer serializer, Object object, Object
fieldName, Type fieldType, int features)
+ throws IOException {
+ if (object == null) {
+ serializer.writeNull();
+ return;
+ }
+
+ SerialArray serialArray = (SerialArray) object;
+ SerializeWriter out = serializer.getWriter();
+
+ out.write('{');
+
+ // Write the correct @type information to ensure the deserializer
is called
+ out.writeFieldName("@type");
+ out.writeString(serialArray.getClass().getName());
+ out.write(',');
+
+ // Write baseType
+ out.writeFieldName("baseType");
+ try {
+ out.writeInt(serialArray.getBaseType());
+ } catch (SQLException e) {
+ out.writeNull();
+ }
+ out.write(',');
+
+ // Write baseTypeName
+ out.writeFieldName("baseTypeName");
+ try {
+ String baseTypeName = serialArray.getBaseTypeName();
+ if (baseTypeName != null) {
+ out.writeString(baseTypeName);
+ } else {
+ out.writeNull();
+ }
+ } catch (SQLException e) {
+ out.writeNull();
+ }
+ out.write(',');
+
+ // Writing elements - using a serializer to ensure correct JSON
formatting and type handling
+ out.writeFieldName("elements");
+ serializer.write(serialArray.getElements());
+
+ out.write('}');
+ }
+ }
+
+ /**
+ * Custom Fastjson deserializer for SerialArray
+ * Enhanced with comprehensive type mapping based on SQL baseType
+ */
+ private static class SerialArrayDeserializer implements ObjectDeserializer
{
+ @Override
+ public SerialArray deserialze(DefaultJSONParser parser, Type type,
Object fieldName) {
+ try {
+ JSONObject json = parser.parseObject();
+ if (json == null) {
+ return null;
+ }
+
+ SerialArray serialArray = new SerialArray();
+
+ // Remove the @type field if it exists (Fastjson automatically
adds this)
+ json.remove("@type");
+
+ // Extract baseType for type conversion
+ int baseType = 0;
+ Object baseTypeObj = json.get("baseType");
+ if (baseTypeObj instanceof Number) {
+ baseType = ((Number) baseTypeObj).intValue();
+ serialArray.setBaseType(baseType);
+ }
+
+ Object baseTypeName = json.get("baseTypeName");
+ if (baseTypeName instanceof String) {
+ serialArray.setBaseTypeName((String) baseTypeName);
+ }
+
+ Object elementsObj = json.get("elements");
+ if (elementsObj instanceof JSONArray) {
+ JSONArray elementsArray = (JSONArray) elementsObj;
+ Object[] elements = new Object[elementsArray.size()];
+ for (int i = 0; i < elementsArray.size(); i++) {
+ Object element = elementsArray.get(i);
+ elements[i] = convertElementByBaseType(element,
baseType);
+ }
+ serialArray.setElements(elements);
+ }
+
+ return serialArray;
+ } catch (Exception e) {
+ LOGGER.error("deserialize SerialArray error: {}",
e.getMessage(), e);
+ return null;
+ }
+ }
+
+ /**
+ * Convert element to appropriate Java type based on SQL baseType
+ */
+ private Object convertElementByBaseType(Object element, int baseType) {
+ if (element == null) {
+ return null;
+ }
+
+ // If not a number, return as-is (String, Boolean, etc.)
+ if (!(element instanceof Number)) {
+ return element;
+ }
+
+ Number numElement = (Number) element;
+
+ // Convert based on SQL type constants
+ switch (baseType) {
+ case TINYINT:
+ return numElement.byteValue();
+ case SMALLINT:
+ return numElement.shortValue();
+ case INTEGER:
+ return numElement.intValue();
+ case BIGINT:
+ return numElement.longValue();
+ case REAL:
+ case FLOAT:
+ return numElement.floatValue();
+ case DOUBLE:
+ return numElement.doubleValue();
+ case DECIMAL:
+ case NUMERIC:
+ return new BigDecimal(numElement.toString());
+ default:
+ return element;
+ }
+ }
+
+ @Override
+ public int getFastMatchToken() {
+ return JSONToken.LBRACE;
+ }
}
}
diff --git
a/rm-datasource/src/test/java/org/apache/seata/rm/datasource/undo/parser/FastjsonUndoLogParserTest.java
b/rm-datasource/src/test/java/org/apache/seata/rm/datasource/undo/parser/FastjsonUndoLogParserTest.java
index d2a83b2d38..a02219132b 100644
---
a/rm-datasource/src/test/java/org/apache/seata/rm/datasource/undo/parser/FastjsonUndoLogParserTest.java
+++
b/rm-datasource/src/test/java/org/apache/seata/rm/datasource/undo/parser/FastjsonUndoLogParserTest.java
@@ -20,6 +20,8 @@ import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.serializer.SerializeConfig;
import com.alibaba.fastjson.serializer.ValueFilter;
import org.apache.seata.common.loader.EnhancedServiceLoader;
+import org.apache.seata.rm.datasource.DataCompareUtils;
+import org.apache.seata.rm.datasource.sql.serial.SerialArray;
import org.apache.seata.rm.datasource.sql.struct.Field;
import org.apache.seata.rm.datasource.sql.struct.KeyType;
import org.apache.seata.rm.datasource.sql.struct.Row;
@@ -30,19 +32,32 @@ import org.apache.seata.rm.datasource.undo.SQLUndoLog;
import org.apache.seata.rm.datasource.undo.UndoLogParser;
import org.apache.seata.sqlparser.SQLType;
import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
+import java.io.IOException;
import java.math.BigDecimal;
+import java.sql.Array;
+import java.sql.JDBCType;
+import java.sql.ResultSet;
+import java.sql.SQLException;
import java.sql.Timestamp;
import java.sql.Types;
import java.util.ArrayList;
import java.util.List;
+import java.util.Map;
public class FastjsonUndoLogParserTest extends BaseUndoLogParserTest {
FastjsonUndoLogParser parser =
(FastjsonUndoLogParser)
EnhancedServiceLoader.load(UndoLogParser.class, FastjsonUndoLogParser.NAME);
+ @BeforeEach
+ public void setUp() {
+ // Ensure init() is called to register SerialArray serializers
+ parser.init();
+ }
+
@Override
public UndoLogParser getParser() {
return parser;
@@ -121,6 +136,251 @@ public class FastjsonUndoLogParserTest extends
BaseUndoLogParserTest {
Assertions.assertTrue(value2 instanceof BigDecimal);
}
+ @Test
+ public void testDirectSerialArraySerialization() throws SQLException {
+ // Test direct SerialArray serialization without UndoLog wrapper
+ Array mockArray = new MockArray();
+ SerialArray serialArray = new SerialArray(mockArray);
+
+ // Test with our custom serializers
+ String json = JSON.toJSONString(serialArray, parser.serializeConfig);
+ System.out.println("Direct SerialArray JSON: " + json);
+
+ // Do not specify a specific type, let Fastjson automatically
determine it based on the @type information
+ SerialArray deserialized = (SerialArray) JSON.parse(json,
parser.parserConfig);
+
+ System.out.println("Original - baseType: " + serialArray.getBaseType()
+ ", baseTypeName: "
+ + serialArray.getBaseTypeName() + ", elements: "
+ + java.util.Arrays.toString(serialArray.getElements()));
+ System.out.println("Deserialized - baseType: " +
deserialized.getBaseType() + ", baseTypeName: "
+ + deserialized.getBaseTypeName() + ", elements: "
+ + java.util.Arrays.toString(deserialized.getElements()));
+
+ Assertions.assertEquals(serialArray.getBaseType(),
deserialized.getBaseType());
+ Assertions.assertEquals(serialArray.getBaseTypeName(),
deserialized.getBaseTypeName());
+ Assertions.assertArrayEquals(serialArray.getElements(),
deserialized.getElements());
+ }
+
+ @Test
+ public void testSerializeAndDeserializeSerialArray() throws IOException,
SQLException {
+ // create a mock Array object for testing SerialArray
+ Array mockArray = new MockArray();
+ SerialArray serialArray = new SerialArray(mockArray);
+
+ // test SerialArray with BIGINT array (PostgreSQL _int8 type)
+ Field field = new Field("dept_ids",
JDBCType.ARRAY.getVendorTypeNumber(), serialArray);
+
+ // Debug: Print JSON to see if our serializer is being used
+ byte[] bytes = getParser().encode(createTestUndoLog(field));
+ String jsonString = new String(bytes);
+ System.out.println("Serialized JSON: " + jsonString);
+
+ BranchUndoLog decodedLog = getParser().decode(bytes);
+ Field decodedField = getFieldFromLog(decodedLog);
+
+ // Debug information: print original and deserialized SerialArray
properties
+ SerialArray original = (SerialArray) field.getValue();
+ SerialArray deserialized = (SerialArray) decodedField.getValue();
+
+ System.out.println("Original - baseType: " + original.getBaseType() +
", baseTypeName: "
+ + original.getBaseTypeName() + ", elements: "
+ + java.util.Arrays.toString(original.getElements()));
+ System.out.println("Deserialized - baseType: " +
deserialized.getBaseType() + ", baseTypeName: "
+ + deserialized.getBaseTypeName() + ", elements: "
+ + java.util.Arrays.toString(deserialized.getElements()));
+
+ Assertions.assertTrue(
+ DataCompareUtils.isFieldEquals(field,
decodedField).getResult());
+
+ // verify the SerialArray properties are correctly
serialized/deserialized
+ SerialArray deserializedArray = (SerialArray) decodedField.getValue();
+ Assertions.assertEquals(serialArray.getBaseType(),
deserializedArray.getBaseType());
+ Assertions.assertEquals(serialArray.getBaseTypeName(),
deserializedArray.getBaseTypeName());
+ Assertions.assertArrayEquals(serialArray.getElements(),
deserializedArray.getElements());
+ }
+
+ @Test
+ public void testSerializeAndDeserializeSerialArrayWithNulls() throws
IOException, SQLException {
+ // create SerialArray with null elements
+ Array mockArrayWithNulls = new MockArrayWithNulls();
+ SerialArray serialArray = new SerialArray(mockArrayWithNulls);
+
+ Field field = new Field("nullable_array",
JDBCType.ARRAY.getVendorTypeNumber(), serialArray);
+ byte[] bytes = getParser().encode(createTestUndoLog(field));
+ BranchUndoLog decodedLog = getParser().decode(bytes);
+ Field decodedField = getFieldFromLog(decodedLog);
+
+ Assertions.assertTrue(
+ DataCompareUtils.isFieldEquals(field,
decodedField).getResult());
+
+ // verify null elements are handled correctly
+ SerialArray deserializedArray = (SerialArray) decodedField.getValue();
+ Object[] elements = deserializedArray.getElements();
+ Assertions.assertEquals(3, elements.length);
+ Assertions.assertEquals(1L, elements[0]);
+ Assertions.assertNull(elements[1]);
+ Assertions.assertEquals(3L, elements[2]);
+ }
+
+ private BranchUndoLog createTestUndoLog(Field field) {
+ TableRecords afterImage = new TableRecords();
+ afterImage.setTableName("test_table");
+ List<Row> rows = new ArrayList<>();
+ Row row = new Row();
+ row.add(field);
+ rows.add(row);
+ afterImage.setRows(rows);
+
+ SQLUndoLog sqlUndoLog = new SQLUndoLog();
+ sqlUndoLog.setSqlType(SQLType.INSERT);
+ sqlUndoLog.setTableName("test_table");
+ sqlUndoLog.setBeforeImage(new TableRecords());
+ sqlUndoLog.setAfterImage(afterImage);
+
+ BranchUndoLog branchUndoLog = new BranchUndoLog();
+ branchUndoLog.setBranchId(123456L);
+ branchUndoLog.setXid("test_xid");
+ List<SQLUndoLog> logList = new ArrayList<>();
+ logList.add(sqlUndoLog);
+ branchUndoLog.setSqlUndoLogs(logList);
+
+ return branchUndoLog;
+ }
+
+ private Field getFieldFromLog(BranchUndoLog log) {
+ return log.getSqlUndoLogs()
+ .get(0)
+ .getAfterImage()
+ .getRows()
+ .get(0)
+ .getFields()
+ .get(0);
+ }
+
+ /**
+ * Mock Array class for testing SerialArray serialization
+ */
+ private static class MockArray implements Array {
+ private final Object[] elements = {1L, 2L, 3L, 4L, 5L};
+
+ @Override
+ public String getBaseTypeName() throws SQLException {
+ return "int8";
+ }
+
+ @Override
+ public int getBaseType() throws SQLException {
+ return Types.BIGINT;
+ }
+
+ @Override
+ public Object getArray() throws SQLException {
+ return elements;
+ }
+
+ @Override
+ public Object getArray(Map<String, Class<?>> map) throws SQLException {
+ return elements;
+ }
+
+ @Override
+ public Object getArray(long index, int count) throws SQLException {
+ return elements;
+ }
+
+ @Override
+ public Object getArray(long index, int count, Map<String, Class<?>>
map) throws SQLException {
+ return elements;
+ }
+
+ @Override
+ public ResultSet getResultSet() throws SQLException {
+ return null;
+ }
+
+ @Override
+ public ResultSet getResultSet(Map<String, Class<?>> map) throws
SQLException {
+ return null;
+ }
+
+ @Override
+ public ResultSet getResultSet(long index, int count) throws
SQLException {
+ return null;
+ }
+
+ @Override
+ public ResultSet getResultSet(long index, int count, Map<String,
Class<?>> map) throws SQLException {
+ return null;
+ }
+
+ @Override
+ public void free() throws SQLException {
+ // do nothing
+ }
+ }
+
+ /**
+ * Mock Array class with null elements for testing edge cases
+ */
+ private static class MockArrayWithNulls implements Array {
+ private final Object[] elements = {1L, null, 3L};
+
+ @Override
+ public String getBaseTypeName() throws SQLException {
+ return "int8";
+ }
+
+ @Override
+ public int getBaseType() throws SQLException {
+ return Types.BIGINT;
+ }
+
+ @Override
+ public Object getArray() throws SQLException {
+ return elements;
+ }
+
+ @Override
+ public Object getArray(Map<String, Class<?>> map) throws SQLException {
+ return elements;
+ }
+
+ @Override
+ public Object getArray(long index, int count) throws SQLException {
+ return elements;
+ }
+
+ @Override
+ public Object getArray(long index, int count, Map<String, Class<?>>
map) throws SQLException {
+ return elements;
+ }
+
+ @Override
+ public ResultSet getResultSet() throws SQLException {
+ return null;
+ }
+
+ @Override
+ public ResultSet getResultSet(Map<String, Class<?>> map) throws
SQLException {
+ return null;
+ }
+
+ @Override
+ public ResultSet getResultSet(long index, int count) throws
SQLException {
+ return null;
+ }
+
+ @Override
+ public ResultSet getResultSet(long index, int count, Map<String,
Class<?>> map) throws SQLException {
+ return null;
+ }
+
+ @Override
+ public void free() throws SQLException {
+ // do nothing
+ }
+ }
+
private class TimestampSerializer implements ValueFilter {
@Override
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]