持续创作,加速成长!这是我参与「掘金日新计划 · 10 月更文挑战」的第26天,点击查看活动详情
前言
在昨天的文章中,我提到了之前发现的Seata TCC模式的一个BUG,并给社区提了一个Issue。根据昨天文章的源码分析,我们发现了问题就出现在Seata反序列化的时候不知道目标参数的数据类型,导致序列化前的参数类型和反序列化后的数据类型不一致。针对上述问题,说说我自己的解决思路。
解决思路
问题出现的原因就是反序列化的时候不知道目标数据类型,如果我们能够拿到数据类型的话,那么是不是这个问题就能很好地解决呢?
根据我的测试,fastjson
可以在知道目标数据类型的情况下,将json
字符串解析成目标数据类型,在Seata源码中,是这样处理的:
Map actionContextMap = null;
if (StringUtils.isNotBlank(applicationData)) {
Map tccContext = JSON.parseObject(applicationData, Map.class);
actionContextMap = (Map)tccContext.get(Constants.TCC_ACTION_CONTEXT);
}
那么根据我们的思路,应该改成根据目标类型来转换:
// 已知目标类型表
Map<String, Class<?>> paramTypeMap = new HashMap<>();
// 解析json字符串为JSONObject
JSONObject jsonObject = JSON.parseObject(applicationData);
// 取出指定的actionContext
Map<String, Object> actionContextMap = jsonObject.getObject("actionContext", Map.class);
Iterator<Map.Entry<String, Object>> iterator = actionContext.entrySet().iterator();
// 遍历里面的每一个元素
while (iterator.hasNext()) {
Map.Entry<String, Object> e = iterator.next();
// 对应的key
String id = e.getKey();
// 目标值
Object obj = e.getValue();
// 对应的目标类型
Class<?> paramType = paramTypeMap.get(id);
// 如果没有指定类型,就跳过
if (Objects.isNull(paramType)) {
continue;
}
// 转换类型并更新actionContext
actionContext.put(id, TypeUtils.cast(obj, new TypeReference(paramType){}.getType(), ParserConfig.getGlobalInstance()));
}
1.咱们这个思路的前提是拿到了目标参数的数据类型,所以我们在上述伪代码中假设有一个
key:value
形式的类型列表;2.我们先反序列化成Map对象,然后逐个遍历检查是否需要转换数据类型;如果有需要就将数据类型进行转换,否则向后继续遍历;
上面我们已经解决了数据类型转换的问题了,剩下的问题就是我们的类型列表从哪里获取?
我们可以观察一下下面这一段代码:
@Override
public BranchStatus branchCommit(BranchType branchType, String xid, long branchId, String resourceId,
String applicationData) throws TransactionException {
// 从缓存中获取TCCResource
TCCResource tccResource = (TCCResource)tccResourceCache.get(resourceId);
if (tccResource == null) {
throw new ShouldNeverHappenException(String.format("TCC resource is not exist, resourceId: %s", resourceId));
}
// 从TCCResource中获取目标对象
Object targetTCCBean = tccResource.getTargetBean();
// 从TCCResource中获取提交逻辑指定的方法
Method commitMethod = tccResource.getCommitMethod();
我们可以从TCCResource
中获取commitMethod
,那么意味着TCCResource
中其实是包含prepareMethod
资源预留方法相关的参数的,那么我们可以根据prepareMethod
获取被BusinessActionContextParameter
注解的参数及其数据类型,代码如下:
private static Map<String, Class<?>> parameterTypes(Method method) {
Map<String, Class<?>> parameterTypeMap = new HashMap<>();
// 获取目标方法的参数列表
Parameter[] parameters = method.getParameters();
if (ArrayUtils.isEmpty(parameters)) {
return parameterTypeMap;
}
// 遍历所有参数
for (Parameter parameter : parameters) {
// 检查是否被BusinessActionContextParameter注解
if (parameter.isAnnotationPresent(BusinessActionContextParameter.class)) {
// 获取目标注解
BusinessActionContextParameter parameterAnnotation = parameter.getDeclaredAnnotation(BusinessActionContextParameter.class);
// 解析出目标参数数据类型
loadActionContextParamClass(parameterAnnotation, parameter.getParameterizedType(), parameterTypeMap);
}
}
// 返回数据类型列表
return parameterTypeMap;
}
private static void loadActionContextParamClass(BusinessActionContextParameter annotation, Type type, Map<String, Class<?>> parameterTypeMap) {
// paramName作为key
String paramName = ActionContextUtil.getParamNameFromAnnotation(annotation);
// 说明是列表,那么找出对应的泛型类型
if (annotation.index() >= 0) {
ParameterizedType parameterizedType = (ParameterizedType) type;
parameterTypeMap.put(paramName, (Class<?>) parameterizedType.getActualTypeArguments()[0]);
return;
}
// 从类字段中查找
if (annotation.isParamInProperty()) {
// 找到所有字段
Field[] fields = ReflectionUtil.getAllFields((Class<?>) type);
if (ArrayUtils.isEmpty(fields)) {
return;
}
// 遍历字段类型
for (Field field : fields) {
// 是否被BusinessActionContextParameter注解
if (field.isAnnotationPresent(BusinessActionContextParameter.class)) {
BusinessActionContextParameter fieldDeclaredAnnotation = field.getDeclaredAnnotation(BusinessActionContextParameter.class);
// 递归调用loadActionContextParamClass方法
loadActionContextParamClass(fieldDeclaredAnnotation, field.getGenericType(), parameterTypeMap);
}
}
} else {
// 直接把参数类型放进去
parameterTypeMap.put(paramName, (Class<?>) type);
}
}
1.通过目标
prepareMethod
找到所有的参数列表,并且遍历所有被BusinessActionContextParameter
注解的参数;2.根据
BusinessActionContextParameter
参数值判断目标参数类型,并解析出目标参数的类型;3.最后把解析出来的参数类型以
paramName:class
的方式存进Map中;
根据以上分析,我们的参数类型列表就出来了,结合前面我们给出的类型转换代码,就很好地解决了Seata TCC模式中,BusinessActionContext
数据对象反序列化导致的数据类型不一致的问题。
结语
根据以上问题两篇文章,我们完成了一次【发现问题】--->【定位问题】--->【解决问题】的日常BUG排查工作,其中难度比较高的是【定位问题】,其次是【解决问题】。在一般情况下,如果没有深入了解源码是很难定位到相关问题出现的原因。后续我将把相关解决方案的代码提交给Seata社区,以便尽快解决该BUG;