简介:针对Java开发者使用Access数据库时常见的Access_JDBC30.jar限制——查询仅50次、单次仅1000条——这份开源的ucanaccess-4.0.4.jar驱动包提供了彻底的替代方案。通过纯JDBC直连方式操作.mdb文件,不再依赖jdbc-odbc桥接,既规避了64位系统下ODBC源配置的兼容性问题,也彻底取消了查询次数和条数上限;使用方式与旧驱动几乎一致,项目迁移成本低。7z压缩包共16个文件,以6个jar驱动及依赖库为主体,包含ucanaccess、hsqldb、jackcess及commons系列工具包;另有8个txt说明文档、1个bat和1个sh脚本,分别用于查看版本、控制台加载驱动以及一键启动,整体仅2.57MB,集成非常轻量。已有736人学习下载,适合接手维护老旧Access数据库应用、希望快速替换受限驱动的Java工程师,也可作为教学演示JDBC访问桌面数据库的完整范例;包内说明书清晰列出各文件作用与连接配置流程,脚本还覆盖Windows和Linux环境,能大幅减少自行拼装依赖的时间,拿到后解压即可按文档配置JDBC连接串,省去从Maven仓库逐一搜集依赖的步骤,对不熟悉命令行构建的开发者同样友好。
1. Java 读 Access 的开源免费 jar 包:ucanaccess 到底解决什么
Java 项目里想直接读 Access 数据库(.mdb / .accdb)需要开源免费 jar 包,几乎所有开放方案最终都会落到 ucanaccess 这个 JDBC 驱动上。它解决的是很多维护场景里的真实痛点:业务数据全在 Access 文件里,接口服务要读取但服务器不能装办公软件,数据又不能随便迁库,更不想用 Excel 中转一遍。ucanaccess 用纯 Java 的方式打开 Access 文件,以标准 JDBC API 暴露给上层业务,查询、更新都由驱动翻译到底层文件,项目无需引入 C 库,也无需额外装驱动。它适合写接口的老系统改造、替代手工导出、以及把 Access 作为过渡数据源的小团队。要说明白的是:它不是分布式数据库,数据量大、并发写高的场景不该让它硬扛,但它确实解决了 Java 直接操作 Access 文件这件既脏又绕的事。下面按落地顺序说清楚选型、依赖、接入、踩坑和调优。
2. 为什么非它不可:驱动定位、依赖组成和内部工作方式
很多人拿到ucanaccess-4.0.4.jar就以为是全部答案,直到编译报缺类才意识到理解不够。先花点篇幅建立准确的心智模型:这个 jar 是什么、它内部怎么工作、为什么它不像普通 JDBC 驱动那样“一个文件跑天下”。
2.1 三条路对比:为什么曾经的 ODBC 桥不再好用
能读 Access 文件的技术路线,现阶段实际上只有三条而且优劣差异很大。
第一条是传统的 JDBC-ODBC 桥。早期 JDK 自带桥接器,配合系统数据源可以连 Access,但桥接器依赖本机 ODBC 环境和驱动,JDK 更新之后已经不再提供这种桥接实现。就算你用老版本 JDK 强行跑,Windows 的环境差异、32 位与 64 位驱动不匹配、服务器上没装桌面组件,这些都会变成不可控因素。桥接方案现在基本退出了主流。
第二条是直接使用 Jackcess 这一类文件解析库。它能以纯 Java 方式读取 Access 文件内部的表结构、行数据和索引,功能确实硬核,问题是暴露给你的不是 JDBC 接口。业务代码要自己封装 ResultSet、自己拼过滤条件、自己处理类型映射,查询逻辑一旦复杂就会失控。它更像是底层工具,不适合直接铺给业务层。
第三条就是 ucanaccess。它把 Jackcess 包在下面,把 HSQLDB 的 SQL 引擎叠在上面,对外提供 JDBC 驱动接口。业务层写Connection、PreparedStatement、ResultSet,驱动负责把 SQL 翻译成对 Access 文件的实际操作。这条路是目前 Java 直接访问 Access 时最省事的开源方案,也是维护型项目里最常见的衔接方式。
2.2 ucanaccess 的内部工作方式:驱动、文件与工作区
接入前必须建立一个心理模型,否则后面优化无从下手。可以把它拆成三层:Access 文件是数据存储层,驱动拿到文件后解析表结构,并在一处临时工作区里构建这个数据库的 SQL 视图;你的 SQL 发给驱动后,由工作区执行并返回结果。写入操作会在提交时同步回 Access 文件。
这个模型能推导出两个关键结论。第一,第一次创建连接不是小事,驱动需要读取文件结构、建索引映射、准备 SQL 环境,所以连接建立时间会比普通查询长不少。一个常见误用就是“每次查询都新建连接、用完立刻关闭”,这会让驱动反复做初始化。更好的做法是复用同一个Connection,至少在一个业务会话内保持不关。第二,写入不是直接改文件每一条记录,而是经过工作区再落回 Access 文件,因此大量单条提交的代价会明显放大。记住“连接复用”和“批量提交”这八个字,后面几章都会围绕它们展开。
2.3 为什么 ucanaccess jar 很小,却经常第一次跑不起来
ucanaccess-4.0.4.jar本身是一个驱动入口包,它的体积不算大,因为它把实际干活的内容分发到了几个依赖库身上:文件解析依赖 Jackcess,SQL 语句执行依赖一个嵌在底层的 SQL 引擎,另外还有一些日志和工具类库。这些依赖不会物理塞进同一个 jar 包里,所以网上下载“单独一个 ucanaccess jar”直接扔进 classpath 的做法,几乎都会在启动阶段报错。
最典型的报错是NoClassDefFoundError,指向某个叫做 jackcess 或者 commons logging 相关的类。遇到这个,不是网络问题,完全是依赖没带全。我的习惯是:永远不要让新人从网盘下载单个 jar 然后手工补依赖,直接用 Maven 坐标引入,让构建工具一次拉齐。坐标和用法下一章展开。
3. 把它装进三种常见项目:Maven、离线 jar 和 Spring Boot 的最小连接
这一章是真正动手的部分。无论项目是传统 libs 目录、标准 Maven 工程还是 Spring Boot,下面的接入路径都能直接照抄。
3.1 用 Maven 坐标引入 ucanaccess-4.0.4,最不容易出错
在标准 Maven 工程里,只需要在pom.xml声明以下坐标。
<dependency> <groupId>net.sf.ucanaccess</groupId> <artifactId>ucanaccess</artifactId> <version>4.0.4</version> </dependency>这段坐标有两点需要注意。第一,groupId是net.sf.ucanaccess,不是ucanaccess,写错之后会拉到不存在的包。第二,这段坐标会自动引入 jackcess、commons-lang3、commons-logging 和 hsqldb 这些传递依赖,省去手工补包的步骤。如果你的构建环境用私服,要确保私服同步了这些传递依赖,否则最终 compile 阶段还是会缺类。
3.2 离线环境:把运行时依赖完整拷进 libs 目录
有些涉密或者内网环境不允许从远程仓库拉包,这时唯一的办法是手工准备 libs 目录。用户也从网上拿到了ucanaccess-4.0.4.jar,但忘记它还需要依赖。不要只复制一个 jar,驱动运行时的最小集合是:ucanaccess、jackcess、hsqldb、commons-lang3、commons-logging,五者缺一不可。
mkdir -p libs cp ucanaccess-4.0.4.jar libs/ cp jackcess-*.jar libs/ cp hsqldb-*.jar libs/ cp commons-lang3-*.jar libs/ cp commons-logging-*.jar libs/如果本地已经有了 Maven 仓库,更优雅的离线方式是用插件一次导出:
mvn dependency:copy-dependencies \ -DoutputDirectory=libs \ -DincludeScope=runtime这条命令会自动把 POM 里声明好的 ucanaccess 及其传递依赖全部复制到libs目录。注意:项目必须先按 3.1 的坐标声明依赖,或者至少pom.xml里有ucanaccess的依赖项,复制命令才有意义。执行完成后用libs/*作为 classpath 启动应用,就能避免在开发环境编译通过、部署环境启动失败这类尴尬情况。
3.3 最小 JDBC 连接:一个通用且可复制的查询方法
下面这段代码是接入 ucanaccess 的最小骨架,读一张表并逐行打印,适合放到本地验证环境是否打通。
public static void queryAccess(String dbPath, String sql) throws Exception { // 关键点:路径里的反斜杠统一成 /,否则驱动解析会有幺蛾子 String url = "jdbc:ucanaccess://" + dbPath.replace("\\", "/"); try (Connection conn = DriverManager.getConnection(url); Statement st = conn.createStatement(); ResultSet rs = st.executeQuery(sql)) { ResultSetMetaData meta = rs.getMetaData(); int colCount = meta.getColumnCount(); while (rs.next()) { StringBuilder line = new StringBuilder(); for (int i = 1; i <= colCount; i++) { line.append(rs.getString(i)).append(", "); } System.out.println(line); } } }这段代码里已经埋了一个高频雷点:Windows 环境下的路径反斜杠必须换成正斜杠。比如C:\data\2024.accdb要写成C:/data/2024.accdb,再拼到jdbc:ucanaccess://后面,最终 URL 形如jdbc:ucanaccess://C:/data/2024.accdb。驱动类会自动注册,不需要手工Class.forName。try-with-resources保证连接和语句都能及时释放,但在实际业务里不要每次查询都新开连接,连接复用收益远大于释放连接带来的资源节省。
3.4 Spring Boot 场景:不要把连接池默认参数当成万能解
Spring Boot 项目接入 ucanaccess,最直观的方式是在配置文件中声明数据源。下面这段是相对保守的连接配置。
spring: datasource: driver-class-name: net.ucanaccess.jdbc.UcanloadDriver url: jdbc:ucanaccess:///data/archive/2024.accdb hikari: maximum-pool-size: 1这个配置里driver-class-name是net.ucanaccess.jdbc.UcanloadDriver,URL 中的三个斜杠表示根路径下的绝对路径。我把maximum-pool-size固定成 1 不是随意设置的:ucanaccess 的每个连接都会对同一个 Access 文件做解析和锁管理,开多个连接并不会提升并发查询能力,反而容易在写操作时互相等待文件锁。Spring Boot 默认会维护一个池,池大小调大在这个场景里基本没有正面价值。
所以这里的建议是:如非多点只读的特殊设计,连接池直接限制为单连接。把写操作集中在同一连接里,读操作也共用连接,整体稳定性反而更好。
4. ucanaccess 高频踩坑与排查:5 个让新手浪费半天的问题
接入一个新驱动,踩坑是常态。下面是这一技术方向我最常被问到、也自己真实遇到过的五个问题,每条按现象、原因、解决三步拆开。
4.1 启动报 NoClassDefFoundError:一个 jar 治不了所有依赖
现象:项目把ucanaccess-4.0.4.jar放进了 classpath,编译没有任何报错,启动时出现类似java.lang.NoClassDefFoundError: com/healthmarketscience/jackcess/Database,导致连接创建失败。
原因:你只带了 ucanaccess 本体,却没有把 jackcess 等依赖一并带上。ucanaccess-4.0.4.jar是一个封装入口,它在运行时动态调用 Jackcess 的类,这些类位于别的 jar 里,不会自动被你的 libs 目录扫描到。
解决:回到第 3 章的方式,用 Maven 坐标引入,或者把 jackcess、hsqldb、commons-lang3、commons-logging 全部放进 libs。排查时可以执行jar tf ucanaccess-4.0.4.jar | grep jackcess,你会看到它确实引用了外部类而并没有打包进来。这不是驱动坏了,是依赖被静态检查阶段隐藏了。
4.2 文件被占用打不开:Access 桌面端可能独占数据库
现象:本地测试正常,把文件放到共享目录后抛java.io.IOException: Unable to open database file,或者提示文件被锁定,重启应用偶尔又能连上。
原因:打开 Access 数据库的桌面端程序可能正以独占模式占用文件;共享目录里也有别的进程在写;文件系统没有释放锁。ucanaccess 打开文件时要读取并解析整个结构,这个操作需要相对稳定的文件锁状态,任何外部干扰都会导致打不开。
解决:先确认桌面端程序已经退出,再确认共享目录没有其他写入进程,用命令测试文件是否可被独占打开。如果文件必须放在共享盘,可以考虑在应用启动时把accdb拷贝到本地临时目录再连接,代价是第一次连接要多一次文件复制,后面就稳定了。生产环境里我一般倾向于用同步任务把共享文件拉回本地,再让服务连接本地副本,只做单向读取,写回另走文件上传,避免锁冲突。
4.3 路径里带着反斜杠:文件明明存在却打不开
现象:代码里写"jdbc:ucanaccess://D:\\data\\2024.accdb"或"jdbc:ucanaccess://D:/data/2024.accdb",实际运行时系统提示找不到文件或者 URL 解析异常。
原因:Java 字符串里的\\会被转义成单个\,而 ucanaccess 的 URL 解析器遇到反斜杠容易理解成转义符或路径分隔符,导致底层拿到的路径和实际路径不一致。尤其是路径带中文、空格时,更容易出现肉眼看不出的差异。
解决:应用统一做一个路径归一化,把\全量转成/,只保留JDBC前缀,再拼 URL。文件名含中文时,优先用绝对路径并检查编码;如果文件名真的怪异,最简单的办法是在启动阶段把这个文件复制一份成简单英文名再连接,省掉编码排查。
String safePath = source.getCanonicalPath().replace("\\", "/"); String url = "jdbc:ucanaccess://" + safePath;这段代码同时把相对路径换成规范绝对路径,再去掉反斜杠,可以解决大多数路径类问题。注意getCanonicalPath()如果文件不存在会抛 IOException,所以连接前先Files.exists()做一道存在性检查。
4.4 读出来的时间不对:时区比想象中更重要
现象:表中记录的日期字段,读取之后发现比预期早/晚若干小时,或者毫秒部分丢失,和桌面端程序看到的数据不一致。
原因:Access 底层用浮点方式保存日期,驱动在读取时按 JVM 默认时区解释成Timestamp。如果运行 Java 的服务器时区与当时录入数据的环境不一致,或者 JVM 启动时没有明确指定时区,就会出现偏移。再加上 Access 日期精度本身并不高,读取时再经过一轮类型加工,会表现成“差几个小时”的效果。
解决:在所有运行节点上固定统一时区,JVM 启动参数里明确指定,而不是依赖操作系统默认值;代码里不要混用Date和LocalDateTime,读取出来一律按Timestamp处理。如果确实要跨时区展示,校验时先按服务器时区读入再转换,避免在 Access 日期上做二次格式化。这是最容易排查但在多环境部署时最容易被忽略的一类问题。
4.5 写入慢且不稳定:不要对着 Access 做逐行提交
现象:查询很快,写操作却特别慢,几百行的insert循环要跑好几秒,偶尔还抛锁冲突。
原因:驱动默认行为是每条语句执行后直接提交,而每次提交都要将数据同步回 Access 文件;Access 本身的单文件锁机制对频繁提交并不友好,落盘开销被放大了。业务流程里为了“尽快保存”而每条写一次,是把数据库当成数组用,完全没利用 JDBC 的事务能力。
解决:关闭自动提交,攒批后统一提交。下面这段代码是把一批 LogLines 插入到一个已有表里的写法。
Connection conn = DriverManager.getConnection(url); conn.setAutoCommit(false); String sql = "INSERT INTO LogLines(lineText, logTime) VALUES(?, ?)"; PreparedStatement ps = conn.prepareStatement(sql); for (LogItem item : items) { ps.setString(1, item.getLineText()); ps.setTimestamp(2, item.getLogTime()); ps.addBatch(); } ps.executeBatch(); conn.commit(); ps.close(); conn.close();这里需要解释三个细节。第一,关闭自动提交后,批量添加的语句在执行完executeBatch()之前不会真正落盘。第二,每批条数建议控制在 500 到 1000 行,不要一次攒十万条,避免长时间持有文件锁。第三,执行完要 commit 并释放连接,避免多次循环共用同一个长事务,否则后面读连接会持续看到旧快照。写成小批、提交完再开下一批,实践下来最稳。
5. 参数和写法调整:读写都稳的落地配置
接入成功之后就要谈调优。这里不是堆文档参数,只挑三个在八成场景里直接影响稳定性的点。
5.1 JDBC URL 参数:默认值不是万金油
ucanaccess 的 JDBC URL 支持;参数=值方式追加配置。最常用的三个参数如下。
| 参数名 | 作用 | 我的建议 |
|---|---|---|
memory | 是否将临时工作区放入 JVM 内存 | 小文件用 true,大文件用 false,防止堆内存被打爆 |
timeZone | 指定读取日期时的时区解释逻辑 | 显式设置,和业务服务器保持一致 |
jackcessOpener | 自定义底层文件打开方式 | 只读场景或加密文件场景使用 |
一个常见配置串可以写成下面这样。
String url = "jdbc:ucanaccess://" + path.replace("\\", "/") + ";memory=true;timeZone=" + TimeZone.getDefault().getID();memory=true对数据结构简单、几百 MB 内的 Access 文件有利,查询速度会明显提升;但如果文件有几千张表或数据量很大,工作区全部塞堆上会侵占应用内存。启动参数固定时区可以规避 4.4 里的时间偏移问题。jackcessOpener属于进阶用法,遇到采用密码保护的数据库文件或必须强制只读打开时才需要自己实现JackcessOpenerInterface,大多数项目用不到。
5.2 大量更新用批处理,但别挑战 Access 的单文件极限
上一章已经提过批量提交,这里把它作为正式配置原则补完。如果你的业务确实需要在 Access 文件里做批量更新,批量大小、事务长度和连接复用是三项同时要考虑的内容。批量太小导致提交频繁,批量太大导致锁占用时间过长,两者都会出问题。我的经验是:更新逻辑尽量在每日低峰窗口执行,事务控制在秒级完成,避免跨越多张表的长事务。如果有多线程需求,写侧务必收敛到单线程。
5.3 读写分离、单向同步,把 Access 当过渡源而非核心库
接入稳定之后,还需要做架构层面的定位。Access 文件适合作为数据源被读取,不适合作为高并发写入的服务端数据库。如果代码已经能把 Access 读完,下一层改造往往是:把 Access 数据同步到项目真正的数据库,让业务只依赖主库,Access 作为离线导入源。
我比较推荐先做单向同步模式:定时任务把 Access 文件的最新数据导出到业务表,业务统一走数据库读取。这样 Java 应用只在同步窗口内打开 Access 文件,冲突面最小,也方便后续彻底迁移。
6. 五分钟判断这套接入方案健不健康
接入完成后,建议用一段固定逻辑做健康检查,而不是只跑通一条业务查询就收工。这里分享一个三步骤的验证习惯。
第一步,检查驱动版本。下面这段代码能直接打印出当前生效的驱动名和版本号,避免因依赖混乱而加载了意外版本的类。
String url = "jdbc:ucanaccess://" + path.replace("\\", "/"); try (Connection conn = DriverManager.getConnection(url)) { DatabaseMetaData meta = conn.getMetaData(); System.out.println("driver=" + meta.getDriverName()); System.out.println("version=" + meta.getDriverVersion()); }第二步,做一次“只读连通性”冒烟测试,执行SELECT COUNT(*) FROM 某张核心表,确认文件解析正常、表映射存在、行数符合预期。第三步,也是最容易漏的一步:手工开启事务、插入一行、再 rollback,验证文件没有因为你 commit 意外被改动。模板可以按下面的方式写。
conn.setAutoCommit(false); stmt.executeUpdate("INSERT INTO HouseKeeping(tempFlag) VALUES(1)"); conn.rollback();执行后重新SELECT该行,确认不存在,证明驱动既能写入也能正确回滚。到这里,这套方案对你当前项目才算真正验证完毕。
这套接入方式我常向做数据迁移、报表对接的同事推荐,但它最大的价值不是在代码层面,而是帮你理解“Java 直接操作 Access”这一技术边界在哪里。读没问题,写要批量,事务要短,并发要收敛。所有踩过的坑几乎都集中在连接生命周期和文件锁上,提前知道这几点就能少走大量弯路。希望帮到你。
本文还有配套的精品资源,点击获取