☰
JNA实战:Java调用读卡器DLL实现Mifare卡读写
2026/10/10 5:12:22 网站建设 项目流程

简介:面向需要在Java工程中集成智能卡或射频识别(RFID)读卡能力的开发者,这套精简示例演示了如何通过Java本地接口(JNI)调用明华RD读卡器官方驱动Mwic_32.dll。明华RD系列读卡器常用于读取智能卡、身份证等射频识别设备,Mwic_32动态链接库为上层提供设备初始化、读卡、控制命令等底层函数;Java程序需要先声明本地方法,再借助System.loadLibrary加载动态库才能完成调用。包内共6个文件:两个Java源码展示了与memory、fuction相关的本地方法接口定义和调用逻辑,三个class文件是编译后产物,方便直接运行或对照学习,一个txt开发包说明则整理了驱动配置、动态库路径设置及常见错误处理要点,对梳理JNI调用流程很有帮助。通过阅读代码,可以掌握声明native方法、加载动态库、传递字节数组参数等关键步骤,也能理解调用约定不匹配、库文件缺失等典型异常的处理思路,为后续封装成独立模块打好基础。整个rar压缩包仅4KB,小巧但结构完整。已有469人学习下载,适合具备基础Java语法、需要为Mwic_32.dll编写Java调用的开发人员参考,也可作为二次开发或排障的起点。

1. 项目背景:为什么Java要碰读卡器的DLL

最近在做一个会员卡管理系统,后端用 Java,前端负责发卡和查询页面。卡机设备是明华 RD 系列读卡器,厂家 SDK 的核心就是 Mwic_32.dll。这个 DLL 只有 C/C++ 接口文档和示例,Java 要操作读卡器,第一关就是解决“Java 怎么调用 DLL”的问题。Mifare 卡(就是市面上最常见的 IC 卡)的扇区读写、密钥认证、UID 读取,全封装在这套 API 里,所以只要能把 DLL 的函数调通,后面所有业务就有了硬件基础。

我在网上翻了很久,大多数帖子停留在“可以用 JNA”这句话,真正把环境坑点、函数映射、调用顺序讲明白的不多。所以这篇内容适合正在做同类设备对接的 Java 开发,也适合第一次接触 JNA 调 DLL 的朋友。里面所有代码都是项目里实际跑通的,不是从文档里抄一遍就发出来。我会把关键“为什么”也讲清楚,不然换个型号、换个 DLL 版本,你大概率还要再踩一遍我的坑。

1.1 技术选型:JNI、JNative、JNA 怎么选

Java 调 Windows DLL 无非三条路:JNI、JNative、JNA。

JNI 是 Java 官方的原生接口,但用起来最折腾。你得先用 javac 生成头文件,再用 C/C++ 写一个中间层,把这个中间层编译成另一个 DLL,Java 通过它去调 Mwic_32.dll。等于自己维护一套 C 桥接代码,编译环境要装,跨平台要烧脑,对业务项目来说成本太高。

JNative 是老牌方案,把底层细节包掉了一部分,但用的人少,维护早就停了,在高版本 JDK 上容易出兼容问题。

JNA 是 JNI 的“动态代理版”,你只需要定义一个 Java 接口,方法名和 DLL 导出函数名一一对应,JNA 运行时自动生成桥接代码。对“只要调通 API、不追求底层控制”的项目来说,这是最省事的。我把三个方案对比放在一起,你心里大概就有数:

方案使用成本是否写 C 代码稳定性适合场景
JNI高必须高需要极致性能或深度定制封装
JNative中否中,多年未更新老项目维护
JNA低否高,社区活跃业务系统对接成熟 DLL

我最终选了 JNA,后面所有内容也基于 JNA。选型理由说到底就一条:公司的目标是做业务,不是为读卡器写驱动。

1.2 为什么要关注 DLL 位数和 JVM 位数

Mwic_32.dll 从名字就能看出来,它是 32 位动态库。Windows 上 32 位进程只能加载 32 位 DLL,64 位进程只能加载 64 位 DLL,这个规则非常硬。也就是说,你用 64 位 JDK 启动 Java 程序,哪怕 JNA 依赖写对了,Native.load 一样会失败,直接抛 UnsatisfiedLinkError。

