Spring Boot 封装 OpenClAW 服务层最佳实践【OpenClAW + Spring Boot 系列 第2篇】

📅 难度:⭐⭐☆☆☆ 进阶 | 阅读约 15 分钟 | 适用:Spring Boot 2.7.x / 3.x | Java 17+

📋 目录

  1. 配置管理

    • 使用 ClawProperties 实现强类型配置
  2. 服务层设计

    • 定义 Service 接口
    • 实现类 ClaudeServiceImpl 包含:
      • 基础单轮对话功能
      • 支持系统提示词的对话场景
      • 采用 Builder 模式封装复杂请求
  3. 异常处理机制

    • 自定义异常体系
    • 全局异常处理器
    • 集成 Spring Retry 实现重试机制
  4. 响应标准化

    • 统一响应体封装
  5. 接口层整合

    • Controller 层实现
  6. 质量保障

    • 基于 Mock 的 ClaudeService 单元测试
  7. 项目结构说明

    • 完整项目目录规划

总结

一、前言:为什么要封装服务层?

在第一篇中,我们直接在 Controller 里注入 ClaudeService 并调用,虽然能跑通,但存在明显问题:

  • 职责混乱:Controller 既处理 HTTP 请求又处理 AI 业务逻辑
  • 难以测试:AI 调用与接口层耦合,单元测试只能做集成测试
  • 无法复用:同样的 AI 调用逻辑在多个 Controller 中重复出现
  • 异常裸奔:API 错误直接抛出,没有统一的错误格式
  • 配置散乱:模型名、token 数量等魔法值散落各处

本篇将按照 生产级标准 重构这套代码,建立完整的服务层架构,涵盖:强类型配置、接口抽象、异常体系、重试机制、统一响应体。

本篇代码是后续所有章节的基础,建议完整跟做一遍,不要跳过。

二、整体架构设计

先看整体分层,明确每层的职责边界:

HTTP 请求 / 前端

↓ REST API

ChatController

① 参数校验、路由分发、响应格式化

↓ 调用业务接口

ClawChatService(接口)

② 业务逻辑抽象层(可 Mock 替换)

↓ 实现

ClawChatServiceImpl(实现)

③ 请求组装、异常处理、重试逻辑

↓ 调用

OpenClAW ClaudeService

④ 底层 HTTP 封装(框架提供,不修改)

↓ HTTPS

Anthropic Claude API

这个分层有几个关键设计决策:

  • 接口与实现分离ClawChatService 是接口,ClawChatServiceImpl 是实现,测试时可以 Mock 掉实现
  • 不直接暴露 OpenClAW 类型:Service 接口的参数和返回值使用我们自己定义的 DTO,不依赖 OpenClAW 的内部类
  • 异常在 Service 层收口:OpenClAW 抛出的各类异常,在 Service 层统一转换为业务异常

三、配置管理:ClawProperties 强类型配置

将所有配置项集中管理,告别魔法字符串。

3.1 创建配置类

java 复制代码
package com.example.openclaw_demo.config;

import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;
import org.springframework.validation.annotation.Validated;

import jakarta.validation.constraints.Max;
import jakarta.validation.constraints.Min;
import jakarta.validation.constraints.NotBlank;

/**
 * OpenClAW 业务层配置项
 * 前缀:app.claw
 *
 * 与 openclaw.* 的区别:
 *   openclaw.*  → OpenClAW 框架的底层配置(API Key、超时等)
 *   app.claw.*  → 我们业务层对 AI 调用的配置(默认提示词、重试次数等)
 */
@Data
@Component
@Validated
@ConfigurationProperties(prefix = "app.claw")
public class ClawProperties {

    /** 默认使用的模型名称 */
    @NotBlank
    private String defaultModel = "claude-sonnet-4-20250514";

    /** 默认最大 token 输出数 */
    @Min(1) @Max(8096)
    private int defaultMaxTokens = 1024;

    /** 全局系统提示词(可被单次请求覆盖) */
    private String globalSystemPrompt = "";

    /** 请求失败时的最大重试次数 */
    @Min(0) @Max(5)
    private int maxRetries = 2;

