SpringBoot 連接 OpenAI API 教程

一、環境準備

1.1 創建 SpringBoot 項目

使用 Spring Initializr 創建項目,選擇以下依賴:

  • Spring Web
  • Spring Boot DevTools
  • Lombok

1.2 添加 OpenAI 依賴

pom.xml 中添加:

<dependencies>
<!-- Spring Boot Starter -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>

<!-- HTTP客户端 -->
<dependency>
<groupId>org.apache.httpcomponents</groupId>
<artifactId>httpclient</artifactId>
<version>4.5.13</version>
</dependency>

<!-- JSON 處理 -->
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
</dependency>

<!-- 配置屬性支持 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>
<optional>true</optional>
</dependency>
</dependencies>

二、配置 OpenAI

2.1 配置文件

application.yml:

openai:
api:
key: ${OPENAI_API_KEY:your-api-key-here}
url: https://api.openai.com/v1
timeout: 30000
max-tokens: 1000
temperature: 0.7
model: gpt-3.5-turbo

spring:
jackson:
serialization:
indent_output: true

2.2 配置類

OpenAiConfig.java:

package com.example.openai.config;

import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Configuration;

@Data
@Configuration
@ConfigurationProperties(prefix = "openai.api")
public class OpenAiConfig {
private String key;
private String url;
private Integer timeout;
private Integer maxTokens;
private Double temperature;
private String model;
}

三、核心實現

3.1 請求/響應 DTO

OpenAiRequest.java:

package com.example.openai.dto;

import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;

import java.util.List;

@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class OpenAiRequest {
private String model;
private List<Message> messages;
private Double temperature;
@JsonProperty("max_tokens")
private Integer maxTokens;

@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public static class Message {
private String role;
private String content;
}
}

OpenAiResponse.java:

package com.example.openai.dto;

import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.Data;

import java.util.List;

@Data
public class OpenAiResponse {
private String id;
private String object;
private Long created;
private String model;
private List<Choice> choices;
private Usage usage;

@Data
public static class Choice {
private Message message;
private Integer index;
@JsonProperty("finish_reason")
private String finishReason;

@Data
public static class Message {
private String role;
private String content;
}
}

@Data
public static class Usage {
@JsonProperty("prompt_tokens")
private Integer promptTokens;
@JsonProperty("completion_tokens")
private Integer completionTokens;
@JsonProperty("total_tokens")
private Integer totalTokens;
}
}

3.2 自定義異常

OpenAiException.java:

package com.example.openai.exception;

public class OpenAiException extends RuntimeException {
private final String code;

public OpenAiException(String message) {
super(message);
this.code = "OPENAI_ERROR";
}

public OpenAiException(String code, String message) {
super(message);
this.code = code;
}

public String getCode() {
return code;
}
}

3.3 服務層實現

OpenAiService.java:

package com.example.openai.service;

import com.example.openai.config.OpenAiConfig;
import com.example.openai.dto.OpenAiRequest;
import com.example.openai.dto.OpenAiResponse;
import com.example.openai.exception.OpenAiException;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.http.HttpEntity;
import org.apache.http.HttpResponse;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.entity.StringEntity;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClients;
import org.apache.http.util.EntityUtils;
import org.springframework.stereotype.Service;

import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.List;

@Slf4j
@Service
@RequiredArgsConstructor
public class OpenAiService {

private final OpenAiConfig config;
private final ObjectMapper objectMapper;

/**
* 發送消息到OpenAI
*/
public String sendMessage(String prompt) {
return sendMessage(prompt, config.getModel());
}

/**
* 發送消息到OpenAI(指定模型)
*/
public String sendMessage(String prompt, String model) {
List<OpenAiRequest.Message> messages = new ArrayList<>();
messages.add(OpenAiRequest.Message.builder()
.role("user")
.content(prompt)
.build());

return sendChatRequest(messages, model);
}

/**
* 發送對話請求
*/
public String sendChatRequest(List<OpenAiRequest.Message> messages, String model) {
OpenAiRequest request = OpenAiRequest.builder()
.model(model)
.messages(messages)
.temperature(config.getTemperature())
.maxTokens(config.getMaxTokens())
.build();

try {
OpenAiResponse response = callOpenAiApi(request);
if (response.getChoices() != null && !response.getChoices().isEmpty()) {
return response.getChoices().get(0).getMessage().getContent();
}
throw new OpenAiException("No response from OpenAI");
} catch (Exception e) {
log.error("調用OpenAI API失敗", e);
throw new OpenAiException("API_CALL_ERROR", "調用OpenAI API失敗: " + e.getMessage());
}
}

/**
* 調用OpenAI API
*/
private OpenAiResponse callOpenAiApi(OpenAiRequest request) throws Exception {
String url = config.getUrl() + "/chat/completions";

try (CloseableHttpClient httpClient = HttpClients.createDefault()) {
HttpPost httpPost = new HttpPost(url);

// 設置請求頭
httpPost.setHeader("Content-Type", "application/json");
httpPost.setHeader("Authorization", "Bearer " + config.getKey());

// 設置請求體
String requestBody = objectMapper.writeValueAsString(request);
httpPost.setEntity(new StringEntity(requestBody, StandardCharsets.UTF_8));

// 執行請求
HttpResponse response = httpClient.execute(httpPost);
HttpEntity entity = response.getEntity();

// 解析響應
String responseString = EntityUtils.toString(entity, StandardCharsets.UTF_8);
log.debug("OpenAI響應: {}", responseString);

if (response.getStatusLine().getStatusCode() == 200) {
return objectMapper.readValue(responseString, OpenAiResponse.class);
} else {
throw new OpenAiException("API_ERROR", "OpenAI API錯誤: " + responseString);
}
}
}

/**
* 多輪對話
*/
public String continueConversation(List<OpenAiRequest.Message> history) {
return sendChatRequest(history, config.getModel());
}
}

