1. 处理多态JSON参数的实战方案
在与第三方系统对接时,最让人头疼的就是接口参数的不确定性。最近我就遇到了一个典型场景:某个字段可能是空字符串,也可能是包含业务数据的数组。这种设计虽然不够规范,但在实际业务中却很常见——特别是对接一些历史悠久的系统时。
1.1 问题场景还原
我们的业务需要接收百度推广线索数据,其中additionalContent字段的文档说明写着:"可能是空字符串或数组"。这种设计会导致:
- 直接使用
JSONArray解析会抛出异常 - 常规的POJO映射会失败
- 需要兼容两种完全不同的数据结构
java复制// 可能的输入示例1:空字符串
{"additionalContent": ""}
// 可能的输入示例2:对象数组
{"additionalContent": [{"contentKey":"省份","contentValue":"贵州省"}]}
1.2 解决方案设计思路
经过多次尝试,我最终采用Object类型作为接收载体,配合运行时类型判断的方案。核心思路是:
- 使用
Object接收任意类型的参数 - 运行时通过
instanceof进行类型检查 - 对数组类型进行二次JSON序列化/反序列化
- 统一返回
List<T>保证业务层使用体验
2. 核心实现代码解析
2.1 基础DTO结构设计
首先定义数据传输对象,关键点在于:
additionalContent声明为Object类型- 内置
BaiduClueSubDTO描述数组元素结构 - 初始化
additionalContentList避免NPE
java复制@Data
public class BaiduClueDTO {
/**
* 注:入参可能是空字符串或者数组
* 使用Object类型接收多态参数
*/
private Object additionalContent;
// 保证始终返回非null列表
private List<BaiduClueSubDTO> additionalContentList = new ArrayList<>();
@Data
public static class BaiduClueSubDTO {
private String contentKey;
private String contentValue;
private String url;
}
}
2.2 类型转换核心逻辑
转换方法getAdditionalContentAsArray包含完整的处理逻辑:
java复制public List<BaiduClueSubDTO> getAdditionalContentAsArray(Object additionalContent) {
// 防御性编程:始终返回不可变空列表
List<BaiduClueSubDTO> resultList = Collections.emptyList();
// 场景1:null或空字符串直接返回
if (additionalContent == null ||
additionalContent instanceof String) {
return resultList;
}
// 场景2:处理数组类型
if (additionalContent instanceof List) {
try {
ObjectMapper objectMapper = new ObjectMapper();
// 二次序列化为JSON字符串
String json = objectMapper.writeValueAsString(additionalContent);
// 使用定制工具类反序列化
this.additionalContentList = JsonUtil.readList(json, BaiduClueSubDTO.class);
return this.additionalContentList;
} catch (Exception e) {
log.error("JSON转换异常", e);
return resultList;
}
}
return resultList;
}
2.3 工具类关键实现
JsonUtil提供了安全的集合反序列化方法,核心是构建正确的JavaType:
java复制public static <T> List<T> readList(String content, Class<T> elementClass) {
if (StringUtil.isBlank(content)) {
return Collections.emptyList();
}
try {
CollectionLikeType listType = getInstance()
.getTypeFactory()
.constructCollectionLikeType(List.class, elementClass);
return getInstance().readValue(content, listType);
} catch (IOException e) {
throw new RuntimeException("JSON解析失败", e);
}
}
3. 业务层使用示例
实际业务中这样调用:
java复制List<BaiduClueSubDTO> contentList = baiduClueDTO
.getAdditionalContentAsArray(baiduClueDTO.getAdditionalContent());
if(CollectionUtils.isNotEmpty(contentList)){
contentList.forEach(item -> {
if("房屋面积".equals(item.getContentKey())){
marketingDataVO.setHouseArea(item.getContentValue());
}
// 其他业务处理...
});
}
4. 踩坑经验与优化建议
4.1 常见问题排查
-
ClassCastException问题:
- 现象:直接强制转换
List<Map>到List<BaiduClueSubDTO> - 原因:Java泛型擦除机制导致运行时类型信息丢失
- 解决:必须通过
ObjectMapper进行完整反序列化
- 现象:直接强制转换
-
JSON格式异常:
- 现象:包含非法字符时解析失败
- 解决:在工具类中配置
ALLOW_UNESCAPED_CONTROL_CHARS等特性
java复制.configure(JsonReadFeature.ALLOW_UNESCAPED_CONTROL_CHARS, true)
4.2 性能优化方案
-
ObjectMapper复用:
- 不要每次调用都new新实例
- 推荐使用静态工具类或Spring注入
-
缓存JavaType:
- 对于固定结构可以缓存Type对象
- 减少重复构建开销
java复制private static final JavaType LIST_TYPE =
TypeFactory.defaultInstance()
.constructCollectionType(List.class, BaiduClueSubDTO.class);
4.3 设计模式建议
对于复杂接口协议,建议采用:
- 适配器模式:隔离第三方接口变化
- 策略模式:针对不同参数类型定义处理策略
- 工厂模式:创建不同类型的解析器
5. 扩展思考:更优雅的解决方案
5.1 自定义JsonDeserializer
可以实现更精细化的控制:
java复制public class PolymorphicDeserializer extends JsonDeserializer<List<BaiduClueSubDTO>> {
@Override
public List<BaiduClueSubDTO> deserialize(JsonParser p, DeserializationContext ctxt) {
// 实现多态解析逻辑
}
}
// 在字段上使用
@JsonDeserialize(using = PolymorphicDeserializer.class)
private List<BaiduClueSubDTO> additionalContent;
5.2 Schema验证方案
对于重要接口,建议:
- 使用JSON Schema定义规范
- 接入验证库如
networknt/json-schema-validator - 在数据入口处进行校验
java复制JsonSchema schema = JsonSchemaFactory
.getInstance()
.getSchema(schemaUrl);
Set<ValidationMessage> errors = schema.validate(jsonNode);
5.3 接口协商建议
虽然技术能解决问题,但长远来看:
- 推动第三方规范接口设计
- 明确每个字段的类型约束
- 建立接口变更通知机制
在实际项目中,这套方案成功处理了日均10w+的线索数据。核心在于平衡了灵活性与健壮性,既兼容了不规范的接口设计,又保证了业务代码的稳定性。