    /** 重试间隔(毫秒) */
    private long retryDelayMs = 1000;

    /** 温度参数(0.0 = 确定性最高,1.0 = 最有创意) */
    private double temperature = 0.7;

    /** 是否在日志中记录请求内容(生产环境建议关闭) */
    private boolean logRequests = false;
}

3.2 在 application.yml 中添加配置

XML 复制代码
openclaw:
  api-key: ${ANTHROPIC_API_KEY}
  timeout: 60

app:
  claw:
    default-model: claude-sonnet-4-20250514
    default-max-tokens: 1024
    global-system-prompt: "你是一名专业的助手,回答简洁准确,使用中文。"
    max-retries: 2
    retry-delay-ms: 1000
    temperature: 0.7
    log-requests: false

在启动类上开启配置扫描:

java 复制代码
@SpringBootApplication
@EnableConfigurationProperties(ClawProperties.class)
public class OpenClawDemoApplication {
    public static void main(String[] args) {
        SpringApplication.run(OpenClawDemoApplication.class, args);
    }
}

四、定义 Service 接口

先定义我们自己的 DTO,再定义接口。Service 接口不依赖任何 OpenClAW 内部类型,这是解耦的关键。

4.1 请求 DTO

java 复制代码
package com.example.openclaw_demo.dto;

import lombok.Builder;
import lombok.Data;

import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.Size;

/** 对话请求 DTO */
@Data
@Builder
public class ChatRequestDTO {

    /** 用户输入的消息,不能为空 */
    @NotBlank(message = "消息内容不能为空")
    @Size(max = 10000, message = "消息长度不能超过 10000 字符")
    private String message;

    /** 覆盖全局 system prompt(可选) */
    private String systemPrompt;

    /** 覆盖默认模型(可选) */
    private String model;

    /** 覆盖默认 maxTokens(可选) */
    private Integer maxTokens;
}

4.2 响应 DTO

java 复制代码
package com.example.openclaw_demo.dto;

import lombok.Builder;
import lombok.Data;

/** 对话响应 DTO */
@Data
@Builder
public class ChatResponseDTO {

    /** AI 回复的文本内容 */
    private String content;

    /** 本次请求消耗的输入 token 数 */
    private int inputTokens;

    /** 本次请求消耗的输出 token 数 */
    private int outputTokens;

    /** 实际使用的模型名称 */
    private String model;

    /** 停止原因(end_turn / max_tokens 等) */
    private String stopReason;

    /** 请求耗时(毫秒) */
    private long elapsedMs;
}

4.3 Service 接口定义

java 复制代码
package com.example.openclaw_demo.service;

import com.example.openclaw_demo.dto.ChatRequestDTO;
import com.example.openclaw_demo.dto.ChatResponseDTO;

/**
 * AI 对话服务接口
 *
 * 这是业务层与底层 AI SDK 之间的抽象屏障:
 *   - 测试时可 Mock 此接口,无需真实 API Key
 *   - 未来切换到其他模型(如 GPT-4),只需替换实现类
 */
public interface ClawChatService {

    /**
     * 简单单轮对话(使用全局默认配置)
     * @param message 用户输入
     * @return AI 回复文本
     */
    String simpleChat(String message);

    /**
     * 完整对话(支持自定义配置)
     * @param request 请求 DTO
     * @return 响应 DTO(含 token 用量、耗时等)
     */
    ChatResponseDTO chat(ChatRequestDTO request);

    /**
     * 带系统提示词的对话(快捷方法)
     * @param systemPrompt 系统提示词
     * @param message      用户消息
     * @return AI 回复文本
     */
    String chatWithSystem(String systemPrompt, String message);
}

五、实现类:ClawChatServiceImpl

5.1 基础框架

java 复制代码
package com.example.openclaw_demo.service.impl;