3.4 控制器層

OpenAiController.java:

package com.example.openai.controller;

import com.example.openai.dto.OpenAiRequest;
import com.example.openai.service.OpenAiService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.*;

import java.util.ArrayList;
import java.util.List;

@RestController
@RequestMapping("/api/openai")
@RequiredArgsConstructor
@Tag(name = "OpenAI控制器", description = "OpenAI API接口")
public class OpenAiController {

private final OpenAiService openAiService;

@PostMapping("/chat")
@Operation(summary = "單次對話")
public String chat(@RequestParam String message) {
return openAiService.sendMessage(message);
}

@PostMapping("/chat-with-model")
@Operation(summary = "指定模型對話")
public String chatWithModel(
@RequestParam String message,
@RequestParam(required = false, defaultValue = "gpt-3.5-turbo") String model) {
return openAiService.sendMessage(message, model);
}

@PostMapping("/conversation")
@Operation(summary = "多輪對話")
public String conversation(@RequestBody List<OpenAiRequest.Message> messages) {
return openAiService.continueConversation(messages);
}

@PostMapping("/translate")
@Operation(summary = "翻譯")
public String translate(
@RequestParam String text,
@RequestParam String targetLanguage) {
String prompt = String.format("請將以下文本翻譯成%s:\n%s", targetLanguage, text);
return openAiService.sendMessage(prompt);
}

@PostMapping("/summarize")
@Operation(summary = "總結")
public String summarize(@RequestParam String text) {
String prompt = String.format("請總結以下內容:\n%s", text);
return openAiService.sendMessage(prompt);
}

@PostMapping("/code-review")
@Operation(summary = "代碼審查")
public String codeReview(@RequestParam String code) {
String prompt = String.format("請審查以下代碼,指出潛在問題和改進建議:\n```\n%s\n```", code);
return openAiService.sendMessage(prompt);
}
}

四、進階功能

4.1 異步調用實現

AsyncOpenAiService.java:

package com.example.openai.service;

import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Service;

import java.util.concurrent.CompletableFuture;

@Slf4j
@Service
@RequiredArgsConstructor
public class AsyncOpenAiService {

private final OpenAiService openAiService;

@Async
public CompletableFuture<String> sendMessageAsync(String prompt) {
log.info("開始異步調用OpenAI: {}", prompt);
try {
String result = openAiService.sendMessage(prompt);
return CompletableFuture.completedFuture(result);
} catch (Exception e) {
log.error("異步調用OpenAI失敗", e);
return CompletableFuture.failedFuture(e);
}
}
}

4.2 配置異步支持

AsyncConfig.java:

package com.example.openai.config;

import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.annotation.EnableAsync;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;

import java.util.concurrent.Executor;

@Configuration
@EnableAsync
public class AsyncConfig {

public Executor taskExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(5);
executor.setMaxPoolSize(10);
executor.setQueueCapacity(100);
executor.setThreadNamePrefix("OpenAI-Async-");
executor.initialize();
return executor;
}
}

4.3 流式響應支持(SSE)

StreamController.java:

package com.example.openai.controller;

import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.MediaType;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;

import java.io.IOException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

