简介:这份文档面向JSP入门学习者与小型Web项目开发者,围绕JSP通过JDBC-ODBC桥连接ACCESS数据库这一基础数据交互方式展开,帮助读者理解在Tomcat环境下完成数据库访问的完整思路。文档先从建库入手,说明如何创建test.mdb及username表(含uid、pwd两个文本字段),再讲解将数据库文件放入Tomcat的webapps/ROOT/data目录并借助application.getRealPath转换为物理路径的做法,最后给出JDBCAccess.jsp示例,逐段解析加载sun.jdbc.odbc.JdbcOdbcDriver、拼接连接字符串、创建Statement、执行查询、遍历ResultSet并输出用户名与密码,以及依次关闭ResultSet、Statement和Connection的要点,同时对易出错的连接异常给出提示信息。资源包共1个文件,为doc文档,大小约31KB,篇幅精炼、代码与说明对照,适合边看边在本地复现调试。目前已有240人学习,可作为理解JDBC与数据库交互原理的入门参考。
1. 从一份打不开的老页面说起:JSP 连接 ACCESS 数据库到底解决什么问题
接手一个早年的课程设计包,首页只有一行红字:java.sql.SQLException: [Microsoft][ODBC 驱动程序管理器] 未发现数据源名称并且未指定默认驱动程序。这套 JSP 连接 ACCESS 数据库的示例,链路其实很短:页面里用Class.forName加载sun.jdbc.odbc.JdbcOdbcDriver,DriverManager拿Connection,SQL 交给Statement,结果集用while(rs.next())逐行out.print。一个.mdb文件加一个 Tomcat 就能跑,不用另装数据库服务,也不用配账号体系。
它适合谁:课程设计、单机演示、内网小后台、离线数据查看页这类读写量极低的场景。不适合高并发写入,Access 是文件型数据库,几个连接同时写就锁表。下面按建库、放文件、写连接串、调页面、排错的顺序拆,每一步都给可对照的参数。
2. 建库与落盘:把 test.mdb 放到 Tomcat 能找到的位置
2.1 建库建表:字段类型与命名约定
先在 Access 里新建一个空数据库,另存为test.mdb。这里有个容易被忽略的点:Access 2007 之后的默认保存格式是.accdb,如果你直接点保存,拿到的是.accdb文件,而示例里的连接串写的是(*.mdb),驱动名对不上就会报「未发现数据源名称」。新建时在「另存为」对话框里把文件类型切回「Access 2002-2003 数据库 (*.mdb)」,或者建完之后用「数据库工具 → 转换数据库」降级一次。
建表这一步,表名定为username,两个字段:
| 字段名 | 数据类型 | 字段大小 | 允许空 | 说明 |
|---|---|---|---|---|
| uid | 文本 | 50 | 是 | 登录名,示例里直接当用户名打印 |
| pwd | 文本 | 50 | 是 | 明文密码,仅演示用 |
| id | 自动编号 | 长整型 | 否 | 可选,建议加,方便后续排序 |
「文本」在 Access 里对应的是Text/VARCHAR,不是「备注」。如果误选成「备注」(Memo),JDBC 读出来是长文本对象,rs.getString()虽然还能取到值,但排序和比较会出幺蛾子。字段大小设 50 是留余量,Access 的文本型上限是 255,超过就得换备注型。
表名和字段名都别用 Access 保留字,password、date、value、level、order这几个都是雷区,写在 SQL 里必须加方括号[password],不加就报语法错误。示例用pwd是聪明的做法,我一般也这么起名。字段名大小写在 Access 里不敏感,但 JSP 里rs.getString("uid")的字符串要和建表时一致,养成小写习惯最省事。
建完表随手插两行测试数据,别等到页面跑起来才发现表是空的——空表也能连上,你会误以为整条链路通了,其实查询逻辑根本没验证过。
2.2 数据库放哪:getRealPath 与 webapps/ROOT/data 的对应关系
示例把数据库放在%TOMCAT_HOME%\webapps\ROOT\data\test.mdb,代码里写的是相对路径data/test.mdb,中间靠application.getRealPath(spath)转成物理路径。这三者的对应关系必须拧成一股:
%TOMCAT_HOME%\webapps\ROOT\ ← Web 应用根目录,也就是 context 的 docBase └─ data\ └─ test.mdb ← 实际文件application.getRealPath("data/test.mdb")返回的是...\webapps\ROOT\data\test.mdb。注意getRealPath的参数是相对于 Web 应用根的,前面不能带斜杠,写成/data/test.mdb在某些容器版本上也能对,但跨版本行为不一致,老老实实写相对路径。
两个坑提前说。第一,getRealPath在应用以未解压的 war 包形式部署时可能返回null,因为容器没有真实目录可映射。开发阶段直接丢文件夹到webapps下最稳,别一上来就打 war。第二,放data目录意味着这个文件在 Web 根下,浏览器直接访问/data/test.mdb就能把整库下载走。课程设计无所谓,真上线要把库挪到WEB-INF下或者容器外的固定目录,再把代码里的路径改成绝对路径。
application是 JSP 的内置对象,本质是ServletContext,生命周期跟着应用走。用它取路径的好处是换机器、换 Tomcat 安装位置都不用改代码,只要目录结构保持一致。
2.3 运行前提:JDK、Tomcat 与 ODBC 驱动的版本账
这套写法最要命的地方在版本,不在代码。sun.jdbc.odbc.JdbcOdbcDriver属于 JDBC-ODBC 桥,从 JDK 7 起被标记为废弃,JDK 8 直接移除。也就是说,如果你现在用 JDK 8 及以上跑这段代码,Class.forName那一行必定抛ClassNotFoundException,跟数据库、路径、权限都没关系。
| 组件 | 可用区间 | 检查方式 | 超出区间后的表现 |
|---|---|---|---|
| JDK | 6 及以下(含桥驱动) | java -version | 8+ 抛 ClassNotFoundException |
| Tomcat | 6/7/8 均可,配 JDK 6 | 启动日志首行 | 高版本 Tomcat 要求 JDK 8+,与桥驱动冲突 |
| Access ODBC 驱动 | 随 Office 或 Access Database Engine 安装 | ODBC 数据源管理器里看「驱动程序」页 | 列表里没有对应项,连接串直接失败 |
| 位数 | JVM 位数 = ODBC 驱动位数 | 任务管理器看进程、ODBC 管理器标题栏 | 32 位 JVM 看不到 64 位数据源,反之亦然 |
现实的做法有两条。一条是把运行环境压到 JDK 6 + Tomcat 6/7,虚拟机或旧机器上跑,纯当教学演示。另一条是换驱动:用第三方的 UCanAccess 或 Jackcess 之类的纯 Java 库替代 ODBC 桥,连接串格式完全不同,但能在新 JDK 上跑。哪条都行,唯一不能做的是拿 JDK 8 硬套这份代码然后怀疑人生。
Windows 上找 ODBC 管理器要分清两个入口:控制面板里的「ODBC 数据源(32 位)」和「ODBC 数据源(64 位)」。名字很像,点错了配出来的 DSN 在程序里就是看不见,这是后面排错章节的头号案例。
3. 连接串两种写法:DBQ 直连与 DSN 配置的参数拆解
3.1 DBQ 直连:一条 URL 里每个分号的含义
示例用的是直连写法,不依赖任何预先配置的数据源,把驱动名和文件路径都塞进 URL:
// Access 数据库相对路径,相对的是 Web 应用根目录 String spath = "data/test.mdb"; // 转成服务器上的绝对物理路径,ODBC 只认绝对路径 String dbpath = application.getRealPath(spath); // 驱动名必须和 ODBC 管理器「驱动程序」页里显示的名字逐字一致 // .mdb 用这一条;.accdb 要换成 {Microsoft Access Driver (*.mdb, *.accdb)} String url = "jdbc:odbc:Driver={Microsoft Access Driver (*.mdb)};DBQ=" + dbpath;拆开看三段。jdbc:odbc:是协议前缀,告诉DriverManager找桥驱动。Driver={...}是 ODBC 层的驱动标识,花括号里那一串是一个完整名字,逗号、空格、星号、括号全都算数,少一个字符就报「未发现数据源名称」——这个报错极具迷惑性,因为它字面上在说数据源,实际是在说驱动名没匹配上。DBQ=后面跟数据库文件的绝对路径,路径里有空格没关系,ODBC 会整体处理;但如果路径里带分号,就会被当成参数分隔符截断,这种目录名趁早改掉。
还有一个常被问到的参数dbname,示例里定义了String dbname = "";却没用上。它是给 DSN 写法留的位置,直连模式下不需要。同理user和password在没给 Access 设密码时都传空串,设了密码就要对着填,Access 的库密码和用户级安全机制是两回事,前者才是连接串要的。
3.2 系统 DSN 写法:什么时候必须去控制面板配数据源
另一条路是先在系统里注册一个数据源,URL 只写名字:
// DSN 名称就是 ODBC 管理器里「名称」那一栏填的值,大小写不敏感 String url = "jdbc:odbc:testdsn"; // 连接时把账号密码单独传,Access 无密码就传空串 Connection conn = DriverManager.getConnection(url, user, password);配置路径是:控制面板 → 管理工具 → ODBC 数据源 → 选「系统 DSN」页 → 添加 → 选Microsoft Access Driver (*.mdb)→ 名称填testdsn→ 点「选择」定位到test.mdb。系统 DSN 对本机所有用户生效,用户 DSN 只对当前登录用户生效;Tomcat 如果注册成了 Windows 服务,跑在系统账户下,用用户 DSN 就会看不见,这个坑后面还会提。
两种写法怎么选:直连适合开发调试,改路径只改代码,不污染系统环境;DSN 适合多套应用共用一个库,路径变了只改一处配置。直连的代价是每次连接都要解析驱动名,理论上稍慢,但在这个量级的应用里测不出来。
3.3 驱动名与位数的对应关系
驱动名不是随便写的,要跟文件格式、Office 版本对上:
| 文件格式 | 驱动名 | 常见来源 |
|---|---|---|
| .mdb | {Microsoft Access Driver (*.mdb)} | 旧版 Office / Access 运行时 |
| .mdb 与 .accdb 混用 | {Microsoft Access Driver (*.mdb, *.accdb)} | Access Database Engine 2010 及以上 |
| .accdb 且无旧驱动 | 同上 | 同上,注意位数 |
装完驱动后打开 ODBC 管理器,切到「驱动程序」页,把那一列名字复制出来,和代码里的字符串做逐字比对,比靠记忆靠谱得多。名字对不上就是「未发现数据源名称」,名字对上了但位数不对,报错会变成「找不到指定的文件」或者干脆挂住,这两种错长得完全不一样,别混为一谈。
4. 页面实现:从加载驱动到结果集遍历的逐行对照
4.1 页面指令与编码声明:gb2312 这一行不能省
<%@ page contentType="text/html; charset=gb2312" language="java" import="java.sql.*"%> <meta http-equiv="Content-Type" content="text/html; charset=gb2312">page指令里import="java.sql.*"是必须的,少了Connection、Statement、ResultSet全部找不到符号。contentType和charset决定响应头里的编码,meta决定浏览器解析编码,两处写同一个值才不打架。
用gb2312是早年环境的惯例,对应的是 JDK 6 时代中文 Windows 的默认编码。如果服务器默认编码是 UTF-8,而页面声明 gb2312,out.print出来的中文就会变成一串问号。我一般会把三处编码统一成UTF-8:page指令的charset、meta的charset、以及文件本身的保存编码。三处不一致是最典型的乱码来源。
4.2 完整 JSP:从加载驱动到关闭连接
<%@ page contentType="text/html; charset=UTF-8" language="java" import="java.sql.*"%> <% Connection conn = null; Statement stmt = null; ResultSet rs = null; try { // 1. 拼数据库物理路径,相对 Web 应用根目录 String spath = "data/test.mdb"; String dbpath = application.getRealPath(spath); if (dbpath == null) { out.print("路径解析失败:应用可能以 war 包形式未解压部署"); return; } // 2. 直连式连接串,驱动名与 ODBC 驱动程序页逐字一致 String url = "jdbc:odbc:Driver={Microsoft Access Driver (*.mdb)};DBQ=" + dbpath; // 3. 加载 JDBC-ODBC 桥驱动,仅 JDK 6 及以下有效 Class.forName("sun.jdbc.odbc.JdbcOdbcDriver"); // 4. 建立连接,无密码则账号密码都为空串 conn = DriverManager.getConnection(url); // 5. 创建可滚动、可更新的语句对象 stmt = conn.createStatement( ResultSet.TYPE_SCROLL_SENSITIVE, ResultSet.CONCUR_UPDATABLE); // 6. 查询并逐行输出,字段名要和建表时一致 String sql = "select uid, pwd from username"; rs = stmt.executeQuery(sql); while (rs.next()) { out.print("用户名:" + rs.getString("uid")); out.print(" 密码:" + rs.getString("pwd") + "<br>"); } out.print("<br>数据库连接成功!"); } catch (Exception e) { // 打印异常类型,比只打 message 更容易定位 out.print("数据库连接错误:" + e.getClass().getName() + "<br>"); out.print(e.getMessage()); } finally { // 关闭顺序必须是 结果集 → 语句 → 连接 if (rs != null) { try { rs.close(); } catch (Exception ignored) {} } if (stmt != null) { try { stmt.close(); } catch (Exception ignored) {} } if (conn != null) { try { conn.close(); } catch (Exception ignored) {} } } %>和原始示例相比改了三处。第一,连接、语句、结果集三个变量提到try外面声明,finally里才能关到;原始代码把close()写在try末尾,一旦查询中途抛异常,后面的close全部跳过,连接就漏了。第二,finally里每个close单独try-catch,避免第一个关闭失败导致后面两个不执行。第三,catch里多打了一行异常类名,ClassNotFoundException和SQLException的处理方向完全不同,先分清类型再查。
4.3 逐段对照:每行代码失败时看什么
application.getRealPath返回null时,大概率是 war 未解压部署,或者参数写成了以斜杠开头的绝对路径。先在页面里把dbpath打出来看实际值,再去文件管理器里核对该路径下test.mdb是否存在,这一步能干掉一半的「找不到文件」。
Class.forName抛ClassNotFoundException,只有两种可能:JDK 版本过高(8 及以上),或者类名拼错。JDK 8 之后这个类彻底不存在,改环境或换驱动,别在类名上打转。
DriverManager.getConnection抛SQLException,看 message 里的中括号内容。[Microsoft][ODBC 驱动程序管理器]开头说明卡在 ODBC 层,问题是驱动名或 DSN;[Microsoft][ODBC Microsoft Access Driver]开头说明驱动找到了,问题在文件路径、权限或文件损坏。
executeQuery抛异常,基本是 SQL 写错或表名不对,把sql变量打出来,粘到 Access 的查询设计器里跑一遍最快。
createStatement那两个参数决定结果集能不能rs.first()、rs.absolute(n)这类前后滚动操作。示例里用了可滚动可更新,实际只做遍历的话传空参conn.createStatement()就够了,开销更小。可更新模式还要求查询必须是单表、带主键,多表关联时会直接降级或报错,用不上就别开。
5. 避坑与排查:驱动、位数、路径、编码的五类翻车现场
5.1 类找不到:ClassNotFoundException 的三个来源
现象:页面刚打开就抛java.lang.ClassNotFoundException: sun.jdbc.odbc.JdbcOdbcDriver,连数据库文件都还没碰到。
原因:一是 JVM 是 JDK 8 及以上,桥驱动已被移除;二是类名少写或多写了包路径,比如写成JdbcOdbcDriver;三是老项目里有人把rt.jar换成了精简版 JRE,里面剔掉了sun.jdbc.odbc包。
解决:先java -version确认版本,8 以上直接判定为环境不匹配,要么换 JDK 6,要么改走纯 Java 驱动。别去网上找JdbcOdbcDriver的 jar 包往WEB-INF/lib里塞,那个包依赖 JNI 本地库,单塞一个 class 文件跑不起来,这是血泪经验。
5.2 位数不匹配:ODBC 里能看到数据源,程序却连不上
现象:在 ODBC 管理器里明明配好了 DSN,测试连接也通过,JSP 一跑就报「未发现数据源名称」或者「找不到指定的驱动程序」。
原因:JVM 位数和 ODBC 驱动位数不一致。64 位 JVM 只会去找 64 位 ODBC 管理器里注册的数据源,而多数人顺手打开的是 32 位那个入口,配出来的东西 64 位进程完全看不见。
解决:确认 Tomcat 跑在哪个位数上——看java -version输出里有没有64-Bit,或者任务管理器里进程名后面有没有(32 位)。然后打开同名位数的 ODBC 管理器重新配一遍。这一步没有捷径,两个入口的界面长得一模一样,只能靠标题栏区分。我一般会在桌面各留一个快捷方式,省得每次翻控制面板翻错。
5.3 路径与权限:getRealPath 返回 null 和只读目录
现象一:页面输出「路径解析失败」,dbpath是null。原因:应用打成了 war 包且容器没解压,getRealPath没有真实目录可映射。解决:开发期直接把文件夹丢进webapps下,或者改配置让容器解压 war。
现象二:路径正确、文件存在,仍然报「找不到指定的文件」或「操作必须使用一个可更新的查询」。原因:test.mdb所在目录对运行 Tomcat 的账户没有写权限。Access 打开数据库时会在同目录生成.ldb锁文件,目录只读就写不了锁,连接直接失败。解决:给data目录加上运行账户的修改权限。把 Tomcat 注册成 Windows 服务后,它跑在系统账户下,和你当前登录用户不是一个人,权限要单独给。
5.4 中文乱码:页面、连接串、字段三处编码
现象:数据库里存的是「张三」,页面显示成??或者一串方块。
原因有三层。页面层的page指令、meta标签、文件实际保存编码三者不一致;数据层 Access 的文本字段本身没有编码概念,但经由 ODBC 桥传输时会按系统默认代码页转换;输出层out.print用的响应编码和浏览器解析编码不一致。
解决:先把三处页面编码统一成UTF-8,文件用编辑器另存为 UTF-8 无 BOM。如果还是乱码,检查 JVM 的file.encoding,启动参数里加-Dfile.encoding=UTF-8。桥驱动这一层对中文的支持取决于系统区域设置,非中文 Windows 上跑中文数据,乱码几乎是玄学,最省事的办法是容器和数据库放同一台机器、同一个区域设置下。
5.5 连接泄漏:Tomcat 跑几天就卡死
现象:刚部署时一切正常,跑一段时间之后页面越来越慢,最后全部超时,重启 Tomcat 又好了。
原因:原始代码把rs.close()、stmt.close()、conn.close()写在try块末尾,任何一步抛异常,后面的关闭全部跳过。Access 对同时打开的连接数有上限,漏几十个之后新连接就建不起来了,而且.ldb锁文件会一直挂着。
解决:按 4.2 的写法,变量提到try外、关闭放finally、每个close单独捕获异常。另外别在循环里反复getConnection,一个请求拿一个连接、用完就还,这是底线。要更稳就上连接池,但 Access 这种文件库配连接池收益有限,池子开大了反而更容易撞锁,一般 3 到 5 个连接封顶。
6. 从能跑到敢用:参数化查询与上线前的自检
6.1 把拼串换成 PreparedStatement
示例里是纯查询,看不出问题。一旦要做登录校验,把用户输入直接拼进 SQL,' or '1'='1这种输入就能绕过验证,这在课程设计答辩时是会被当场问住的点。换成预编译语句,参数走占位符,输入里的引号不再参与 SQL 解析:
// ? 是占位符,用户输入只作为值传入,不参与 SQL 语法解析 String sql = "select uid from username where uid=? and pwd=?"; PreparedStatement ps = conn.prepareStatement(sql); ps.setString(1, request.getParameter("uid")); // 参数下标从 1 开始 ps.setString(2, request.getParameter("pwd")); ResultSet rs = ps.executeQuery(); boolean ok = rs.next(); // 有记录即通过setString的下标从 1 起,不是 0,写错一位不报错但结果全错。预编译语句对 Access 的兼容性没问题,桥驱动会把?转成 ODBC 的参数绑定,性能上还省掉了重复解析 SQL 的开销。
6.2 连接池与只读打开的取舍
Access 是文件库,连接池的意义和 MySQL 那种完全不同。池子里的连接长期不关,.ldb锁文件就一直存在,其他进程想写库就会被挡。我的做法是:只读页面用连接池,池大小 3;有任何写操作的页面走短连接,用完立刻关。写操作前先确认没有别的进程占着库,比如你开着 Access 界面没关,JSP 那边写就会失败,报「操作必须使用一个可更新的查询」,这个报错跟权限报错长得一样,很容易查错方向。
6.3 上线前的一张自检表
| 检查项 | 通过标准 | 不通过的表现 |
|---|---|---|
| JDK 版本 | 6 及以下,或已换纯 Java 驱动 | ClassNotFoundException |
| ODBC 驱动名 | 与驱动程序页逐字一致 | 未发现数据源名称 |
| 位数 | JVM 与 ODBC 管理器同位数 | 数据源配了但看不见 |
| 文件路径 | getRealPath 返回值非 null | 路径解析失败 |
| 目录权限 | 运行账户可写,能生成 .ldb | 找不到指定的文件 |
| 编码 | 页面、文件、JVM 三处一致 | 中文显示为问号 |
| 资源关闭 | close 全在 finally 里 | 跑几天后连接超时 |
这张表我每次部署老项目都会过一遍,七项里通常能提前拦下五六个问题。那张表后来被我贴在工位显示器边上,因为同一类错换台机器就会重来一次——尤其是位数和权限这两项,看着最简单,翻车率最高。希望帮到你。
本文还有配套的精品资源,点击获取