import com.example.openclaw_demo.config.ClawProperties;
import com.example.openclaw_demo.dto.ChatRequestDTO;
import com.example.openclaw_demo.dto.ChatResponseDTO;
import com.example.openclaw_demo.exception.ClawApiException;
import com.example.openclaw_demo.exception.ClawRateLimitException;
import com.example.openclaw_demo.service.ClawChatService;
import io.openclaw.client.ClaudeService;
import io.openclaw.exception.ClaudeRateLimitException;
import io.openclaw.exception.ClaudeApiException;
import io.openclaw.model.request.ChatRequest;
import io.openclaw.model.response.ChatResponse;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.util.StringUtils;

@Slf4j
@Service
@RequiredArgsConstructor
public class ClawChatServiceImpl implements ClawChatService {

    private final ClaudeService claudeService;   // OpenClAW 提供
    private final ClawProperties clawProperties; // 我们自定义的配置

    // --------------------------------------------------------
    // 5.2  简单对话(内部调用完整实现)
    // --------------------------------------------------------
    @Override
    public String simpleChat(String message) {
        ChatRequestDTO req = ChatRequestDTO.builder()
                .message(message)
                .build();
        return chat(req).getContent();
    }

    // --------------------------------------------------------
    // 5.3  带系统提示词的快捷方法
    // --------------------------------------------------------
    @Override
    public String chatWithSystem(String systemPrompt, String message) {
        ChatRequestDTO req = ChatRequestDTO.builder()
                .systemPrompt(systemPrompt)
                .message(message)
                .build();
        return chat(req).getContent();
    }

    // --------------------------------------------------------
    // 5.4  核心实现:请求组装 + 异常转换 + 耗时统计
    // --------------------------------------------------------
    @Override
    public ChatResponseDTO chat(ChatRequestDTO request) {
        long startMs = System.currentTimeMillis();

        if (clawProperties.isLogRequests()) {
            log.info("[ClAW] 发送请求 model={}, msgLen={}",
                request.getModel() != null ? request.getModel() : clawProperties.getDefaultModel(),
                request.getMessage().length());
        }

        try {
            // 组装 OpenClAW 请求
            ChatRequest clawReq = buildClawRequest(request);

            // 调用底层 SDK
            ChatResponse clawResp = claudeService.chat(clawReq);

            // 转换为业务 DTO
            return toResponseDTO(clawResp, System.currentTimeMillis() - startMs);

        } catch (ClaudeRateLimitException e) {
            // 429 限流 → 转换为业务异常,让重试机制处理
            log.warn("[ClAW] 触发限流:{}", e.getMessage());
            throw new ClawRateLimitException("AI 服务请求频率超限,请稍后重试", e);

        } catch (ClaudeApiException e) {
            // 其他 API 错误
            log.error("[ClAW] API 调用失败 code={}, msg={}", e.getStatusCode(), e.getMessage());
            throw new ClawApiException("AI 服务调用失败: " + e.getMessage(), e);

        } catch (Exception e) {
            // 兜底:网络超时、序列化错误等
            log.error("[ClAW] 未知异常", e);
            throw new ClawApiException("AI 服务暂时不可用,请稍后重试", e);
        }
    }

    // --------------------------------------------------------
    // 私有方法:请求组装
    // --------------------------------------------------------
    private ChatRequest buildClawRequest(ChatRequestDTO dto) {
        // 确定使用的 systemPrompt:DTO 中指定的 > 全局配置
        String systemPrompt = StringUtils.hasText(dto.getSystemPrompt())
                ? dto.getSystemPrompt()
                : clawProperties.getGlobalSystemPrompt();

        return ChatRequest.builder()
                .model(dto.getModel() != null
                        ? dto.getModel()
                        : clawProperties.getDefaultModel())
                .maxTokens(dto.getMaxTokens() != null
                        ? dto.getMaxTokens()
                        : clawProperties.getDefaultMaxTokens())
                .systemPrompt(systemPrompt)
                .message(dto.getMessage())
                .build();
    }

    // --------------------------------------------------------
    // 私有方法:响应转换
    // --------------------------------------------------------
    private ChatResponseDTO toResponseDTO(ChatResponse resp, long elapsedMs) {
        return ChatResponseDTO.builder()
                .content(resp.getText())
                .model(resp.getModel())
                .stopReason(resp.getStopReason())
                .inputTokens(resp.getUsage().getInputTokens())
                .outputTokens(resp.getUsage().getOutputTokens())
                .elapsedMs(elapsedMs)
                .build();
    }
}

