简介:面向需要在 Java 项目中直连 Access 数据库(mdb 文件)的开发者,这套 ucanaccess-4.0.4 开源免费 jar 包可替代 Access_JDBC30.jar,规避其单次查询 50 次、每次最多 1000 条的限制,通过 JDBC 直连方式访问 mdb,无需依赖 JDBC-ODBC 桥连,查询次数与条数均不受约束,适合数据迁移、报表导出、老系统维护等场景。压缩包共 16 个文件,以 6 个 jar 依赖(含 ucanaccess 主包及 hsqldb、jackcess、commons 系列支撑库)、8 个 txt 说明与许可文本、1 个 bat 和 1 个 sh 启动脚本为主,整体约 2.57MB,体积轻量、依赖齐全,解压即可引入工程。已有 741 人学习下载,使用方法与 Access_JDBC30.jar 大同小异,包内附有说明文件,便于快速替换现有连接方案并完成调试。
1. 用 Java 直连 Access:一个 jar 包解决的老系统数据孤岛
接手一套跑了十来年的内部系统,数据库是.accdb文件,业务方要求新写的 Java 服务能直接读写它,这种场景比想象中常见。Access 没有官方 JDBC 驱动,纯靠 JDBC 硬连是走不通的,而ucanaccess-4.0.4.jar就是绕开这个死结的现成方案——它用纯 Java 实现了 Access 文件的读写,把.mdb/.accdb包装成标准 JDBC 数据源,代码里DriverManager.getConnection就能拿到连接。适合谁?需要做老系统数据迁移、报表导出、跨库同步的 Java 后端,尤其是不能改动原 Access 文件、又不想装 Office 或 ODBC 驱动的环境。这篇不聊概念,直接拆这个 jar 怎么落地、参数怎么配、哪些坑会让人半夜爬起来改代码。
2. 拆开 ucanaccess-4.0.4.jar:它到底靠什么读写 Access
2.1 纯 Java 实现的原理与依赖链
Access 文件本质是一个复合文档容器,表结构、索引、数据页都按 Jet/ACE 引擎的格式存放。UCanAccess 不去调用系统里的 Access 引擎,而是自己解析这套二进制格式,再把 SQL 翻译成对底层页的操作。它内部依赖几个库协同工作:Jackcess 负责文件级的读写,HSQLDB 在内存里建一个临时库做 SQL 执行层,再加上对LIKE、日期、金额等类型的适配。理解这一点很关键——你写的 SQL 并不是直接打到 Access 文件上,而是先经过 HSQLDB 解析,再由 Jackcess 落到文件。所以有些 Access 特有的语法、函数,能不能用取决于这层翻译的覆盖度,而不是 Access 本身支持不支持。
版本 4.0.4 对应的是 Java 8 及以上环境,JDBC 规范走的是 4.x。选这个版本的理由很实际:它对.accdb(Access 2007 以后格式)支持完整,对旧.mdb也能读,且依赖的 Jackcess、HSQLDB 版本相对稳定,不像更早的 2.x 那样在中文编码和长文本字段上频繁翻车。如果你的项目还在 Java 8,这个版本是省心的选择;上到 Java 11、17 一般也能跑,但要注意模块化环境下反射访问的限制,后面避坑章节会展开。
2.2 依赖清单与引入方式
单放一个ucanaccess-4.0.4.jar是不够的,它运行时需要几个伴生 jar。常见做法是把下面这组一起放进lib或交给构建工具管理:
| 依赖 | 作用 | 是否必需 |
|---|---|---|
| ucanaccess-4.0.4.jar | 主驱动,JDBC 入口 | 必需 |
| jackcess-2.1.x.jar | Access 文件底层读写 | 必需 |
| hsqldb-2.3.x.jar | 内存 SQL 执行层 | 必需 |
| commons-lang-2.x.jar | 工具类支撑 | 必需 |
| commons-logging-1.x.jar | 日志门面 | 必需 |
| commons-collections-3.x.jar | 集合工具 | 视版本而定 |
Maven 项目直接引坐标最省事,注意把传递依赖带上:
<dependency> <groupId>net.sf.ucanaccess</groupId> <artifactId>ucanaccess</artifactId> <version>4.0.4</version> </dependency>如果是老式的非构建项目,就把上表里的 jar 全部丢进WEB-INF/lib或-cp路径。这里有个血泪经验:只放主 jar 时,启动会报NoClassDefFoundError,指向的往往是com.healthmarketscience.jackcess.*或org.hsqldb.*,看到这类异常先查伴生 jar 齐不齐,别急着怀疑驱动本身。
2.3 连接串的写法与参数含义
连接串是这套方案里最容易写错的地方。标准格式如下:
String url = "jdbc:ucanaccess://D:/data/legacy.accdb;" + "memory=false;" + "showSchema=true;" + "newDatabaseVersion=V2010"; Connection conn = DriverManager.getConnection(url, "user", "pwd");逐项说明:jdbc:ucanaccess://后面跟文件的绝对路径,Windows 下反斜杠要转义或统一用正斜杠;memory=false表示不把整个库加载进内存,大文件必须设成 false,否则几十万行的表能把堆撑爆;showSchema=true会在日志里打印解析出的表结构,调试期很有用,生产环境可以关掉减少噪音;newDatabaseVersion只在新建库时生效,读已有文件时忽略。密码参数对没有设密码的 Access 文件传空字符串即可,传null在某些版本会抛异常,稳妥起见用""。
还有一个常被忽略的点:路径里带空格或中文时,连接串要做 URL 编码,否则解析会在空格处截断。我一般先用new File(path).toURI().toString()拿到编码后的路径再拼,省得手动处理。
3. 从建连到读写:一份能直接跑的 CRUD 骨架
3.1 建连、查询与结果集处理
下面这段是能直接抄的查询骨架,覆盖了建连、预编译、遍历、关资源的完整链路:
public List<Map<String, Object>> queryRows(String dbPath, String sql) throws SQLException { List<Map<String, Object>> rows = new ArrayList<>(); // 显式加载驱动,老容器里 Class.forName 仍是最稳的写法 try { Class.forName("net.ucanaccess.jdbc.UcanaccessDriver"); } catch (ClassNotFoundException e) { throw new IllegalStateException("驱动未引入", e); } String url = "jdbc:ucanaccess://" + dbPath + ";memory=false"; try (Connection conn = DriverManager.getConnection(url, "", ""); PreparedStatement ps = conn.prepareStatement(sql); ResultSet rs = ps.executeQuery()) { ResultSetMetaData meta = rs.getMetaData(); int cols = meta.getColumnCount(); while (rs.next()) { Map<String, Object> row = new LinkedHashMap<>(); for (int i = 1; i <= cols; i++) { // 用列标签取值,避免 Access 字段名大小写差异带来的坑 row.put(meta.getColumnLabel(i), rs.getObject(i)); } rows.add(row); } } return rows; }逻辑上没什么花活,但几个细节值得说。Class.forName在 JDBC 4.0 以后理论上可以省略,可一旦部署到某些老应用服务器,SPI 扫描不一定生效,显式加载是后悔药。用getColumnLabel而不是getColumnName,是因为 Access 字段名在解析后大小写可能被改写,标签取值更稳。getObject拿到的类型对金额字段可能是BigDecimal,对备注字段是String,对是/否字段是Boolean,下游做类型转换前先判空,别直接强转。
3.2 插入、更新与批量写入
单条写入和普通 JDBC 没区别,真正要讲的是批量。Access 文件对逐条提交非常敏感,几千条 insert 一条条 commit,耗时会呈指数级上升:
public int batchInsert(String dbPath, List<String[]> data) throws SQLException { String url = "jdbc:ucanaccess://" + dbPath + ";memory=false;batchSizes=100"; String sql = "INSERT INTO t_record (code, name, amount, create_time) VALUES (?, ?, ?, ?)"; int total = 0; try (Connection conn = DriverManager.getConnection(url, "", ""); PreparedStatement ps = conn.prepareStatement(sql)) { conn.setAutoCommit(false); // 关自动提交,攒批 for (String[] d : data) { ps.setString(1, d[0]); ps.setString(2, d[1]); ps.setBigDecimal(3, new BigDecimal(d[2])); ps.setTimestamp(4, Timestamp.valueOf(d[3])); ps.addBatch(); } int[] result = ps.executeBatch(); conn.commit(); // 一次性提交 for (int r : result) { if (r >= 0 || r == Statement.SUCCESS_NO_INFO) total++; } } return total; }batchSizes=100是连接串参数,控制底层每多少条刷一次页,默认值偏小,大批量写入时调到 100 到 500 之间能明显提速。setAutoCommit(false)配合最后统一commit是必须的,中途异常要rollback,否则文件可能处于半写状态。注意 Access 对并发写不友好,多个线程同时往一个文件写会锁冲突,批量任务尽量单线程串行,或者按文件分片。
3.3 元数据读取与表结构探查
迁移项目第一步往往是摸清对方有哪些表、字段什么类型。用DatabaseMetaData能直接拿到:
try (Connection conn = DriverManager.getConnection(url, "", "")) { DatabaseMetaData md = conn.getMetaData(); // 只取用户表,过滤系统表 try (ResultSet tables = md.getTables(null, null, "%", new String[]{"TABLE"})) { while (tables.next()) { String table = tables.getString("TABLE_NAME"); System.out.println("表: " + table); try (ResultSet cols = md.getColumns(null, null, table, "%")) { while (cols.next()) { System.out.printf(" %s -> %s(%d)%n", cols.getString("COLUMN_NAME"), cols.getString("TYPE_NAME"), cols.getInt("COLUMN_SIZE")); } } } } }getTables的第四个参数传new String[]{"TABLE"}是为了把 Access 内部的系统表滤掉,不滤的话会混进一堆MSys开头的表,干扰判断。字段类型名返回的是 Access 的类型描述,比如VARCHAR、LONGCHAR、CURRENCY,映射到 Java 类型时LONGCHAR对应String,CURRENCY对应BigDecimal,COUNTER是自增主键。这一步做完,迁移的字段映射表基本就有了。
4. 避坑与排查:五条踩出来的经验
4.1 中文乱码:现象、原因与解决
现象是查出来的中文字段全是问号或方块。原因多半出在文件本身的编码和 JVM 默认编码不一致,Access 的文本字段按 UTF-16 存储,但 UCanaccess 在转换时依赖平台默认字符集。解决办法是在启动参数里显式指定-Dfile.encoding=UTF-8,连接串上也可以加;charSet=UTF-8兜底。如果文件是从别的机器拷来的、原本就是 GBK 环境写入的,那要在读取后按字节重新解码,这种情况少见但确实存在,先确认源文件来源再动手。
4.2 驱动类找不到:伴生 jar 缺失的连锁反应
现象是ClassNotFoundException: net.ucanaccess.jdbc.UcanaccessDriver,或者驱动加载了但一建连就NoClassDefFoundError。原因几乎都是伴生 jar 没带全,尤其是用-cp手动指定 classpath 时只写了主 jar。解决是把第 2 章表格里的依赖补齐,构建工具项目执行一次mvn dependency:tree看传递依赖有没有被provided或exclusion干掉。还有一种隐蔽情况:容器里已经有一个旧版 Jackcess,版本冲突导致方法签名对不上,排查时打印jackcess的实际版本号比对。
4.3 大文件内存溢出:memory 参数用反了
现象是读几十万行的表时抛OutOfMemoryError,堆 dump 里全是 HSQLDB 的缓存对象。原因是连接串写了memory=true,整个库被加载进内存。解决是改成memory=false,让数据按需从文件读。但要注意,memory=false下复杂查询会变慢,因为每次都要回文件取页,这是空间换时间的取舍。如果查询确实重、文件又不大(几十 MB 以内),memory=true反而更快,关键看文件体量和堆大小,别一刀切。
4.4 并发写入锁冲突:多线程同时写一个文件
现象是批量任务跑着跑着抛UcanaccessSQLException,提示文件被占用或锁超时。原因是 Access 文件是单写入者模型,多个连接同时写会争锁。解决是写入串行化,用单线程队列消费;如果业务上必须并行,就按表或按文件分片,每个分片独立文件。读多写少的场景可以开多个只读连接,但写连接始终只保留一个。这条在迁移脚本里最容易翻车,因为开发时单线程测不出问题,一上批量就暴露。
4.5 Java 9+ 模块化反射限制
现象是在 Java 11 或 17 上运行报InaccessibleObjectException,指向 HSQLDB 或 Jackcess 内部的反射调用。原因是模块系统默认不允许跨模块深度反射。解决有两个方向:启动参数加--add-opens java.base/java.lang=ALL-UNNAMED等开放指令,或者干脆把相关依赖放到 classpath 而非 module-path 上跑。我一般倾向后者,老库配老跑法,别硬上模块化,省得给自己找事。
5. 进阶:把 UCanaccess 包进连接池与迁移校验
连接池这块要泼盆冷水:UCanaccess 的连接创建成本不低,每次getConnection都要解析文件头、初始化内存库,频繁建连会拖垮吞吐。用 HikariCP 这类池子能缓解,但配置要改——maximumPoolSize别开大,Access 文件扛不住多写连接,读多写少的场景设 3 到 5 就够,写场景设 1。连接测试语句用SELECT 1不一定被支持,稳妥用SELECT COUNT(*) FROM 某张小表或者干脆关掉connectionTestQuery靠isValid。池化之后记得在连接归还时确保事务已提交或回滚,否则脏连接会污染下一次借用。
迁移校验是我每次做完 Access 导出都会强制走一遍的环节。思路很简单:源库和目标库各跑一遍聚合查询,比对行数、金额合计、关键字段的COUNT(DISTINCT),差异超过阈值就停下来查。下面这段是校验骨架:
// 对每张表做行数与金额合计比对,差异即报警 String checkSql = "SELECT COUNT(*) AS cnt, SUM(amount) AS total FROM t_record"; Map<String, Object> src = queryRows(srcPath, checkSql).get(0); Map<String, Object> dst = queryRows(dstPath, checkSql).get(0); BigDecimal diff = ((BigDecimal) src.get("total")) .subtract((BigDecimal) dst.get("total")).abs(); if (diff.compareTo(new BigDecimal("0.01")) > 0) { throw new IllegalStateException("金额合计不一致,差异: " + diff); }金额用BigDecimal比对、留一分钱容差,是因为浮点转换和四舍五入在不同链路上会有微小偏差,卡死零差异反而误报。行数必须严格相等,金额允许一分以内,这个标准是我踩过几次“看起来对、对账对不上”之后定下来的。还有个小技巧:校验前先把源文件复制一份只读副本再跑,避免校验过程中的连接把原文件锁住影响业务。
从那以后我每次接 Access 相关的活,都先把依赖清单、连接串参数、校验脚本这三样备齐再动手,缺一样都不开工。希望帮到你。
本文还有配套的精品资源,点击获取