Skip to content

FeignNode 动态服务调用框架使用文档 ​

一、概述 ​

FeignNode 是一个基于 Spring Cloud Feign 的动态服务调用框架,专为 LangGraph 智能体工作流设计。它允许在运行时动态调用任意已注册的 Feign 服务接口,无需硬编码调用逻辑,极大提升了微服务间调用的灵活性。

核心特性 ​

  • 自动注册: 启动时自动扫描并注册所有 Feign 客户端接口
  • 动态调用: 支持通过类名和方法名动态调用远程服务
  • 类型安全: 完善的参数类型转换和校验机制
  • 智能体集成: 无缝集成到 LangGraph 工作流节点中
  • 统一错误处理: 标准化的结果返回和异常处理
  • 性能监控: 内置调用耗时统计和详细日志记录

二、架构设计 ​

2.1 核心组件 ​

FeignNode框架
├── FeignRegistryInitializer    # Feign接口注册器
├── FeignClassInfoDTO            # Feign类元信息
├── FeignMethodInfoDTO           # Feign方法元信息
├── MethodInvoker                # 方法调用器
├── FeignNodeAction              # LangGraph节点动作
└── MemberFieldConstructorDTO    # 参数类型转换器

2.2 工作流程 ​

1. 应用启动
   └─> FeignRegistryInitializer 扫描所有 @FeignClient 注解的接口
       └─> 提取接口的类信息、方法信息、参数信息
           └─> 存储到注册表 registry 中

2. 运行时调用
   └─> 从 registry 获取 Feign 接口信息
       └─> 构建 MethodInvoker 调用器
           └─> 转换和校验参数
               └─> 执行反射调用
                   └─> 返回标准化结果

三、快速开始 ​

3.1 定义 Feign 客户端 ​

java
@FeignClient(value = ServiceNameConstants.AUDIT_SERVICE)
public interface AuditTaskExecutionMaterialFeignService {

    /**
     * 根据任务执行ID获取关联的文档库列表
     * @param taskExecutionId 任务执行ID
     * @return 文档库列表
     */
    @Operation(
        summary = "获取任务执行资料关联的文档库",
        description = "根据任务执行ID查询所有关联的文档库信息"
    )
    @GetMapping("/audit-task-execution-material/document-libraries/{taskExecutionId}")
    R<List<Long>> getDocumentLibrariesByTaskExecutionId(
        @PathVariable("taskExecutionId") Long taskExecutionId
    );
}

说明:

  • 使用 @Operation 注解添加方法描述,便于后续查询和使用
  • 方法签名必须清晰,参数类型明确

3.2 在 LangGraph 工作流中使用 ​

java
@Component
public class AuditReportProcessAgent {

    @Resource
    ReportMiddleContentNodeAction reportMiddleContentNodeAction;

    public Boolean start() throws GraphStateException {
        // 1. 准备调用参数
        String feignClass = "com.pig4cloud.pigx.audit.feign.service.AuditTaskExecutionMaterialFeignService";
        String feignMethod = "getDocumentLibrariesByTaskExecutionId";
        Object[] requestParams = new Object[] {
            1989250342346940417L  // 任务执行ID
        };

        // 2. 从注册表获取Feign接口信息
        FeignClassInfoDTO feignClassInfo = FeignRegistryInitializer.registry.get(feignClass);
        FeignMethodInfoDTO methodInfo = feignClassInfo.getMethodInfos().get(feignMethod);

        // 3. 构建方法调用器
        MethodInvoker methodInvoker = MethodInvoker.builder()
                .instance(feignClassInfo.getInstance())
                .method(methodInfo.getMethod())
                .methodName(feignMethod)
                .args(requestParams)
                .build();

        // 4. 创建FeignNode动作
        FeignNodeAction feignNodeAction = new FeignNodeAction(methodInvoker);

        // 5. 定义工作流
        StateGraph<ConversationState> stateGraph = new StateGraph<>(ConversationState.SCHEMA, ConversationState::new)
                .addNode("getAuditReportData", node_async(feignNodeAction))
                .addNode("processReportContent", node_async(reportMiddleContentNodeAction))
                .addEdge(StateGraph.START, "getAuditReportData")
                .addEdge("getAuditReportData", "processReportContent")
                .addEdge("processReportContent", StateGraph.END);

        // 6. 编译并执行
        CompiledGraph<ConversationState> compiledGraph = stateGraph.compile();
        Map<String, Object> initialState = Map.of(
            ConversationState.MESSAGES_KEY,
            "开始执行审计报告生成流程"
        );

        compiledGraph.invoke(initialState).ifPresent(finalState -> {
            System.out.println("工作流执行完成: " + finalState);
        });

        return true;
    }
}