@RequiredArgsConstructor 会为所有 final 字段生成构造函数,配合 Spring 自动注入,无需手写 @Autowired。这是 Spring Boot 项目的推荐写法。

六、统一异常处理

6.1 自定义异常体系

java 复制代码
// ===== ClawException.java(基类)=====
package com.example.openclaw_demo.exception;

public class ClawException extends RuntimeException {
    private final int errorCode;

    public ClawException(String message, int errorCode) {
        super(message);
        this.errorCode = errorCode;
    }

    public ClawException(String message, int errorCode, Throwable cause) {
        super(message, cause);
        this.errorCode = errorCode;
    }

    public int getErrorCode() { return errorCode; }
}


// ===== ClawApiException.java(通用 API 错误)=====
public class ClawApiException extends ClawException {
    public ClawApiException(String message, Throwable cause) {
        super(message, 5001, cause);
    }
}


// ===== ClawRateLimitException.java(限流异常,可被重试)=====
public class ClawRateLimitException extends ClawException {
    public ClawRateLimitException(String message, Throwable cause) {
        super(message, 4029, cause);  // 4029 对应 HTTP 429
    }
}


// ===== ClawContextTooLongException.java(上下文超长)=====
public class ClawContextTooLongException extends ClawException {
    public ClawContextTooLongException(String message) {
        super(message, 4013);
    }
}

6.2 全局异常处理器

java 复制代码
package com.example.openclaw_demo.exception;

import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.validation.FieldError;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;

import java.time.LocalDateTime;
import java.util.HashMap;
import java.util.Map;

@Slf4j
@RestControllerAdvice
public class GlobalExceptionHandler {

    /** 限流异常 → 429 Too Many Requests */
    @ExceptionHandler(ClawRateLimitException.class)
    public ResponseEntity<ErrorResponse> handleRateLimit(ClawRateLimitException e) {
        log.warn("[ClAW] 限流: {}", e.getMessage());
        return ResponseEntity
                .status(HttpStatus.TOO_MANY_REQUESTS)
                .body(ErrorResponse.of(e.getErrorCode(), e.getMessage()));
    }

    /** 通用 AI 调用失败 → 502 Bad Gateway */
    @ExceptionHandler(ClawApiException.class)
    public ResponseEntity<ErrorResponse> handleApiError(ClawApiException e) {
        log.error("[ClAW] API 错误: {}", e.getMessage(), e);
        return ResponseEntity
                .status(HttpStatus.BAD_GATEWAY)
                .body(ErrorResponse.of(e.getErrorCode(), e.getMessage()));
    }

    /** 参数校验失败 → 400 Bad Request */
    @ExceptionHandler(MethodArgumentNotValidException.class)
    public ResponseEntity<ErrorResponse> handleValidation(MethodArgumentNotValidException e) {
        String msg = e.getBindingResult().getFieldErrors().stream()
                .map(FieldError::getDefaultMessage)
                .findFirst()
                .orElse("参数校验失败");
        return ResponseEntity
                .badRequest()
                .body(ErrorResponse.of(4000, msg));
    }

    /** 兜底 → 500 Internal Server Error */
    @ExceptionHandler(Exception.class)
    public ResponseEntity<ErrorResponse> handleAll(Exception e) {
        log.error("[ClAW] 未捕获异常", e);
        return ResponseEntity
                .internalServerError()
                .body(ErrorResponse.of(5000, "服务器内部错误"));
    }

    // ---- 内部 DTO ----
    public record ErrorResponse(int code, String message, String timestamp) {
        public static ErrorResponse of(int code, String message) {
            return new ErrorResponse(code, message, LocalDateTime.now().toString());
        }
    }
}

现在调用接口出错时,客户端会收到统一的 JSON 格式错误响应:

java 复制代码
{
  "code": 4029,
  "message": "AI 服务请求频率超限,请稍后重试",
  "timestamp": "2025-04-20T10:23:45.123"
}

七、重试机制:Spring Retry 集成

限流(429)是高频场景,加上自动重试可以大幅提升成功率。

