一、概述

走进一间崭新的画室,墙上挂着“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 特性 中文友好,阿里云认证 英文提示词最佳

现在你明白了吧?我们要做的,就是参照哥哥的优点,把弟弟教会。


三、设计图纸:技术架构逐层拆解

先别急着写代码,让我们用一张图看清数据如何流动。未来补全后的完整链路是这样的:

OpenAI API (DALL·E) ImageModel (Spring AI) Controller OpenAI API (DALL·E) ImageModel (Spring AI) Controller 用户 GET /example/image?prompt=... call(ImagePrompt) POST https://api.openai.com/v1/images/generations 图像 URL(临时链接) ImageResponse 对象 流式传输 PNG 图片 用户

原理浅谈

  • 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 实现

下面就是我为你准备的“教学版”完整代码。它包含三个核心能力:

  1. 普通单张生成(带缓冲流)
  2. 批量生成(DALL·E 2 最多 10 张)
  3. 高级生成(可指定模型、尺寸、质量)
@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张, 便宜

需要高清或
特殊尺寸?

可用 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 先决条件检查

  1. Java 21+
    java -version
    
  2. Maven 3.9+
  3. 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 个问题

我们在原项目中发现了几个必须纠正的地方:

  1. Controller 空壳:必须替换为我们上面给出的完整代码。
  2. README 描述错误:原文写成了 DashScope 示例,实际应为 OpenAI。
  3. 潜在 OOM 风险:直接 readAllBytes() 处理大图极其危险,已用流式传输规避。

此外,若使用中文提示词,可以预先调用翻译服务转成英文,效果会明显提升。


Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