四、高级用法 ​

4.1 参数类型转换 ​

框架支持自动进行参数类型转换,适用于以下场景:

4.1.1 从字符串转换为目标类型 ​

java
// 准备字符串参数
String[] args = {"1989250342346940417"};  // Long类型参数的字符串形式

// 获取方法参数信息
List<MemberFieldConstructorDTO> parameters = methodInfo.getParameters();

// 自动转换为目标类型
Object[] requestParams = new Object[args.length];
for (int i = 0; i < parameters.size(); i++) {
    MemberFieldConstructorDTO paramConverter = parameters.get(i);
    // 自动将字符串转换为Long类型
    requestParams[i] = paramConverter.getConvertedValue(args[i]);
}

4.1.2 支持的类型转换 ​

  • 基本类型: int, long, double, float, boolean, byte, short, char
  • 包装类型: Integer, Long, Double, Float, Boolean, Byte, Short, Character
  • 字符串: String
  • 复杂对象: 通过 JSON 反序列化转换
  • 集合类型: List, Set, Map (JSON格式)

4.1.3 特殊格式支持 ​

java
// 数字格式(带千分位)
"1,000,000" -> 1000000 (Integer/Long)

// 布尔值多种表示
"true", "1", "yes", "on" -> true
"false", "0", "no", "off" -> false

// JSON对象
"{\"id\":123,\"name\":\"test\"}" -> Java Object

4.2 复杂参数场景 ​

4.2.1 多参数调用 ​

java
// Feign接口定义
@PostMapping("/report/create")
R<ReportEntity> createReport(
    @RequestParam Long projectId,
    @RequestParam String reportName,
    @RequestBody ReportConfig config
);

// 调用示例
Object[] requestParams = new Object[] {
    1234567890L,                    // projectId
    "2024年度审计报告",              // reportName
    new ReportConfig(...)           // config 对象
};

MethodInvoker methodInvoker = MethodInvoker.builder()
        .instance(feignInstance)
        .method(method)
        .methodName("createReport")
        .args(requestParams)
        .build();

4.2.2 可选参数处理 ​

java
// Feign接口定义
R<List<User>> queryUsers(
    @RequestParam String name,
    @Nullable @RequestParam String email  // 可选参数
);

// 调用示例
Object[] requestParams = new Object[] {
    "张三",      // name (必填)
    null        // email (可选,允许为null)
};

注意:

  • 使用 @Nullable 注解标注的参数允许传入 null
  • 未标注 @Nullable 的参数如果传入 null 会抛出异常

4.3 错误处理 ​

4.3.1 调用失败处理 ​

java
FeignNodeAction feignNodeAction = new FeignNodeAction(methodInvoker);

try {
    Map<String, Object> result = feignNodeAction.apply(conversationState);

    // 获取调用结果
    JSONObject toolResult = (JSONObject) result.get(ConversationState.TOOL_RESULT_KEY);

    // 检查调用是否成功
    if (toolResult.containsKey("success") && !toolResult.getBoolean("success")) {
        // 处理失败情况
        String errorMsg = toolResult.getString("message");
        log.error("服务调用失败: {}", errorMsg);
    } else {
        // 处理成功情况
        JSONObject data = toolResult.getJSONObject("data");
        // 业务处理...
    }

} catch (Exception e) {
    log.error("FeignNode执行异常", e);
}

4.3.2 返回结果格式 ​

成功时:

json
{
  "data": {
    // 实际返回的业务数据
  }
}