7.1 添加依赖

XML 复制代码
<!-- pom.xml -->
<dependency>
    <groupId>org.springframework.retry</groupId>
    <artifactId>spring-retry</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-aspects</artifactId>
</dependency>

7.2 启用重试并配置

启动类上加注解:

java 复制代码
@SpringBootApplication
@EnableRetry   // 开启 Spring Retry
@EnableConfigurationProperties(ClawProperties.class)
public class OpenClawDemoApplication { ... }

ClawChatServiceImplchat() 方法上加重试注解:

java 复制代码
/**
 * @Retryable:
 *   retryFor   - 遇到这类异常时才重试(其他异常不重试,立即抛出)
 *   maxAttempts - 最多尝试次数(含第 1 次,所以是 1 + 重试次数)
 *   backoff    - 重试等待策略:delay 初始等待,multiplier 指数退避系数
 */
@Override
@Retryable(
    retryFor    = ClawRateLimitException.class,
    maxAttempts = 3,
    backoff     = @Backoff(delay = 1000, multiplier = 2.0)
)
public ChatResponseDTO chat(ChatRequestDTO request) {
    // ... 同上,省略
}

/**
 * @Recover:所有重试失败后的兜底方法
 * 方法签名必须与原方法兼容(返回值相同,第一个参数是异常类型)
 */
@Recover
public ChatResponseDTO recoverFromRateLimit(ClawRateLimitException e, ChatRequestDTO req) {
    log.error("[ClAW] 重试 3 次后仍然限流,放弃请求: {}", req.getMessage());
    throw new ClawRateLimitException("AI 服务繁忙,请 1 分钟后重试", e);
}

@Retryable 基于 Spring AOP,同一个类内部调用不会触发重试 。因此 simpleChat() 内部调用 chat() 时,重试注解是生效的(因为 simpleChat() 通过 Spring 代理调用 chat())。但如果你直接 this.chat(),则不会触发。

7.3 重试时序说明

第几次尝试 等待时间 说明
第 1 次(原始请求) --- 直接发送
第 2 次(第 1 次重试) 1000ms 遇到 429 后等 1 秒
第 3 次(第 2 次重试) 2000ms 指数退避,等 2 秒
仍失败 --- 进入 @Recover 方法

八、统一响应体封装

所有接口统一返回 ApiResult<T> 格式,方便前端统一处理:

java 复制代码
package com.example.openclaw_demo.common;

import lombok.Getter;

/**
 * 统一 API 响应体
 * @param <T> 数据类型
 */
@Getter
public class ApiResult<T> {

    private final boolean success;
    private final int     code;
    private final String  message;
    private final T       data;

    private ApiResult(boolean success, int code, String message, T data) {
        this.success = success;
        this.code    = code;
        this.message = message;
        this.data    = data;
    }

    public static <T> ApiResult<T> ok(T data) {
        return new ApiResult<>(true, 200, "success", data);
    }

    public static <T> ApiResult<T> fail(int code, String message) {
        return new ApiResult<>(false, code, message, null);
    }
}

响应示例:

java 复制代码
// 成功
{
  "success": true,
  "code": 200,
  "message": "success",
  "data": {
    "content": "Spring Boot 是一个简化 Spring 开发的框架...",
    "model": "claude-sonnet-4-20250514",
    "inputTokens": 24,
    "outputTokens": 98,
    "elapsedMs": 1423
  }
}

九、Controller 层整合

现在 Controller 变得非常简洁,只负责接收请求和返回响应:

java 复制代码
package com.example.openclaw_demo.controller;

import com.example.openclaw_demo.common.ApiResult;
import com.example.openclaw_demo.dto.ChatRequestDTO;
import com.example.openclaw_demo.dto.ChatResponseDTO;
import com.example.openclaw_demo.service.ClawChatService;
import lombok.RequiredArgsConstructor;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;

@RestController
@RequestMapping("/api/v1/chat")
@RequiredArgsConstructor
public class ChatController {

    private final ClawChatService clawChatService;   // 注入接口,不依赖实现