这个坑太常见了,很多人第一反应是“代码哪里写错了”,实际是 JDK 位数不对。所以环境准备的第一件事不是写代码,而是确认 JVM 是 32 位的。你可以命令行执行 java -version,输出里如果带 64-Bit,就得装一个 32 位 JDK。我这边用 1.8 的 32 位版本跑了几个月,很稳。另外,开发机和生产机的位数最容易不一致,发布前一定检查两边的 JVM。

2. 项目准备:依赖、硬件和运行环境

2.1 Maven 依赖与基础工程配置

要用 JNA,先在 pom.xml 里加依赖。我用的是 5.13.0,跑了一年多没出现问题,后续版本接口也兼容。

<dependency> <groupId>net.java.dev.jna</groupId> <artifactId>jna</artifactId> <version>5.13.0</version> </dependency>

顺便提醒一句,如果项目编译时出现“源发行版 17 需要目标发行版 17”的报错,别慌,这不是业务代码问题,是 Maven 编译器插件默认用的 JDK 版本和项目设置不一致。两个版本必须统一,可以在 pom 里固定:

<properties> <maven.compiler.source>1.8</maven.compiler.source> <maven.compiler.target>1.8</maven.compiler.target> </properties>

如果你团队统一用 JDK 17,那 source 和 target 都写 17,关键是两个不能一边高一边低,否则每次编译都会跳出这种提示。这类问题跟读卡器没关系,但经常在准备环境阶段和 DLL 加载问题混在一起,容易让人误判。

2.2 硬件连接与端口确认

明华 RD 读卡器有 RS232 串口版,也有 USB 版。USB 版插上以后,系统通常识别成一个虚拟串口,设备管理器里能看到 COMx。写代码之前先打开设备管理器,在“端口(COM 和 LPT)”下看当前是哪个 COM 口,然后把厂商 Demo 跑一遍,确认这个串口下读卡、写卡都正常,再开始做 Java 调试。

这一步看着多余,但能把“硬件问题”和“代码问题”切开。我有一次折腾到半夜,MF_Init 一直初始化失败,后来发现读卡器根本没被识别成 COM 口,而是被驱动装成了别的设备类别。先拿 Demo 验证,能省掉后面一大半排查时间。

默认波特率按厂商文档一般用 9600。如果你改动过读卡器设置,这里也要跟着改,否则会出现初始化成功但发指令没响应的情况。

2.3 DLL 放置位置与最小加载测试

Mwic_32.dll 到底放哪,这是个容易忽略的问题。JNA 的 Native.load 会按这个顺序找:jna.library.path 属性指定路径、系统 PATH 环境变量、当前目录。所以我习惯在程序启动时显式指定:

System.setProperty("jna.library.path", "D:/rdreader/libs");

如果不指定,就把 DLL 放到系统 PATH 包含的目录里,或者项目根目录。但生产环境不要依赖系统 PATH,显式指定最稳妥。

我建议正式功能之前先写一个最小加载测试,只调用 MF_Init,确认 DLL 能被正确加载并执行。加载失败时,除了位数问题,还可能是系统缺 Microsoft Visual C++ Redistributable 运行库,顺手装上再试。这个点厂商文档一般不写,但线上环境踩到过。

3. 核心函数解析:Mwic_32.dll 常用 API

3.1 一张表看明白常用函数

明华 Mwic_32.dll 函数风格很统一,绝大多数返回 0 表示成功,非 0 表示失败。下面是我项目里实际用到的函数,参数以我手头这个 DLL 版本为准:

函数作用关键参数说明
MF_Init初始化读卡器并打开串口port:串口号;baud:波特率
MF_Close关闭读卡器,释放串口无
MF_Request寻卡,查卡是否在感应区ctrl:控制字;mode:0x52 常用于包含休眠卡
MF_Anticoll防冲突,拿到卡 UIDsnr 是 4 字节缓冲区,返回卡序列号
MF_Select选中一张卡,后续操作针它snr 必须传刚才拿到的 UID
MF_Halt让卡休眠常用于一卡一密流程中复位
MF_LoadKey把密钥加载到读卡器内部密钥区sec 传扇区号;key 是 6 字节密钥
MF_Authentication用密钥对指定扇区认证mode:0x60 验 A 密钥,0x61 验 B;sec 传扇区号
MF_Read读一个数据块block 传块号;data 是 16 字节缓冲区
MF_Write写一个数据块block 传块号;data 是 16 字节数据