失败时:

json
{
  "success": false,
  "code": 500,
  "message": "错误描述",
  "data": null
}

异常时:

json
{
  "success": false,
  "error": "异常类型",
  "message": "异常信息"
}

五、最佳实践 ​

5.1 定义常量类 ​

避免硬编码类名和方法名:

java
public class FeignServiceConstants {

    // Feign 服务类名
    public static final String AUDIT_TASK_MATERIAL_SERVICE =
        "com.pig4cloud.pigx.audit.feign.service.AuditTaskExecutionMaterialFeignService";

    public static final String AUDIT_REPORT_SERVICE =
        "com.pig4cloud.pigx.audit.feign.service.AuditReportFeignService";

    // 方法名
    public static final String METHOD_GET_DOC_LIBRARIES =
        "getDocumentLibrariesByTaskExecutionId";

    public static final String METHOD_CREATE_REPORT =
        "createReport";
}

// 使用
String feignClass = FeignServiceConstants.AUDIT_TASK_MATERIAL_SERVICE;
String feignMethod = FeignServiceConstants.METHOD_GET_DOC_LIBRARIES;

5.2 封装通用调用方法 ​

java
@Component
public class FeignNodeHelper {

    /**
     * 快速创建FeignNodeAction
     */
    public static FeignNodeAction createFeignNode(
            String feignClass,
            String methodName,
            Object... args) {

        // 获取Feign接口信息
        FeignClassInfoDTO classInfo = FeignRegistryInitializer.registry.get(feignClass);
        if (classInfo == null) {
            throw new IllegalArgumentException("未找到Feign服务: " + feignClass);
        }

        FeignMethodInfoDTO methodInfo = classInfo.getMethodInfos().get(methodName);
        if (methodInfo == null) {
            throw new IllegalArgumentException("未找到方法: " + methodName);
        }

        // 构建调用器
        MethodInvoker methodInvoker = MethodInvoker.builder()
                .instance(classInfo.getInstance())
                .method(methodInfo.getMethod())
                .methodName(methodName)
                .args(args)
                .build();

        return new FeignNodeAction(methodInvoker);
    }
}

// 使用
FeignNodeAction action = FeignNodeHelper.createFeignNode(
    FeignServiceConstants.AUDIT_TASK_MATERIAL_SERVICE,
    FeignServiceConstants.METHOD_GET_DOC_LIBRARIES,
    1989250342346940417L
);

5.3 添加详细的接口文档 ​

在 Feign 接口上使用 @Operation 注解添加详细说明:

java
@Operation(
    summary = "获取任务执行资料",
    description = "根据任务ID和执行阶段查询相关的审计资料列表," +
                  "包括文档、图片、视频等多种类型的资料"
)
@GetMapping("/materials/{taskId}")
R<List<MaterialVO>> getMaterials(
    @PathVariable Long taskId,
    @RequestParam(required = false) String stage
);

5.4 性能优化建议 ​

  1. 缓存 Feign 接口信息

    java
    // 不要每次都从registry查找
    // 不推荐
    for (int i = 0; i < 1000; i++) {
        FeignClassInfoDTO info = FeignRegistryInitializer.registry.get(className);
    }
    
    // 推荐
    FeignClassInfoDTO info = FeignRegistryInitializer.registry.get(className);
    for (int i = 0; i < 1000; i++) {
        // 使用缓存的info
    }
  2. 批量调用优化

    java
    // 如果需要调用多个服务,可以并行执行
    List<CompletableFuture<R<?>>> futures = new ArrayList<>();
    
    for (TaskConfig config : taskConfigs) {
        CompletableFuture<R<?>> future = CompletableFuture.supplyAsync(() -> {
            MethodInvoker invoker = createInvoker(config);
            return invoker.invokeService();
        });
        futures.add(future);
    }
    
    // 等待所有调用完成
    CompletableFuture.allOf(futures.toArray(new CompletableFuture[0])).join();

六、故障排查 ​

6.1 常见问题 ​

问题1: "未找到Feign客户端" ​

错误信息:

