1服务概述
通用打印中间件是一个运行在 Windows 上的 JavaFX 本地打印服务,以 Socket-HTTP 方式对外提供打印能力。 业务系统(Java / C# / 前端 JS 等任何能发 HTTP 请求的语言)把打印内容发到本服务, 由服务调用本机打印机驱动完成真实打印,并返回真实的打印结果 (任务未离开打印队列、缺纸、卡纸、超时等均返回失败并删除任务)。
| 项目 | 说明 |
|---|---|
| 默认地址 | http://127.0.0.1:8089(端口可在程序界面修改,勾选"允许局域网访问"后监听 0.0.0.0) |
| 协议 | HTTP/1.1,支持 GET / POST,参数为 application/x-www-form-urlencoded 或 QueryString |
| 字符编码 | UTF-8(中文参数必须 URL 编码) |
| 跨域 | 已支持 CORS(浏览器可直接调用) |
| 配置持久化 | 程序目录下 print-middleware.properties,设置后自动保存、重启生效 |
| 内置打印类型 | LABEL 标签、A4、A5、BARCODE 条码、RECEIPT 票据,另支持自定义类型 |
2通用约定
2.1 请求方式
所有接口同时兼容 GET 与 POST。内容较长时(Base64 图片 / 文件 / ESC/POS 脚本)请务必使用 POST, GET 的 URL 长度受浏览器和服务器限制(一般 8KB 以内)。
2.2 参数编码
参数值必须用 URLEncoder.encode(value, "UTF-8") 编码后再拼接,否则中文会乱码。
本文档第 9 节的工具类已统一处理。
2.3 请求超时设置
readTimeout
必须大于打印超时时间(例如打印超时 10 秒,readTimeout 建议设 30 秒以上),
否则服务还没返回,客户端就先抛 SocketTimeoutException 了。3统一返回结构
/print 与 /setPrinter 返回统一 JSON 结构(/printers、/types 为各自结构,见对应章节):
{
"success": true, // 是否成功
"printer": "XP-80C", // 实际使用的打印机名
"reason": null, // 失败原因分类;成功时为 null
"message": "打印成功" // 详细描述
}
| 字段 | 类型 | 说明 |
|---|---|---|
success | boolean | true = 纸张真正打印完成;false = 失败(任务已被删除) |
printer | string | 实际执行打印的打印机名称,可能为 null |
reason | string | 失败原因分类:卡纸 / 缺纸 / 机器故障 / 打印机不可用 / 参数错误;成功时为 null |
message | string | 人类可读的详细描述,可直接提示给操作员 |
失败示例(打印机缺纸):
{
"success": false,
"printer": "XP-80C",
"reason": "缺纸",
"message": "检测到打印机异常,任务已删除: 缺纸"
}
4服务状态查询
无参数。用于检测打印服务是否已启动。
返回示例
{
"service": "JavaFX通用打印中间件",
"port": 8089,
"running": true,
"printApi": "POST /print",
"types": ["LABEL","A4","A5","BARCODE","RECEIPT"],
"time": "2026-09-07 15:30:00"
}
5获取电脑所有打印机
无参数。返回系统打印机名列表及系统默认打印机。
返回示例
{
"success": true,
"defaultPrinter": "HP LaserJet P1108",
"printers": ["HP LaserJet P1108", "XP-80C", "Microsoft Print to PDF"]
}
6获取打印类型及映射
无参数。返回每个类型绑定的打印机、超时时间、默认纸张等信息。
返回示例
{
"success": true,
"activated": true, // 程序是否已激活(未激活打印会带水印)
"types": [
{
"type": "RECEIPT", // 类型标识(调用 /print 时传这个)
"name": "票据", // 显示名称
"printer": "XP-80C", // 绑定的打印机
"custom": false, // 是否自定义类型
"timeout": 10, // 该类型打印超时(秒)
"paperWidth": 80, // 默认纸张(毫米,可能无此字段)
"paperHeight": 200
}
]
}
7设置类型与打印机映射
请求参数
| 参数 | 必填 | 说明 |
|---|---|---|
type | 是 | 类型标识,如 LABEL / A4 / RECEIPT 或自定义标识(不区分大小写,也支持传显示名称) |
printer | 是 | 打印机名,必须是 /printers 返回的真实打印机名 |
name | 新增时必填 | type 不存在时的显示名称,提供后自动注册为自定义类型 |
width | 否 | 新增自定义类型的默认纸张宽度(毫米),与 height 成对提供 |
height | 否 | 新增自定义类型的默认纸张高度(毫米) |
timeout | 否 | 同时设置该类型的打印超时(秒),>0 生效 |
设置成功后立即写入 print-middleware.properties 并刷新程序界面。返回统一结构(见第 3 节)。
8打印接口(核心)
8.1 请求参数
| 参数 | 必填 | 说明 |
|---|---|---|
type | 否 | 打印类型标识,缺省 A4;不区分大小写,也支持传显示名称 |
content | 六选一 | 文本内容(支持 \n 换行),文本/标签/票据打印 |
code | 条码内容,BARCODE 类型专用(自动转大写) | |
data | Base64 图片(PNG/JPG),支持 data:image/png;base64, 前缀,分页打印 | |
url | 远程 PDF 文件地址(http/https),服务端自动下载打印,完成后删除临时文件;打印方式见 urlMode | |
file | Base64 文件内容,配合 fileName 使用(docx/pdf 服务端提取文本打印) | |
raw | Base64 原始指令字节(ESC/POS 等),不渲染直接透传 | |
escpos | 否 | ESC/POS 行式指令文本(服务端编译为字节,指令表见第 11 节) |
charset | 否 | escpos 文本编码,默认 GBK |
fileName | 否 | file 通道的文件名(用于判断 docx / pdf) |
urlMode | 否 | url 通道打印方式:system(默认)调用系统默认 PDF 阅读器打印,版式与原文件完全一致(可能弹出打印窗口);text 提取文本静默打印(不保留排版/图片) |
copies | 否 | 打印份数,默认 1 |
width | 否 | 纸张宽度(毫米),<=0 时按类型默认纸张 |
height | 否 | 纸张高度(毫米) |
fontSize | 否 | 文本字号(磅),<=0 时按类型默认字号 |
timeout | 否 | 打印超时(秒);不传则使用程序中该类型设置的秒数,类型也未设置时默认 10 秒 |
8.2 内容通道优先级
同一请求携带多个内容参数时,按以下优先级取第一个非空通道:
1 raw 原始字节→ 2 escpos 指令→ 3 data 图片→ 4 url 远程PDF→ 5 file 文件→ 6 content / code 文本条码
8.3 同步语义
success:true;
过程中检测到缺纸/卡纸/脱机等异常,或到达超时秒数,都会删除打印任务并返回
success:false 及对应 reason。到达设定秒数一定返回,不会无限等待。9通用调用工具类(JDK 1.8,零依赖)
下面这个工具类只使用 JDK 自带的 HttpURLConnection,
兼容 JDK 1.8,不需要任何第三方 jar,可直接复制到项目中使用:
import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.net.HttpURLConnection; import java.net.URL; import java.net.URLEncoder; import java.util.LinkedHashMap; import java.util.Map; /** * 通用打印中间件 Java 调用客户端(JDK 1.8,零第三方依赖)。 * 用法: * PrintApiClient client = new PrintApiClient("http://127.0.0.1:8089"); * String json = client.print("A4", "你好,世界!", 1); */ public class PrintApiClient { private final String baseUrl; // 如 http://127.0.0.1:8089 private int connectTimeout = 5000; // 连接超时(毫秒) private int readTimeout = 60000; // 读取超时(毫秒),必须大于打印超时 public PrintApiClient(String baseUrl) { // 去掉末尾的 /,方便拼接 if (baseUrl.endsWith("/")) { baseUrl = baseUrl.substring(0, baseUrl.length() - 1); } this.baseUrl = baseUrl; } /** 打印超时设得较长时,相应调大读取超时(必须大于打印超时秒数) */ public void setReadTimeout(int millis) { this.readTimeout = millis; } // ---------------- 便捷方法 ---------------- /** 服务状态探活 */ public String status() throws IOException { return get("/status", null); } /** 获取电脑所有打印机(JSON) */ public String listPrinters() throws IOException { return get("/printers", null); } /** 获取已设置的打印类型及对应打印机(JSON) */ public String listTypes() throws IOException { return get("/types", null); } /** 设置类型 -> 打印机映射(type 不存在时传 name 可新增自定义类型) */ public String setPrinter(String type, String printer, String name, Integer timeoutSec) throws IOException { Map<String, String> p = new LinkedHashMap<String, String>(); p.put("type", type); p.put("printer", printer); if (name != null) { p.put("name", name); } if (timeoutSec != null) { p.put("timeout", String.valueOf(timeoutSec)); } return post("/setPrinter", p); } /** 文本打印(type 如 A4 / LABEL / RECEIPT;copies 份数) */ public String print(String type, String content, int copies) throws IOException { Map<String, String> p = new LinkedHashMap<String, String>(); p.put("type", type); p.put("content", content); p.put("copies", String.valueOf(copies)); return post("/print", p); } /** 通用打印:params 里放 /print 支持的任意参数(见第 8 节) */ public String print(Map<String, String> params) throws IOException { return post("/print", params); } // ---------------- 底层实现 ---------------- /** GET 请求,params 拼到 QueryString */ public String get(String path, Map<String, String> params) throws IOException { String url = baseUrl + path; String qs = encodeParams(params); if (qs.length() > 0) { url += "?" + qs; } HttpURLConnection conn = (HttpURLConnection) new URL(url).openConnection(); conn.setRequestMethod("GET"); conn.setConnectTimeout(connectTimeout); conn.setReadTimeout(readTimeout); return readResponse(conn); } /** POST 请求,application/x-www-form-urlencoded */ public String post(String path, Map<String, String> params) throws IOException { byte[] body = encodeParams(params).getBytes("UTF-8"); HttpURLConnection conn = (HttpURLConnection) new URL(baseUrl + path).openConnection(); conn.setRequestMethod("POST"); conn.setRequestProperty("Content-Type", "application/x-www-form-urlencoded; charset=UTF-8"); conn.setDoOutput(true); conn.setConnectTimeout(connectTimeout); conn.setReadTimeout(readTimeout); OutputStream os = conn.getOutputStream(); os.write(body); os.flush(); os.close(); return readResponse(conn); } /** 参数 URL 编码(UTF-8),空值参数跳过 */ private static String encodeParams(Map<String, String> params) throws IOException { StringBuilder sb = new StringBuilder(); if (params != null) { for (Map.Entry<String, String> e : params.entrySet()) { if (e.getValue() == null || e.getValue().length() == 0) { continue; } if (sb.length() > 0) { sb.append('&'); } sb.append(URLEncoder.encode(e.getKey(), "UTF-8")); sb.append('='); sb.append(URLEncoder.encode(e.getValue(), "UTF-8")); } } return sb.toString(); } /** 读取响应体(含错误码时的 errorStream) */ private static String readResponse(HttpURLConnection conn) throws IOException { int code = conn.getResponseCode(); InputStream in = code >= 400 ? conn.getErrorStream() : conn.getInputStream(); ByteArrayOutputStream buf = new ByteArrayOutputStream(); if (in != null) { byte[] tmp = new byte[4096]; int n; while ((n = in.read(tmp)) != -1) { buf.write(tmp, 0, n); } in.close(); } conn.disconnect(); return new String(buf.toByteArray(), "UTF-8"); } }
10完整调用示例
以下示例均基于第 9 节的 PrintApiClient,返回值为 JSON 字符串,
业务系统可自行解析(或简单用 json.contains("\"success\":true") 判断)。
10.1 查询服务状态与打印机列表
PrintApiClient client = new PrintApiClient("http://127.0.0.1:8089"); System.out.println(client.status()); // {"service":"JavaFX通用打印中间件",...} System.out.println(client.listPrinters()); // {"success":true,"printers":[...]} System.out.println(client.listTypes()); // {"success":true,"activated":true,"types":[...]}
10.2 设置类型与打印机映射
// 把"票据"类型绑定到 XP-80C,超时设为 15 秒 client.setPrinter("RECEIPT", "XP-80C", null, 15); // 新增自定义类型"发货单"(标识 SHIP),绑定到 HP 打印机,纸张 210x140mm Map<String, String> p = new LinkedHashMap<String, String>(); p.put("type", "SHIP"); p.put("name", "发货单"); p.put("printer", "HP LaserJet P1108"); p.put("width", "210"); p.put("height", "140"); p.put("timeout", "20"); client.post("/setPrinter", p);
10.3 打印文本(A4)
String json = client.print("A4", "检测报告\n编号:JC2026090701\n结论:合格", 2); System.out.println(json); // {"success":true,"printer":"HP LaserJet P1108",...}
10.4 打印条码
Map<String, String> p = new LinkedHashMap<String, String>(); p.put("type", "BARCODE"); p.put("code", "SN20260907001"); p.put("content", "产品序列号"); // 可选:条码下方附加文字 p.put("copies", "1"); System.out.println(client.print(p));
10.5 打印标签(自定义纸张)
Map<String, String> p = new LinkedHashMap<String, String>(); p.put("type", "LABEL"); p.put("content", "品名:打印纸\n规格:A4-70g\n批次:2026-09"); p.put("width", "60"); // 标签宽 60mm p.put("height", "40"); // 标签高 40mm p.put("fontSize", "10"); System.out.println(client.print(p));
10.6 打印图片(Base64 PNG)
import java.nio.file.Files; import java.nio.file.Paths; import java.util.Base64; byte[] imgBytes = Files.readAllBytes(Paths.get("D:/label.png")); String base64 = Base64.getEncoder().encodeToString(imgBytes); Map<String, String> p = new LinkedHashMap<String, String>(); p.put("type", "LABEL"); p.put("data", base64); p.put("copies", "3"); System.out.println(client.print(p));
10.7 打印文件(docx / pdf,服务端提取文本)
byte[] fileBytes = Files.readAllBytes(Paths.get("D:/合同.docx")); Map<String, String> p = new LinkedHashMap<String, String>(); p.put("type", "A4"); p.put("file", Base64.getEncoder().encodeToString(fileBytes)); p.put("fileName", "合同.docx"); p.put("fontSize", "12"); System.out.println(client.print(p));
10.8 ESC/POS 指令打印(小票)
String script =
"init\n" +
"align:center\n" +
"size:2,2\n" +
"text:星烁超市\n" +
"size:1,1\n" +
"line\n" +
"align:left\n" +
"text:打印纸 x2 10.00\n" +
"text:签字笔 x1 5.50\n" +
"line\n" +
"text:合计 15.50\n" +
"qrcode:https://sparx.gx.cn\n" +
"feed:3\n" +
"cut\n";
Map<String, String> p = new LinkedHashMap<String, String>();
p.put("type", "RECEIPT");
p.put("escpos", script);
// p.put("charset", "GBK"); // 可选,默认 GBK
System.out.println(client.print(p));
10.9 原始指令透传(raw)
// 自己拼好的 ESC/POS 字节(中文需自行按 GBK 编码),Base64 后透传 byte[] escpos = new byte[]{0x1B, 0x40}; // 初始化 ByteArrayOutputStream bos = new ByteArrayOutputStream(); bos.write(escpos, 0, escpos.length); bos.write("你好打印机\n".getBytes("GBK")); bos.write(new byte[]{0x1D, 0x56, 0x00}, 0, 3); // 全切纸 Map<String, String> p = new LinkedHashMap<String, String>(); p.put("type", "RECEIPT"); p.put("raw", Base64.getEncoder().encodeToString(bos.toByteArray())); System.out.println(client.print(p));
10.10 使用超时参数
Map<String, String> p = new LinkedHashMap<String, String>(); p.put("type", "A4"); p.put("content", "大文件打印,给足时间"); p.put("timeout", "60"); // 本次请求 60 秒超时 client.setReadTimeout(90000); // 客户端读取超时必须更大! System.out.println(client.print(p)); // 不传 timeout:用程序界面为该类型设置的秒数;都没设则默认 10 秒
10.11 打印远程 PDF(传入文件 URL)
Map<String, String> p = new LinkedHashMap<String, String>(); p.put("type", "A4"); p.put("url", "http://192.168.1.10:8228/PDFprint/20260902/18653556.pdf"); System.out.println(client.print(p)); // 默认系统打印:服务端下载 PDF 后调用本机默认 PDF 阅读器打印, // 版式/图片与原文件完全一致;可能弹出阅读器的打印窗口,需人工确认; // 临时文件在阅读器读取后自动删除(约 10 分钟,程序退出时兜底删除)。 // // 静默模式(无弹窗、支持 type 打印机映射与 timeout 控制,但不保留排版): p.put("urlMode", "text"); p.put("timeout", "60"); System.out.println(client.print(p)); // 无扩展名的下载接口(如 /file/download?id=123)也支持,按 PDF 处理; // 下载连接超时 10 秒、读取超时 30 秒,文件最大 50MB。
10.12 判断打印结果
String json = client.print("A4", "测试页", 1); if (json.contains("\"success\":true")) { System.out.println("打印成功"); } else if (json.contains("\"reason\":\"缺纸\"")) { System.out.println("请装纸后重试(任务已删除,不会重复打印)"); } else if (json.contains("\"reason\":\"卡纸\"")) { System.out.println("打印机卡纸,请检查"); } else { System.out.println("打印失败: " + json); }
11ESC/POS 行式指令参考
/print 的 escpos 参数使用易读的行式指令,
服务端编译为 ESC/POS 字节后透传到打印机(文本默认按 GBK 编码,兼容绝大多数国产热敏打印机)。
规则:每行一条指令;# 开头为注释;空行忽略;指令名不区分大小写;
指令与参数之间用英文冒号 : 分隔(参数里允许再出现冒号,如 URL)。
| 指令 | 说明 | 对应字节 |
|---|---|---|
init | 初始化打印机 | ESC @ |
align:left|center|right | 对齐方式(左/中/右) | ESC a n |
bold:on|off | 加粗开关 | ESC E n |
underline:on|off | 下划线开关 | ESC - n |
size:w,h | 字号倍率(宽,高 各 1~8),如 size:2,2 | GS ! n |
text:任意文本 | 文本(按当前 charset 编码,不自动换行) | 编码后字节 |
line | 换行 | LF |
feed:n | 走纸 n 行(默认 1) | ESC d n |
cut | 走纸并切纸(半切) | GS V 66 0 |
cutfull | 全切 | GS V 0 |
drawer:pin | 开钱箱(pin 0/1,默认 0) | ESC p pin 25 250 |
beep:n,t | 蜂鸣 n 次(默认 1 次),t 为每次时长档位 | ESC B n t |
qrcode:内容 | 打印二维码(Model 2,容错 M) | GS ( k 序列 |
barcode:内容 | 打印 Code39 条码(内容仅 ASCII) | GS k 序列 |
hex:1B40 ... | 透传十六进制字节(可含空格分隔) | 原样字节 |
base64:xxxx | 透传 Base64 字节 | 解码后字节 |
charset:GBK|UTF-8 | 切换后续 text / qrcode 的编码 | — |
success:false、
reason:"参数错误",message 中带具体行号,如 "第 3 行: ..."。hex / base64 是逃生舱:任何上面没有的 ESC/POS 指令
(如自定义指令、厂商扩展指令)都可以直接拼字节透传。12失败原因与超时语义
12.1 失败原因(reason 字段)
| reason | 触发场景 |
|---|---|
缺纸 | 打印前或打印过程中检测到打印机缺纸;任务已删除 |
卡纸 | 检测到卡纸;任务已删除 |
机器故障 | 脱机、舱门未关、耗材用尽、需维修、打印超时等其他异常;任务已删除 |
打印机不可用 | 该类型未绑定打印机,或绑定的打印机在系统中不存在 |
参数错误 | 类型未知、内容为空、Base64 不合法、ESC/POS 指令错误等(不会提交打印) |
12.2 超时语义
| 情况 | 生效的超时时间 |
|---|---|
请求带 timeout 参数(>0) | 使用请求指定的秒数(优先级最高) |
请求不传 timeout | 使用程序界面为该类型设置的秒数 |
| 类型也未设置过 | 默认 10 秒 |
"打印超时(任务超过 10 秒仍在打印队列中,已删除)"。readTimeout = (打印超时秒数 + 20) * 1000,否则客户端会先抛超时异常而拿不到真实结果。13注意事项
| 主题 | 说明 |
|---|---|
| 激活水印 | 程序未激活时,文本/图片/文件打印内容会叠加"星烁信息sparx.gx.cn"水印;raw / escpos 原始字节通道不加水印。用 GET /types 的 activated 字段可查询激活状态。 |
| URL 编码 | 所有参数值必须 UTF-8 URL 编码;GET 方式传长内容(图片/文件)会受 URL 长度限制,请用 POST。 |
| 同步调用 | /print 是同步接口,线程会阻塞到出结果;业务系统请勿在 UI 线程直接调用,请放工作线程。 |
| 并发 | 同一台打印机的并发请求会排队竞争,建议业务侧串行化同一打印机的任务。 |
| 失败重试 | 失败时任务已被删除,不会重复打印;业务侧可直接重发请求实现重试。 |
| 远程 PDF(url) | 默认调用系统默认 PDF 阅读器打印(版式保真,可能弹打印窗口,临时文件约 10 分钟后自动删除);urlMode=text 为文本提取静默打印(不保留排版/图片,扫描件不可用,打印后立即删除临时文件);下载连接超时 10 秒、读取 30 秒,文件最大 50MB;下载耗时不计入打印超时。 |
| 局域网调用 | 需在程序界面勾选"允许局域网访问",并放行 Windows 防火墙对应端口。 |
| 系统要求 | 服务端:JDK 1.8+(带 JavaFX)、Windows 7 / Server 2008 及以上。客户端示例代码同样兼容 JDK 1.8、零依赖。 |