feign-list-param-crash-cpp

Feign调用C++服务报文格式踩坑实录:List直接传参导致服务崩溃

前言

最近在项目中进行微服务间Feign调用时,遇到了一个隐蔽且严重 的问题:Java端通过Feign调用C++服务的删除接口,直接传递List<String>参数,导致C++服务解析报文失败直接崩溃。本文记录完整的排查过程和解决方案。

一、问题背景

项目架构中,Java微服务通过Feign调用C++服务进行设备管理(控制器、一体机、摄像机、门的增删改查)。C++服务端要求所有删除接口的请求报文格式如下:

json 复制代码
{
  "controllerIds": ["id1", "id2"]
}

即:必须是一个JSON对象,内部用字段名包装一个数组

二、事故现场

2.1 修改前的代码(问题代码)

Feign接口定义:

java 复制代码
@FeignClient(name = "doorOperation", url = "EMPTY")
public interface DoorOperationService {

    @RequestMapping(value = "/doorService/controller/delete", method = RequestMethod.POST)
    ResponseBean<RequestReturnDTO> controllerDelete(URI uri, List<String> controllerIds);

    @RequestMapping(value = "/doorService/faceMachine/delete", method = RequestMethod.POST)
    ResponseBean<RequestReturnDTO> faceMachineDelete(URI uri, List<String> machineIds);

    @RequestMapping(value = "/doorService/accessControlCamera/delete", method = RequestMethod.POST)
    ResponseBean<RequestReturnDTO> cameraDelete(URI uri, List<String> cameraIds);

    @RequestMapping(value = "/doorService/door/delete", method = RequestMethod.POST)
    ResponseBean<RequestReturnDTO> doorDelete(URI uri, List<String> doorIds);
}

调用处:

java 复制代码
// 直接传 Arrays.asList(controllerId)
ResponseBean<RequestReturnDTO> responseBean = 
    doorOperationService.controllerDelete(uri, Arrays.asList(controllerId));

2.2 实际发出的HTTP报文

Feign将List<String>序列化后,实际发出的请求体为:

json 复制代码
["id1"]

只是一个纯JSON数组,没有外层对象包装,也没有字段名。

2.3 C++服务端期望的报文

json 复制代码
{
  "controllerIds": ["id1"]
}

2.4 结果

C++服务端接收到报文后,尝试按对象解析,发现是一个数组而非对象,解析失败导致服务直接崩溃

三、根因分析

对比项 实际发送 C++期望
报文结构 ["id1"] {"controllerIds": ["id1"]}
顶层类型 JSON Array JSON Object
字段名 controllerIds

核心原因 :Feign在序列化List<String>时,只会将其序列化为JSON数组[...],而不会自动包装成{"fieldName": [...]}的对象格式。C++服务端使用的是严格的JSON对象解析(如nlohmann::json),遇到顶层为数组直接抛出异常。

四、解决方案

4.1 创建包装DTO

为每个删除接口创建专门的请求DTO,用对象包装数组字段:

ControllerDeleteDTO:

java 复制代码
@Data
public class ControllerDeleteDTO {
    /** 控制器id列表 */
    private List<String> controllerIds;
}

FaceMachineDeleteDTO:

java 复制代码
@Data
public class FaceMachineDeleteDTO {
    /** 一体机id列表 */
    private List<String> machineIds;
}

CameraDeleteDTO:

java 复制代码
@Data
public class CameraDeleteDTO {
    /** 摄像机id列表 */
    private List<String> cameraIds;
}

DoorDeleteDTO:

java 复制代码
@Data
public class DoorDeleteDTO {
    /** 门id列表 */
    private List<String> doorIds;
}

4.2 修改Feign接口

java 复制代码
@FeignClient(name = "doorOperation", url = "EMPTY")
public interface DoorOperationService {

    @RequestMapping(value = "/doorService/controller/delete", method = RequestMethod.POST)
    ResponseBean<RequestReturnDTO> controllerDelete(URI uri, ControllerDeleteDTO controllerDeleteDTO);

    @RequestMapping(value = "/doorService/faceMachine/delete", method = RequestMethod.POST)
    ResponseBean<RequestReturnDTO> faceMachineDelete(URI uri, FaceMachineDeleteDTO faceMachineDeleteDTO);

    @RequestMapping(value = "/doorService/accessControlCamera/delete", method = RequestMethod.POST)
    ResponseBean<RequestReturnDTO> cameraDelete(URI uri, CameraDeleteDTO cameraDeleteDTO);

    @RequestMapping(value = "/doorService/door/delete", method = RequestMethod.POST)
    ResponseBean<RequestReturnDTO> doorDelete(URI uri, DoorDeleteDTO doorDeleteDTO);
}

4.3 修改FallbackFactory

java 复制代码
@Component
public class DoorOperationServiceFactory implements FallbackFactory<DoorOperationService> {
    @Override
    public DoorOperationService create(Throwable cause) {
        return new DoorOperationService() {
            @Override
            public ResponseBean<RequestReturnDTO> controllerDelete(URI uri, ControllerDeleteDTO dto) {
                return null;
            }
            @Override
            public ResponseBean<RequestReturnDTO> faceMachineDelete(URI uri, FaceMachineDeleteDTO dto) {
                return null;
            }
            @Override
            public ResponseBean<RequestReturnDTO> cameraDelete(URI uri, CameraDeleteDTO dto) {
                return null;
            }
            @Override
            public ResponseBean<RequestReturnDTO> doorDelete(URI uri, DoorDeleteDTO dto) {
                return null;
            }
            // ... 其他方法省略
        };
    }
}