    /**
     * GET /api/v1/chat/simple?q=xxx
     * 快速测试接口
     */
    @GetMapping("/simple")
    public ApiResult<String> simpleChat(@RequestParam String q) {
        String reply = clawChatService.simpleChat(q);
        return ApiResult.ok(reply);
    }

    /**
     * POST /api/v1/chat
     * 完整对话接口,支持自定义参数
     * Body: { "message": "...", "systemPrompt": "...", "maxTokens": 512 }
     */
    @PostMapping
    public ApiResult<ChatResponseDTO> chat(@RequestBody @Validated ChatRequestDTO request) {
        ChatResponseDTO response = clawChatService.chat(request);
        return ApiResult.ok(response);
    }

    /**
     * POST /api/v1/chat/with-system
     * 带系统提示词的快捷接口
     */
    @PostMapping("/with-system")
    public ApiResult<String> chatWithSystem(@RequestParam String system,
                                               @RequestParam String q) {
        String reply = clawChatService.chatWithSystem(system, q);
        return ApiResult.ok(reply);
    }
}

测试接口

bash 复制代码
# GET 快速测试
curl "http://localhost:8080/api/v1/chat/simple?q=什么是依赖注入"

# POST 完整对话
curl -X POST http://localhost:8080/api/v1/chat \
  -H "Content-Type: application/json" \
  -d '{
    "message": "用 100 字解释 Spring AOP",
    "systemPrompt": "你是 Java 专家,回答简洁精准",
    "maxTokens": 256
  }'

# 参数校验测试(message 为空,应返回 400)
curl -X POST http://localhost:8080/api/v1/chat \
  -H "Content-Type: application/json" \
  -d '{"message": ""}'

十、单元测试:Mock ClawChatService

接口与实现分离的最大收益就在这里------Controller 层可以不启动 Spring 上下文进行测试:

java 复制代码
package com.example.openclaw_demo.controller;

import com.example.openclaw_demo.dto.ChatRequestDTO;
import com.example.openclaw_demo.dto.ChatResponseDTO;
import com.example.openclaw_demo.service.ClawChatService;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.http.MediaType;
import org.springframework.test.web.servlet.MockMvc;

import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.when;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;

// 只加载 Web 层,不启动完整 Spring 上下文
@WebMvcTest(ChatController.class)
class ChatControllerTest {

    @Autowired
    private MockMvc mockMvc;

    // Mock 掉 Service,无需真实 API Key
    @MockBean
    private ClawChatService clawChatService;

    @Test
    void testSimpleChat_success() throws Exception {
        when(clawChatService.simpleChat("你好"))
            .thenReturn("你好!有什么可以帮助你的?");

        mockMvc.perform(get("/api/v1/chat/simple").param("q", "你好"))
               .andExpect(status().isOk())
               .andExpect(jsonPath("$.success").value(true))
               .andExpect(jsonPath("$.data").value("你好!有什么可以帮助你的?"));
    }

    @Test
    void testChat_withFullResponse() throws Exception {
        ChatResponseDTO mockResp = ChatResponseDTO.builder()
                .content("Spring AOP 是面向切面编程框架...")
                .model("claude-sonnet-4-20250514")
                .inputTokens(20).outputTokens(80).elapsedMs(1200)
                .build();

        when(clawChatService.chat(any(ChatRequestDTO.class)))
            .thenReturn(mockResp);

        mockMvc.perform(post("/api/v1/chat")
                   .contentType(MediaType.APPLICATION_JSON)
                   .content("{\"message\":\"解释 Spring AOP\"}"))
               .andExpect(status().isOk())
               .andExpect(jsonPath("$.data.inputTokens").value(20))
               .andExpect(jsonPath("$.data.elapsedMs").value(1200));
    }

    @Test
    void testChat_emptyMessage_returns400() throws Exception {
        mockMvc.perform(post("/api/v1/chat")
                   .contentType(MediaType.APPLICATION_JSON)
                   .content("{\"message\":\"\"}"))
               .andExpect(status().isBadRequest())
               .andExpect(jsonPath("$.code").value(4000));
    }
}

