
本文探讨了jedis 4.2.3版本中`unifiedjedis.jsonget()`方法在处理json数据时,将字节数组值意外地表示为以`.0`结尾的`double`类型的问题。该现象源于jedis底层使用的gson或org.json库对数字类型进行向上转型。文章提供了三种有效的解决方案:通过类型转换后手动处理`linkedhashmap`、利用`path`参数直接获取指定类型的字节数组,以及执行原始命令进行手动解析,旨在帮助开发者正确获取和处理原始字节数据。
Jedis jsonGet方法中字节数组值类型转换的挑战
在使用Jedis客户端库(尤其是4.2.3版本)与Redis的JSON模块交互时,开发者可能会遇到一个特定问题:当通过UnifiedJedis.jsonGet()方法获取存储在JSON中的字节数组(例如,表示为数字列表)时,返回的结果会将这些字节值显示为以.0结尾的double类型,而非原始的字节值。例如,一个存储为[60, 63, 120]的字节序列,可能会被错误地解析为[60.0, 63.0, 120.0]。
这一现象的根本原因在于Jedis 4.2.3版本内部处理JSON数据时,依赖于如Gson或org.json:json等第三方库。这些库在默认情况下,会将所有数字类型(包括原本可以表示为byte或int的整数)统一向上转型(upcast)为double类型,从而导致原始的字节值被附加了.0的浮点表示。这对于需要精确字节数据或希望避免不必要类型转换的应用程序来说,是一个需要解决的问题。
接下来,我们将探讨几种解决此问题的方法,以确保能够正确获取和处理Redis中存储的原始字节数组。
解决方案
针对Jedis jsonGet方法返回double类型字节值的问题,以下提供三种不同的解决方案,每种方案都有其适用场景和优缺点。
1. 后处理LinkedHashMap结果
UnifiedJedis.jsonGet()方法在未指定返回类型时,通常会返回一个Object类型的结果,该结果在内部通常是一个LinkedHashMap的实例,用于表示JSON对象的结构。我们可以将此Object强制转换为LinkedHashMap,然后遍历其内容,识别并转换那些被错误表示为double的字节值。
实现思路:
- 调用jsonGet方法获取原始Object结果。
- 将Object强制转换为LinkedHashMap
。 - 递归遍历LinkedHashMap的键值对。
- 当遇到特定的键(例如示例中的ReturnValue)且其值为List
时,将List 中的每个double值转换为byte或int,并构建新的字节数组或列表。
示例代码片段(概念性):
import redis.clients.jedis.HostAndPort;
import redis.clients.jedis.UnifiedJedis;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
public class JedisJsonByteConverter {
public static void main(String[] args) {
HostAndPort config = new HostAndPort("localhost", 6379);
UnifiedJedis client = new UnifiedJedis(config);
String key = "StandaloneResponse:9b970b5f-32c2-4265-92cb-9af9d6707782";
Object rawObject = client.jsonGet(key);
if (rawObject instanceof LinkedHashMap) {
LinkedHashMap resultMap = (LinkedHashMap) rawObject;
// 假设我们知道ReturnValue在variables下
if (resultMap.containsKey("variables") && resultMap.get("variables") instanceof LinkedHashMap) {
LinkedHashMap variables = (LinkedHashMap) resultMap.get("variables");
if (variables.containsKey("ReturnValue") && variables.get("ReturnValue") instanceof List) {
List> rawReturnValue = (List>) variables.get("ReturnValue");
List byteList = new ArrayList<>();
for (Object item : rawReturnValue) {
if (item instanceof Double) {
byteList.add(((Double) item).byteValue());
} else {
// 处理非Double类型,或者抛出异常
System.err.println("Unexpected type in ReturnValue: " + item.getClass().getName());
}
}
System.out.println("Converted ReturnValue as bytes: " + byteList);
// 替换原始map中的ReturnValue
variables.put("ReturnValue", byteList);
}
}
System.out.println("Processed Object: " + resultMap);
} else {
System.out.println("Raw object is not a LinkedHashMap: " + rawObject);
}
client.close();
}
} 注意事项:
- 这种方法需要对JSON结构有清晰的了解,以便准确地定位需要转换的字段。
- 如果JSON结构复杂或不固定,手动遍历和转换会增加代码复杂性和维护成本。
- 它是在客户端进行后处理,不影响Redis存储的数据。
2. 利用Path参数直接获取指定类型的字节数组
Jedis的jsonGet方法支持通过Path对象来指定要获取的JSON路径,并且可以指定期望的返回类型。这是获取特定字节数组最直接且推荐的方法。通过明确告知Jedis我们期望ReturnValue字段返回Byte[]类型,Jedis将尝试进行相应的类型转换。
实现思路:
- 使用Path.of()方法构建指向目标字节数组字段的路径。
- 调用client.jsonGet(key, Byte[].class, path),指定返回类型为Byte[].class。
示例代码:
import redis.clients.jedis.HostAndPort;
import redis.clients.jedis.UnifiedJedis;
import redis.clients.jedis.json.Path;
public class JedisJsonGetByteArray {
public static void main(String[] args) {
HostAndPort config = new HostAndPort("localhost", 6379);
UnifiedJedis client = new UnifiedJedis(config);
String key = "StandaloneResponse:9b970b5f-32c2-4265-92cb-9af9d6707782";
// 假设ReturnValue在顶层对象的variables字段下
// 路径应根据实际JSON结构调整。例如,如果ReturnValue直接在根,路径为Path.ROOT
// 根据提供的问题输出,ReturnValue在 `variables` 字段下
Path returnValuePath = Path.of(".variables.ReturnValue");
try {
Byte[] returnValue = client.jsonGet(key, Byte[].class, returnValuePath);
if (returnValue != null) {
System.out.print("Retrieved ReturnValue as Byte array: [");
for (int i = 0; i < returnValue.length; i++) {
System.out.print(returnValue[i]);
if (i < returnValue.length - 1) {
System.out.print(", ");
}
}
System.out.println("]");
} else {
System.out.println("ReturnValue not found or could not be converted.");
}
} catch (Exception e) {
System.err.println("Error retrieving ReturnValue as Byte array: " + e.getMessage());
e.printStackTrace();
} finally {
client.close();
}
}
}注意事项:
- 确保Path与实际JSON结构完全匹配,否则可能返回null或抛出异常。
- 这种方法依赖于Jedis内部的类型转换机制,它能够更好地处理底层库的类型向上转型问题。
- 如果期望返回的是原始字节数组(byte[])而不是包装类型数组(Byte[]),可能需要进一步手动转换。
3. 执行原始命令并手动解析结果
如果前两种方法无法满足需求,或者需要更底层的控制,可以直接执行Redis的原始JSON GET命令,并手动解析返回的字符串结果。这种方法绕过了Jedis内部的JSON解析器,提供了最大的灵活性,但同时也增加了开发者的工作量。
实现思路:
- 构建一个CommandArguments对象,指定JSON GET命令和键。
- 使用client.executeCommand()方法执行命令。
- executeCommand返回的Object通常是原始的字符串表示,需要手动解析这个字符串来提取字节值。
示例代码:
import redis.clients.jedis.HostAndPort;
import redis.clients.jedis.UnifiedJedis;
import redis.clients.jedis.json.JsonProtocol;
import redis.clients.jedis.args.CommandArguments;
import redis.clients.jedis.util.SafeEncoder;
import java.util.ArrayList;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
public class JedisRawCommandGet {
public static void main(String[] args) {
HostAndPort config = new HostAndPort("localhost", 6379);
UnifiedJedis client = new UnifiedJedis(config);
String key = "StandaloneResponse:9b970b5f-32c2-4265-92cb-9af9d6707782";
try {
// 执行原始JSON GET命令,获取整个JSON字符串
// 注意:JsonProtocol.JsonCommand.GET 默认是获取整个JSON文档
// 如果需要特定路径,可能需要构造更复杂的命令,例如 JSON.GET .variables.ReturnValue
// Jedis 4.x 的 executeCommand 接受 CommandArguments
// 这里我们先获取整个JSON,然后手动解析
CommandArguments args = new CommandArguments(JsonProtocol.JsonCommand.GET).add(key);
Object rawResponse = client.executeCommand(args);
if (rawResponse instanceof byte[]) {
String jsonString = SafeEncoder.encode((byte[]) rawResponse);
System.out.println("Raw JSON string from executeCommand: " + jsonString);
// 假设我们知道ReturnValue是一个数字列表,并且格式是 [val1, val2, ...]
// 这里需要更复杂的JSON解析逻辑,可以使用Jackson, Gson等库
// 简单示例:从字符串中提取数字列表
Pattern pattern = Pattern.compile("ReturnValue=\\[([\\d.,\\s]+)\\]");
Matcher matcher = pattern.matcher(jsonString);
if (matcher.find()) {
String numbersStr = matcher.group(1);
String[] numberTokens = numbersStr.split(",\\s*");
List byteList = new ArrayList<>();
for (String token : numberTokens) {
try {
// 移除 .0 后缀,并转换为 byte
byteList.add((byte) Double.parseDouble(token));
} catch (NumberFormatException e) {
System.err.println("Could not parse number token: " + token);
}
}
System.out.println("Parsed ReturnValue as bytes: " + byteList);
} else {
System.out.println("ReturnValue not found in raw JSON string.");
}
} else {
System.out.println("Raw response is not a byte array: " + rawResponse);
}
} catch (Exception e) {
System.err.println("Error executing raw command: " + e.getMessage());
e.printStackTrace();
} finally {
client.close();
}
}
} 注意事项:
- executeCommand返回的Object类型可能因命令和Redis响应而异,通常是byte[]或List
。 - 手动解析JSON字符串是此方法最复杂的部分,推荐使用成熟的JSON解析库(如Jackson、Gson)来处理,而不是简单的正则表达式。
- 这种方法提供了最大的控制权,但也意味着开发者需要承担更多的错误处理和解析逻辑。
总结与建议
Jedis 4.2.3版本中jsonGet方法返回字节数组值时出现的.0后缀问题,主要是由于底层JSON解析库的默认类型向上转型行为。针对这个问题,我们提供了三种解决方案:
- 后处理LinkedHashMap: 适用于对JSON结构有一定了解,且需要全面处理返回对象的情况。优点是通用性强,缺点是代码可能较为复杂。
- 利用Path参数获取指定类型: 这是最推荐的解决方案,因为它直接、简洁,并且利用了Jedis API的特性。通过指定Byte[].class和精确的Path,可以直接获取到期望的字节数组,避免了不必要的中间转换和手动解析。
- 执行原始命令并手动解析: 适用于需要极致控制,或者前两种方法无法满足特殊需求的情况。优点是灵活性最高,缺点是开发工作量大,需要处理底层的协议细节和JSON解析。
在大多数场景下,使用Path参数直接指定返回类型是解决此问题的最佳实践。它既保持了代码的简洁性,又有效地解决了类型转换的困扰。开发者应根据实际需求和对JSON结构的了解程度,选择最合适的解决方案。










