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类,用字段名包装列表数据。

相关推荐
Chase_______1 小时前
【Java基础 | 10】异常处理入门:Throwable、try-catch-finally 与异常调用栈一次讲清
java
计算机安禾1 小时前
【算法设计与分析】第40篇:空间数据结构:KD树与四叉树的查询分析
数据结构·算法
努力努力再努力wz1 小时前
【C++高阶数据结构系列】:跳表 SkipList 详解:多层索引、随机晋升与C++ 完整实现(附跳表实现的源码)
开发语言·数据结构·数据库·c++·redis·缓存·skiplist
莫逸风1 小时前
【AgentScope】3. 工作空间(Workspace)详解
java·ai·agent·springai·agentscope
Devin~Y1 小时前
从Spring Boot到AI Agent:大厂Java微服务面试三轮实战问答解析
java·spring boot·redis·spring cloud·微服务·ai·kafka
brave_zhao1 小时前
http 403 HTTP 403(Forbidden)表示服务器理解请求,但拒绝授权访问
java
爱吃羊的老虎1 小时前
【JAVA】python转java:Spring Boot 如何处理 Web 请求
java·前端·spring boot·http
装不满的克莱因瓶1 小时前
DDD 设计与 Maven 多模块拆分:从单体项目到领域驱动架构实践
java·架构·maven·ddd
码不停蹄的玄黓1 小时前
SpringBoot 循环依赖解决方案
java·spring boot·后端