这里要特别说明 sector 和 block 的关系。Mifare S50 卡有 16 个扇区,每个扇区 4 个块,块号从 0 到 63。块号等于 sector * 4 加上块内偏移(0~3)。认证是针对扇区级别的,读数据是以块为单位的,这两个概念不能混。我一开始以为认证传块号,读卡器返回成功但读出来全是 00,折腾半天才发现认证参数得传扇区号,数据读写才传块号。

3.2 JNA 接口定义:把 C 函数翻译成 Java

JNA 的定义方式很直接:建一个接口继承 Library,方法名和 DLL 导出函数保持一致,Native.load 加载动态库。注意加载名不写 .dll 后缀。

import com.sun.jna.Library; import com.sun.jna.Native; public interface Mwic32 extends Library { Mwic32 INSTANCE = Native.load("Mwic_32", Mwic32.class); int MF_Init(int port, int baud); int MF_Close(); int MF_Request(int ctrl, int mode, byte[] tagType); int MF_Anticoll(int ctrl, byte[] snr); int MF_Select(int ctrl, byte[] snr); int MF_Halt(int ctrl); int MF_LoadKey(int ctrl, int mode, int sec, byte[] key); int MF_Authentication(int ctrl, int mode, int sec); int MF_Read(int ctrl, int block, byte[] data); int MF_Write(int ctrl, int block, byte[] data); }

这里有个关键点:C 函数里的 unsigned char* 指针参数,在 Java 里对应 byte[],不能用 String 或 int[]。JNA 会把数组直接映射成底层内存缓冲区,DLL 直接读写这块内存,所以数据才能正确返回。另外函数返回值必须用 int,不要用 boolean,因为错误码值很多,不是只有 0 和 1。

我见过有人把密钥参数声明成 String,认证一直失败,原因就是 JNA 把 Java String 转成了 C 的 char*,UTF-8 编码和字节数全对不上。密钥、UID、数据块这些二进制内容,统一用 byte[] 是最安全的做法。

4. 实操:从初始化到读写卡完整跑通

4.1 初始化读卡器

我封装了一个 CardService,第一步是初始化。端口号不要猜,直接遍历 0、1、2 去试,哪个返回 0 就说明读卡器在哪个口。部分 DLL 版本里 port 0 代表自动探测,但显式端口更可控:

private Mwic32 initReader() { int baud = 9600; int ret = -1; for (int p = 0; p < 4; p++) { ret = Mwic32.INSTANCE.MF_Init(p, baud); if (ret == 0) { System.out.println("读卡器初始化成功,端口=" + p); return Mwic32.INSTANCE; } } throw new IllegalStateException("读卡器初始化失败,请检查串口连接和驱动"); }

初始化失败时,除了端口问题,还要看是不是有别的程序占用了串口。Windows 的串口是独占的,厂商 Demo 没退出,Java 这边就打不开,这是最容易被忽略的“假故障”。实测下来,关掉 Demo 后立刻就好了。

4.2 寻卡、防冲突和选卡:三步拿到卡片身份

卡片放到感应区后,流程是固定的:MF_Request 寻卡,MF_Anticoll 防冲突拿 UID,MF_Select 选中卡片。用生活例子理解:寻卡是问“有没有人”,防冲突是“人多了先排好队,一个一个来”,选卡是“叫的就是你,别动”。

byte[] tagType = new byte[2]; int ret = Mwic32.INSTANCE.MF_Request(0, 0x52, tagType); if (ret != 0) { throw new IllegalStateException("寻卡失败,请确认卡片已放在感应区"); } byte[] snr = new byte[4]; ret = Mwic32.INSTANCE.MF_Anticoll(0, snr); if (ret != 0) { throw new IllegalStateException("防冲突失败,错误码=" + ret); } ret = Mwic32.INSTANCE.MF_Select(0, snr); if (ret != 0) { throw new IllegalStateException("选卡失败,错误码=" + ret); } String uidHex = bytesToHex(snr); System.out.println("当前卡片 UID: " + uidHex);

MF_Anticoll 返回的 snr 就是卡 UID,正好 4 字节。很多业务系统的“卡号”用的就是这个 UID,如果你想直接拿 UID 当业务主键,到这一步就够了,不一定要进扇区读写。打印 UID 时必须用 & 0xFF 转成无符号再看,否则负数补位会显示成一长串 F,看着像 bug,实际是 Java byte 有符号导致的展示问题。

