
简介中控Java二次开发demo.zip是一套面向Java研发工程师的考勤机二次开发示例适用于企业考勤系统集成场景帮助开发者实现考勤数据自动采集、员工信息新增与岗位调动等核心功能。压缩包约37.77MB文件总数显示为0平台暂未解析内部列表实际含可直接运行的源码和开发文档覆盖TCP/IP网络通信、API接口调用、二进制/JSON/XML数据解析及SQL数据库维护等关键环节。已有258人学习/下载适合具备Java基础并熟悉网络编程的开发者参考。源码可快速搭建与中控考勤机的连接流程理解请求发送、响应解析、异常捕获与调试方法配套文档补充数据安全与版本控制实践帮助规避网络中断、设备故障等风险有效缩短二次开发周期。1. 这个 demo.zip 到底能帮你做什么“中控Java二次开发demo.zip”听起来像是一个普普通通的压缩包但伸手去搜它的人多半已经被考勤机、门禁控制器或者人脸识别终端来回折腾过几轮。这个 zip 里装的是中控ZKTeco考勤门禁设备的 Java 对接示例核心资产是 SDK 动态库、一个封装好的 jar 包外加几个能直接改的示例类。它的价值就一句话让 Java 代码连上一台真实设备读打卡记录、下发人员信息、接收刷卡事件而不是只停留在设备自身的屏幕上。适合刚接手设备集成的 Java 工程师、要把老设备数据迁进新系统的实施人员也给做私有化交付的开发者提供了最省事的起步模板。它既不是 Servlet 或 Spring Boot 那类 Web 示例工程也不是工业 SCADA 监控平台和 Creo、NX 那种驱动建模内核的工业软件二次开发也不是一回事——它就是 Java 与物理设备之间的一根数据管道别拿写接口文档的思路来看它。2. 中控 Java 二次开发的技术底座SDK 结构与通信方式2.1 中控设备 SDK 的 Java 对接原理JNI、DLL 与接口分层中控这类考勤门禁设备底层核心是 C 实现的生物识别算法和通信协议官方对外提供的 SDK 也是一套 C 接口。Java 要调用它中间隔着一层 Java Native InterfaceJNI。你在 demo 里看到的 jar 包本质上只是一层 JNI 声明它把 C 侧的函数暴露成 Java 类的方法真正干活的代码全部在动态库里。动态库的文件名按操作系统来划分Windows 上是 zkemkeeper.dll 之类Linux 上是 .so少数还能见到 .dylib。搞懂分层之后再看接口命名就顺眼多了。中控这套接口有几类命名规律连接类用动词加下划线比如 Connect_Net、Connect_Com数据读取类以 Get、SSR_Get 开头属性设置类以 Set、SSR_Set 开头。事件回调则是 On 开头。这套东西对 Java 基础的要求反而比 Spring Boot 应用更高JNI 加载机制、类加载器、线程模型、字符集任何一环出问题都足以让程序在运行期翻车。有两点在动手前就要记住。第一jar 包只是壳运行环境里必须能找到动态库否则一运行就报 UnsatisfiedLinkError。第二SDK 的接口是同步阻塞的尤其读考勤记录和注册指纹这类调用如果设备响应慢你的调用线程会被卡住所以生产代码里一般要放在异步线程池里跑而不是直接塞进 HTTP 请求的调用链。2.2 读 demo 源码前先搞懂三个核心对象与一条数据流demo 里的代码看着方法很多但归拢一下只需要抓住三个对象。第一个是设备连接对象通常叫 ZKEMKeeper 或类似名字它代表一条与设备的 TCP 连接连接前要设置 IP 和端口连接后所有读写、设置操作都从它身上引出。第二个是考勤记录对象它不是 Java Bean而是一组输出参数工号、刷卡时间、验证方式、出入方向。验证方式各型号定义不同常见的有指纹、密码、刷卡、人脸。第三个是事件回调对象用于接收设备主动推送的实时刷卡和门禁事件比如有人按指纹开门SDK 会立刻回调你的方法。一条完整的数据流可以这样看设备内存先存着几千条打卡记录SDK 通过私有协议把记录同步到本地缓存应用侧再用 Get 类方法一条条从缓存取出组装成自己的数据结构最后交给 MyBatis-Plus 或者 JPA 落库。读 demo 时容易绕晕的地方就在这拉取记录分两步先要从设备同步到 SDK 缓存再从缓存取数据。如果你发现接口返回成功但日志条数为零八成是第一步没执行或者时间范围设成了设备侧根本不认为的“新记录”。2.3 开发环境选型JDK 位数、操作系统与动态库匹配环境选型这一关能卡掉不少人。先说 JDK 版本Java 8 和 Java 11 跑这套 SDK 都常见但安装版本的位数必须和动态库一致32 位 DLL 配 32 位 JDK64 位 DLL 配 64 位 JDK。现在新装的 JDK 基本都是 64 位而某些老型号设备配套的 SDK 还有 32 位 DLL这就容易在 Windows 服务器上出现“代码一模一样环境一换就崩”的现象。我一般到现场的第一件事就是敲 java -version确认是不是 64 位。然后是部署系统。如果你只对接局域网里的一台考勤机Windows 服务器最常见dll 放好就行。但如果要把服务部署到 Linux 服务器就得找 Linux 版动态库并确认系统装齐了 libusb 这类底层库。有时候 SDK 还会要求 libstdc 的版本符合要求。还有一条容易被忽略JDK 是 headless 模式还是带图形界面不影响运行但有些老 SDK 在纯 Linux 服务器上初始化会失败先装一个图形库或 xvfb 就能绕过去。这个属于纯粹的环境玄学遇到再解决不迟。3. 把 demo 跑起来从解压到设备连接的最小可行步骤3.1 解压后的目录结构jar、动态库和源码各归其位拿到压缩包先别急着双击工程文件。常见的中控 demo 压缩包一般会按 lib、src、doc 三层组织lib 里面放的是 jar 和按平台分好的动态库目录src 里是示例代码doc 里是接口说明。先把压缩包解压到一个路径里不含中文和空格的目录比如 D:\zk-demo。路径带中文时Windows 上加载动态库经常出现不明原因的失败这就是第一处坑。工程导入按你的构建工具来。如果是 Maven 工程我不推荐把 jar 直接塞进本地仓库而是用 system scope 引用 lib 目录下的文件这样换机器时只要同步整个 lib 目录就能跑。动态库不要打进 jar原因很简单Java 在运行期加载动态库时找的是文件系统路径从 jar 里读出来的临时文件路径既不稳定又容易出现权限问题。更好的做法是用 -Dsdk.lib.dir/绝对路径 把动态库目录传给程序由代码决定加载位置。3.2 连接设备的参数清单IP、端口、超时与通信密码连接设备之前先把参数搞清楚。下面是常见的连接配置参数表。参数常见取值说明设备 IP192.168.x.x设备管理界面里可查需要和电脑在同一网段端口4370中控考勤门禁设备 TCP/IP 连接最常见的默认端口连接超时3000-5000 ms老设备握手慢太短会误报离线通信密码0 或设备管理端设置的值部分新型号默认开启不匹配会连接失败串口波特率9600 / 19200走串口连接时用网线连接不需要注意4370 只是最常见的默认值部分门禁控制器和人脸终端会不同以设备型号对应的说明书为准。连接前先在电脑上 ping 通设备 IP再 telnet 一下端口通不通这样能把网络问题与 SDK 问题分开。我见过太多人连不上设备就怀疑代码结果最后是电脑开了防火墙根本没放行 4370。3.3 第一个可运行的 Java 类连接、获取设备信息与断开把环境准备好之后可以先写一个最小的连接类。下面这段代码用的是通常的演示接口写法你的 SDK 版本如果方法名有出入以 jar 包反编译出来的签名为准。import com.zkteco.zkemkeeper.ZKEMKeeper; import java.io.File; /** * 最小连接示例连接设备、读取设备编号、断开 */ public class ZKConnectDemo { static { // 从外部目录加载动态库目录用 -Dsdk.lib.dir 指定 String libDir System.getProperty(sdk.lib.dir, lib); try { System.loadLibrary(zkemkeeper); // 先试系统路径 } catch (UnsatisfiedLinkError e) { System.load(new File(libDir, System.mapLibraryName(zkemkeeper)).getAbsolutePath()); } } public static void main(String[] args) { ZKEMKeeper zk new ZKEMKeeper(); // Connect_Net 第一个参数是设备 IP第二个是端口 boolean connected zk.Connect_Net(192.168.1.100, 4370); if (!connected) { System.err.println(连接失败检查 IP、端口和网络连通性); return; } int[] deviceNumber new int[1]; boolean got zk.GetDeviceNumber(deviceNumber); // 获取设备编号 if (got) { System.out.println(设备编号: deviceNumber[0]); } zk.Disconnect(); // 记得断开否则 SDK 的线程会一直占着连接 System.out.println(连接测试完成); } }这段代码的加载逻辑做了两次尝试先 System.loadLibrary 找系统库路径失败再退回到外部目录。这样开发机上把动态库放进 JDK 的库搜索路径也能跑部署时用 -Dsdk.lib.dir 指定绝对路径反而更稳。Connect_Net 是阻塞调用返回布尔值true 代表握手成功。GetDeviceNumber 是读取设备基本信息的第一个常用方法类似的还有获取固件版本、设备名称等。真机上连接成功后有些型号会立刻要求你设置通信密码否则会定时断连。遇到这种型号连接后马上调设置密码的接口把管理端统一的密码写进去后面的操作才会稳定。注意部分中控设备在连接成功后要求立即设置通信密码否则过几分钟自动断连。把密码放在配置文件里不要硬编码在代码中。4. 二次开发的核心场景在 demo 基础上改出自己的功能4.1 读取考勤记录从设备拉取原始数据到本地数据库连接跑通之后最常见的需求就是把考勤记录拉回自己的业务系统。中控的读记录流程是两步走先把设备里的记录同步到 SDK 缓存再从缓存逐条取出。下面这段是拉取一段时间内全部记录的骨架方法名以你手里的 jar 包为准。import com.zkteco.zkemkeeper.ZKEMKeeper; import java.text.SimpleDateFormat; import java.util.Date; public class AttLogReader { private ZKEMKeeper zk; public AttLogReader(ZKEMKeeper zk) { this.zk zk; } public void pullAttLogs(int deviceId, Date start, Date end) throws Exception { SimpleDateFormat sdf new SimpleDateFormat(yyyy-MM-dd HH:mm:ss); // 第一步同步指定时间段的新记录到 SDK 缓存 boolean synced zk.SSR_GetGeneralAttendanceData( deviceId, sdf.format(start), sdf.format(end), true); // true 表示只取设备中未取走过的新记录 if (!synced) { System.err.println(同步失败请检查设备是否在线); return; } // 第二步从缓存中逐条取出记录 while (true) { int count zk.GetGeneralAttendanceDataLength(); if (count 0) { break; // 缓存已取空 } String enrollId ; // 工号 int verifyMode -1; // 验证方式0指纹 1密码 2刷卡 15人脸 int inOutMode -1; // 出入标志 int year 0, month 0, day 0, hour 0, minute 0, second 0; boolean ok zk.SSR_GetGeneralAttendanceData( count - 1, enrollId, verifyMode, inOutMode, year, month, day, hour, minute, second); if (!ok) { continue; // 单条失败就跳过去别让一条脏数据卡死整个循环 } Date recordTime new Date(year - 1900, month - 1, day, hour, minute, second); saveToDatabase(enrollId, recordTime, verifyMode, inOutMode); } } private void saveToDatabase(String enrollId, Date time, int verifyMode, int inOutMode) { // 实际项目里这里用 MyBatis-Plus 或 JDBC 落库 System.out.printf(工号%s 时间%s 验证方式%d 出入%d%n, enrollId, time, verifyMode, inOutMode); } }这段代码里有两个关键的参数语义要搞明白。SSR_GetGeneralAttendanceData 的最后一个布尔参数表示“是否只取新记录”如果你每次同步完不清设备缓存这个参数能避免重复拉取但如果多台服务器同时连同一台设备可能出现互相抢占新记录的问题落地时要约定好只允许一台服务器执行拉取。取记录的循环用的是 count - 1因为 SDK 缓存索引从零开始取完一条缓存长度减一这个循环才能平稳结束。时间参数在取出时是独立的年月日时分秒整数我上面用了已经过时的 Date 构造方法演示实际项目里建议拼成字符串再用 LocalDateTime 解析。这里还有个容易出乱码的地方设备里存的中文姓名如果乱码多半是字符集问题常见的是 GBK你在 JDBC 连接串里指定 characterEncodingGBK 一般就能解决而不是在 Java 代码里反复编码解码。如果业务上要求记录按时间正序展示我习惯先收集到 List 里用 List.sort 按记录时间排好再批量落库直接依赖设备返回的顺序并不可靠。落地用的持久层如果是 MyBatis-Plus实体类标好注解就能自动建表这种模型和考勤记录的结构刚好匹配。4.2 用户管理和指纹下发同步人员信息的关键调用考勤记录要对应到人前提是设备里已经有人员信息。中控的 demo 一般会包含添加用户、删除用户、修改权限的示例。添加用户的典型流程是先通过 SSR_SetUserInfo 写入用户基础信息再给需要生物识别的用户录入指纹模板。把用户管理器、考勤记录读取器、事件监听器这些用面向对象的方式封装成独立类后续加设备型号适配会轻松很多。public class UserManager { private ZKEMKeeper zk; private int deviceId; public UserManager(ZKEMKeeper zk, int deviceId) { this.zk zk; this.deviceId deviceId; } /** * 添加一个普通用户 */ public boolean addUser(String enrollId, String name, String cardNumber) { String password ; // 密码留空表示不启用密码验证 int privilege 0; // 0普通用户1管理员 boolean enabled true; // 是否启用该用户 boolean ok zk.SSR_SetUserInfo( deviceId, enrollId, // 工号/用户编号 name, // 显示姓名 password, // 密码 cardNumber, // 卡号可为空 privilege, enabled); return ok; } /** * 注册一枚指纹模板到指定用户 */ public boolean enrollFingerprint(String enrollId, int fingerIndex) throws Exception { // 开始模板采集fingerIndex 从 1 开始 zk.BeginFPTemplateCapture(enrollId, fingerIndex); // 等待用户在设备上按压指纹 long deadline System.currentTimeMillis() 10_000; while (System.currentTimeMillis() deadline) { if (zk.GetFPTemplateCount() 0) { byte[] template zk.GetFPTemplate(); return zk.SetUserTemplate(enrollId, template, fingerIndex); } Thread.sleep(200); // 轮询间隔 200ms别太频繁 } return false; // 超时未采集到指纹 } }指纹注册这段有几个注意点。BeginFPTemplateCapture 会进入“采集模式”这时设备上的指纹窗口会亮起来提示用户按指纹而不是在代码侧模拟一枚指纹所以整个注册流程天然适合做成一个人机交互的桌面程序。10 秒超时是我常用的值现场用户操作慢可以调到 15 秒。GetFPTemplateCount 返回的是已经采集到的模板数量拿到模板之后 SetUserTemplate 把它绑定到用户。有些型号用 SaveUserTemplate 或 SetUserTemplate 系列接口看 SDK 版本。批量同步人员的正确做法是先拉取设备里已有的用户列表做增量比对再按差异下发。别把几百人无脑全量下发设备的内存量不大用户多的时候全量写会非常慢还会把屏幕卡住。4.3 实时监控与门禁事件回调线程的实现方式第三个高频场景是实时监控刷卡记录。考勤机的意义不只是事后拉记录很多客户想要“有人刷卡立即出现在大屏上”。中控 SDK 通过事件回调来实现demo 里一般会提供一个实现事件接口的监听器类。不同 SDK 封装的回调方式不一样有的是属性赋值有的是 addListener下面写法是其中一种。import com.zkteco.zkemkeeper.ZKEMKeeper; public class RealtimeMonitor { private ZKEMKeeper zk; private volatile boolean running true; public RealtimeMonitor(ZKEMKeeper zk) { this.zk zk; } public void start() { // 注册回调SDK 线程在设备推送事件时调用这个实例的方法 zk.OnAttTransactionEx (enrollId, verifyMode, inOutMode, year, month, day, hour, minute, second) - { String time String.format(%d-%02d-%02d %02d:%02d:%02d, year, month, day, hour, minute, second); System.out.println(实时刷卡 工号 enrollId 时间 time 方式 verifyMode 方向 inOutMode); // 这里可以推送 WebSocket 到前端大屏 }; } public void stop() { running false; zk.Disconnect(); // 断开连接同时结束 SDK 内置的事件分发线程 } }这里面有个容易踩的现实问题回调方法是在 SDK 的内部线程里执行的不是你的业务线程。如果你在回调里做数据库写入或者 HTTP 调用会直接拖慢 SDK 的事件分发极端情况下会造成事件丢失。正确的做法是回调里只做轻量处理把数据塞进一个有界队列或者交给线程池异步消费。我一般用 ArrayBlockingQueue 做缓冲消费者线程批量落库这样既不影响接收速度又能控制数据库压力。事件模式需要长连接保持如果设备在二层网络里被交换机隔离或者设备休眠策略开启事件会断开且不自动重连。生产环境一定要在回调里记录断线时间配合心跳检测发现连接断了就重连并主动补拉断开期间的考勤记录这样才能保证大屏数据不间断。5. 中控二次开发避坑指南常见的翻车点和排查顺序5.1 程序启动就崩UnsatisfiedLinkError 的三种成因现象JVM 才启动就抛 java.lang.UnsatisfiedLinkError提示找不到 zkemkeeper 或者无法加载某个依赖库。这个报错在第一次接触中控 SDK 时几乎必出现其实这是等价的“Java 启动失败”问题先别怀疑设备。原因通常是三种。第一种最简单动态库文件根本不在加载路径里代码里 System.loadLibrary 找不到。第二种是位数不匹配32 位 DLL 被 64 位 JVM 加载Windows 上通常报“Cant load IA 32-bit .dll on a AMD 64-bit platform”这种提示已经是明说了。第三种最隐蔽主 DLL 依赖的 VC 运行库或第三方库缺失报错信息里可能带着一串别的 DLL 名字比如 msvcp140.dll这种情况先补 Visual C Redistributable。解决顺序也很固定。先用 Process Explorer 或直接看部署目录确认 DLL 确实存在且位数匹配再写一个三行的 Java 类只做 System.load不要在完整业务工程里排查最后把 SDK 自带的 C 示例或 C# 示例跑一遍如果 C 能跑而 Java 不能问题就在 JVM 位数或加载路径而不是 DLL 本身损坏。5.2 连接超时但设备在线网段、防火墙与 SDK 版本差异现象设备管理界面显示在线电脑也能 ping 通但 Connect_Net 返回 false或者连接成功后过几分钟自动断线重连。首先检查电脑和设备是否在同一网段。考勤门禁设备通常没有跨网段的路由配置设备配置的网关不对就会出现“能 ping 通但 TCP 握手失败”的假象。其次检查 Windows 防火墙用 telnet 试一下设备端口是最快的判断方式很多公司内部网络会拦截非标准端口4370 不在默认放行列表里。第三原因一般是设备开了加密通信参数而你的 demo 用的是老 SDK 的明文协议这种在较新的设备上比较常见把设备的“通信加密”选项关掉或者换新版 SDK。还有一个版本层面的坑某些中控 OEM 型号对接了其他品牌的协议外观看是中控内部固件却是第三方的官方 demo 里的标准接口根本连不进去。这种设备只能找设备背面型号去官网查对应 SDK或者直接用设备自带的上位机软件抓一下通信方式。遇到这种别硬啃很多老设备支持以 U 盘导出文本格式考勤记录先用导出兜底再处理自动化。5.3 读到的记录时间不对时区、格式与设备存储的坑现象拉回来的考勤记录比实际时间快了 8 小时或者日期对不上还有的 Unix 时间戳是 10 位或 13 位混着来。大多数中控设备存储的时间是设备本地时间拉取接口返回的也是设备本地时间的各字段根本不该做时区换算。如果你习惯性按 UTC 处理就会出现少 8 小时的情况。另一种是设备本身时间不准考勤机没有 NTP常年累月走慢这属于设备运维问题代码里做不了太多但在业务逻辑里可以做“时间偏移量校准”设定一个允许误差范围超了告警。比较隐蔽的是 13 位毫秒时间戳被当成 10 位秒时间戳解析或者反过来这在混合设备型号的系统里常见。解决办法是写一个自适应时间戳解析方法先判断位数再判断数值是否落在合理年份区间比如大于 10 亿按秒、大于 100 亿按毫秒处理。这个逻辑值得放在工具类里复用因为所有来源的时间数据都不一定是同一规范。5.4 64 位系统上 DLL 不兼容选错 JDK 位数的典型症状现象代码在开发者电脑上跑得好好的部署到客户服务器上就报链接错误或者出现内存访问错误、进程直接崩溃。客户服务器绝大多数是 64 位系统而 SDK 提供的 DLL 可能是 32 位或 64 位两套混淆之后就是这种表现。处理方法是先明确两件事DLL 是哪一套JVM 是哪一套。如果只有 32 位 DLL 可用那 JDK 也必须装 32 位即使操作系统是 64 位的。Java 8 官方还提供 32 位 Windows 安装包Java 11 之后 32 位版本越来越少这时候要么换一个支持 64 位的 SDK要么把服务降级到老 JDK。Linux 服务器上如果报 “wrong ELF class”那说明 .so 的位数和 JVM 也不匹配解法一样。这类问题最难受的点在于它不影响编译只在运行期爆炸而且常在凌晨定时任务里出现。所以我的习惯是在部署文档里直接写明“JDK 位数必须与 SDK 动态库位数一致”并把 java -version 的输出作为交付清单的一项省得客户自己装了一个新 JDK 把环境搞坏。5.5 并发调用的崩溃问题SDK 线程安全边界现象系统刚上线时正常考勤高峰期多个请求同时查考勤服务进程直接崩溃或者出现偶发的脏数据取到半条记录。原因基本是同一个 SDK 连接对象被多个线程同时调用。多数中控 SDK 的 JNI 层不是线程安全的其内部状态机依赖单线程访问并发调用会造成底层缓冲区数据错乱。把 SDK 调用封装成单线程执行是标准解法用一个单独的工作线程串行消费任务队列其他业务线程把请求丢进队列等待结果。另一个相关坑是频繁 Connect、Disconnect 导致内存泄漏。SDK 每次连接都会在底层分配资源应用里如果放在请求里反复开关连接一两天后就会出现 native 内存持续上涨。正确做法是采用连接池思路进程内维持一个长连接配合心跳和重连机制只有断线时才重建连接。中控设备本身同时支持的连接数也有限几台服务器轮询同一台设备时最好做成只有一台服务器持有长连接其他服务器通过内部的 HTTP 接口转发请求别都去直连设备。6. 把 demo 变成生产代码日志、重试与掉线自愈的落地技巧demo 的价值在于证明“能连上、能取到数”但要真的放进考勤系统里跑还得补三块日志、重试、掉线自愈。先说日志中控 SDK 是黑匣子失败时不会告诉你底层原因所以应用侧日志要做到“调用前记参数调用后记返回码和耗时”。每次 Connect_Net 失败时把 IP、端口、耗时、异常栈全部落盘这样客户报“连不上”时不用远程到现场也能定位是大面积网络中断还是单台设备故障。SDK 里的时间同步、模板下发等操作日志里最好带设备编号因为一个系统可能管理几十台设备没有设备维度的日志排查就像大海捞针。重试策略要分层。连接层用指数退避重试第一次 1 秒、第二次 2 秒、第三次 4 秒最多次数限制在 5 次以内防止设备恢复后瞬间收到大量连接请求。数据拉取层的重试要小心SSR_GetGeneralAttendanceData 已经同步过的记录重复同步不会丢数据但取记录循环中单条失败要跳过而不是退出退出会导致整批数据缺失。对账逻辑建议以“设备侧记录数”和“本地入库数”做对比每天跑一次差量不匹配再重拉这是最直接的验证方法比任何单元测试都可靠。掉线自愈要区分两种情况。主动断开和异常断开处理方式不同主动断开后直接关闭资源即可异常断开后要等 3 秒再重连让设备的 TCP 栈把残留连接清掉否则重连请求会被设备当成冲突连接拒绝。我习惯在连接对象里加一个 lastEventTime 字段实时监控模式下如果超过两分钟没有收到任何事件就主动断线重连一次消除静默丢事件的可能。这套做法已经在我手里处理过多种型号的设备虽然 SDK 是老代码但只要把它的生命周期管理好稳定性并不差。最后说一个验证技巧设备端自己新建一个测试用户打两枚不同指纹分别刷卡后拉取记录核对工号、时间、验证方式三个字段是否完全一致。这个操作几乎能验证整条数据链路从协议解析到数据库入库都覆盖了比对着文档看半天接口定义有用得多。希望帮到你。本文还有配套的精品资源点击获取