ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Java调用明华RD读卡器:JNA操作Mwic_32.dll实践指南

Java调用明华RD读卡器:JNA操作Mwic_32.dll实践指南 简介面向需要在Java工程中调用明华RD读卡器硬件能力的开发人员这份资源以Mwic_32.dll为入口展示了通过JNI技术完成本地方法声明、DLL加载以及读卡函数调用的完整示例典型适用场景包括智能卡、身份证等RFID设备的读取与交互。压缩包共收录6个文件包括2个Java源文件、3个编译后的class文件与1份开发包说明txt整体仅4KB结构紧凑适合作为轻量级参考代码直接查看或快速集成到业务模块中。目前已有474人学习/下载。Java源文件清晰标注了System.loadLibrary的加载方式、native方法的定义以及读写缓冲区处理class文件则便于直接复用开发包说明文档对驱动安装、DLL路径配置、调用约定不匹配等常见问题给出了提示。对于正在与明华RD系列读卡器对接的Java开发者可对照示例理解JNI接口的封装思路并覆盖读卡器初始化、数据读取等常用操作从而避开环境配置与本地调用上的典型陷阱缩短联调周期。1. 用 Java 操作明华 RD 读卡器问题不在 DLL而在调用链做过会员卡、门禁或食堂卡系统的人大概率都遇到过这台设备明华 RD 读卡器一个串口或者 USB 转串口的小盒子卡片往上一贴就能读出卡号。这种项目后端多半是 Java而明华随包提供的驱动核心是 Mwic_32.dll——里面导出了一批以 MF_ 开头的函数负责从串口初始化、寻卡、防碰撞到卡片块读写的完整链路。这篇文章直接回答三件事Java 怎么加载这个 DLL、读写一张卡最短要写多少代码、以及那些让人怀疑人生的报错到底出在哪。先说结论真正让项目翻车的从来不是 DLL 本身而是 JVM 位数不一致、串口被占用和 JNA 参数映射错误这三座大山。2. 明华 RD 读卡器与 Mwic_32.dll 的调用模型JNI、JNative、JNA 怎么选2.1 先看懂 Mwic_32.dll 的函数调用链Mwic_32.dll 是非接触式 IC 卡读卡器的专用驱动函数命名基本是 MF_ 前缀比如 MF_Init、MF_OpenCOM、MF_Request、MF_Anticoll、MF_Select、MF_Read、MF_Write、MF_CloseCOM。别看函数多它的工作流程和 ISO14443 协议的交互过程是一一对应的先初始化串口再轮询天线感应区里有没有卡有卡就做防碰撞解析出卡号选卡成功之后才能对卡内的块数据做读写。这个调用顺序非常关键不能乱调。我见过有人直接把 MF_Read 放在 MF_Request 后面结果读出来的数据偶尔对、偶尔错后来才发现中间少了 MF_Anticoll 和 MF_Select 两步。DLL 内部是有状态的上一条指令没把卡选定下一条读写指令发给谁就说不清了。所以生产代码里哪怕你只是想要卡号也应该把 REQA、防碰撞、SELECT 这三步都走完再进入读写逻辑。下面是明华 Mwic_32.dll 里最常见的导出函数和它们的职责建议保存下来对照驱动自带的 mwic32.h 头文件看函数名位置作用对应协议阶段MF_Init上电初始化初始化串口参数、设备类型初始化MF_OpenCOM打开串口占住 COM 口并建立通信初始化MF_Request寻卡向天线区发送请求命令检测是否有卡进入REQA/WUPAMF_Anticoll防碰撞多张卡同时进场时选出一张并返回卡号防碰撞循环MF_Select选卡选中指定卡号后续读写只对这张卡生效SELECTMF_Read读块读取指定块的 16 字节数据块读MF_Write写块写入指定块的数据块写MF_CloseCOM关闭串口释放串口资源关闭部分固件版本还有 MF_Halt、MF_GetBaudRate、MF_SetBaudRate、MF_LoadKey 之类的扩展函数作用分别是让卡片休眠、查询和修改波特率、加载密钥。包装驱动包里一般会带一份函数说明写代码前花十分钟把这份文件扫一遍比瞎猜参数靠谱得多。2.2 用 JNA 而不是 JNI三种方案对比Java 调 Windows DLL 的方案大致有三种JNI、JNative、JNA。JNI 是正规军但要自己写 C 语言桥接层再拿 VC 或 GCC 编译成新的 DLL中间还得处理 JNI 头文件和 native method 的导出。单就“读一张卡”这个需求来说为了一个 Mwic_32.dll 再编一层 DLL维护成本完全不划算。JNative 是更老一批项目里常见的库它把 JNI 封装了一层用起来比裸 JNI 简单。但这个库已经多年不活跃遇到较新版本的 Windows 和 Java 8 时容易出兼容问题。我评估下来现在做这类调用的首选依然是 JNA。JNA 的底层也是 JNI但它不需要你写任何 C 代码。你只要定义一个 Java 接口声明和 DLL 导出函数签名一致的方法JNA 会在运行时动态生成代理去调用本地函数。C 语言里的 char* 和 int* 可以分别映射成 byte[] 和 IntByReference对 Mwic_32.dll 这种以字节数组为主参数的驱动来说非常顺手。选 JNA 还有一个现实原因网上能找到的 Java 读写卡参考例子绝大多数也是基于 JNA 写的遇到问题有参照。3. 环境准备把 Mwic_32.dll 挂到 JNA 上位数、路径、classloader3.1 位数匹配与运行参数Mwic_32.dll 从名字就能看出来这是 32 位版本的动态库。Java 进程如果跑在 64 位 JVM 上JNA 加载这个 DLL 时会直接报错错误通常是UnsatisfiedLinkError或者Native library not found in resource path而且没有任何额外提示。很多人写了几百行业务代码才发现问题出在 JDK 位数这一条一定要最先排查。检查方式很简单在命令行里执行java -version输出里有64-Bit就是 64 位 JVM如果显示的是32-Bit或者没有标明 64-Bit说明 JVM 本身就是 32 位。手头只有 64 位 JDK 的话要么去装一个 32 位 JDK要么确认厂商是否提供了 64 位版本的 DLL。接着确认 DLL 本身的位数。Windows 上不方便直接看可以在 Git Bash 或者 WSL 里用 file 命令file Mwic_32.dll # PE32 executable (DLL) (console) Intel 80386 - 32 位 DLL # PE32 executable (DLL) (console) x86-64 - 64 位 DLL位数确认之后再把 DLL 放到 JNA 能找到的位置。JNA 的查找顺序大致是jna.library.path、java.library.path、当前目录、PATH。开发调试时最省事的就是把 Mwic_32.dll 放到项目根目录启动参数里显式给一下路径java -Djna.library.path./dll -Djava.library.path./dll -jar card-reader.jar有一点要特别注意JNA 默认找的文件名是Mwic_32.dllNative.load 的字符串参数不要带 .dll 后缀大小写也不要在意Windows 会自己匹配。如果你的 DLL 实际文件名是Mwic32.dll那 Native.load 里也要写成Mwic32。如果用 Spring Boot 打包别直接往 classes 根目录塞 DLL 然后在代码里 Native.load很多东西会在构建时被 Maven 过滤掉。我一般把 DLL 放 src/main/resources 下应用启动时用 ClassLoader 读成 InputStream抽到临时目录再设置 jna.library.path 指过去。这样 jar 包在任何机器上都能跑避免路径问题。3.2 用 JNA 写 Mwic32.java 接口的最小代码接下来是最核心的一步定义一个接口去映射 Mwic_32.dll 的导出函数。先看一份最小实现import com.sun.jna.Library; import com.sun.jna.Native; import com.sun.jna.ptr.IntByReference; public interface Mwic32 extends Library { Mwic32 INSTANCE Native.load(Mwic_32, Mwic32.class); int MF_Init(int comAddr, int baud, int comType); int MF_OpenCOM(int comAddr, int baud, int comType); int MF_CloseCOM(); int MF_Request(int tagType, byte[] pData, IntByReference pLen); int MF_Anticoll(byte[] pSnr, IntByReference pLen); int MF_Select(byte[] pSnr, IntByReference pLen); int MF_Read(int tagType, int block, byte[] pBuffer, IntByReference pSize); int MF_Write(int tagType, int block, byte[] pData); }这里有几个映射规则要解释清楚。C 函数的unsigned char*参数在 JNA 里统一映射成 byte[]int*参数映射成 IntByReference纯 int 直接映射成 int。MF_Read 的第四个参数在很多头文件里是int *Size用 IntByReference 传进去DLL 会把实际读到的字节数写回这个引用调用结束后再 getValue 取出来用。注意所有方法默认返回 int。返回 0 一般代表成功非 0 对应错误码。每个固件的错误码表略有不同驱动包里通常会有一页说明比如 0x0F 代表操作超时0x04 代表命令执行失败。写代码时别偷懒每个返回值都打日志后面排查问题全靠这些数字。提示一定要先看驱动包里的 mwic32.h不同批次的 DLL 函数签名可能有细微差别。比如 MF_Anticoll 有些版本带一个 Bcnt 参数有些版本不带。这次接口里不写 Bcnt如果你的头文件里有就加上一个 int 参数调用时传 8 或 16 即可。签名不对不会导致编译报错但运行时会拿到错误数据。4. 完整读写流程从 MF_Init 到 MF_Write 的 Java 可运行代码4.1 串口初始化与开启MF_Init 和 MF_OpenCOM 的参数怎么设把接口定义好之后读卡主流程就按部就班了。先初始化并打开串口。以下代码可以直接跑通“打开读卡器”这一步public class MfCardReader { private static final Mwic32 DLL Mwic32.INSTANCE; // 设备管理器里看到的 COM 口编号 private static final int COM_PORT 6; // 明华默认波特率特殊型号可能不同 private static final int COM_BAUD 9600; // 设备类型对照说明书枚举串口型一般传 0 private static final int COM_TYPE 0; public static void main(String[] args) { int ret DLL.MF_Init(COM_PORT, COM_BAUD, COM_TYPE); if (ret ! 0) { System.err.println(MF_Init failed, ret ret); return; } ret DLL.MF_OpenCOM(COM_PORT, COM_BAUD, COM_TYPE); if (ret ! 0) { System.err.println(MF_OpenCOM failed, ret ret); return; } System.out.println(read card ready, waiting for card...); } }MF_Init 和 MF_OpenCOM 是两个独立调用缺一不可。MF_Init 负责把驱动内部状态重置MF_OpenCOM 负责真正占用串口。串口号不是随便填的要在设备管理器里看“端口(COM 和 LPT)”下读卡器对应的编号插 USB 转串口线时每次位置不同 COM 号也可能变。如果设备的 COM 号大于 9老版本的 DLL 经常处理不了我一般会先在设备管理器里把端口改成 COM6 或 COM7 再跑代码。波特率默认 9600这是明华读卡器最常见的出厂设置。如果你的设备之前被改动过波特率大概率会出现 MF_OpenCOM 成功但 MF_Request 永远超时的情况。这时候要么用驱动自带的配置工具重置要么在代码里先调 MF_SetBaudRate 切换。COM_TYPE 是设备类型需要对照你手上那台设备的说明书填。串口型读卡器填 0 是最常见的并口型和 USB 型会对应其他枚举值。这个参数错了通常不会报错但后续所有指令都没有响应。4.2 寻卡、防碰撞与选卡MF_Request、MF_Anticoll、MF_Select 的调用顺序串口打开之后进入循环读卡阶段。先放一段完整的寻卡、防碰撞和选卡代码// 等待卡片进入感应区 IntByReference len new IntByReference(); byte[] requestData new byte[8]; int ret DLL.MF_Request(0, requestData, len); if (ret ! 0) { System.out.println(no card in area, ret ret); return; } // 防碰撞解析出唯一一张卡的序列号 byte[] cardSnr new byte[4]; ret DLL.MF_Anticoll(cardSnr, len); if (ret ! 0) { System.err.println(anticoll failed, ret ret); return; } String uidHex bytesToHex(cardSnr); System.out.println(card uid uidHex); // 选卡让后续读写只针对这张卡 ret DLL.MF_Select(cardSnr, len); if (ret ! 0) { System.err.println(select failed, ret ret); return; }MF_Request 本质上是往天线区发一个标准 REQA 命令返回值非 0 说明当前没有合法卡片在感应区内。这个函数在循环里调用的频率不用太高一般几百毫秒一次就够了调太频繁对读卡器硬件没有好处只会让日志刷屏。MF_Anticoll 是真正的碰撞检测函数对应 ISO14443 的防碰撞循环。当几张卡同时放在感应区这个函数会按照协议把其中一张卡的 4 字节序列号解析出来。打印卡号时有一个容易混淆的点有些测试软件按大端显示卡号有些按小端显示。如果发现读出来的卡号和驱动自带工具显示的不一致把字节数组倒序再打印一次试试。MF_Select 是选卡函数选中之后DLL 内部会维护一个“当前已选卡”的状态后续 MF_Read 和 MF_Write 都作用于这张卡。顺序不能变不要先 Select 再 Anticoll也不要直接用 MF_Request 的结果当卡号。下面补一个 bytesToHex 工具方法直接复制就能用public static String bytesToHex(byte[] bytes) { StringBuilder sb new StringBuilder(); for (byte b : bytes) { sb.append(String.format(%02X, b)); } return sb.toString(); }4.3 数据块读写MF_Read 与 MF_Write 的参数和块号选择选卡成功后就能对 Mifare 卡的数据块做读写。Mifare 1K 卡片有 16 个扇区每个扇区 4 个块每个块 16 字节。第 0 扇区的块 0 是厂商块出厂时烧录了卡号和厂商信息每个扇区的第 3 块比如块 3、块 7、块 11是扇区尾块存放密钥和控制位不能用 MF_Write 乱写。安全的读写块号是各扇区的第 0、1、2 块。下面示例用扇区 1 的块 4int block 4; byte[] buffer new byte[16]; IntByReference size new IntByReference(buffer.length); ret DLL.MF_Read(0, block, buffer, size); if (ret 0 size.getValue() 0) { System.out.println(block data bytesToHex(buffer)); } else { System.err.println(MF_Read failed, ret ret); } byte[] data new byte[16]; data[0] 0x11; data[1] 0x22; ret DLL.MF_Write(0, block, data); if (ret 0) { System.out.println(write ok); } else { System.err.println(MF_Write failed, ret ret); }MF_Read 的缓冲区必须给足 16 字节。只给 4 字节是非常典型的错误轻则读到的数据是残缺的重则 DLL 直接在内存里越界写引发 JVM 崩溃。MF_Write 写入的数据也必须正好 16 字节字节数不足或超出都会让驱动命令异常。另外一个常见的读写失败原因是密钥问题。默认出厂卡通常没改过密钥明华 DLL 内部会自动完成认证但如果卡片之前在别的系统里被写入了自定义密钥MF_Read 就会返回错误码。这时候要确认驱动包里有没有 MF_LoadKey 或 MF_Authentication 之类的函数在 MF_Select 之后、MF_Read 之前先调用认证。如果驱动不提供显式认证卡片又改了密钥那系统侧就没法直接读这张卡了。流程收尾时要释放串口。代码可以加一个 finally但注意第 6 章会讲频繁关串口反而会带来新的问题下面这个关闭动作应当是应用退出时才做一次。finally { DLL.MF_Halt(); DLL.MF_CloseCOM(); }5. 避坑指南Java 调 Mwic_32.dll 最常见的 5 个翻车现场5.1 UnsatisfiedLinkErrorDLL 明明存在JNA 就是加载不上现象控制台抛java.lang.UnsatisfiedLinkError: Unable to load library Mwic_32但 DLL 文件就在项目根目录。很多人会反复检查路径依然无解。原因绝大多数情况是 JVM 位数和 DLL 位数不匹配。Mwic_32.dll 是 32 位动态库你启动的是 64 位 JVMWindows 加载器会直接拒绝这个 PE 文件。少数情况是 jna.library.path 没配DLL 放在 jar 包内部没有抽出来。解决执行java -version看 JVM 位数确认是 32 位 JVM 后重试。另外把 Mwic_32.dll 从 jar 包里移到外部目录用-Djna.library.path显式指定。如果 DLL 是 64 位而项目必须跑 64 位 JVM就去找厂商要 64 位版本推动态库。这个坑只要在项目第一天排查掉后面能省下大量时间。5.2 MF_OpenCOM 返回错误码串口一直打不开现象读卡器设备管理器里正常COM 号也填对了但 MF_OpenCOM 总是返回非 0程序一开始就退出。原因串口被其他软件占用。明华读卡器的联机调试工具、串口助手、甚至上上一个没有释放句柄的 Java 进程都会把 COM 口占住。老版本驱动对 COM10 以上的端口号也支持不好导致 open 命令直接失败。解决先关闭所有可能占用串口的软件重启 IDE 或电脑后再试。设备管理器里把 COM 口号改成 COM6 到 COM9 范围内的任意一个改完重新插拔读卡器 USB 线。如果项目里之前写过 MF_CloseCOM确认它没有在每轮循环里被调用只有退出时才关。5.3 JNA 参数映射错JVM 毫无征兆地崩溃现象程序跑了十几分钟突然hs_err_pid日志出现在工作目录JVM 直接死掉没有 Java 异常栈。这种问题在本地调试时很难重现往往一上生产就频繁发生。原因JNA 参数映射和 DLL 的 C 签名不一致。最常见的是 MF_Read 缓冲区给太小DLL 按 16 字节写内存Java 侧只给了 4 字节数组属于内存越界另一种是把int*参数错误映射成 byte[]导致 JNA 写入长度时越界。解决MF_Read 的 pBuffer 严格定义成new byte[16]长度参数优先用 IntByReference不要用 byte[]。所有方法签名和 mwic32.h 逐字核对特别是指针参数。这类崩溃发生后保留 hs_err 日志把 dump 信息里的线程名和本地栈截图存下来对照 DLL 函数定位。5.4 MF_Request 一直超时驱动测试工具却正常现象代码进入循环后MF_Request 永远返回超时错误码但用明华自带的读卡演示软件卡片一贴就能读出卡号。资深工程师容易把矛头指向卡换了三张卡依然如此。原因卡片天线对准的位置不对或者 MF_Request 的 tagType 参数填错了。驱动演示工具会使用和代码不同的寻卡参数某些固件对 tagType 传值非常敏感传 1 表示探测 Type B 卡而你手上的卡是 Type A就会无限超时。解决把卡片水平贴在读卡器正中央的 LED 指示灯区域不要拿着卡来回晃动。MF_Request 的 tagType 先按驱动文档里 Type A 卡对应的默认值传通常是 0。如果还不行降低轮询频率把连续读卡改成 500ms 一次给 DLL 足够时间从串口收到数据。另外确认读卡器的蜂鸣器在贴卡时有没有响不响说明底层的 REQA 都没收到回应问题在物理层不在代码。5.5 MF_Write 返回成功读回来却全是 0 或者数据丢失现象MF_Write 的返回值是 0但紧接着用 MF_Read 读同一块发现数据不对甚至整块全 0。更糟的情况是写完之后卡片直接不可读。原因写错了块号。很多新手从块 0 开始写块 0 是厂商块里面写着 UID 和厂商数据每个扇区的块 3 是尾块存放的是密钥和控制条件。这两个位置都不是普通数据区向尾块写入非预期内容会直接改写密钥和控制位轻则这块扇区无法再写重则整个扇区数据报废。解决写数据只选择数据块位置扇区 0 选块 1、块 2扇区 1 选块 4、块 5、块 6。永远不要去写块 0 和任何尾块。如果已经写坏了控制位先看能不能用驱动工具重新初始化卡片不行就换新卡。生产环境里写卡前要做块号校验超出数据块范围直接抛业务异常别让脏数据碰硬件。6. 进阶把读卡逻辑封装成单线程串口服务并验证6.1 单线程队列模型不要在业务线程里直接调 DLL读写卡在大多数业务系统里只是很小的一步但如果把 DLL 调用直接丢进多线程业务线程池会引出串口竞争问题。读卡器是独占串口设备两个线程同时 MF_Request最终只能有一个拿到结果另一个永久超时。我一般做法是让所有读卡命令走一个单线程的串口服务队列public interface CardCommand { void execute(); } public class CardReaderService implements Runnable { private final BlockingQueueCardCommand queue new LinkedBlockingQueue(); private volatile boolean running true; public void enqueue(CardCommand command) { queue.offer(command); } Override public void run() { while (running) { try { CardCommand command queue.poll(200, TimeUnit.MILLISECONDS); if (command ! null) { command.execute(); } } catch (InterruptedException e) { Thread.currentThread().interrupt(); } } } public void shutdown() { running false; } }然后把寻卡、读块的业务逻辑塞到 CardCommand 里。这样无论上层是 HTTP 接口还是 Swing 事件都只负责往队列里放任务读卡器同一时刻只处理一个命令。如果某个操作需要超时控制调用方可以用Future.get(3, TimeUnit.SECONDS)拿结果避免业务线程卡死在 DLL 调用上。这个模型还有一层好处DLL 内部状态不会因为多线程交叉调用而错乱。MF_Select 选中一张卡之后队列里的下一个命令可能是另一张卡的读块单线程保证上一个命令已经把卡片状态清理干净。6.2 验证技巧写入后回读比对和错误码日志写卡操作是最难验证的因为返回值只能说明“DLL 把这个命令发出去了”不能说明“卡片真的写进了你的数据”。所以我在写卡逻辑后面一定会补一段回读比对ret DLL.MF_Write(0, 4, data); if (ret 0) { byte[] verify new byte[16]; IntByReference vSize new IntByReference(16); ret DLL.MF_Read(0, 4, verify, vSize); if (ret 0 Arrays.equals(data, verify)) { System.out.println(verify ok); } else { System.err.println(verify failed, ret ret); } }验证通过才算写卡成功不要轻信 MF_Write 的返回值。注意写入后立刻读回时卡片不要移出感应区否则 MF_Read 会因为没有选中卡而失败。另一个习惯是给所有 DLL 调用加日志埋点记录时间戳、函数名、参数和返回值。读卡器这种硬件联动程序最怕的问题就是“偶尔失败”。偶发问题出现时日志里任何一条足够长的超时间隔都是突破口。我遇到过一个问题读卡器每运行半小时就寻卡失败查日志发现 MF_Init 只在启动时空闲时被安全释放后来改成只在应用退出时关闭串口就再没复现。现在每次跟这类 DLL 打交道我都坚持把所有返回值先打印出来再做业务判断这个习惯帮我避开了不少坑。希望帮到你。本文还有配套的精品资源点击获取
返回列表