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 的回复像打字机一样实时呈现,这是用户体验最敏感的功能点之一。

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

相关推荐
qyr67892 小时前
全球多旋翼无人机动力系统市场分析报告
大数据·人工智能·数据分析·市场报告·多旋翼无人机动力系统
Techblog of HaoWANG2 小时前
目标检测与跟踪(15)-- conda 环境与 roslaunch 节点解释器不一致问题的排查与工程化修复
人工智能·目标检测·计算机视觉·机器人·conda
2501_947908202 小时前
2026钢铁冶金重载机器人怎么选?五大品牌深度对比与焊接应用方案
人工智能·机器人
说实话起个名字真难啊2 小时前
2026数字中国创新大赛数字安全赛道writeup之web题目一
java·前端·安全
后端AI实验室2 小时前
我用AI把一个外包需求从30天压到5天交付,然后客户说:下次还找你
java·ai
爱编程的小吴2 小时前
PyTorch+Transformer大模型入门到精通:LLM训练、推理、量化、部署全攻略
人工智能·pytorch·transformer
Yuanxl9032 小时前
pytorch-优化器
人工智能·pytorch·python
沅柠-AI营销2 小时前
TOB 工业制造与高端装备行业:AI 语义搜索赋能企业精准获客
人工智能·ai搜索优化·geo优化·企业降本·制造业获客·tob营销·b2b获客
2601_949816682 小时前
如何在 Spring Boot 中配置数据库?
数据库·spring boot·后端