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 票据,另支持自定义类型
典型部署:打印服务装在连接打印机的电脑上(或 Windows Server 2008+ 服务器), 业务系统通过 HTTP 调用;同一局域网的其他电脑把"允许局域网访问"打开后也可直接调用。

2通用约定

2.1 请求方式

所有接口同时兼容 GET 与 POST。内容较长时(Base64 图片 / 文件 / ESC/POS 脚本)请务必使用 POST, GET 的 URL 长度受浏览器和服务器限制(一般 8KB 以内)。

2.2 参数编码

参数值必须用 URLEncoder.encode(value, "UTF-8") 编码后再拼接,否则中文会乱码。 本文档第 9 节的工具类已统一处理。

2.3 请求超时设置

重要:打印是同步阻塞接口——服务会一直等到"打印成功 / 检测到异常 / 到达超时秒数"才返回。因此 Java 侧的 readTimeout 必须大于打印超时时间(例如打印超时 10 秒,readTimeout 建议设 30 秒以上), 否则服务还没返回,客户端就先抛 SocketTimeoutException 了。

3统一返回结构

/print 与 /setPrinter 返回统一 JSON 结构(/printers、/types 为各自结构,见对应章节):

{
  "success": true,                 // 是否成功
  "printer": "XP-80C",           // 实际使用的打印机名
  "reason":  null,                 // 失败原因分类;成功时为 null
  "message": "打印成功"           // 详细描述
}
字段类型说明
successbooleantrue = 纸张真正打印完成;false = 失败(任务已被删除)
printerstring实际执行打印的打印机名称,可能为 null
reasonstring失败原因分类:卡纸 / 缺纸 / 机器故障 / 打印机不可用 / 参数错误;成功时为 null
messagestring人类可读的详细描述,可直接提示给操作员

失败示例(打印机缺纸):

{
  "success": false,
  "printer": "XP-80C",
  "reason":  "缺纸",
  "message": "检测到打印机异常,任务已删除: 缺纸"
}

4服务状态查询

GET /status 根路径 / 等效;用于探活

无参数。用于检测打印服务是否已启动。

返回示例

{
  "service": "JavaFX通用打印中间件",
  "port": 8089,
  "running": true,
  "printApi": "POST /print",
  "types": ["LABEL","A4","A5","BARCODE","RECEIPT"],
  "time": "2026-09-07 15:30:00"
}

5获取电脑所有打印机

GET /printers 枚举系统已安装的全部打印机

无参数。返回系统打印机名列表及系统默认打印机。

返回示例

{
  "success": true,
  "defaultPrinter": "HP LaserJet P1108",
  "printers": ["HP LaserJet P1108", "XP-80C", "Microsoft Print to PDF"]
}
设置打印机映射(/setPrinter)时,printer 参数必须从这里返回的名字中取(支持忽略大小写匹配)。

6获取打印类型及映射

GET /types 已配置的全部打印类型(内置 + 自定义)

无参数。返回每个类型绑定的打印机、超时时间、默认纸张等信息。

返回示例

{
  "success": true,
  "activated": true,          // 程序是否已激活(未激活打印会带水印)
  "types": [
    {
      "type": "RECEIPT",        // 类型标识(调用 /print 时传这个)
      "name": "票据",           // 显示名称
      "printer": "XP-80C",     // 绑定的打印机
      "custom": false,          // 是否自定义类型
      "timeout": 10,            // 该类型打印超时(秒)
      "paperWidth": 80,       // 默认纸张(毫米,可能无此字段)
      "paperHeight": 200
    }
  ]
}

7设置类型与打印机映射

GET / POST /setPrinter 绑定类型到打印机;type 不存在且带 name 时新增自定义类型

请求参数

参数必填说明
type是类型标识,如 LABEL / A4 / RECEIPT 或自定义标识(不区分大小写,也支持传显示名称)
printer是打印机名,必须是 /printers 返回的真实打印机名
name新增时必填type 不存在时的显示名称,提供后自动注册为自定义类型
width否新增自定义类型的默认纸张宽度(毫米),与 height 成对提供
height否新增自定义类型的默认纸张高度(毫米)
timeout否同时设置该类型的打印超时(秒),>0 生效

设置成功后立即写入 print-middleware.properties 并刷新程序界面。返回统一结构(见第 3 节)。

8打印接口(核心)

GET / POST /print 所有打印能力的统一入口

8.1 请求参数

参数必填说明
type否打印类型标识,缺省 A4;不区分大小写,也支持传显示名称
content六选一文本内容(支持 \n 换行),文本/标签/票据打印
code条码内容,BARCODE 类型专用(自动转大写)
dataBase64 图片(PNG/JPG),支持 data:image/png;base64, 前缀,分页打印
url远程 PDF 文件地址(http/https),服务端自动下载打印,完成后删除临时文件;打印方式见 urlMode
fileBase64 文件内容,配合 fileName 使用(docx/pdf 服务端提取文本打印)
rawBase64 原始指令字节(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,2GS ! 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 秒
精准超时:任务状态由 Windows 打印后台处理程序(spooler) 事件驱动(完成 / 失败 / 取消 / 挂起在状态变化瞬间回调),配合毫秒级等待, 到达设定秒数精准返回,全程不轮询;驱动不上报事件时,返回超时前会再做最终确认—— 任务已不在打印队列、或打印机并不在打印中,则按成功返回; 只有任务确实仍在打印/挂起时才返回超时并删除任务。 超时 message 形如 "打印超时(任务超过 10 秒仍在打印队列中,已删除)"。
客户端 readTimeout 必须大于打印超时!建议: 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、零依赖。