4.3 密钥认证:不认证就读取只会得到 00

拿到卡还不能马上读数据,得先通过密钥认证。Mifare 卡出厂密钥默认是 6 个 0xFF,很多测试卡没有改过密钥,用默认值能通的概率很高。

byte[] key = new byte[] { (byte) 0xFF, (byte) 0xFF, (byte) 0xFF, (byte) 0xFF, (byte) 0xFF, (byte) 0xFF }; int sector = 1; int ret = Mwic32.INSTANCE.MF_LoadKey(0, 0x60, sector, key); if (ret != 0) { throw new IllegalStateException("加载密钥失败,错误码=" + ret); } ret = Mwic32.INSTANCE.MF_Authentication(0, 0x60, sector); if (ret != 0) { throw new IllegalStateException("扇区认证失败,错误码=" + ret); }

注意 MF_Authentication 的第三参,在部分 DLL 版本里也可以是块号,我手上的版本传扇区号。如果你照着写总是认证失败,把这里改成 sector * 4(该扇区第一个块的块号)再试。这种版本差异厂商文档基本不标,只能靠实际试。所以建议留一个测试台专门验证参数差异。

4.4 读写数据块并释放资源

认证成功后,就能对扇区内的块做读写。以扇区 1 为例,块号是 4、5、6、7,其中 7 是控制块,虽然能读但不要乱写,控制字写坏了整张卡就报废。安全做法是只写 4、5、6 三个数据块。

int block = sector * 4; // 扇区1 -> 块4 byte[] data = new byte[16]; ret = Mwic32.INSTANCE.MF_Read(0, block, data); if (ret != 0) { throw new IllegalStateException("读块失败,错误码=" + ret); }

写数据的重点是一个块固定 16 字节,不够要补 0,多了塞不下:

byte[] writeData = new byte[16]; byte[] content = "HELLO_RD".getBytes(StandardCharsets.UTF_8); System.arraycopy(content, 0, writeData, 0, Math.min(content.length, 16)); ret = Mwic32.INSTANCE.MF_Write(0, block, writeData); if (ret != 0) { throw new IllegalStateException("写块失败,错误码=" + ret); }

每次操作结束,尤其是一次性发多张卡时,建议先 MF_Halt 让卡休眠,再 MF_Close 释放串口。不要小看这一步,不 Halt 直接让下一张卡上来,经常出现“上一张卡的操作结果串到下一张卡”的诡异现象,实际就是卡片状态机没有复位。这个坑在批量发卡时几乎必然遇到。

5. 常见问题与排查技巧速查表

5.1 现象、可能原因与解决办法

现象可能原因解决办法
Native.load 抛 UnsatisfiedLinkErrorJVM 是 64 位,DLL 是 32 位换 32 位 JDK 启动项目
MF_Init 返回非 0串口被占用或端口号不对关闭厂商 Demo;遍历 0~3 测试端口
能寻卡但认证失败密钥错误;认证模式不对;参数版本差异确认密钥;检查 0x60/0x61;试传块号
读取返回全 00未认证,或认证扇区不匹配先认证再读;检查扇区号
连续发卡时读卡错乱没有 MF_Halt 复位卡片状态每张卡流程结束调 MF_Halt
程序加载 DLL 慢或失败系统缺 VC 运行库安装 Microsoft Visual C++ Redistributable

这张表是多次踩坑后整理的,基本覆盖了设备对接 90% 的初期问题。你可以直接拿去当项目验收前的检查清单。

5.2 Java 环境那些“插曲”

做这套对接时还会遇到一类和读卡器无关的 Java 环境问题,这类问题也经常被误判成读卡器代码的问题。比如“mvn、pnpm、git 等命令无法识别为 cmdlet、函数、脚本文件或可运行程序的名称”,这是因为这些可执行文件所在的目录没加到 PATH 环境变量,跟项目本身没关系,Windows 下把对应 bin 目录加进 PATH,重开终端就好。

再一个是 Lombok 在高版本 JDK 下报“you aren't using a compiler supported by lombok”,这是 Lombok 版本太旧,升级到 1.18.30 以上就解决。还有“java: OutOfMemoryError: Insufficient memory”,如果发卡量大,JVM 堆直接调大,比如

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询