
工作流java使用教程:新手完成第一张作品的方法
本文所说的“工作流 Java”,是指使用 Java 程序调用工作流服务,完成一张图片的生成与下载。示例选择 ComfyUI:它把模型加载、提示词编码、采样和图片保存组织成节点工作流,并提供 HTTP API。Java 负责提交工作流、等待结果、取回图片;实际推理由 ComfyUI 及其配置的模型完成。
如果你想在 Java 中直接编写图形节点编辑器,或寻找一个名为“工作流 Java”的特定产品,本文并不覆盖那类实现。这里的目标很具体:在本机启动 ComfyUI,使用 Java 提交一个最小图片工作流,得到第一张作品。
一、适用场景与边界
这种组合适合已有 Java 服务需要调用图像生成能力的情况,例如内部创作工具、批量素材任务或原型验证。它也便于把生成流程拆成可复用的节点图,再由 Java 传入提示词、尺寸等参数。
它不等于把模型嵌入 Java 程序。ComfyUI 仍需单独运行,模型文件、显存、Python 环境和节点依赖也由它管理。若只是个人尝试,先用 ComfyUI 自带界面跑通工作流通常更容易定位问题;若要承载多人或高并发请求,还需额外设计队列、限流、超时、鉴权和资源隔离,不能把下面的单机示例直接当作生产服务。
示例采用 ComfyUI 常见的本地 HTTP API:向 /prompt 提交 API 格式工作流,随后查询 /history/{prompt_id},再从 /view 下载结果。不同版本、扩展节点和部署方式可能带来差异;如接口响应结构不符,应以当前服务版本及其 API 文档为准。
二、准备运行环境
- 安装并启动 ComfyUI,确认它能在界面中生成图片。默认本机地址通常是
http://127.0.0.1:8188;如果启动参数改过监听地址或端口,后文代码也要相应修改。 - 在 ComfyUI 可用的模型目录中放入兼容的 checkpoint,并记下文件名。模型许可、适用范围和生成效果取决于具体模型,不由 Java 或工作流自动保证。
- 在界面载入模型,先用一条简单提示词生成一次。如果界面本身都无法完成生成,先检查模型、节点和显存,不要急着排查 Java。
- 准备 JDK 11 或更高版本。下面使用 Java 11 的
HttpClient,并用 Jackson 处理 JSON。
在 ComfyUI 界面中创建工作流并使用“保存(API 格式)”或相应的 API 工作流导出功能,可以获得提交给 /prompt 的节点图。普通界面工作流 JSON 与 API 格式不一定相同:API 格式通常以节点 ID 为键,每个节点包含 class_type 和 inputs。不要把任意界面导出文件原样当成 API 工作流提交。
三、先理解最小工作流
本文的工作流包含六个节点:加载 checkpoint、编码正向提示词、编码负向提示词、创建空 latent、采样、解码并保存图片。节点间通过 ["节点ID", 输出序号] 引用上游结果。下面的 JSON 只是结构示意,模型文件名需要替换成你本机实际可用的名称;若模型或 ComfyUI 版本不接受某项输入,请先在界面生成 API 格式工作流,再按本机导出的结构调整。
{
"1": {
"class_type": "CheckpointLoaderSimple",
"inputs": {
"ckpt_name": "your-model.safetensors"
}
},
"2": {
"class_type": "CLIPTextEncode",
"inputs": {
"text": "a small red fox in a quiet forest, illustration",
"clip": ["1", 1]
}
},
"3": {
"class_type": "CLIPTextEncode",
"inputs": {
"text": "blurry, low quality",
"clip": ["1", 1]
}
},
"4": {
"class_type": "EmptyLatentImage",
"inputs": {
"width": 512,
"height": 512,
"batch_size": 1
}
},
"5": {
"class_type": "KSampler",
"inputs": {
"seed": 123456789,
"steps": 20,
"cfg": 7.0,
"sampler_name": "euler",
"scheduler": "normal",
"denoise": 1.0,
"model": ["1", 0],
"positive": ["2", 0],
"negative": ["3", 0],
"latent_image": ["4", 0]
}
},
"6": {
"class_type": "VAEDecode",
"inputs": {
"samples": ["5", 0],
"vae": ["1", 2]
}
},
"7": {
"class_type": "SaveImage",
"inputs": {
"filename_prefix": "java_first_art",
"images": ["6", 0]
}
}
}
节点编号是工作流内部标识,不是固定值。采样器名称、调度器名称、尺寸、步数和提示词效果都可能受模型、节点版本及硬件影响。示例中的 512×512、20 步只是便于入门的示例设置,不是所有模型的推荐值或硬性上限。高分辨率和更大的批量通常会增加显存及运行时间;具体可用范围应根据模型说明和本机资源逐步测试。
四、创建 Java 项目
下面给出一个 Maven 项目。Jackson 只负责 JSON 编解码;发请求使用 JDK 自带的 HTTP 客户端。创建 pom.xml:
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>example</groupId>
<artifactId>comfyui-java-demo</artifactId>
<version>1.0.0</version>
<properties>
<maven.compiler.release>11</maven.compiler.release>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<dependencies>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>2.17.2</version>
</dependency>
</dependencies>
</project>
版本号是一个明确的依赖示例;在实际项目中,应依据团队依赖策略选择并维护 Jackson 版本。将下面代码保存为 src/main/java/example/ComfyUiFirstImage.java。把 MODEL 改成 ComfyUI 能识别的 checkpoint 文件名。
五、可运行示例:提交、轮询并下载图片
package example;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.ObjectNode;
import java.net.URI;
import java.net.URLEncoder;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.time.Duration;
import java.util.UUID;
public class ComfyUiFirstImage {
private static final String BASE = "http://127.0.0.1:8188";
private static final String MODEL = "your-model.safetensors";
private static final ObjectMapper JSON = new ObjectMapper();
private static final HttpClient HTTP = HttpClient.newBuilder()
.connectTimeout(Duration.ofSeconds(10))
.build();
public static void main(String[] args) throws Exception {
ObjectNode workflow = buildWorkflow();
ObjectNode requestBody = JSON.createObjectNode();
requestBody.put("client_id", UUID.randomUUID().toString());
requestBody.set("prompt", workflow);
HttpResponse<String> submitted = send(
HttpRequest.newBuilder(URI.create(BASE + "/prompt"))
.timeout(Duration.ofSeconds(30))
.header("Content-Type", "application/json")
.POST(HttpRequest.BodyPublishers.ofString(
JSON.writeValueAsString(requestBody)))
.build());
JsonNode submitJson = JSON.readTree(submitted.body());
if (submitJson.has("error")) {
throw new IllegalStateException("工作流提交失败:" + submitJson);
}
JsonNode promptIdNode = submitJson.get("prompt_id");
if (promptIdNode == null) {
throw new IllegalStateException("响应中没有 prompt_id:" + submitted.body());
}
String promptId = promptIdNode.asText();
System.out.println("已提交任务:" + promptId);
JsonNode history = waitForHistory(promptId);
JsonNode outputs = history.path(promptId).path("outputs");
JsonNode image = findFirstImage(outputs);
if (image == null) {
throw new IllegalStateException(
"任务完成,但历史记录中没有找到图片输出:" + outputs);
}
String filename = image.path("filename").asText();
String subfolder = image.path("subfolder").asText("");
String type = image.path("type").asText("output");
String query = "filename=" + enc(filename)
+ "&subfolder=" + enc(subfolder)
+ "&type=" + enc(type);
HttpResponse<byte[]> downloaded = HTTP.send(
HttpRequest.newBuilder(URI.create(BASE + "/view?" + query))
.timeout(Duration.ofSeconds(60))
.GET()
.build(),
HttpResponse.BodyHandlers.ofByteArray());
checkStatus(downloaded.statusCode(), new String(
downloaded.body(), StandardCharsets.UTF_8));
Path output = Path.of("first-art.png");
Files.write(output, downloaded.body());
System.out.println("图片已保存到:" + output.toAbsolutePath());
}
private static ObjectNode buildWorkflow() {
ObjectNode graph = JSON.createObjectNode();
ObjectNode loader = node(graph, "1", "CheckpointLoaderSimple");
loader.putObject("inputs").put("ckpt_name", MODEL);
ObjectNode positive = node(graph, "2", "CLIPTextEncode");
ObjectNode posInputs = positive.putObject("inputs");
posInputs.put("text",
"a small red fox in a quiet forest, warm morning light, illustration");
posInputs.set("clip", link("1", 1));
ObjectNode negative = node(graph, "3", "CLIPTextEncode");
ObjectNode negInputs = negative.putObject("inputs");
negInputs.put("text", "blurry, low quality");
negInputs.set("clip", link("1", 1));
ObjectNode latent = node(graph, "4", "EmptyLatentImage");
ObjectNode latentInputs = latent.putObject("inputs");
latentInputs.put("width", 512);
latentInputs.put("height", 512);
latentInputs.put("batch_size", 1);
ObjectNode sampler = node(graph, "5", "KSampler");
ObjectNode sampleInputs = sampler.putObject("inputs");
sampleInputs.put("seed", 123456789);
sampleInputs.put("steps", 20);
sampleInputs.put("cfg", 7.0);
sampleInputs.put("sampler_name", "euler");
sampleInputs.put("scheduler", "normal");
sampleInputs.put("denoise", 1.0);
sampleInputs.set("model", link("1", 0));
sampleInputs.set("positive", link("2", 0));
sampleInputs.set("negative", link("3", 0));
sampleInputs.set("latent_image", link("4", 0));
ObjectNode decode = node(graph, "6", "VAEDecode");
ObjectNode decodeInputs = decode.putObject("inputs");
decodeInputs.set("samples", link("5", 0));
decodeInputs.set("vae", link("1", 2));
ObjectNode save = node(graph, "7", "SaveImage");
ObjectNode saveInputs = save.putObject("inputs");
saveInputs.put("filename_prefix", "java_first_art");
saveInputs.set("images", link("6", 0));
return graph;
}
private static ObjectNode node(ObjectNode graph, String id, String type) {
ObjectNode value = JSON.createObjectNode();
value.put("class_type", type);
graph.set(id, value);
return value;
}
private static com.fasterxml.jackson.databind.node.ArrayNode link(
String nodeId, int outputIndex) {
com.fasterxml.jackson.databind.node.ArrayNode value =
JSON.createArrayNode();
value.add(nodeId);
value.add(outputIndex);
return value;
}
private static JsonNode waitForHistory(String promptId) throws Exception {
long deadline = System.nanoTime() + Duration.ofMinutes(10).toNanos();
String url = BASE + "/history/" + enc(promptId);
while (System.nanoTime() < deadline) {
HttpResponse<String> response = send(
HttpRequest.newBuilder(URI.create(url))
.timeout(Duration.ofSeconds(30))
.GET()
.build());
JsonNode root = JSON.readTree(response.body());
JsonNode result = root.get(promptId);
if (result != null) {
JsonNode status = result.path("status");
if (status.path("status_str").asText().equals("error")) {
throw new IllegalStateException("ComfyUI 执行失败:" + result);
}
if (result.has("outputs")) {
return root;
}
}
Thread.sleep(1000);
}
throw new IllegalStateException("等待任务超时;可检查 ComfyUI 队列和服务日志");
}
private static JsonNode findFirstImage(JsonNode outputs) {
var nodes = outputs.elements();
while (nodes.hasNext()) {
JsonNode images = nodes.next().path("images");
if (images.isArray() && !images.isEmpty()) {
return images.get(0);
}
}
return null;
}
private static HttpResponse<String> send(HttpRequest request)
throws Exception {
HttpResponse<String> response = HTTP.send(
request, HttpResponse.BodyHandlers.ofString());
checkStatus(response.statusCode(), response.body());
return response;
}
private static void checkStatus(int code, String body) {
if (code < 200 || code >= 300) {
throw new IllegalStateException(
"HTTP 请求失败,状态码 " + code + ",响应:" + body);
}
}
private static String enc(String value) {
return URLEncoder.encode(value, StandardCharsets.UTF_8);
}
}
运行前检查:服务地址与端口正确;MODEL 的文件名和服务端模型清单一致;工作流所需节点可用。使用 Maven 编译运行时,可执行:
mvn -q compile
mvn -q dependency:build-classpath -Dmdep.outputFile=classpath.txt
java -cp "target/classes:$(cat classpath.txt)" example.ComfyUiFirstImage
上面的 classpath 命令适用于 macOS/Linux shell。Windows 命令行的路径分隔符和命令替换写法不同;也可以在 IDE 中导入 Maven 项目后运行主类。成功后,图片会保存到运行程序的当前目录,文件名为 first-art.png。
六、代码做了什么
- 提交任务:程序把工作流放进请求体的
prompt字段,并生成client_id。正常响应中会返回prompt_id;若是 HTTP 错误或响应缺少任务编号,程序直接报错。 - 轮询状态:示例每秒查询一次历史记录,最多等待十分钟。生成耗时受硬件、模型、队列长度和工作流影响,十分钟只是客户端等待上限,不代表服务端保证在该时间内完成。正式服务应把等待时间做成配置,并考虑取消任务和退避重试。
- 提取输出:程序遍历历史记录中的输出节点,找到第一张图片,再用返回的文件名、子目录和类型请求
/view。输出节点不一定只有一个;这里仅取第一张,方便完成入门流程。 - 保存文件:下载内容按字节写入磁盘。示例文件名固定,重复运行会覆盖当前目录中的同名文件;需要保留多次结果时,应使用任务 ID 或时间戳生成文件名。
历史接口在不同版本或执行状态下,响应字段可能有差异。示例使用常见的任务 ID、outputs 和图片元数据结构。如果任务确已完成但代码提示没有图片,不要直接断定模型失败:先查看服务端历史记录、输出目录和版本对应的 API 响应,再调整解析逻辑。
七、参数怎么改才不容易踩坑
提示词与负面提示词
正向提示词描述希望得到的内容,负向提示词表达希望模型避免的特征,但它们不是严格约束,也不能保证每次生成都精确遵守。不同模型对提示词的训练和解释方式不同。先从单一主体、明确场景和简洁风格开始,逐步调整;一次改动多个因素,很难判断效果变化来自哪里。
宽度、高度与批量
EmptyLatentImage 的宽高以像素表示,批量大小表示一次生成的 latent 数量。数值需要符合所用模型和工作流的要求,资源占用也会随尺寸、批量和模型变化。某些模型或节点对尺寸有额外偏好或限制,不能仅凭示例中的 512 推断适用于所有模型。第一次运行建议使用较小尺寸和批量 1,确认链路后再提高。
seed、steps、cfg 与采样器
seed 是随机种子。固定种子有助于在条件相近时进行对比,但不保证跨模型、软件版本、硬件或不同工作流得到完全相同的像素结果。steps 是采样步数;步数增加会影响耗时和效果,但并非越高越好。cfg 控制提示词引导相关行为,具体表现因模型而异。sampler_name 与 scheduler 必须是当前环境支持的值,不能假设所有安装都具有相同选项。
denoise 在这个从空 latent 开始的示例中设为 1.0。图生图工作流会有不同的 latent 输入和降噪设置,不能只把这里的数值改掉就认为工作流已经变成图生图。
八、常见错误与排查顺序
- 连接被拒绝:先在浏览器或命令行访问本机服务地址,确认 ComfyUI 正在运行、端口未改变。Java 与 ComfyUI 若在不同容器或主机中,
127.0.0.1指向的不是同一台机器;应使用正确的服务地址和容器网络配置。 - HTTP 400 或工作流校验失败:检查提交的是 API 格式;节点 ID 引用、输出序号、输入字段和节点类型都要与当前节点定义匹配。最稳妥的起点是让界面生成一次 API 工作流,再只修改提示词、模型名等已知字段。
- 模型找不到:确认模型已放到服务可扫描的位置,文件名拼写与
ckpt_name完全一致,并在 ComfyUI 中刷新或重启模型列表。路径不是任意本机路径字符串,通常应填写节点下拉列表显示的模型名称。 - 找不到 sampler 或节点:检查名称是否来自当前安装版本的选项列表。自定义节点缺失、节点升级或工作流依赖不全,都会导致节点不可用;不要只修改 Java 代码来掩盖服务端缺少依赖的问题。
- 显存不足或生成失败:先降低宽高和批量,关闭其他占用显存的任务,再查看 ComfyUI 控制台日志。不要把客户端超时误认为服务端已停止计算;请求超时后应核对队列和历史状态,避免重复提交造成额外负载。
- 任务有结果但下载失败:核对历史输出中的
filename、subfolder和type,并确保 Java 请求的是同一个 ComfyUI 实例。不要自行拼接未经编码的文件名到 URL;示例使用 UTF-8 URL 编码处理查询参数。 - 运行时找不到 Jackson:确认 Maven 依赖已下载,并在 IDE 中以 Maven 项目导入。直接使用
java -cp target/classes不会自动包含第三方依赖。
九、安全与上线边界
本例假设 ComfyUI 只在可信本机环境使用。开发者不应把没有访问控制的生成服务直接暴露到公网,也不应将开放接口交给不可信用户任意提交工作流。未授权的请求可能消耗 GPU、磁盘和网络资源;具体风险取决于服务部署、接口配置及可用节点。若必须远程访问,应在受控网络或经过身份认证的服务后使用,并配置访问控制、限流、请求体大小限制和资源监测。不要把管理接口的可访问性误当成安全认证。
生产环境中还应限制输出文件的保存期限和可下载范围,避免把用户输入直接拼成文件路径或命令;Java 应对服务端响应进行校验,记录必要的任务 ID 和错误摘要,避免日志泄露敏感提示词或凭据。若通过反向代理访问,需确认代理转发规则和超时设置适合较长任务。具体的鉴权与部署配置随 ComfyUI 版本、代理和部署架构而异,本文示例没有提供,也不能声称默认接口具备身份认证。
使用模型生成内容时,还要遵守所用模型及素材的许可条款、平台规则和适用法律。模型输出并不自动获得可商用、可公开发布或不侵权的保证;涉及人物、商标、受保护作品或敏感主题时,应单独评估风险。工作流和 Java 客户端只是调用与处理工具,不会替代内容审核、授权确认或安全策略。
十、完成第一张作品之后
先确认这条最小链路完整:ComfyUI 能单独生成;Java 能成功提交并取得任务 ID;历史记录能找到图片;Java 能下载并在本地打开文件。之后再把提示词、尺寸、seed 和模型名改为命令行参数或配置项,按业务需要增加任务队列、并发控制和错误处理。每次只改变一项,保存工作流与运行记录,能让结果复现和问题排查都更容易。
用 Java 调用工作流的核心并不神秘:提交符合服务端要求的图,等待异步任务完成,再按服务返回的元数据取回产物。真正需要谨慎的是模型与节点的兼容性、资源消耗,以及接口暴露后的安全边界。把这些边界厘清,第一张作品就不只是“跑出来”,也能成为后续开发的可靠起点。





