【第38篇-续】打造你的 AI 画师:Spring AI + OpenAI DALL·E 图像生成
一、概述
走进一间崭新的画室,墙上挂着“OpenAI DALL·E 绘画示例”的牌子,但画架是空的,颜料还没挤出来。这就是 spring-ai-alibaba-openai-image 项目的现状——一个配置正确、依赖齐全,唯独核心逻辑没有实现的 Spring Boot 工程。
别担心,我们一起把它填满。在动手之前,我们先来理解这幅“设计蓝图”为什么这样画。
二、俯瞰全局:项目到底长什么样?
2.1 项目定位
| 项目 | 说明 |
|---|---|
| 名称 | spring-ai-alibaba-openai-image-example |
| 技术栈 | Spring Boot 3.x + Spring AI + OpenAI DALL·E |
| 运行端口 | 10009 |
| 现状 | ⚠️ Controller 仅有一个空类,标记 // todo: not impl yet. |
它和上文已经完工的 DashScope 图像生成项目(端口 10008)是一对双胞胎。哥哥用阿里云的通义万相,弟弟用 OpenAI 的 DALL·E,但弟弟还没学会画画。
2.2 兄弟俩对比
| 对比维度 | DashScope 图像生成 | OpenAI 图像生成(本项目) |
|---|---|---|
| 使用的模型 | 阿里云通义万相 | OpenAI DALL·E 2/3 |
| Spring AI 依赖 | spring-ai-alibaba-starter-dashscope |
spring-ai-starter-model-openai |
| 实现状态 | ✅ 完成(可直跑) | ❌ 空壳(需补充) |
| API 特性 | 中文友好,阿里云认证 | 英文提示词最佳 |
现在你明白了吧?我们要做的,就是参照哥哥的优点,把弟弟教会。
三、设计图纸:技术架构逐层拆解
先别急着写代码,让我们用一张图看清数据如何流动。未来补全后的完整链路是这样的:
原理浅谈:
- Spring AI 抽象出了
ImageModel接口,我们只需要注入它,调用call()就屏蔽了底层 HTTP 细节。 - 你看到的那一行
imageModel.call(new ImagePrompt(...)),背后其实是 Spring AI 帮我们拼装 HTTP 请求头、解析 JSON 响应、封装成ImageResponse对象。 - 拿到图片 URL 后,项目采用流式复制(8KB 缓冲区)直接写回客户端,避免一次加载整张大图导致内存溢出。
2.3 依赖分析:只需一块积木
整个项目只多了一行关键依赖:
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-starter-model-openai</artifactId>
</dependency>
这个 starter 会自动注册 ImageModel 实现类,读取 application.yml 里的 OpenAI 配置,我们几乎不需要再写任何对接代码。
四、把“空壳”填肉:完整 Controller 实现
下面就是我为你准备的“教学版”完整代码。它包含三个核心能力:
- 普通单张生成(带缓冲流)
- 批量生成(DALL·E 2 最多 10 张)
- 高级生成(可指定模型、尺寸、质量)
@RestController
@RequestMapping("/example")
public class OpenAIImageController {
private final ImageModel imageModel;
private static final String DEFAULT_PROMPT =
"A futuristic AI-themed image with vibrant colors";
public OpenAIImageController(ImageModel imageModel) {
this.imageModel = imageModel;
}
@GetMapping("/image")
public void image(HttpServletResponse response) {
ImageResponse imageResponse =
imageModel.call(new ImagePrompt(DEFAULT_PROMPT));
String imageUrl = imageResponse.getResult().getOutput().getUrl();
try {
URL url = URI.create(imageUrl).toURL();
try (InputStream in = url.openStream();
OutputStream out = response.getOutputStream()) {
response.setContentType(MediaType.IMAGE_PNG_VALUE);
byte[] buffer = new byte[8192];
int bytesRead;
while ((bytesRead = in.read(buffer)) != -1) {
out.write(buffer, 0, bytesRead);
}
out.flush();
}
} catch (IOException e) {
response.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
}
}
@GetMapping("/image/multi")
public ResponseEntity<Collection<String>> generateMultiImages(
@RequestParam(defaultValue = "A cute coding cat") String prompt,
@RequestParam(defaultValue = "2") int n) {
int count = Math.min(Math.max(n, 1), 10);
ImageOptions options = ImageOptionsBuilder.builder()
.N(count)
.build();
ImageResponse response = imageModel.call(new ImagePrompt(prompt, options));
Set<String> urls = response.getResults().stream()
.map(r -> r.getOutput().getUrl())
.collect(Collectors.toSet());
return ResponseEntity.ok(urls);
}
@GetMapping("/image/advanced")
public ResponseEntity<?> generateAdvancedImage(
@RequestParam String prompt,
@RequestParam(defaultValue = "dall-e-3") String model,
@RequestParam(defaultValue = "1024x1024") String size,
@RequestParam(defaultValue = "standard") String quality) {
if (!Set.of("256x256","512x512","1024x1024","1792x1024","1024x1792").contains(size))
return ResponseEntity.badRequest().body(Map.of("error", "Invalid size"));
if (!Set.of("dall-e-2","dall-e-3").contains(model))
return ResponseEntity.badRequest().body(Map.of("error", "Invalid model"));
ImageOptions options = ImageOptionsBuilder.builder()
.model(model)
.width(Integer.parseInt(size.split("x")[0]))
.height(Integer.parseInt(size.split("x")[1]))
.quality(quality)
.N(1)
.build();
try {
ImageResponse resp = imageModel.call(new ImagePrompt(prompt, options));
return ResponseEntity.ok(resp.getResult().getOutput().getUrl());
} catch (Exception e) {
return ResponseEntity.internalServerError()
.body(Map.of("error", e.getMessage()));
}
}
}
人性化细节解释:
- 我们之所以用 英文默认提示词,是因为 DALL·E 对英文的理解远比中文精准。
- 流式传输时,我们用 8KB 的“小勺子”一勺一勺地舀数据,而不是端着一大盆水直接浇,避免服务器内存被一张大图撑爆。
- 高级接口里加了尺寸和模型的合法性校验,防止传错参数直接 500 错误,用户会一头雾水。
五、模型挑选指南:DALL-E 2 还是 DALL-E 3?
两种模型特性差异很大,选错了可能又贵又不好用。我画了一个决策流程图帮你选:
简单总结:
| 模型 | 最大批量 | 分辨率选项 | 质量级别 | 费用(约) |
|---|---|---|---|---|
| DALL·E 2 | 10 张 | 256x256 – 1024x1024 | 固定 | 低成本 |
| DALL·E 3 | 1 张 | 1024x1024,1792x1024,1024x1792 | standard / hd | 较高 |
建议:日常开发调试用 DALL·E 2 就好,最终给老板看效果时再换成 DALL·E 3。
六、从零跑起来:部署实操指南
好,蓝图和代码齐了,现在让我们把项目唤醒。
6.1 先决条件检查
- Java 21+
java -version - Maven 3.9+
- OpenAI API Key
- 前往 OpenAI Platform 注册
- 需要海外手机号验证,并绑定信用卡获取额度
- 生成类似
sk-xxxxxxxx的 Key
6.2 本地启动七步走
# 1. 进入项目目录
cd spring-ai-alibaba-openai-image-example/openai-image
# 2. 设置环境变量(务必替换成你的 key)
export AI_OPENAI_API_KEY=sk-your-key-here
# 3. (可选)设置代理环境变量
export HTTP_PROXY=http://127.0.0.1:7890
export HTTPS_PROXY=http://127.0.0.1:7890
# 4. 将前面提供的 Controller 代码粘贴到指定文件
# 5. 启动应用
mvn spring-boot:run
看到 Started OpenAIImageApplication in ... 即表示成功。
6.3 测试你的 AI 画师
打开新终端,用 curl 和它对话:
# 生成一张未来感图片,保存到 test.png
curl http://localhost:10009/example/image -o test.png
# 批量生成 3 张“机器人”
curl "http://localhost:10009/example/image/multi?prompt=A robot&n=3"
# 调用 DALL·E 3 生成赛博朋克宽幅高清图
curl "http://localhost:10009/example/image/advanced?prompt=A cyberpunk city at night&model=dall-e-3&size=1792x1024&quality=hd"
6.4 打包与持久化部署(Docker 方式)
生产环境建议容器化:
FROM eclipse-temurin:21-jre-alpine
WORKDIR /app
COPY target/*.jar app.jar
ENV AI_OPENAI_API_KEY=""
EXPOSE 10009
ENTRYPOINT ["java", "-jar", "app.jar"]
部署命令:
docker build -t openai-image .
docker run -d --name openai-image -p 10009:10009 \
-e AI_OPENAI_API_KEY=sk-xxx \
openai-image
七、避开这些坑!原项目遗留的 3 个问题
我们在原项目中发现了几个必须纠正的地方:
- Controller 空壳:必须替换为我们上面给出的完整代码。
- README 描述错误:原文写成了 DashScope 示例,实际应为 OpenAI。
- 潜在 OOM 风险:直接
readAllBytes()处理大图极其危险,已用流式传输规避。
此外,若使用中文提示词,可以预先调用翻译服务转成英文,效果会明显提升。
AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐

所有评论(0)