@Slf4j
@RestController
@RequestMapping("/api/stream")
@RequiredArgsConstructor
public class StreamController {

private final OpenAiService openAiService;
private final ExecutorService executor = Executors.newCachedThreadPool();

@GetMapping(value = "/chat", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
public SseEmitter streamChat(@RequestParam String message) {
SseEmitter emitter = new SseEmitter(60000L);

executor.execute(() -> {
try {
// 模擬流式響應
String response = openAiService.sendMessage(message);
String[] words = response.split(" ");

for (int i = 0; i < words.length; i++) {
Thread.sleep(100); // 模擬延遲
emitter.send(SseEmitter.event()
.name("message")
.data(words[i] + " "));

if (i % 10 == 0) {
emitter.send(SseEmitter.event()
.name("progress")
.data("已發送 " + (i + 1) + "/" + words.length + " 個單詞"));
}
}

emitter.send(SseEmitter.event()
.name("complete")
.data("消息發送完成"));
emitter.complete();

} catch (Exception e) {
emitter.completeWithError(e);
}
});

return emitter;
}
}

五、測試

5.1 測試控制器

OpenAiControllerTest.java:

package com.example.openai.controller;

import com.example.openai.config.OpenAiConfig;
import com.example.openai.service.OpenAiService;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.mockito.Mockito;
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.test.web.servlet.MockMvc;

import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;

@WebMvcTest(OpenAiController.class)
class OpenAiControllerTest {

@Autowired
private MockMvc mockMvc;

@MockBean
private OpenAiService openAiService;

@MockBean
private OpenAiConfig openAiConfig;

@BeforeEach
void setUp() {
Mockito.when(openAiService.sendMessage(Mockito.anyString()))
.thenReturn("這是OpenAI的回覆");
}

@Test
void testChat() throws Exception {
mockMvc.perform(post("/api/openai/chat")
.param("message", "你好"))
.andExpect(status().isOk())
.andExpect(content().string("這是OpenAI的回覆"));
}
}

5.2 測試Service

OpenAiServiceTest.java:

package com.example.openai.service;

import com.example.openai.config.OpenAiConfig;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.mockito.Mockito;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.mock.mockito.MockBean;

import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Paths;

import static org.junit.jupiter.api.Assertions.assertNotNull;

@SpringBootTest
class OpenAiServiceTest {

@Autowired
private OpenAiService openAiService;

@MockBean
private OpenAiConfig openAiConfig;

@MockBean
private ObjectMapper objectMapper;

@BeforeEach
void setUp() throws IOException {
Mockito.when(openAiConfig.getKey()).thenReturn("test-key");
Mockito.when(openAiConfig.getUrl()).thenReturn("https://api.openai.com/v1");
Mockito.when(openAiConfig.getTemperature()).thenReturn(0.7);
Mockito.when(openAiConfig.getMaxTokens()).thenReturn(1000);
Mockito.when(openAiConfig.getModel()).thenReturn("gpt-3.5-turbo");
}

@Test
void testSendMessage() {
String response = openAiService.sendMessage("測試消息");
assertNotNull(response);
}
}

六、配置文件示例

6.1 Docker 配置

Dockerfile:

FROM openjdk:11-jre-slim
WORKDIR /app
COPY target/*.jar app.jar
EXPOSE 8080
ENTRYPOINT ["java", "-jar", "app.jar"]

6.2 Docker Compose 配置

docker-compose.yml:

version: '3.8'
services:
openai-app:
build: .
ports:
- "8080:8080"
environment:
- OPENAI_API_KEY=${OPENAI_API_KEY}
- SPRING_PROFILES_ACTIVE=prod
restart: unless-stopped

七、使用示例

7.1 啓動應用

# 設置環境變量
export OPENAI_API_KEY=your-api-key

# 啓動應用
mvn spring-boot:run

7.2 API 調用示例

# 單次對話
curl -X POST "http://localhost:8080/api/openai/chat?message=你好,介紹一下Spring Boot"

# 翻譯功能
curl -X POST "http://localhost:8080/api/openai/translate?text=Hello World&targetLanguage=中文"

# 代碼審查
curl -X POST "http://localhost:8080/api/openai/code-review?code=public class Test { public static void main(String[] args) { System.out.println(\"Hello\"); } }"

7.3 流式響應測試

<!-- 前端測試頁面 -->
<!DOCTYPE html>
<html>
<head>
<title>OpenAI SSE Test</title>
</head>
<body>
<input id="message" placeholder="輸入消息">
<button onclick="sendMessage()">發送</button>
<div id="response"></div>

<script>
function sendMessage() {
const message = document.getElementById('message').value;
const eventSource = new EventSource(`/api/stream/chat?message=${encodeURIComponent(message)}`);

eventSource.onmessage = function(event) {
document.getElementById('response').innerHTML += event.data;
};

eventSource.onerror = function() {
eventSource.close();
};
}
</script>
</body>
</html>

八、安全配置

8.1 添加API密鑰驗證

SecurityConfig.java:

package com.example.openai.config;

import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;

@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {

@Override
protected void configure(HttpSecurity http) throws Exception {
http
.cors().disable()
.csrf().disable()
.authorizeRequests()
.antMatchers("/api/openai/**").authenticated()
.antMatchers("/api/stream/**").permitAll()
.anyRequest().permitAll()
.and()
.httpBasic();
}
}

總結

本教程實現了 SpringBoot 連接 OpenAI 的完整方案,包括:

  1. 基礎配置:API密鑰、超時設置、模型配置
  2. 核心功能:對話、翻譯、總結、代碼審查
  3. 進階功能:異步調用、流式響應(SSE)
  4. 測試覆蓋:單元測試和集成測試
  5. 部署方案:Docker容器化部署
  6. 安全配置:基本的API安全保護

可以根據實際需求擴展更多功能,如:

  • 添加對話歷史管理
  • 實現Token使用統計
  • 添加限流和熔斷機制
  • 集成其他AI服務(如文心一言、通義千問等)