java.lang.NullPointerException: Cannot invoke "FeignClassInfoDTO.getMethodInfos()"
because "feignClassInfo" is null

原因:

  • Feign 接口未正确注册
  • 类名拼写错误

解决方案:

java
// 检查注册表中是否存在该服务
Set<String> registeredServices = FeignRegistryInitializer.registry.keySet();
System.out.println("已注册的Feign服务: " + registeredServices);

// 使用正确的全限定类名
String correctClassName = "com.pig4cloud.pigx.audit.feign.service.YourFeignService";

问题2: "参数类型不匹配" ​

错误信息:

参数 taskId 类型不匹配: 期望类型=java.lang.Long, 实际类型=java.lang.Integer

解决方案:

java
// 确保传入的参数类型与方法签名一致
Object[] args = new Object[] {
    1234567890L,     // Long 类型,注意后缀 L
    // 而不是 1234567890  // Integer 类型
};

问题3: "方法不存在" ​

错误信息:

java.lang.NullPointerException: Cannot invoke "FeignMethodInfoDTO.getMethod()"
because "methodInfo" is null

解决方案:

java
// 检查方法名是否正确
FeignClassInfoDTO classInfo = FeignRegistryInitializer.registry.get(className);
Map<String, FeignMethodInfoDTO> methods = classInfo.getMethodInfos();
System.out.println("可用的方法: " + methods.keySet());

// 使用正确的方法名(区分大小写)
String correctMethodName = "getDocumentLibrariesByTaskExecutionId";

6.2 调试技巧 ​

开启详细日志 ​

yaml
# application.yml
logging:
  level:
    com.pig4cloud.pigx.ai.agent.langgraph.nodeAction: DEBUG
    com.pig4cloud.pigx.ai.util: DEBUG
    com.pig4cloud.pigx.ai.config: DEBUG

打印注册表信息 ​

java
@Component
public class FeignRegistryDebugger {

    @PostConstruct
    public void printRegistry() {
        System.out.println("========== Feign Registry Info ==========");
        FeignRegistryInitializer.registry.forEach((className, classInfo) -> {
            System.out.println("服务: " + className);
            classInfo.getMethodInfos().forEach((methodName, methodInfo) -> {
                System.out.println("  方法: " + methodName);
                System.out.println("    描述: " + methodInfo.getDescription());
                System.out.println("    摘要: " + methodInfo.getSummary());
            });
        });
        System.out.println("=========================================");
    }
}

七、注意事项 ​

7.1 线程安全 ​

  • 注册表使用 ConcurrentHashMap 实现,支持并发读取
  • MethodInvoker 实例不是线程安全的,不要在多线程间共享

7.2 性能考虑 ​

  • 反射调用有一定性能开销,不适合超高频调用场景(>10000 QPS)
  • 建议在智能体编排、定时任务等场景使用

7.3 异常处理 ​

  • 所有异常都会被捕获并转换为标准错误格式
  • 业务代码应该检查返回结果中的 success 字段
  • 原始异常信息会记录到日志中

7.4 参数校验 ​

  • 框架会自动校验参数数量、类型、Nullable约束
  • 建议在 Feign 接口定义中明确标注参数约束
  • 复杂对象参数建议使用 DTO 并添加 JSR-303 校验注解

八、版本历史 ​

v1.0.0 (2024-11-14) ​

  • ✅ 初始版本发布
  • ✅ 支持基本类型和包装类型转换
  • ✅ 支持复杂对象 JSON 转换
  • ✅ 集成 LangGraph 工作流
  • ✅ 完善的错误处理和日志记录

九、后续规划 ​

  • [ ] 支持异步调用模式
  • [ ] 增加调用缓存机制
  • [ ] 支持熔断降级
  • [ ] 支持重试策略
  • [ ] 提供图形化的服务调用监控面板

十、技术支持 ​

如遇到问题,请按以下步骤处理:

  1. 查看本文档的"故障排查"章节
  2. 检查日志输出,开启 DEBUG 级别日志
  3. 联系框架维护人员

维护人员: Xuejx 联系方式: 931956648@qq.com