📅 难度:⭐⭐☆☆☆ 进阶 | 阅读约 15 分钟 | 适用:Spring Boot 2.7.x / 3.x | Java 17+
📋 目录
-
配置管理
- 使用 ClawProperties 实现强类型配置
-
服务层设计
- 定义 Service 接口
- 实现类 ClaudeServiceImpl 包含:
- 基础单轮对话功能
- 支持系统提示词的对话场景
- 采用 Builder 模式封装复杂请求
-
异常处理机制
- 自定义异常体系
- 全局异常处理器
- 集成 Spring Retry 实现重试机制
-
响应标准化
- 统一响应体封装
-
接口层整合
- Controller 层实现
-
质量保障
- 基于 Mock 的 ClaudeService 单元测试
-
项目结构说明
- 完整项目目录规划
总结
一、前言:为什么要封装服务层?
在第一篇中,我们直接在 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 { ... }
在 ClawChatServiceImpl 的 chat() 方法上加重试注解:
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 单元测试
十二、本篇总结
✅ 本篇完成的核心工作
- ClawProperties:集中管理所有 AI 相关配置,支持 @Validated 校验,告别魔法字符串
- ClawChatService 接口:建立业务抽象层,不暴露 OpenClAW 内部类型,与底层实现解耦
- ClawChatServiceImpl:封装请求组装、默认值 fallback、耗时统计,逻辑内聚
- 异常体系:ClawException 继承树,按业务语义分类,配合 GlobalExceptionHandler 输出标准错误格式
- Spring Retry:@Retryable + 指数退避,429 限流自动重试,@Recover 兜底
- ApiResult<T>:统一响应体格式,前后端约定清晰
- @WebMvcTest + @MockBean:不依赖真实 API,秒级跑完 Controller 层单元测试
至此,你的项目已经具备了一套真正工程化的 AI 服务层。后续无论增加多少接口,都只需调用 ClawChatService 接口,底层的异常、重试、日志都已经在服务层统一处理。
下一篇我们将在这套架构基础上实现 流式输出(SSE),让 AI 的回复像打字机一样实时呈现,这是用户体验最敏感的功能点之一。
如果本文对你有帮助,欢迎 点赞 👍 · 收藏 ⭐ · 关注,你的支持是我持续创作的最大动力!