
简介这是一份面向Java开发者的明华RD读卡器JNI调用示例重点关注在Java应用中通过Mwic_32.dll操作读卡器的实现提供了一套可直接参考的轻量级方案适合需要对接智能卡、身份证等RFID设备的项目开发者。压缩包体积仅4KB共6个文件包含2个Java源码、3个class字节码文件以及1份Java开发包说明文档源码与编译产物并存既方便阅读学习也可直接部署使用。资源以Memory等类为核心示例完整展示了加载本地库、声明native方法、调用DLL函数并处理返回数据的步骤说明文档还补充了开发环境搭建、DLL路径配置及常见异常排查建议有效降低JNI入门门槛。目前已有469人浏览学习这份小体积资源尤其适合初涉Java与硬件交互的开发者在无需额外依赖的情况下快速掌握通过JNI调用Mwic_32.dll的关键思路与代码骨架。 最近在做门禁系统对接现场用了一台明华RD系列的IC读卡器设备SDK是Windows下的Mwic_32.dll但业务侧是Java服务。要在Java里直接操作这个读卡器绕不开的就是调用Mwic_32.dll里的函数。折腾了两天把调通的过程、踩过的坑和可复用的代码整理出来给后面要接这款设备的兄弟省点时间。文章适合这几类人看第一次在Java里对接Windows DLL的、手里有明华RD读卡器但官方只给了C接口的、以及想了解JNA怎么映射C函数的。我会从环境准备、函数拆解、JNA接口映射、完整代码和问题排查几个部分讲透代码直接能跑。1. 项目背景与整体思路1.1 为什么Java要直接调DLL明华RD系列读卡器比如RD600、RD800、RD900本身是USB或串口设备官方SDK提供的是Windows动态链接库核心就是Mwic_32.dll里面封装了底层射频通信、卡片操作、密钥认证等C函数。问题是官方没有Java SDK只有C/C的头文件和示例。所以要实现Java发指令、读卡器响应这个链路必须通过某种桥接技术让JVM能调用这个DLL里的导出函数。Java调Windows DLL有几种常见手段JNI、JNA、JNative、Spring Native等。JNI是正统做法但要自己写C/C中间层、用javac生成头文件、再编译成另一个DLL环境配置和跨平台维护成本都偏高。JNAJava Native Access则是在JNI之上封装好的动态代理库它允许我们创建一个Java接口接口方法对应DLL导出函数JNA在运行时自动完成类型转换和函数调用不需要编译一行C代码。对于Mwic_32.dll这种函数数量几十个、类型以字节数组和整数为主的场景JNA是性价比最高的方案。1.2 项目环境准备我在实际操作中使用的环境如下JDK 1.8这一步很关键后面会专门讲原因Maven 3.6JNA 5.13.0明华RD读卡器一台安装好官方驱动Mwic_32.dll及同目录下的依赖文件比如mwdrv32.dll、usb驱动相关文件maven依赖就一个dependency groupIdnet.java.dev.jna/groupId artifactIdjna/artifactId version5.13.0/version /dependency建议先把读卡器自带的测试工具比如IC卡读写测试程序跑通确认设备本身没问题再进入Java开发否则你会分不清是设备问题还是代码问题。1.3 JNA加载DLL的底层逻辑JNA加载DLL时实际调用的是Native.load(Mwic_32, Mwic32.class)这里的字符串Mwic_32会自动拼接成Mwic_32.dll。系统寻找DLL的顺序大致是jna.library.path系统属性指定的路径jna.boot.library.path当前进程的PATH环境变量当前工作目录实际开发中我建议直接把Mwic_32.dll所在的整个目录注意是整个目录不是单个文件复制到项目的resources目录然后在代码里动态设置jna.library.path。为什么是整个目录因为Mwic_32.dll本身可能依赖同目录下的驱动文件只复制一个DLL会导致加载时报找不到指定的模块。2. Mwic_32.dll核心函数拆解2.1 读卡器SDK的函数分层明华RD读卡器的SDK函数大致能分成两层控制层和卡片层。控制层负责打开设备、获取版本号、设置蜂鸣器、关闭设备卡片层负责跟IC卡通信包括寻卡、防冲突、选卡、密钥认证、读写块。实际开发中用到最多的还是卡片层因为业务流程基本都是围绕读卡号和读写扇区数据展开。在Java里操作读卡器时我们要模拟的是C代码里那一套标准的Mifare卡操作流程先请求卡片Request、再防冲突拿卡号Anticoll、然后选卡Select、接着做密钥认证Authentication最后才读写块数据。这个顺序不能乱这是Mifare协议规定的。2.2 核心API原型与参数含义我整理了一份实际用到的高频函数表C语言原型和参数含义如下函数名原型参数说明用途MF_Requestint MF_Request(unsigned char* pTagType)pTagType输出参数返回卡片类型代码寻卡检测是否有卡进入天线范围MF_Anticollint MF_Anticoll(unsigned char* pSnr)pSnr输出参数4字节卡序列号防冲突取得卡的4字节UIDMF_Selectint MF_Select(unsigned char* bcSnr)bcSnr输入参数4字节卡序列号选卡选中一张卡进行后续操作MF_Haltint MF_Halt()无使当前卡片进入休眠状态MF_Readint MF_Read(unsigned char bcBlock, unsigned char* pData)bcBlock块号pData输出参数16字节数据读取指定块的数据MF_Writeint MF_Write(unsigned char bcBlock, unsigned char* pData)bcBlock块号pData输入参数16字节数据向指定块写入数据MF_LoadKeyint MF_LoadKey(unsigned char* bcKey, unsigned char bcKeyNo)bcKey6字节密钥bcKeyNo密钥位置把密钥加载到读卡器密钥缓冲区MF_Authenticationint MF_Authentication(unsigned char bcKeyNo, unsigned char bcBlock)bcKeyNo密钥位置bcBlock要认证的块号对指定块进行密钥认证MF_ReadCardint MF_ReadCard(unsigned char* pData)pData输出参数返回卡号数据直接读取非接触式卡卡号RF_ReadCardint RF_ReadCard(unsigned char* pData)pData输出参数返回卡号数据RF卡专用读卡号IC_ReadCardint IC_ReadCard(unsigned char* pData)pData输出参数返回卡号数据接触式IC卡专用读卡号2.3 返回值和错误码明华的这些DLL函数返回值基本都是0表示成功非0表示失败。但具体错误码的含义不同固件版本存在差异我在实际中遇到的情况是返回1一般表示通用错误比如卡未放好或操作失败返回2可能是命令格式错误返回3可能表示没有感应到卡。做代码判断时重点判断ret 0表示成功即可非0具体是什么错误最好结合设备附带文档确认。注意Mwic_32.dll里的函数约定是C的__stdcall调用方式。在JNA里定义一个接口继承StdCallLibrary可以自动按照stdcall方式调用省去手动设置调用约定的麻烦。3. JNA接口映射与核心代码实现3.1 定义JNA接口我们创建一个接口文件用JNA的方式映射Mwic_32.dll的导出函数。核心的映射规则是C语言里的unsigned char*对应Java里的byte[]或ByteByReferenceunsigned char对应byteint对应int。import com.sun.jna.Library; import com.sun.jna.Native; import com.sun.jna.ptr.ByteByReference; import com.sun.jna.win32.StdCallLibrary; public interface Mwic32 extends StdCallLibrary { Mwic32 INSTANCE Native.load(Mwic_32, Mwic32.class); int GetDllVersion(byte[] strVer, byte len); int MF_Request(ByteByReference pTagType); int MF_Anticoll(byte[] pSnr); int MF_Select(byte[] bcSnr); int MF_Halt(); int MF_Read(byte bcBlock, byte[] pData); int MF_Write(byte bcBlock, byte[] pData); int MF_LoadKey(byte[] bcKey, byte bcKeyNo); int MF_Authentication(byte bcKeyNo, byte bcBlock); int MF_ReadCard(byte[] pData); int RF_ReadCard(byte[] pData); int IC_ReadCard(byte[] pData); }3.2 完整的读卡工具类下面是一个可以直接用的工具类实现了两个功能读取M1卡的UID卡号、读取M1卡指定扇区中某一块的数据。这个算是实际项目里最高频的需求。import com.sun.jna.ptr.ByteByReference; public class RdCardReader { private final Mwic32 dll; public RdCardReader() { this.dll Mwic32.INSTANCE; } /** * 读取M1卡UID * return 8位卡号字符串例如 9A2B3C4D */ public String readCardNo() { ByteByReference tagType new ByteByReference(); byte[] snr new byte[4]; int ret dll.MF_Request(tagType); if (ret ! 0) { throw new RuntimeException(寻卡失败错误码 ret 请确认卡片已放在感应区); } ret dll.MF_Anticoll(snr); if (ret ! 0) { throw new RuntimeException(防冲突失败错误码 ret); } StringBuilder sb new StringBuilder(); for (byte b : snr) { sb.append(String.format(%02X, b)); } return sb.toString(); } /** * 读取M1卡指定块的16字节数据 * param block 块号0~63 * return 16字节的块数据 */ public byte[] readBlock(byte block) { ByteByReference tagType new ByteByReference(); byte[] snr new byte[4]; int ret dll.MF_Request(tagType); if (ret ! 0) { throw new RuntimeException(寻卡失败错误码 ret); } ret dll.MF_Anticoll(snr); if (ret ! 0) { throw new RuntimeException(防冲突失败错误码 ret); } ret dll.MF_Select(snr); if (ret ! 0) { throw new RuntimeException(选卡失败错误码 ret); } // 默认使用读卡器内置密钥常见出厂默认密钥为 0xFF * 6 // 如果默认密钥不对需要先调用 MF_LoadKey 加载密钥再验证 ret dll.MF_Authentication((byte) 0, block); if (ret ! 0) { throw new RuntimeException(密钥认证失败错误码 ret); } byte[] data new byte[16]; ret dll.MF_Read(block, data); if (ret ! 0) { throw new RuntimeException(读取块数据失败错误码 ret); } return data; } }其中MF_Request的参数类型我用了ByteByReference这是JNA里专门用来接收C函数输出单字节参数的类。读卡UID时MF_Request其实只关心是否寻到卡卡片类型码可以不管所以就传一个引用进去等它填充。MF_Anticoll拿到的4字节数组就是卡的序列号我在工具类里格式化成8位十六进制字符串一般的刷卡软件显示的卡号就是这4个字节。在readBlock方法里认证用了一个(byte) 0表示使用密钥缓冲区0号位置默认密钥是设备出厂的6个0xFF大部分新卡或者没改过密钥的卡都能通过认证。如果卡被换过密钥需要先调用MF_LoadKey把密钥加载进去byte[] key new byte[]{(byte)0xFF, (byte)0xFF, (byte)0xFF, (byte)0xFF, (byte)0xFF, (byte)0xFF}; int ret dll.MF_LoadKey(key, (byte) 0); ret dll.MF_Authentication((byte) 0, block);3.3 函数签名里的版本差异这里必须提醒一个非常容易踩坑的地方不同型号的明华读卡器MF_Select函数的参数形式可能不一样。我见过两种一种是int MF_Select(unsigned char* bcSnr)参数是4字节数组指针另一种是int MF_Select(unsigned long bcSnr)参数是一个4字节整数区分方法很简单看官方头文件里函数声明用的是指针还是值。如果是数组指针就用byte[]定义如果是整数需要把4字节卡号拼成一个int传进去。写之前先确认头文件别凭经验抄我一开始就是按数组写的结果换了一台设备型号就返回参数错误。4. 常见问题与排查实录4.1 java.lang.UnsatisfiedLinkError: Unable to load library Mwic_32这个问题是Java调DLL时最经典的报错。原因基本是三类第一DLL不在这台机器的搜索路径里。解决办法是设置jna.library.path在main方法或静态代码块里加一行System.setProperty(jna.library.path, D:/lib);第二Mwic_32.dll依赖的同目录驱动文件缺失。这个最难排查因为报错提示就是找不到Mwic_32但你明明看到文件存在。此时用Dependency Walker或Process Monitor查一下DLL的依赖项或者直接把整个SDK目录里的DLL全拷过去。第三位数不匹配。JVM是64位的DLL是32位的加载时会直接报Unable to load library或者是不是有效的Win32应用程序。解决办法是给Java服务换32位JDK或者找官方要64位的DLL版本。4.2 MF_Request总是返回错误码这个情况遇到过两次一次是卡片放的位置不对读卡器天线范围比较小卡片要贴近感应区另一次是读卡器驱动没装好Windows设备管理器里识别成了未知设备重新安装官方USB驱动后解决。还有个隐藏坑读卡器上电需要时间。如果程序启动后立刻调MF_Request设备还没完成初始化会连续返回错误。稳妥做法是在初始化时加一段延时或者先调一次MF_Halt()作为唤醒操作再正式寻卡。4.3 读到的卡号和第三方软件显示不一致排查思路是字节序。读卡器返回的4字节UID比如0A 0B 0C 0D有的软件显示为0A0B0C0D有的软件显示为0D0C0B0A这是因为高低位顺序不同。如果发现和刷卡软件不一致将字节数组倒序再格式化即可。另外有的CPU卡UID是7字节如果用4字节数组去接收只会截取前4字节后面会漏。4.4 内存访问报错或程序崩溃这类情况常见于函数参数类型映射错误。C语言里unsigned char*如果传入Java的byte[]数组必须提前指定长度。例如MF_Read要读16字节数据new byte[16]就是标准写法如果传了new byte[0]或nullJNA虽然不会直接报错但DLL内部会越界访问轻则返回错误重则导致JVM崩溃。4.5 Maven打包后无法找到DLL开发环境能跑打成jar包后部署到服务器就找不到DLL。原因是System.setProperty(jna.library.path, D:/lib)这种写法在开发机上有效到服务器上路径不存在。推荐做法是部署时把DLL目录放到服务器固定路径通过外部配置文件指定路径或者把DLL复制到JVM的bin目录下。不建议把DLL塞进jar包再释放操作繁琐且容易被杀毒软件拦截。5. 实操中的一些经验与扩展5.1 生产环境里的稳定性设计如果这个工具类要跑在Spring Boot这种长期运行的服务里我建议做几件事将RdCardReader设计成单例全局只保存一个DLL接口实例避免反复Native.load造成资源浪费对readCardNo和readBlock方法加synchronized因为同一时间Mwic_32.dll只能处理一个读卡指令并发调用会导致读卡器状态错乱每次读写结束后主动调MF_Halt()让卡片休眠减少读卡器的功耗和误读概率另外读卡器不是工业级设备长时间高频调用会有不稳定现象。我实际压测过连续刷卡几万次后会出现偶发寻卡失败此时重启读卡器能恢复。如果业务要求高可靠性建议加一层重试机制例如失败后延时300毫秒重试3次成功率能明显提升。5.2 扩展方向读写扇区和接触式卡这套基础打通之后可以继续扩展读写M1卡指定扇区先调用MF_Select选卡然后MF_Authentication认证目标块所在扇区再MF_Read或MF_Write加载一卡一密用MF_LoadKey把每张卡不同的密钥加载进去再认证接触式IC卡调用IC_ReadCard操作方式和非接触式类似把读卡能力封装成HTTP接口对接前端的发卡、充值、开门等业务我实际做门禁系统时就是封装成了/api/card/read接口前端走WebSocket轮询刷卡后自动回传卡号整套方案很稳定。5.3 最后一点个人体会折腾完这个项目我最大的感受是Java调Windows DLL没有想象中那么可怕关键是搞清楚调用约定、参数类型和DLL依赖这三个基础问题。遇到报错不要慌先确认DLL能不能加载再确认函数签名对不对最后才怀疑代码逻辑。JNA这种方案虽然不是性能最优但对明华RD读卡器这种低频设备操作场景完全够用而且开发效率比JNI高得多维护起来也省心。希望这篇分享能帮你少踩几个坑有问题欢迎在评论区一起交流。本文还有配套的精品资源点击获取