三个测试用例覆盖:正常响应、完整 DTO 字段、参数校验。全程不需要 API Key,毫秒级运行,CI 友好。

十一、完整项目结构

bash 复制代码
openclaw-demo/ 
├── src/main/java/com/example/openclaw_demo/ 
│ ├── OpenClawDemoApplication.java # @EnableRetry 新增 
│ │ │ ├── config/ 
│ │ └── ClawProperties.java # ★ 新增:强类型配置 
│ │ │ ├── dto/ 
│ │ ├── ChatRequestDTO.java # ★ 新增:请求 DTO 
│ │ └── ChatResponseDTO.java # ★ 新增:响应 DTO 
│ │ │ ├── service/ 
│ │ ├── ClawChatService.java # ★ 新增:Service 接口 
│ │ └── impl/ 
│ │ └── ClawChatServiceImpl.java # ★ 新增:Service 实现 
│ │ │ ├── exception/ 
│ │ ├── ClawException.java # ★ 新增:异常基类 
│ │ ├── ClawApiException.java # ★ 新增
│ │ ├── ClawRateLimitException.java # ★ 新增 
│ │ └── GlobalExceptionHandler.java # ★ 新增:全局处理器 
│ │ │ ├── common/
│ │ └── ApiResult.java # ★ 新增:统一响应体
│ │ │ └── controller/ 
│ └── ChatController.java # 重构:注入接口 
│ ├── src/main/resources/ 
│ └── application.yml # 新增 app.claw.* 配置 
│ └── src/test/ 
└── ChatControllerTest.java # ★ 新增:Mock 单元测试

十二、本篇总结

✅ 本篇完成的核心工作

  1. ClawProperties:集中管理所有 AI 相关配置,支持 @Validated 校验,告别魔法字符串
  2. ClawChatService 接口:建立业务抽象层,不暴露 OpenClAW 内部类型,与底层实现解耦
  3. ClawChatServiceImpl:封装请求组装、默认值 fallback、耗时统计,逻辑内聚
  4. 异常体系:ClawException 继承树,按业务语义分类,配合 GlobalExceptionHandler 输出标准错误格式
  5. Spring Retry:@Retryable + 指数退避,429 限流自动重试,@Recover 兜底
  6. ApiResult<T>:统一响应体格式,前后端约定清晰
  7. @WebMvcTest + @MockBean:不依赖真实 API,秒级跑完 Controller 层单元测试

至此,你的项目已经具备了一套真正工程化的 AI 服务层。后续无论增加多少接口,都只需调用 ClawChatService 接口,底层的异常、重试、日志都已经在服务层统一处理。

下一篇我们将在这套架构基础上实现 流式输出(SSE),让 AI 的回复像打字机一样实时呈现,这是用户体验最敏感的功能点之一。

如果本文对你有帮助,欢迎 点赞 👍 · 收藏 ⭐ · 关注,你的支持是我持续创作的最大动力!

相关推荐
fai厅的秃头姐!5 小时前
OpenCV实战篇
人工智能·opencv·计算机视觉
TMT星球6 小时前
虎鲸文娱春苗编剧计划全面升级,已孵化20个剧本进入前期开发
大数据·人工智能
奔跑中的小象13 小时前
统信UOS + 天数AI卡部署SGLang服务手册
人工智能·uos·sglang·天数智芯
DevSecOps选型指南13 小时前
中国版Mythos,为何是悬镜安全灵脉CodeAI?
人工智能·安全
Shockang13 小时前
LangGraph 状态机实战
人工智能
刹那芳华199213 小时前
循环神经网络的从零开始实现(RNN)
人工智能·rnn·深度学习
阿童木写作13 小时前
跨境图片翻译工具多合一,批量图片视频字幕翻译加智能抠图
人工智能·python·音视频·语音识别
科技之门13 小时前
AI3D从建模到贴图、绑骨和动画的完整流程怎么做?V2Fun完整工作流指南
人工智能·3d·贴图
宇宙第一小趴菜13 小时前
二、机器学习的应用领域和发展史
人工智能·机器学习
前端开发江鸟13 小时前
我写过 MCP Server,却一直以为 MCP 只有 Tool
人工智能