
简介针对 Java 平台调用 DLL 操作外设的需求这份资源提供了一套基于明华RD读卡器的 JNI 实现样例。内容聚焦于通过 Mwic_32.dll 完成读卡器初始化、数据读取等交互并给出从环境配置、本地方法声明到 C/C 封装与调试的完整思路适合需要接触硬件驱动或 JNI 开发的 Java 工程师参考。压缩包共 6 个文件其中 2 个 java 源码与 3 个 class 文件可直接对照或复用另有 1 个 txt 说明文档梳理开发要点整体仅 4KB轻量且便于快速查看。资源已吸引 469 人浏览学习具有一定的实践参考价值。通过学习资源中的实现细节读者能掌握 System.loadLibrary 加载 DLL 的写法、javah 生成头文件的方法以及处理调用约定不匹配、异常捕获等常见问题从而快速上手类似读卡设备的 Java 集成。1. 为什么Java调Mwic_32.dll会卡在“跨语言”这一步1.1 明华RD读卡器和Mwic_32.dll到底是什么关系先交代一下背景。明华RD系列读卡器是典型的非接触式IC卡读写设备会员卡、就餐卡、门禁卡、一卡通这类项目里非常常见。它本身不提供直接给Java用的接口而是给了一套Windows平台的C语言动态库就是这个Mwic_32.dll。厂商SDK里通常包含几个文件头文件比如Mwic32.h、导入库Mwic_32.lib、动态库Mwic_32.dll以及一份C语言示例工程。你要做的事情本质上是把C示例里的逻辑翻译成Java能调用的形式。我第一次接手这种需求时第一个念头也是“直接调DLL不就行了”。但事实是Java跑在JVM上不能像C#那样直接写一个DllImport就完事。Java和DLL之间必须有一个桥接层这个桥接层要么是Java自己写的JNI代码要么就是JNA这类现成的封装库。很多人卡住就卡在这里官方给的文档全是C语言的结构体、指针、char数组Java这边要怎么把这些东西翻译过去没有任何现成答案。1.2 JNI不是不能做而是对一个“发卡功能”来说太重了传统的JNI路线是这样走的先在Java里写native方法声明然后通过javac生成class文件再用javah或者java -h生成C头文件接着自己动手写一份C代码把Mwic_32.dll的调用封装成新的JNI动态库最后让Java加载这个新生成的DLL。这套链路本身没有任何问题但对于一个只想在管理后台里加个“发卡”按钮的项目来说成本明显过高。具体来说JNI有几个非常折磨人的点。第一你需要搭一套C/C编译环境Windows下可能是MinGW或者Visual Studio光把环境弄好就得折腾大半天。第二Mwic_32.dll的很多函数参数是unsigned char*指针Java这边是byte数组你要在JNI包装层里做内存拷贝、类型转换C端写回的数据还要再拷贝回Java线程这部分代码写起来很容易出错。第三JNI崩溃时往往直接让JVM退出排查问题靠日志还看不到多少有效信息。所以我的判断是除非项目有极高的性能要求、或者公司对JNI有硬性规定否则在这种“对接第三方硬件DLL”的场景里用JNA这种桥接库是更聪明的选择。读卡器操作本身是毫秒级的人机交互JNA那点调用损耗完全感知不到换来的是纯Java代码、不写一行C、不用配编译环境省下大量时间。2. 技术选型对比JNI、JNA、JNative我为什么最终用JNA2.1 三条路线的直观对比选型之前我认真对比过三条路线的差异做成表格会非常清楚方案需要写C代码环境配置复杂度维护成本适用场景JNI需要高高对性能有极致要求、需要深度定制的情况JNA不需要低低快速对接第三方DLL日常业务绰绰有余JNative不需要低中老项目遗留代码新项目不建议再用JNative这个名字老一代Java开发可能听过。它就是专门用来解决Java调用DLL问题的第三方包曾经在很多读卡器、打印机的对接项目里出现过。但问题是这个项目维护不太积极在新版本JDK上跑时遇到过兼容性问题新项目再引入它相当于给自己挖坑。JNA相比之下就健康得多社区活跃、文档齐全Java 8以上版本都能正常使用。2.2 JNA的核心原理用Java接口“翻译”C函数签名JNA的原理一句话可以讲清楚你写一个Java接口继承com.sun.jna.Library然后在接口里声明和DLL导出函数一一对应的Java方法。JNA底层会自动完成Java类型和C类型之间的内存转换你调用接口方法就等于调用了DLL里的那个C函数。举个最直接的例子。Mwic_32.dll里有一个函数IC_OpenC语言签名是int IC_Open(int port, int baud)那么在Java接口里就写int IC_Open(int port, int baud)。JNA看到这个接口会在加载DLL之后自动把Java调用转成对IC_Open这个导出函数的调用参数照着int类型传进去返回值也照着int类型接回来。整个过程不需要你写任何C代码也不需要生成头文件。Maven依赖如下dependency groupIdnet.java.dev.jna/groupId artifactIdjna/artifactId version5.13.0/version /dependency3. Mwic_32.dll核心函数拆解初始化、寻卡、读写卡到底怎么调3.1 基础API与Java映射建议不同型号的明华RD读卡器SDK函数名可能有一些差异有的型号还有IC_CPU、IC_Reset这类针对CPU卡的指令函数。但最常用的一套基础API基本是固定的我把我实际用到的整理出来C函数签名作用JNA里的Java签名int IC_Open(int port, int baud)打开读卡器设备返回设备句柄int IC_Open(int port, int baud)int IC_Close(int icdev)关闭读卡器设备int IC_Close(int icdev)int IC_ExistCard(int icdev, int csc)检测是否有卡片int IC_ExistCard(int icdev, int csc)int IC_Read(int icdev, int csc, int offset, unsigned char* buffer, int len)读取卡片数据int IC_Read(int icdev, int csc, int offset, byte[] buffer, int length)int IC_Write(int icdev, int csc, int offset, unsigned char* buffer, int len)写入卡片数据int IC_Write(int icdev, int csc, int offset, byte[] buffer, int length)参数的作用也说明一下。icdev是IC_Open成功返回的设备句柄后续所有操作都要带上它csc一般表示卡类型或扇区参数很多型号传0就行具体要看SDK文档offset是卡内字节偏移量比如你要从第4个字节开始读就传4buffer是数据缓冲区读取时用来接收卡里的数据写入时用来放要写进去的内容len是缓冲区长度。3.2 返回码判断逻辑关于返回值多数明华SDK的约定是0代表成功非0代表失败或者未初始化。但不同批次、不同型号的SDK错误码的具体含义可能不一样。我强烈建议拿到手之后先写一个最简测试程序把每次调用的返回值都打印出来再去对照SDK手册里的错误码表。别凭感觉去猜猜错方向会浪费大量时间。比如我之前遇到过IC_Open返回-1一开始以为是串口被占用折腾了半天后来才发现是DLL没找到换了加载路径之后就好了。所以“返回码非0”就老老实实去查文档和排查环境不要盲目重试。4. Java端完整实现从打开读卡器到成功写卡4.1 编写JNA接口文件先创建一个接口继承Library把Mwic_32.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 IC_Open(int port, int baud); int IC_Close(int icdev); int IC_ExistCard(int icdev, int csc); int IC_Read(int icdev, int csc, int offset, byte[] buffer, int length); int IC_Write(int icdev, int csc, int offset, byte[] buffer, int length); }这里有一个非常关键的选择读卡和写卡函数的buffer参数我在Java侧用的是byte[]而不是String。原因是C函数里它是unsigned char*本质上是字节缓冲区里面可能含有0x00这样的不可见字节。如果用StringJNA会按照平台默认字符集做编码转换数据内容可能被改变而且String是不可变对象C端往缓冲区里写数据时Java侧的String对象根本收不到修改结果。所以凡是涉及“C函数要往缓冲区里写入数据”的情况一律用byte[]这是我用了一个项目之后总结出的铁律。4.2 初始化设备与寻卡操作打开读卡器之前先设置DLL的查找路径。如果Mwic_32.dll放在项目根目录、或者已经加入系统PATH可以不用设置否则可以用System.setProperty(jna.library.path, D:/rd_libs)这种方式指定路径。我实测下来JNA查找DLL时对这个属性很敏感设置后能解决大部分加载失败问题。public class RdCardDemo { public static void main(String[] args) { System.setProperty(jna.library.path, D:/rd_libs); int icdev Mwic32.INSTANCE.IC_Open(1, 9600); if (icdev 0) { System.out.println(打开读卡器失败返回码 icdev); return; } try { int ret Mwic32.INSTANCE.IC_ExistCard(icdev, 0); if (ret ! 0) { System.out.println(未检测到卡片请放卡后重试); } else { System.out.println(已检测到卡片); } } finally { Mwic32.INSTANCE.IC_Close(icdev); } } }IC_Open的第一个参数是端口号第二个是波特率。明华RD系列有的型号走的是串口在设备管理器里能看到具体的COM号有的型号是USB虚拟串口也是以一个COM口的形式存在。这个port参数要注意有些SDK里传的是COM号有些传的是索引需要看SDK文档。我建议第一次调试时把设备管理器里的实际COM号填进去比如COM3就传3先跑通再根据实际情况调整。4.3 读卡数据与解码方式读卡操作的代码来看一下byte[] buf new byte[16]; int ret Mwic32.INSTANCE.IC_Read(icdev, 0, 0, buf, buf.length); if (ret 0) { String content new String(buf, StandardCharsets.US_ASCII).trim(); System.out.println(卡内数据 content); } else { System.out.println(读卡失败返回码 ret); }这里的解码方式是个容易踩坑的地方。如果卡里存的是英文字符串用US_ASCII解码最安全如果存的是中文很多老系统用的是GBK编码要用new String(buf, GBK)来解最怕的就是一上来统一用UTF-8解码读出来全是乱码还误以为读卡器有问题。我的经验是在不确定存储编码的时候先把buf里的字节按十六进制打印出来人工确认一下数据格式再去选对应的解码方式。4.4 写卡数据与补位技巧写卡和读卡是对称的但在“数据补位”这个细节上有讲究。import java.nio.charset.StandardCharsets; import java.util.Arrays; byte[] content HELLO123.getBytes(StandardCharsets.US_ASCII); byte[] buf new byte[16]; Arrays.fill(buf, (byte) 0x20); System.arraycopy(content, 0, buf, 0, content.length); int ret Mwic32.INSTANCE.IC_Write(icdev, 0, 0, buf, buf.length); if (ret 0) { System.out.println(写入成功); } else { System.out.println(写入失败返回码 ret); }buf的长度是16但内容只有8个字节剩下8个字节我用0x20空格来填充而不是常见的0x00。为什么因为卡里的数据在后续读取时如果尾部全是0x00一些SDK示例程序、或者下游系统按字符串处理时会直接把字符串截断在第一个0x00的位置导致后面解析出错。补成空格虽然也不完美但在定长字符串这种业务场景下是最省事、最不容易出意外的处理方式。4.5 资源释放是硬要求IC_Open成功后返回的设备句柄是底层系统资源一定要在finally块里调用IC_Close释放。我见过不少人写的代码打开失败或者操作异常时直接return忘了关闭程序跑久了读卡器就卡死重新打开也打不开。前面示例里把IC_Close放在finally里就是出于这个考虑这个习惯值得养成。5. 实测踩坑记录类型映射、数据编码和32位DLL5.1 参数类型一律选byte[]别用String这个问题我在前面已经强调过一次但因为太重要值得单独拿出来再说。JNA对接C函数时如果C函数签名是char*Java侧可以用String也可以byte[]。很多刚上手的人图省事读操作也写成String参数结果读出来的数据要么是空的、要么是乱码、要么C端写入的修改完全不起作用。凡是需要接收C函数写入数据的参数、或者要保存二进制数据的参数一律用byte[]。String只在纯参数传递、不需要回写的场景下可以用但读卡器这种场景里几乎没有纯参数传递的需求所以统一用byte[]是最省心的。5.2 Mwic_32.dll是32位DLL必须配32位JRE这个名字里写着“32”的DLL基本可以断定是32位动态库。在64位Windows上如果你用的是64位JDKJNA加载时会直接抛java.lang.UnsatisfiedLinkError错误信息类似“%1 不是有效的 Win32 应用程序”。这个报错很经典也很容易让人怀疑是DLL损坏实际上就是位数不匹配。解决办法有两个方向。第一给项目安装32位JDK在IDE里把项目的JRE切换成32位版本再用32位JDK启动应用。第二联系读卡器厂商看有没有Mwic_64.dll之类的64位版本如果有就直接换成64位。但我实际接触过的情况是这种老设备厂商往往早就不维护了64位DLL基本拿不到所以老老实实切32位JDK是更靠谱的路。5.3 DLL加载失败与依赖库问题除了位数问题DLL加载失败还有几个常见原因。Native.load(Mwic_32, Mwic32.class)这个方法名里不要带.dll后缀写了反而找不到。DLL要放在jna.library.path指定的目录、项目根目录、或者系统PATH里三选一即可。还有一种是DLL本身依赖了VC运行库但目标机器上没装也会加载失败报错信息可能非常隐晦这时候去微软官网装一个对应版本的Visual C Redistributable即可解决。我建议所有遇到加载问题的朋友先把加载动作单独写一个测试类跑一遍只做一句Native.load看看到底成不成功。这一步成功了再往下写业务代码能省下大量排查时间。5.4 驱动、串口号和设备识别的排查路径如果DLL加载正常但IC_Open返回失败问题大概率出在驱动或端口号上。先在设备管理器里看读卡器是否被识别正常情况会出现一个“端口(COM和LPT)”下的USB串行设备记住它的COM号。如果设备没出现要么驱动没装要么线没插好要么USB口供电不足读卡器的指示灯如果没亮优先查硬件。老款明华读卡器在Windows 10、Windows 11上偶尔会出现驱动兼容问题设备管理器里显示一个带黄色感叹号的未知设备。这个时候不要慌去读卡器配套光盘或者厂商官网找驱动用兼容模式安装很多是可以救回来的。我曾经遇到过一个老旧型号折腾了快两天驱动最后发现是必须用32位驱动安装程序64位安装程序跑完看似成功实际驱动根本没起来。5.5 句柄泄漏与连接管理最后一个坑是句柄泄漏。IC_Open成功之后如果程序因为异常没有走到IC_Close底层资源就泄漏一次。日志里如果出现“打开读卡器失败”并且重启进程前一直失败十有八九就是句柄泄漏导致的。我的处理方式是如果是一次性任务就确保在finally里关闭如果是后台常驻服务最好把读卡器连接做成单例启动时打开一次进程退出时统一关闭不要每读一张卡就open一次、close一次。反复开关设备在Windows下有时会残留状态导致下一次open失败。实测下来常驻连接的方式最稳定也省掉了每次open的开销。6. 一点实操体会这几天重新整理这个对接过程最大的感受是跟老硬件打交道最先要解决的不是代码问题而是环境问题。我的工作顺序是这样的先把SDK自带的C示例工程在Windows上编译跑通确认驱动、DLL、读卡器本身没问题然后在Java里写一个最简demo只做IC_Open和IC_Close把桥接层跑通最后再加寻卡、读卡、写卡这些业务操作。三步走下来每一步的问题范围都很小遇到异常能很快定位。还有一个小技巧处理这类第三方DLL对接时日志一定要打得足够细。每次调用都记录入参和返回码特别是IC_Read、IC_Write这种带缓冲区的函数还要把读取后的字节内容按十六进制打出来。很多时候数据对不对看十六进制比看字符串直观得多。如果你的DLL不是Mwic_32.dll而是别的厂商的什么xx.dll这套排查思路和代码骨架照样适用把接口方法名和参数按头文件换掉就行。这也是我把这次经验整理出来的原因思路通一通百通。本文还有配套的精品资源点击获取