简介:本资源为基于Java实现的RFID技术设计源码,面向希望深入理解无线射频识别系统开发的学生、工程师及Java学习者,可用于物流、供应链管理、门禁安全等场景的二次开发与课程实践。压缩包共150个文件,约8.85MB,包含42个XML配置文件、31个Java源文件、27个SO库文件、19个JAR依赖包,以及PNG界面素材、WAV音频、Gradle构建脚本和属性文件等,覆盖配置管理、业务逻辑、硬件接口调用与依赖打包等环节。项目以Java跨平台能力为核心,结合XML配置灵活性与Gradle自动化构建,完整呈现RFID标签识别、数据读取传输与处理流程,并附带README与版本说明便于部署上手。目前已有411人学习下载,适合作为掌握Java实际项目应用与RFID设计方法的参考案例。
1. 从一张 M1 卡到 Java 服务:RFID 源码到底在解决什么问题
门禁闸机前刷卡 0.3 秒开门,仓库月台上手持机扫托盘,这些场景背后都是 RFID。但真正落到 Java 工程师手里,问题往往不是“读不到卡”,而是读到了之后怎么办:串口数据怎么解析、卡号怎么去重、多读写器怎么并发、离线记录怎么补传。基于 Java 实现的 RFID 技术设计源码,核心价值就在于把“硬件交互层”和“业务系统层”用一套可维护的代码串起来,而不是让上位机软件变成一堆Thread.sleep和字符串截取的堆砌。这套东西适合谁?做门禁考勤、仓储盘点、产线追溯的 Java 后端,或者课程设计需要完整跑通“读卡→入库→查询”链路的开发者。热词里常出现“java课程设计案例源码”“rfid考勤系统”,说明大量需求集中在“能跑通、能改、能交作业”这个区间,但真正上线时,串口粘包、多卡冲突、数据一致性才是分水岭。
2. 先定架构再写代码:Java 侧 RFID 的分层与选型
2.1 为什么不能把串口读写直接塞进 Controller
很多初版代码是这样的:HTTP 请求进来,直接打开串口,发指令,等 200ms,读返回,解析,返回 JSON。单机测试没问题,一上产线就翻车。原因在于 RFID 读写器是长连接、事件驱动的设备,卡进入场强范围是异步事件,不是请求-响应模型。正确做法是分三层:设备接入层负责串口/TCP 连接、心跳、断线重连;协议解析层负责帧头帧尾校验、卡号提取、多卡去重;业务服务层负责把卡号映射到人员、物料、工单,并写入数据库。Java 侧常用选型:串口通信用jSerialComm(比 RXTX 维护活跃,跨平台少踩坑),网络读写器用 Netty 做 TCP 客户端,业务层用 Spring Boot + MyBatis-Plus。热词里“mybatisplus根据java实体类生成创建表的sql语句”正好对应这里——实体类设计好了,建表 SQL 可以自动生成,减少手写 DDL 的字段错位。
2.2 最小可跑通的串口读卡工程
下面这段代码演示用jSerialComm打开串口、发送轮询指令、读取返回帧的最小闭环。假设读写器协议为:帧头0xBB,长度 1 字节,命令 1 字节,数据 N 字节,校验和 1 字节,帧尾0x7E。
import com.fazecast.jSerialComm.SerialPort; import java.io.InputStream; import java.io.OutputStream; public class RfidSerialReader { public static void main(String[] args) throws Exception { // 列出所有串口,Windows 下通常是 COM3,Linux 下 /dev/ttyUSB0 SerialPort[] ports = SerialPort.getCommPorts(); for (SerialPort p : ports) { System.out.println("发现串口: " + p.getSystemPortName()); } SerialPort port = SerialPort.getCommPort("COM3"); // 波特率必须与读写器手册一致,常见 57600 或 115200 port.setComPortParameters(57600, 8, 1, SerialPort.NO_PARITY); port.setComPortTimeouts(SerialPort.TIMEOUT_READ_SEMI_BLOCKING, 200, 0); if (!port.openPort()) { throw new IllegalStateException("串口打开失败,检查占用或驱动"); } OutputStream out = port.getOutputStream(); InputStream in = port.getInputStream(); // 轮询指令:帧头 BB,长度 03,命令 01,校验和,帧尾 7E byte[] pollCmd = new byte[]{(byte) 0xBB, 0x03, 0x01, 0x00, 0x7E}; while (true) { out.write(pollCmd); out.flush(); Thread.sleep(100); // 给读写器响应时间,太短会读到空帧 byte[] buf = new byte[64]; int len = in.read(buf); if (len > 0) { String cardNo = parseFrame(buf, len); if (cardNo != null) { System.out.println("读到卡号: " + cardNo); } } } } // 解析帧:校验帧头帧尾,提取数据区,转十六进制卡号 private static String parseFrame(byte[] buf, int len) { if (len < 5) return null; if (buf[0] != (byte) 0xBB || buf[len - 1] != 0x7E) return null; int dataLen = buf[1] & 0xFF; if (len < dataLen + 3) return null; StringBuilder sb = new StringBuilder(); for (int i = 3; i < 3 + dataLen - 1; i++) { sb.append(String.format("%02X", buf[i])); } return sb.toString(); } }逻辑说明:setComPortTimeouts的读超时设为 200ms,避免in.read永久阻塞导致线程卡死。轮询间隔 100ms 是经验值,太快读写器来不及响应,太慢会漏卡。parseFrame里先校验帧头帧尾,再按长度字段截取数据区,最后把字节转成十六进制字符串作为卡号。参数说明:波特率、校验位、停止位必须和读写器手册完全一致,改一个都可能读到乱码;TIMEOUT_READ_SEMI_BLOCKING表示读不到数据时返回 0 而不是抛异常,适合轮询场景。
2.3 多读写器并发时的线程模型
一个仓库有 4 个出入口,每个口一台读写器,如果每台开一个线程轮询,线程数随设备数线性增长,而且卡号去重、入库操作散落在各线程里,后期加一个“同一张卡 3 秒内只记一次”的规则就要改 4 个地方。常见做法是:每台设备一个采集线程,只负责读原始帧并丢进BlockingQueue;单独一个消费线程从队列取卡号,做去重、映射、入库。去重可以用ConcurrentHashMap加时间戳,或者用 Caffeine 做带过期时间的本地缓存。这样设备增减不影响业务逻辑,消费线程还能批量写库,减少数据库压力。
3. 协议解析与数据落地:从字节流到业务表
3.1 帧解析的三个必调参数
RFID 读写器协议五花八门,但解析时绕不开三个参数:帧头帧尾、长度字段位置、校验方式。以常见的BB ... 7E帧为例,长度字段在第 2 字节,表示命令+数据+校验的总长度;校验和通常是前面所有字节的异或或累加和取低 8 位。调这三个参数时,不要靠猜,用串口调试助手先抓一帧真实数据,对着手册逐字节标注。我一般会写一个FrameDecoder类,把这三个参数做成配置项,换读写器型号时只改配置不改代码。
public class FrameDecoder { private final byte head; private final byte tail; private final int lenOffset; private final ChecksumType checksumType; public FrameDecoder(byte head, byte tail, int lenOffset, ChecksumType type) { this.head = head; this.tail = tail; this.lenOffset = lenOffset; this.checksumType = type; } // 从缓冲区尝试解出一帧,返回 null 表示数据不够 public byte[] decode(byte[] buf, int available) { if (available < lenOffset + 1) return null; if (buf[0] != head) return null; int dataLen = buf[lenOffset] & 0xFF; int totalLen = dataLen + lenOffset + 2; // 头 + 长度 + 数据 + 校验 + 尾 if (available < totalLen) return null; if (buf[totalLen - 1] != tail) return null; if (!verifyChecksum(buf, totalLen)) return null; byte[] frame = new byte[totalLen]; System.arraycopy(buf, 0, frame, 0, totalLen); return frame; } private boolean verifyChecksum(byte[] buf, int totalLen) { if (checksumType == ChecksumType.XOR) { byte sum = 0; for (int i = 0; i < totalLen - 2; i++) sum ^= buf[i]; return sum == buf[totalLen - 2]; } return true; } public enum ChecksumType { XOR, SUM } }逻辑说明:decode方法不假设一次read就能拿到完整帧,而是根据长度字段判断缓冲区里是否够一帧,不够就返回 null 让调用方继续读。参数说明:lenOffset是长度字段的字节下标,不同协议可能是 1 或 2;ChecksumType根据手册选 XOR 或累加和。这个类可以单独单元测试,用构造的字节数组验证边界情况,比连着硬件调试快得多。
3.2 卡号去重与业务映射的表设计
读到卡号只是开始,业务表设计决定了后面查询和统计好不好做。常见三张表:rfid_device(读写器编号、位置、IP/串口)、rfid_card(卡号、绑定人员/物料、状态)、rfid_record(记录 ID、卡号、设备编号、时间、业务类型)。卡号建议存成VARCHAR(32)并建唯一索引,避免前导零丢失。记录表按时间分区或按月分表,否则半年后单表千万行,查询卡顿。热词里“java 判断字符串中是否不是字母和数字”在这里有用——卡号入库前做一次格式校验,过滤掉解析错误产生的乱码。
CREATE TABLE rfid_record ( id BIGINT PRIMARY KEY AUTO_INCREMENT, card_no VARCHAR(32) NOT NULL, device_no VARCHAR(16) NOT NULL, biz_type TINYINT COMMENT '1考勤 2盘点 3追溯', read_time DATETIME(3) NOT NULL, KEY idx_card_time (card_no, read_time), KEY idx_device_time (device_no, read_time) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;逻辑说明:read_time用DATETIME(3)保留毫秒,因为同一张卡可能在极短时间内被多个设备读到,秒级精度无法排序。两个联合索引分别支撑“查某张卡的轨迹”和“查某台设备的记录”。参数说明:biz_type用 TINYINT 而不是 VARCHAR,省空间且查询快;如果记录量极大,可以把read_time作为分区键按月分区。
3.3 离线补传与断线重连
产线环境网络抖动是常态,读写器断线后采集线程要能自动重连,并且断线期间的卡记录不能丢。常见做法是采集线程检测到read返回 -1 或异常时,关闭串口,休眠 3 秒后重试打开,同时把未确认的记录先写入本地文件或内存队列。重连成功后优先补传。这里有个坑:重连后读写器可能还在缓存断线前的卡事件,补传时要去重,否则同一张卡会记两次。我一般会在记录表加一个trace_id,由设备编号+卡号+时间戳毫秒生成,入库时用INSERT IGNORE或ON DUPLICATE KEY UPDATE兜底。
4. 避坑与排查:那些让 RFID 项目翻车的细节
4.1 现象:串口能打开但读不到任何数据
原因:波特率不匹配是最常见的,其次是读写器处于“主动上报”模式而代码在“轮询”模式,或者串口被其他进程占用(比如调试助手没关)。解决:先用串口调试助手确认能收到数据,再对比代码里的波特率、数据位、停止位、校验位是否和助手一致;检查读写器工作模式配置,主动上报模式下不需要发轮询指令,直接读即可。
4.2 现象:同一张卡连续读到几十条记录
原因:读写器轮询间隔太短,卡还在场强范围内就被反复读取;或者去重逻辑只做了内存缓存,服务重启后缓存失效。解决:在业务层加时间窗口去重,比如同一卡号 3 秒内只处理一次;用 Caffeine 或 Redis 做带过期的去重缓存;数据库层用唯一索引兜底,但不要只依赖数据库,否则无效写入太多。
4.3 现象:多台读写器同时读卡时卡号错乱
原因:多个线程共用一个InputStream或OutputStream,或者帧解析时用了共享的缓冲区。解决:每台设备独立持有自己的串口对象和缓冲区,采集线程之间不共享任何可变状态;如果必须共享,用ThreadLocal或加锁,但加锁会降低吞吐,不如每设备独立。
4.4 现象:Linux 下串口权限不足
原因:/dev/ttyUSB0默认属于dialout组,普通用户没有读写权限。解决:把运行服务的用户加入dialout组,或者用 udev 规则固定设备权限。不要用chmod 777,重启后会失效且不安全。
4.5 现象:卡号前导零丢失导致匹配不上
原因:卡号被当成数字解析,0012AB变成12AB。解决:从协议解析到数据库存储全程用字符串,十六进制格式化时用%02X保证每字节两位;数据库字段用VARCHAR而不是INT。
5. 进阶技巧:用状态机管读写器,用压测定参数
5.1 把读写器连接做成状态机
设备接入层最容易写乱,因为要处理“未连接→连接中→已连接→断线→重连”多个状态。我习惯用一个枚举状态机,每个状态只允许特定操作,比如“未连接”只能触发connect(),“已连接”才能send()。这样断线重连的逻辑不会散落在各处,排查时看当前状态就知道卡在哪一步。
public enum DeviceState { DISCONNECTED, CONNECTING, CONNECTED, RECONNECTING; public boolean canSend() { return this == CONNECTED; } public DeviceState onDisconnect() { return this == CONNECTED ? RECONNECTING : DISCONNECTED; } }逻辑说明:canSend()在发送前调用,避免在断线状态下写数据导致异常;onDisconnect()统一处理状态迁移。参数说明:状态枚举可以按需扩展,比如加SUSPENDED表示人工暂停。
5.2 用压测确定轮询间隔和线程数
轮询间隔不是拍脑袋定的。拿一台读写器,用不同间隔(50ms、100ms、200ms)各跑 10 分钟,统计读到的卡次数和漏卡次数。漏卡率低于 0.1% 的最小间隔就是可用值。多设备场景下,逐步增加设备数,观察 CPU 和内存,找到单机支撑上限。我一般会在测试环境用脚本模拟 100 张卡轮流进入场强,跑一轮就知道参数该设多少。
5.3 日志里必须有的三个字段
排查 RFID 问题时,日志没有设备编号、卡号、原始帧,等于黑匣子。我习惯在解析层打一条 DEBUG 日志,包含设备编号、原始字节的十六进制、解析出的卡号、时间戳。上线后把 DEBUG 关掉,但保留 WARN 级别记录解析失败和校验失败的帧,方便事后追溯。这个习惯帮我省过很多次“后悔药”——有一次现场说读不到卡,翻日志发现是校验和算法换了,十分钟定位。
5.4 一个容易被忽略的细节:天线功率
读写器天线功率可调,功率太高会读到隔壁通道的卡,功率太低读距不够。调功率时不要只看读距,还要看误读率。我一般把功率从默认值往下调 3dB 开始测,找到刚好覆盖目标区域且不串读的值。这个参数在代码里通常通过配置指令下发,记得把配置持久化到读写器,否则重启后恢复默认。
希望帮到你。
本文还有配套的精品资源,点击获取