4.4 修改调用处

java 复制代码
// ✅ 修改后:构造DTO对象
ControllerDeleteDTO controllerDeleteDTO = new ControllerDeleteDTO();
controllerDeleteDTO.setControllerIds(Arrays.asList(controllerId));
ResponseBean<RequestReturnDTO> responseBean = 
    doorOperationService.controllerDelete(uri, controllerDeleteDTO);
java 复制代码
// ✅ 一体机删除
FaceMachineDeleteDTO faceMachineDeleteDTO = new FaceMachineDeleteDTO();
faceMachineDeleteDTO.setMachineIds(Arrays.asList(faceMachineId));
doorOperationService.faceMachineDelete(uri, faceMachineDeleteDTO);

// ✅ 摄像机删除
CameraDeleteDTO cameraDeleteDTO = new CameraDeleteDTO();
cameraDeleteDTO.setCameraIds(Arrays.asList(cameraId));
doorOperationService.cameraDelete(uri, cameraDeleteDTO);

// ✅ 门删除
DoorDeleteDTO doorDeleteDTO = new DoorDeleteDTO();
doorDeleteDTO.setDoorIds(Arrays.asList(doorId));
doorOperationService.doorDelete(uri, doorDeleteDTO);

五、修改前后报文对比

接口 修改前(崩溃) 修改后(正常)
控制器删除 ["ctrl_001"] {"controllerIds":["ctrl_001"]}
一体机删除 ["fm_001"] {"machineIds":["fm_001"]}
摄像机删除 ["cam_001"] {"cameraIds":["cam_001"]}
门删除 ["door_001"] {"doorIds":["door_001"]}

六、涉及修改的文件清单

文件 修改内容
ControllerDeleteDTO.java 字段从 String controllerId 改为 List<String> controllerIds
FaceMachineDeleteDTO.java 字段从 String machineId 改为 List<String> machineIds
CameraDeleteDTO.java 字段从 String cameraId 改为 List<String> cameraIds
DoorDeleteDTO.java 字段从 String doorId 改为 List<String> doorIds
DoorOperationService.java 4个删除方法参数从 List<String> 改为对应DTO
DoorOperationServiceFactory.java Fallback同步修改参数类型
ThirdServiceServiceImpl.java 4处调用改为构造DTO对象再调用

七、经验总结

7.1 Feign序列化陷阱

Feign默认使用Jackson进行JSON序列化:

  • List<String>["a","b"](纯数组)
  • XxxDTO(含List<String> ids字段)→ {"ids":["a","b"]}(对象包装)

当对方服务要求请求体是JSON Object时,绝不能直接传List作为参数。

7.2 接口契约意识

微服务间调用,报文格式就是接口契约。Java和C++属于不同技术栈,对JSON的解析严格程度不同:

  • Java的Jackson/Gson通常能容错
  • C++的nlohmann::json、rapidjson等库往往是严格解析,格式不对直接崩溃

7.3 最佳实践

复制代码
✅ 正确做法:
1. 为每个接口定义明确的请求/响应DTO
2. DTO字段名与对方服务报文格式严格对齐
3. 联调前先用Postman/curl验证报文格式
4. 添加Feign日志拦截器,记录实际发出的HTTP请求体

❌ 错误做法:
1. 直接传List、Map等裸集合类型
2. 凭感觉定义参数,不对齐报文格式
3. 跨语言调用不做报文格式验证

7.4 快速排查技巧

如果Feign调用后对方服务报错/崩溃,第一步应该抓包看实际发出的报文

yaml 复制代码
# application.yml 开启Feign日志
feign:
  client:
    config:
      default:
        loggerLevel: FULL
java 复制代码
@Bean
public Logger.Level feignLoggerLevel() {
    return Logger.Level.FULL; // 打印完整的请求和响应
}

通过日志可以看到Feign实际序列化的JSON内容,快速定位报文格式问题。


总结 :跨语言微服务调用中,报文格式一致性至关重要。List<String>直接传参会序列化为纯数组,如果对方期望的是对象包装的数组,就会导致解析失败甚至服务崩溃。正确做法是定义专门的DTO类,用字段名包装列表数据。

相关推荐
程序员老舅2 小时前
啃透 I2C 驱动开发,才算入门嵌入式 Linux 内核驱动
数据结构·驱动开发·b树·内核·嵌入式·嵌入式开发·i2c
都叫我大帅哥6 小时前
Java JJWT 完全指南:从入门到生产级实践
java
前端开发张小七8 小时前
Java 学习笔记 · 第一课:基础语法与核心概念
java·后端
ThsPool8 小时前
【ENVI二次开发学习整理 04】ENVI功能扩展与二次开发
java·前端·javascript
赤壁小虾9 小时前
【渲染流水线】[逐片元阶段]-[透明度测试]以UnityURP为例
java·前端·数据库
青山木10 小时前
Hot 100 --- 搜索插入位置
java·数据结构·算法·leetcode
Jul1en_10 小时前
【Claude Code Compact】源码级别的学习上下文压缩
java·前端·学习·github·ai编程
摇滚侠10 小时前
《SpringBoot 3:入门与应用实战》第 15 章 生产级特性 管理 Spring Boot 应用 阅读笔记
java·spring boot·笔记
XWalnut10 小时前
SpringBoot快速入门
java·spring boot·后端
前端 贾公子10 小时前
第06章:结构化输出 (上)
java·服务器·前端