From 8d716e37ddc1136c6fd196f0e6bab6305cb81809 Mon Sep 17 00:00:00 2001 From: Thomas Segismont Date: Thu, 16 Jul 2026 17:50:03 +0200 Subject: [PATCH] Eliminate String intermediaries in JSON codecs Encode and decode JSON directly between ByteBuf and Jackson, avoiding unnecessary charset conversions through String objects. Note that the PG size estimator now uses a fixed 256-byte estimate for non-String JSON values (JsonObject, JsonArray, etc.) since computing the exact size would require serializing the JSON twice. Resizing shouldn't cost too much with the adaptive ByteBuf allocator. Assisted-by: Claude Opus 4.6 Signed-off-by: Thomas Segismont --- .../mssqlclient/impl/codec/DataType.java | 37 ++++++++---- .../impl/datatype/DataTypeCodec.java | 59 ++++++++----------- .../vertx/pgclient/impl/codec/DataType.java | 8 +-- .../pgclient/impl/codec/DataTypeCodec.java | 49 +++++---------- .../impl/codec/DataTypeEstimator.java | 11 ++-- .../pgclient/impl/codec/ParamExtractor.java | 14 ----- 6 files changed, 72 insertions(+), 106 deletions(-) diff --git a/vertx-mssql-client/src/main/java/io/vertx/mssqlclient/impl/codec/DataType.java b/vertx-mssql-client/src/main/java/io/vertx/mssqlclient/impl/codec/DataType.java index c4bc90a0e..19fd76fb4 100644 --- a/vertx-mssql-client/src/main/java/io/vertx/mssqlclient/impl/codec/DataType.java +++ b/vertx-mssql-client/src/main/java/io/vertx/mssqlclient/impl/codec/DataType.java @@ -11,9 +11,8 @@ package io.vertx.mssqlclient.impl.codec; -import io.netty.buffer.ByteBuf; -import io.netty.buffer.ByteBufAllocator; -import io.netty.buffer.Unpooled; +import io.netty.buffer.*; +import io.netty.handler.codec.EncoderException; import io.netty.util.collection.IntObjectHashMap; import io.netty.util.collection.IntObjectMap; import io.vertx.core.buffer.Buffer; @@ -22,6 +21,7 @@ import io.vertx.core.json.JsonArray; import io.vertx.core.json.JsonObject; +import java.io.*; import java.math.BigDecimal; import java.math.BigInteger; import java.math.RoundingMode; @@ -984,8 +984,9 @@ public Object decodeValue(ByteBuf byteBuf, TypeInfo typeInfo) { if (isPLPNull(payloadLength)) { return null; } - String jsonString = readPLP(byteBuf).toString(StandardCharsets.UTF_16LE); - return Json.decodeValue(jsonString); + ByteBuf heapBuf = readPLP(byteBuf); + Reader reader = new InputStreamReader(new ByteBufInputStream(heapBuf), StandardCharsets.UTF_16LE); + return Json.decodeValue(reader); } @Override @@ -995,14 +996,24 @@ public String paramDefinition(Object value) { @Override public void encodeParam(ByteBuf byteBuf, String name, boolean out, Object value) { - writeParamDescription(byteBuf, name, out, NVARCHAR.id); - String val = value.toString(); - byteBuf.writeShortLE(0xFFFF); - writeCollation(byteBuf); - byteBuf.writeLongLE(val.length() * 2L); - byteBuf.writeIntLE(val.length() * 2); - byteBuf.writeCharSequence(val, StandardCharsets.UTF_16LE); - byteBuf.writeIntLE(0); + ByteBuf tmp = byteBuf.alloc().buffer(); + try { + Writer writer = new OutputStreamWriter(new ByteBufOutputStream(tmp), StandardCharsets.UTF_16LE); + Json.encodeTo(value, writer); + writer.flush(); + int byteLen = tmp.readableBytes(); + writeParamDescription(byteBuf, name, out, NVARCHAR.id); + byteBuf.writeShortLE(0xFFFF); + writeCollation(byteBuf); + byteBuf.writeLongLE(byteLen); + byteBuf.writeIntLE(byteLen); + byteBuf.writeBytes(tmp); + byteBuf.writeIntLE(0); + } catch (IOException e) { + throw new EncoderException(e); + } finally { + tmp.release(); + } } }, diff --git a/vertx-mysql-client/src/main/java/io/vertx/mysqlclient/impl/datatype/DataTypeCodec.java b/vertx-mysql-client/src/main/java/io/vertx/mysqlclient/impl/datatype/DataTypeCodec.java index 19d73fb86..d153e4cd5 100644 --- a/vertx-mysql-client/src/main/java/io/vertx/mysqlclient/impl/datatype/DataTypeCodec.java +++ b/vertx-mysql-client/src/main/java/io/vertx/mysqlclient/impl/datatype/DataTypeCodec.java @@ -12,23 +12,24 @@ package io.vertx.mysqlclient.impl.datatype; import io.netty.buffer.ByteBuf; +import io.netty.buffer.ByteBufInputStream; +import io.netty.buffer.ByteBufOutputStream; import io.netty.buffer.Unpooled; import io.netty.handler.codec.DecoderException; +import io.vertx.core.buffer.Buffer; import io.vertx.core.internal.buffer.BufferInternal; import io.vertx.core.json.Json; import io.vertx.core.json.JsonArray; import io.vertx.core.json.JsonObject; -import io.vertx.mysqlclient.data.spatial.*; +import io.vertx.mysqlclient.data.spatial.Geometry; import io.vertx.mysqlclient.impl.MySQLCollation; import io.vertx.mysqlclient.impl.util.BufferUtils; -import io.vertx.core.buffer.Buffer; import io.vertx.sqlclient.Tuple; import io.vertx.sqlclient.data.Numeric; import io.vertx.sqlclient.impl.Utils; import java.math.BigInteger; import java.nio.charset.Charset; -import java.nio.charset.StandardCharsets; import java.time.Duration; import java.time.LocalDate; import java.time.LocalDateTime; @@ -95,7 +96,7 @@ public static Object decodeText(DataType dataType, int collationId, ByteBuf buff case TIMESTAMP: return textDecodeDateTime(collationId, buffer, index, length); case JSON: - return textDecodeJson(collationId, buffer, index, length); + return textDecodeJson(buffer, index, length); case GEOMETRY: return textDecodeGeometry(buffer, index, length); case BINARY: @@ -178,7 +179,7 @@ public static void encodeBinary(DataType dataType, Object value, Charset charset case VARSTRING: default: if (value instanceof JsonObject || value instanceof JsonArray) { - binaryEncodeJson(value, buffer, charset); + binaryEncodeJson(value, buffer); return; } else if (value == Tuple.JSON_NULL) { // we have to make JSON literal null send as a STRING data type @@ -231,7 +232,7 @@ public static Object decodeBinary(DataType dataType, int collationId, ByteBuf bu case TIMESTAMP: return binaryDecodeDatetime(buffer); case JSON: - return binaryDecodeJson(collationId, buffer); + return binaryDecodeJson(buffer); case GEOMETRY: return binaryDecodeGeometry(buffer); case BINARY: @@ -466,8 +467,15 @@ private static void binaryEncodeDatetime(LocalDateTime value, ByteBuf buffer) { } } - private static void binaryEncodeJson(Object value, ByteBuf buffer, Charset charset) { - BufferUtils.writeLengthEncodedString(buffer, Json.encode(value), charset); + private static void binaryEncodeJson(Object value, ByteBuf buffer) { + ByteBuf tmp = buffer.alloc().buffer(); + try { + Json.encodeTo(value, new ByteBufOutputStream(tmp)); + BufferUtils.writeLengthEncodedInteger(buffer, tmp.readableBytes()); + buffer.writeBytes(tmp); + } finally { + tmp.release(); + } } private static Byte binaryDecodeInt8(ByteBuf buffer) { @@ -629,9 +637,9 @@ private static Duration binaryDecodeTime(ByteBuf buffer) { } } - private static Object binaryDecodeJson(int collationId, ByteBuf buffer) { + private static Object binaryDecodeJson(ByteBuf buffer) { int length = (int) BufferUtils.readLengthEncodedInteger(buffer); - Object result = textDecodeJson(collationId, buffer, buffer.readerIndex(), length); + Object result = textDecodeJson(buffer, buffer.readerIndex(), length); buffer.skipBytes(length); return result; } @@ -743,33 +751,12 @@ private static LocalDateTime textDecodeDateTime(int collationId, ByteBuf buffer, return LocalDateTime.parse(cs, DATETIME_FORMAT); } - private static Object textDecodeJson(int collationId, ByteBuf buffer, int index, int length) { - Charset charset = StandardCharsets.UTF_8; // MySQL JSON data type will only be UTF-8 string - // Try to do without the intermediary String (?) - CharSequence cs = buffer.getCharSequence(index, length, charset); - Object value = null; - String s = cs.toString(); - int pos = 0; - while (pos < s.length() && Character.isWhitespace(s.charAt(pos))) { - pos++; - } - if (pos == s.length()) { - return null; - } else if (s.charAt(pos) == '{') { - value = new JsonObject(s); - } else if (s.charAt(pos) == '[') { - value = new JsonArray(s); - } else { - Object o = Json.decodeValue(s); - if (o == null) { - return Tuple.JSON_NULL; - } - if (o instanceof Number || o instanceof Boolean || o instanceof String) { - return o; - } - return null; + private static Object textDecodeJson(ByteBuf buffer, int index, int length) { + Object o = Json.decodeValue(new ByteBufInputStream(buffer.slice(index, length))); + if (o == null) { + return Tuple.JSON_NULL; } - return value; + return o; } private static Long decodeBit(ByteBuf buffer, int index, int length) { diff --git a/vertx-pg-client/src/main/java/io/vertx/pgclient/impl/codec/DataType.java b/vertx-pg-client/src/main/java/io/vertx/pgclient/impl/codec/DataType.java index 220a0415a..f999cde28 100644 --- a/vertx-pg-client/src/main/java/io/vertx/pgclient/impl/codec/DataType.java +++ b/vertx-pg-client/src/main/java/io/vertx/pgclient/impl/codec/DataType.java @@ -96,10 +96,10 @@ public enum DataType { MACADDR8(774, true, Object[].class, JDBCType.OTHER, DataTypeEstimator.UNSUPPORTED), UUID(2950, true, UUID.class, JDBCType.OTHER, null, Tuple::getUUID, DataTypeEstimator.UUID), UUID_ARRAY(2951, true, UUID[].class, JDBCType.OTHER, null, Tuple::getArrayOfUUIDs, DataTypeEstimator.UUID), - JSON(114, true, Object.class, JDBCType.OTHER, ParamExtractor::prepareJson, Tuple::getJson, DataTypeEstimator.JSON), - JSON_ARRAY(199, true, Object[].class, JDBCType.OTHER, ParamExtractor::prepareJson, Tuple::getArrayOfJsons, DataTypeEstimator.JSON), - JSONB(3802, true, Object.class, JDBCType.OTHER, ParamExtractor::prepareJson, Tuple::getJson, DataTypeEstimator.JSONB), - JSONB_ARRAY(3807, true, Object[].class, JDBCType.OTHER, ParamExtractor::prepareJson, Tuple::getArrayOfJsons, DataTypeEstimator.JSONB), + JSON(114, true, Object.class, JDBCType.OTHER, null, Tuple::getJson, DataTypeEstimator.JSON), + JSON_ARRAY(199, true, Object[].class, JDBCType.OTHER, null, Tuple::getArrayOfJsons, DataTypeEstimator.JSON), + JSONB(3802, true, Object.class, JDBCType.OTHER, null, Tuple::getJson, DataTypeEstimator.JSONB), + JSONB_ARRAY(3807, true, Object[].class, JDBCType.OTHER, null, Tuple::getArrayOfJsons, DataTypeEstimator.JSONB), XML(142, true, Object.class, JDBCType.OTHER, DataTypeEstimator.UNSUPPORTED), XML_ARRAY(143, true, Object[].class, JDBCType.OTHER, DataTypeEstimator.UNSUPPORTED), POINT(600, true, Point.class, JDBCType.OTHER, DataTypeEstimator.POINT), diff --git a/vertx-pg-client/src/main/java/io/vertx/pgclient/impl/codec/DataTypeCodec.java b/vertx-pg-client/src/main/java/io/vertx/pgclient/impl/codec/DataTypeCodec.java index 02998f1f3..6f4b2f6af 100644 --- a/vertx-pg-client/src/main/java/io/vertx/pgclient/impl/codec/DataTypeCodec.java +++ b/vertx-pg-client/src/main/java/io/vertx/pgclient/impl/codec/DataTypeCodec.java @@ -17,17 +17,13 @@ package io.vertx.pgclient.impl.codec; -import io.netty.buffer.ByteBuf; -import io.netty.buffer.ByteBufAllocator; -import io.netty.buffer.Unpooled; +import io.netty.buffer.*; import io.netty.handler.codec.DecoderException; import io.vertx.core.buffer.Buffer; import io.vertx.core.internal.buffer.BufferInternal; import io.vertx.core.internal.logging.Logger; import io.vertx.core.internal.logging.LoggerFactory; import io.vertx.core.json.Json; -import io.vertx.core.json.JsonArray; -import io.vertx.core.json.JsonObject; import io.vertx.pgclient.data.*; import io.vertx.pgclient.impl.util.UTF8StringEndDetector; import io.vertx.sqlclient.Tuple; @@ -279,13 +275,13 @@ public static void encodeBinary(DataType id, Object value, ByteBuf buff) { binaryEncodeArray((UUID[]) value, DataType.UUID, buff); break; case JSON: - binaryEncodeJSON((CharSequence) value, buff); + binaryEncodeJSON(value, buff); break; case JSON_ARRAY: binaryEncodeArray((Object[]) value, DataType.JSON, buff); break; case JSONB: - binaryEncodeJSONB((CharSequence) value, buff); + binaryEncodeJSONB(value, buff); break; case JSONB_ARRAY: binaryEncodeArray((Object[]) value, DataType.JSONB, buff); @@ -1378,37 +1374,20 @@ private static Object binaryDecodeJSON(int index, int len, ByteBuf buff) { return textDecodeJSONB(index, len, buff); } - private static void binaryEncodeJSON(CharSequence value, ByteBuf buff) { - buff.writeCharSequence(value, StandardCharsets.UTF_8); + private static void binaryEncodeJSON(Object value, ByteBuf buff) { + if (value == Tuple.JSON_NULL) { + buff.writeCharSequence("null", StandardCharsets.UTF_8); + } else { + Json.encodeTo(value, new ByteBufOutputStream(buff)); + } } private static Object textDecodeJSONB(int index, int len, ByteBuf buff) { - - // Try to do without the intermediary String (?) - CharSequence cs = buff.getCharSequence(index, len, StandardCharsets.UTF_8); - Object value = null; - String s = cs.toString(); - int pos = 0; - while (pos < s.length() && Character.isWhitespace(s.charAt(pos))) { - pos++; - } - if (pos == s.length()) { - return null; - } else if (s.charAt(pos) == '{') { - value = new JsonObject(s); - } else if (s.charAt(pos) == '[') { - value = new JsonArray(s); - } else { - Object o = Json.decodeValue(s); - if (o == null) { - return Tuple.JSON_NULL; - } - if (o instanceof Number || o instanceof Boolean || o instanceof String) { - return o; - } - return null; + Object o = Json.decodeValue(new ByteBufInputStream(buff.slice(index, len))); + if (o == null) { + return Tuple.JSON_NULL; } - return value; + return o; } private static Object binaryDecodeJSONB(int index, int len, ByteBuf buff) { @@ -1416,7 +1395,7 @@ private static Object binaryDecodeJSONB(int index, int len, ByteBuf buff) { return textDecodeJSONB(index + 1, len - 1, buff); } - private static void binaryEncodeJSONB(CharSequence value, ByteBuf buff) { + private static void binaryEncodeJSONB(Object value, ByteBuf buff) { buff.writeByte(1); // version binaryEncodeJSON(value, buff); } diff --git a/vertx-pg-client/src/main/java/io/vertx/pgclient/impl/codec/DataTypeEstimator.java b/vertx-pg-client/src/main/java/io/vertx/pgclient/impl/codec/DataTypeEstimator.java index 61e5a7ff9..ba4520d8e 100644 --- a/vertx-pg-client/src/main/java/io/vertx/pgclient/impl/codec/DataTypeEstimator.java +++ b/vertx-pg-client/src/main/java/io/vertx/pgclient/impl/codec/DataTypeEstimator.java @@ -82,8 +82,11 @@ private static int estimateUnknown(String value) { return estimateUTF8(value); } - private static int estimateJSONB(String value) { - return 1 + estimateUTF8(value); + private static int estimateJSONB(Object value) { + if (value instanceof String) { + return 1 + estimateUTF8((String) value); + } + return 256; } private static int estimateNumeric(String value) { @@ -144,7 +147,7 @@ static int estimate(int estimator, Object o) { case DataTypeEstimator.INET: return estimateInetOrCidr((Inet) o); case DataTypeEstimator.JSONB: - return estimateJSONB((String) o); + return estimateJSONB(o); case DataTypeEstimator.UNKNOWN: return estimateUnknown((String) o); case DataTypeEstimator.NUMERIC: @@ -152,7 +155,7 @@ static int estimate(int estimator, Object o) { case DataTypeEstimator.NUMERIC_ARRAY: return estimateNumericArray((Object[]) o); case DataTypeEstimator.UTF8: - return estimateUTF8((String) o); + return o instanceof String ? estimateUTF8((String) o) : 256; case DataTypeEstimator.BUFFER: return estimateBuffer((Buffer) o); case DataTypeEstimator.POLYGON: diff --git a/vertx-pg-client/src/main/java/io/vertx/pgclient/impl/codec/ParamExtractor.java b/vertx-pg-client/src/main/java/io/vertx/pgclient/impl/codec/ParamExtractor.java index 50ed2a640..906d2d42b 100644 --- a/vertx-pg-client/src/main/java/io/vertx/pgclient/impl/codec/ParamExtractor.java +++ b/vertx-pg-client/src/main/java/io/vertx/pgclient/impl/codec/ParamExtractor.java @@ -10,8 +10,6 @@ */ package io.vertx.pgclient.impl.codec; -import io.vertx.core.json.Json; -import io.vertx.sqlclient.Tuple; import io.vertx.sqlclient.internal.TupleBase; import java.util.Arrays; @@ -31,22 +29,10 @@ static String extractUnknownType(TupleBase tuple, int pos) { T get(TupleBase tuple, int idx); - private static String encodeJsonToString(Object value) { - if (value == Tuple.JSON_NULL) { - return "null"; - } else { - return Json.encode(value); - } - } - static Object prepareUnknown(Object value) { return String.valueOf(value); } - static Object prepareJson(Object value) { - return encodeJsonToString(value); - } - static Object prepareNumeric(Object value) { assert value instanceof Number; return value.toString();