☰
JSP动漫网站毕设:Servlet+Oracle分页鉴权与二次开发
2026/10/27 14:23:11 网站建设 项目流程

简介:这份《动漫网站的设计与实现》毕业论文文档面向计算机相关专业学生与网站开发初学者,围绕动漫类网站从需求分析到系统测试的完整开发流程展开,可作为课程设计、毕业设计及小型Web项目实践的参考范例。文档为单个doc文件,压缩包约569KB,内容按章节组织,涵盖MyEclipse、Tomcat、B/S结构、ORACLE数据库、Dreamweaver、JavaScript与JSP等技术选型说明,并依次讨论需求分析、可行性分析、系统设计、数据库表结构与表间联系、总体设计、项目包部署、数据连接配置、前后台页面实现及系统测试等环节,目录结构清晰,便于按模块查阅和模仿写作。目前已有2170人学习下载,适合需要了解动漫网站开发脉络、整理论文框架或对照实现步骤的读者参考。

1. 一份毕设文档能跑出什么:JSP 动漫站的功能边界

「动漫网站」这四个字听起来门槛不高,真正动手做「网站制作」的人才知道,难的是把前台展示、后台管理、数据库和权限串成一条能跑通的线。我手里这份文档目录从引言排到致谢,十章里有七章在讲设计,代码散在「系统实现」那几页里,很多同学拿到之后的第一反应是——看着挺全,但不知道从哪下手跑起来。它其实是一个很典型的 JSP 方案:B/S 结构,前台有动漫新闻、动漫杂谈、动漫在线和留言本,后台管内容增删改查,Oracle 存数据。适合两类人:一类是要照着做课设或毕设、需要一份能落地骨架的;另一类是想看看不用框架、纯 JSP + Servlet + JavaBean 的三层结构到底怎么分层的新手。下面按「能干什么 → 怎么跑 → 怎么改」的顺序把它拆开。

2. 技术栈拆解:B/S + JSP + Tomcat + Oracle 这套组合怎么落地

这套选型放在今天看是「老」,但老不代表不能用。它的价值在于层次干净:浏览器只负责渲染,业务逻辑全在 Tomcat 里跑,数据落在 Oracle,中间靠 JDBC 连起来。相比现在一上手就是 Spring Boot 加一堆注解,这种写法反而更容易看清一个请求到底经过了哪几层。理解清楚这条链路,后面看代码、改代码、排查问题才有落点。

2.1 一次请求从浏览器到 Oracle 的完整链路

拿「点击动漫在线,打开某部番剧的简介页」这个动作举例。用户在浏览器里点一个链接,地址栏变成/animeDetail.jsp?animeid=1024,接着发生的事大致是这么几步:

  1. 浏览器把 HTTP 请求发给 Tomcat 的 8080 端口,Tomcat 按web.xml里的映射找到对应的 Servlet 或 JSP。
  2. Servlet 从request.getParameter("animeid")拿到参数,做一次类型转换和空值判断。
  3. 调用 DAO 层的findById(int id),DAO 用 JDBC 向 Oracle 发一条select。
  4. ResultSet被逐行读出来,塞进一个Anime实体 Bean,再放进request作用域。
  5. 请求转发到animeDetail.jsp,页面把 Bean 的属性输出成 HTML,Tomcat 把这段 HTML 回给浏览器。

这条链路里有两个容易忽略的点。一是参数永远不可信,animeid可能是空、可能是abc、可能是1 or 1=1,所以 Servlet 里必须做转换和校验,DAO 里必须用PreparedStatement。二是作用域要选对,详情页用request就够,因为一次请求就结束;而登录后的用户信息必须放session,否则翻个页面就丢了。这个项目里前台列表页、后台管理页大量依赖session判断身份,作用域选错就会出现「明明登录了,点进后台又被踢出来」这种问题。

还有一层是 B/S 结构本身带来的约束。客户端只有浏览器,不装任何东西,所有状态都保存在服务端,这意味着每次请求都是「无状态」的,服务端只能靠session或cookie认出你。这个项目里后台权限判断、论坛发帖人身份、积分等级展示,全部建立在这一个前提上。

2.2 工具与版本矩阵:哪些版本能凑到一起

这份文档给的是 2012 年前后的组合,具体版本和职责对应关系如下表:

层次选型文档给出的版本在这套系统里的实际作用
开发工具MyEclipse8.5JSP/Servlet 编辑、工程管理、一键部署到 Tomcat
Web 容器Tomcat6.0跑 JSP 与 Servlet,对应 Servlet 2.5、JSP 2.1 规范
运行环境JDK1.6(建议)编译 Java 源码、提供 JDBC 基础类库
数据库Oracle10g存新闻、杂谈、动漫、论坛、管理员等全部数据
页面工具DreamweaverMX静态 HTML 排版、CSS 与表格布局
交互脚本JavaScriptES3/ES5 混用表单校验、页面跳转、弹窗提示
目标浏览器IE 系列文档写 IE5.0 起,推荐 IE6.0决定了 CSS 和 JS 的兼容写法

这里有几个版本上的硬约束,不是随便换的。Tomcat 6.0 只认 Servlet 2.5 的web.xml头,如果你把新版本 Tomcat 的web-app_3_0.xsd拷过来,老容器会直接报解析错误。Oracle 10g 的 JDBC 驱动一般是ojdbc14.jar,它对应 JDK 1.4 及以上,放 JDK 1.6 没问题,但放到 JDK 17 上就会因为模块化限制出现反射报错。至于 IE6 这个目标浏览器,它带来的最大影响是 CSS 不能用flex、JS 不能用addEventListener,页面上大量用的是document.getElementById加onclick内联写法。

提示:现在要在新机器上复现,最省事的路子是把 JDK 锁在 1.8、Tomcat 锁在 6.0 或 7.0、Oracle 用 10g 或 11g 的 XE 版本,浏览器用 Chrome 加开发者工具模拟,页面兼容性问题优先级往后放,先把功能跑通。

2.3 四层职责划分:实体 Bean、DAO、Action、JSP 各管什么

文档第五章把模块拆成「身份验证模块、数据操作模块、页面操作模块、实体 Bean 模块、Action 模块」,其中 Action 被描述成「页面的中转站」。翻成现在的话说,这是一个没有用重框架的手写 MVC:

  • 实体 Bean:一个表对应一个 Java 类,字段一一对应,提供无参构造、有参构造、getter/setter,重写equals。文档特别强调「自动写入的列不必传参」,指的就是主键由序列生成,构造时不传。
  • DAO(数据操作模块):所有 SQL 都关在这里,对外只暴露方法名。文档列了十几个方法,包括按类别分页查、按类别查总数、按时间排序查集合、按编号查单条、发表、删除、查最近发表、查排行榜。
  • Action / Servlet(页面操作模块):收参数、调 DAO、往作用域塞数据、决定跳哪个页面。发布、修改、删除这类操作都在这一层做跳转验证。
  • JSP:只负责取值和渲染,原则上不写业务 SQL。

这套分法在今天看来粗糙,但它解决了一个核心问题:改数据库字段的时候,只需要动实体 Bean 和 DAO,页面不用碰。这也是后面二次开发能顺利迁移到连接池、甚至换数据库的前提。

3. 数据库与工程结构:十张表和 WebRoot 目录怎么对应

数据库是这个项目里信息密度最高的部分。文档第四章把十张表逐张列了字段、类型、是否为空、是否主键,第五章再讲表间联系。真正要跑起来,光看字段名不够,得知道每张表属于哪条业务线、谁引用谁、删除的时候先动哪张。工程结构也一样,WebRoot下放什么、WEB-INF下放什么、驱动包放哪里,直接决定启动会不会报错。

3.1 两条业务线的表设计:动漫内容线与论坛线

十张表可以清晰地分成两组。第一组是动漫内容线,负责「有什么可看」:

表名中文名主键关键字段作用
anime动漫表animeidanimename、animepic一部作品的主记录,存名称和封面
catoon漫画内容表catoonidcatoontitle、updatetime、animeid、countid漫画的每一话,靠countid标记集数
catoonpic漫画图片表picidcatoonid、picurl一话里的多张图,和catoon一对多
film影片表filmidfilmbrief、updatetime、animeid、countid动画视频,存简介和播放相关路径
downloadclass下载类别表gameclassidgameclassname资源的分类
download资源表downloadidgameclassid、brief、downcount可下载资源,带下载计数

第二组是论坛线,负责「用户怎么交流」:

表名中文名主键关键字段作用
users用户表USERIDUSERNAME、USERPWD、HEAD、REGTIME、INTERGAL登录、头像、注册时间、积分等级
topic主贴表TOPICIDTITLE、USERID、BOARDID、REPLYCOUNT、CLICKCOUNT帖子主体,带回复数和点击数
reply回帖表REPLYIDTOPICID、USERID、PUBLISHTIME依附主贴的回复
grade等级表GRADEIDGRADENAME积分到等级的映射

这两组表的设计思路不一样。内容线是「一部作品 → 多话 → 多图」的三级结构,animeid是贯穿的纽带;论坛线是「用户 → 主贴 → 回帖」的三级结构,USERID和TOPICID是两条外键链。文档里提到「板块表」但没给出完整字段,按这个场景的常见做法,BOARDID指向一张板块表,用来分「新闻区」「杂谈区」「资源区」这类版块,父板块编号用0表示顶层,因为序列最小值从 1 开始。

还有一组细节字段值得注意:topic和reply里都有FONTSIZE、FONTCOLOR、BOLD、ITALIC、UNDERLINE。这是那个年代论坛的标配,允许用户发带格式的帖子,本质上是把一小部分 HTML 样式存成字段,渲染时再拼回去。这套设计在今天看来有 XSS 风险,但在封闭的课设环境里是能用的。

3.2 Oracle 建表、序列与分页 SQL

Oracle 和 MySQL 最大的差别在自增主键上。MySQL 一个AUTO_INCREMENT就完事,Oracle 得先建序列,再在插入时取nextval。用户表按文档字段还原出来大致是这样:

-- 用户表:登录、头像、积分等级都在这 CREATE TABLE users ( USERID NUMBER(8) NOT NULL, -- 用户编号,由序列生成 USERNAME VARCHAR2(20) NOT NULL, -- 登录名 USERPWD VARCHAR2(20) NOT NULL, -- 密码,课设环境一般明文,生产必须加盐哈希 USERNAME2 VARCHAR2(20) NOT NULL, -- 昵称/姓名 HEAD VARCHAR2(20) NOT NULL, -- 头像路径 BIRTHDAY DATE NOT NULL, -- 出生日期 REGTIME DATE NOT NULL, -- 注册时间 SEX NUMBER(4), -- 性别,0/1 INTERGAL NUMBER(4), -- 积分,驱动等级变化 CONSTRAINT PK_USERS PRIMARY KEY (USERID) ); -- 序列:每次插入前取 nextval 当主键 CREATE SEQUENCE SEQ_USERS START WITH 1 INCREMENT BY 1 NOCACHE; -- 主贴表:注意回复数、点击数默认给 0,避免页面显示 null CREATE TABLE topic ( TOPICID NUMBER(8) NOT NULL, TITLE VARCHAR2(255) NOT NULL, USERID NUMBER(8) NOT NULL, BOARDID NUMBER(8) NOT NULL, PUBLISHTIME DATE NOT NULL, MODIFYTIME DATE NOT NULL, REPLYCOUNT NUMBER(8) DEFAULT 0 NOT NULL, CLICKCOUNT NUMBER(8) DEFAULT 0 NOT NULL, FONTSIZE NUMBER(4) DEFAULT 12 NOT NULL, FONTCOLOR VARCHAR2(20) DEFAULT '#000000' NOT NULL, BOLD NUMBER(1) DEFAULT 0 NOT NULL, ITALIC NUMBER(1) DEFAULT 0 NOT NULL, UNDERLINE NUMBER(1) DEFAULT 0 NOT NULL, CONSTRAINT PK_TOPIC PRIMARY KEY (TOPICID), CONSTRAINT FK_TOPIC_USER FOREIGN KEY (USERID) REFERENCES users(USERID) );

字段类型的取舍说明一下。文档里TITLE写的是LONG,那是 Oracle 的老大对象类型,现在建表应该换成CLOB或直接VARCHAR2(2000),因为LONG在很多驱动版本上读出来会有兼容问题。NUMBER(1)用来当布尔标记(粗体、斜体、下划线)是这个项目的惯用做法,Oracle 没有原生BOOLEAN列类型,用 0/1 最省事。外键FK_TOPIC_USER是有意加的,它保证了发帖人必须是已注册用户,代价是删用户之前必须先处理他的帖子。

分页是这类网站绕不开的查询。Oracle 没有LIMIT,得靠ROWNUM做两层嵌套:

-- 查询第 2 页,每页 10 条(即第 11 到 20 条) SELECT * FROM ( SELECT A.*, ROWNUM RN FROM ( SELECT topicid, title, userid, replycount, publishtime FROM topic WHERE boardid = ? -- 版块过滤,? 由 DAO 传入 ORDER BY publishtime DESC -- 排序必须在最内层完成 ) A WHERE ROWNUM <= 20 -- 上界:pageSize * pageNo ) WHERE RN > 10; -- 下界:(pageNo - 1) * pageSize

这段 SQL 的关键在于排序必须放在最内层。如果ORDER BY放到外层,ROWNUM在排序之前就已经分配好了,分页出来的结果会重复或者漏数据,这是 Oracle 分页最经典的翻车点。另外ROWNUM <= 20这个条件只能写在内层,写成RN > 10 AND RN <= 20在同层是不生效的,因为RN在WHERE执行时还没生成。

3.3 WebRoot 目录结构与数据源配置

MyEclipse 建 Web 工程默认会生成WebRoot作为站点根目录,部署时它整体被打包成war扔进 Tomcat 的webapps。这个项目的目录结构大致是这样:

WebRoot/ ├── index.jsp # 首页入口,展示最新新闻和推荐动漫 ├── anime/ # 动漫在线模块页面 │ ├── list.jsp # 动漫列表,带分页 │ └── detail.jsp # 单部作品详情与播放 ├── news/ # 动漫新闻模块 ├── chat/ # 动漫杂谈模块 ├── forum/ # 留言本/论坛:主贴列表、发帖、回帖 ├── admin/ # 后台所有管理页 │ ├── login.jsp │ ├── newsManage.jsp │ └── animeManage.jsp ├── images/ # 封面、头像、按钮图 ├── js/ # 表单校验、弹窗、分页跳转 ├── css/ └── WEB-INF/ ├── web.xml # Servlet 映射、欢迎页、过滤器配置 ├── classes/ # 编译后的 .class 与 db.properties └── lib/ # ojdbc14.jar、jstl.jar 等依赖

WEB-INF这个目录是重点,它下面的东西浏览器直接访问不到,只能由服务端转发进去,所以配置文件、类文件、驱动包放这里最安全。驱动包ojdbc14.jar必须放在WEB-INF/lib下,而不是只加进 MyEclipse 的 Build Path——后者只影响编译期,Tomcat 运行时会找不到类。

数据库连接参数一般抽成一个properties文件,避免写死在代码里:

# WEB-INF/classes/db.properties driver=oracle.jdbc.driver.OracleDriver url=jdbc:oracle:thin:@127.0.0.1:1521:orcl user=anime password=anime123

url里的thin表示纯 Java 驱动,不需要装 Oracle 客户端;orcl是数据库实例名,装 XE 版本的话一般叫XE。这三个值和本机实际环境必须完全对上,差一个字符就是连不上。

4. 核心模块实现:登录鉴权、分页搜索、留言本与后台 CRUD

前面把结构铺完了,这里开始上代码。这一段挑四个最关键的模块:实体与 DAO 的写法、登录会话怎么管、分页和模糊搜索怎么拼、后台删除怎么处理外键。每个模块给一段能直接抄的骨架,再说清楚参数和容易改的地方。

4.1 实体 Bean 与 DAO:把 ResultSet 关进方法里

实体 Bean 的写法文档里说得很明确:属性对应字段、无参有参构造、getter/setter、重写equals。以动漫表为例:

public class Anime implements Serializable { private int animeid; // 主键,序列生成,构造时可不传 private String animename; // 作品名 private String animepic; // 封面图相对路径,如 /images/cover/1.jpg public Anime() {} public Anime(String animename, String animepic) { this.animename = animename; this.animepic = animepic; } public int getAnimeid() { return animeid; } public void setAnimeid(int animeid) { this.animeid = animeid; } public String getAnimename() { return animename; } public void setAnimename(String animename) { this.animename = animename; } public String getAnimepic() { return animepic; } public void setAnimepic(String animepic) { this.animepic = animepic; } // 重写 equals:后台批量操作时按主键判断是否是同一条记录 @Override public boolean equals(Object o) { if (this == o) return true; if (!(o instanceof Anime)) return false; return animeid == ((Anime) o).animeid; } }

DAO 的职责是「所有 JDBC 都关在方法内部」。连接、语句、结果集三件套必须成对关闭,否则连接池用不了多久就被耗尽,这是血泪经验:

public class AnimeDao { // 按主键查单条,返回 null 表示不存在,调用方要判空 public Anime findById(int animeid) { Anime anime = null; String sql = "SELECT animeid, animename, animepic FROM anime WHERE animeid = ?"; try (Connection conn = DBUtil.getConnection(); PreparedStatement ps = conn.prepareStatement(sql)) { ps.setInt(1, animeid); try (ResultSet rs = ps.executeQuery()) { if (rs.next()) { anime = new Anime(); anime.setAnimeid(rs.getInt("animeid")); anime.setAnimename(rs.getString("animename")); anime.setAnimepic(rs.getString("animepic")); } } } catch (SQLException e) { e.printStackTrace(); // 课设环境直接打日志,生产要换成日志框架 } return anime; } // 分页查询:pageNo 从 1 开始,pageSize 一般取 10 或 15 public List<Anime> findByPage(int pageNo, int pageSize) { List<Anime> list = new ArrayList<>(); int end = pageNo * pageSize; int start = (pageNo - 1) * pageSize; String sql = "SELECT * FROM (" + " SELECT A.*, ROWNUM RN FROM (" + " SELECT animeid, animename, animepic FROM anime ORDER BY animeid DESC" + " ) A WHERE ROWNUM <= ?" + ") WHERE RN > ?"; try (Connection conn = DBUtil.getConnection(); PreparedStatement ps = conn.prepareStatement(sql)) { ps.setInt(1, end); ps.setInt(2, start); try (ResultSet rs = ps.executeQuery()) { while (rs.next()) { Anime a = new Anime(); a.setAnimeid(rs.getInt("animeid")); a.setAnimename(rs.getString("animename")); a.setAnimepic(rs.getString("animepic")); list.add(a); } } } catch (SQLException e) { e.printStackTrace(); } return list; } }

findById的参数是一个主键,返回单条或null,调用方拿到之后必须判空,否则 JSP 上${anime.animename}会直接抛空指针。findByPage的两个参数pageNo和pageSize决定了end和start的取值,pageNo从 1 开始算,传 0 会得到负数下界,Oracle 会报错,所以 Servlet 层要做一次pageNo < 1 ? 1 : pageNo的兜底。JDK 7 以上的try-with-resources写法能保证连接和语句自动关闭,这比手动finally里关三层要稳。

4.2 登录与会话鉴权:一个过滤器拦住整个后台

登录的逻辑本身很简单:查users表,账号密码对上就往session里塞一个用户对象。真正的关键在于「后台的每个页面都要检查这个 session」,而这件事不能靠每个 JSP 自己写一遍,得用一个 Filter 统一拦。

@WebFilter(urlPatterns = {"/admin/*"}) public class AdminFilter implements Filter { @Override public void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain) throws IOException, ServletException { HttpServletRequest request = (HttpServletRequest) req; HttpServletResponse response = (HttpServletResponse) resp; HttpSession session = request.getSession(false); // false:不存在就不新建 Object admin = session == null ? null : session.getAttribute("admin"); if (admin == null) { // 没登录,直接打回登录页,避免后台页面被直接输地址访问 response.sendRedirect(request.getContextPath() + "/admin/login.jsp"); return; } chain.doFilter(req, resp); // 已登录,放行 } @Override public void init(FilterConfig config) {} @Override public void destroy() {} }

这段代码里有两个参数值得说。getSession(false)的false表示「不存在就不要新建」,如果写成true,每个未登录的请求都会创建一个空 session,服务端内存会被白白占掉。request.getContextPath()拿到的是应用名,比如/anime,拼在跳转地址前面,这样部署到任何路径下都不会 404。登录成功的 Servlet 里对应写session.setAttribute("admin", user)和session.setMaxInactiveInterval(30 * 60),后者是 30 分钟超时,后台操作慢的时候不至于刚写完一个表单就被踢出去。

前台论坛的鉴权要松一些:浏览帖子不用登录,只有发帖、回帖才检查。这种场景一般不在 Filter 里拦,而是在发帖的 Servlet 里判断session.getAttribute("user"),为空就跳登录页并带上返回地址。

4.3 分页与模糊查询:Oracle rownum 的两层嵌套

分页 SQL 在 3.2 已经给过,这里补上「带模糊搜索的分页」这个更贴近实际的变体。后台的新闻管理、动漫管理基本都要支持按标题搜,这时候WHERE条件要同时出现在计数 SQL和取数 SQL里,两边漏一个就会出现「总共 3 页,翻到第 2 页是空的」这种怪现象。

// 先查总数,用于算总页数 public int countByKeyword(String keyword) { String sql = "SELECT COUNT(*) FROM anime WHERE animename LIKE ?"; try (Connection conn = DBUtil.getConnection(); PreparedStatement ps = conn.prepareStatement(sql)) { ps.setString(1, "%" + keyword + "%"); // 前后都加 %,实现包含匹配 try (ResultSet rs = ps.executeQuery()) { return rs.next() ? rs.getInt(1) : 0; } } catch (SQLException e) { e.printStackTrace(); return 0; } } // 再查当前页数据,条件必须和 countByKeyword 完全一致 public List<Anime> searchByPage(String keyword, int pageNo, int pageSize) { List<Anime> list = new ArrayList<>(); int end = pageNo * pageSize; int start = (pageNo - 1) * pageSize; String sql = "SELECT * FROM (" + " SELECT A.*, ROWNUM RN FROM (" + " SELECT animeid, animename, animepic FROM anime" + " WHERE animename LIKE ? ORDER BY animeid DESC" + " ) A WHERE ROWNUM <= ?" + ") WHERE RN > ?"; try (Connection conn = DBUtil.getConnection(); PreparedStatement ps = conn.prepareStatement(sql)) { ps.setString(1, "%" + keyword + "%"); ps.setInt(2, end); ps.setInt(3, start); try (ResultSet rs = ps.executeQuery()) { while (rs.next()) { Anime a = new Anime(); a.setAnimeid(rs.getInt("animeid")); a.setAnimename(rs.getString("animename")); a.setAnimepic(rs.getString("animepic")); list.add(a); } } } catch (SQLException e) { e.printStackTrace(); } return list; }

keyword参数在外面拼%的时候要注意,如果用户输入的就是%或_,会变成通配符,搜出全表。稳妥的做法是在 Servlet 层把%、_、\这三个字符转义掉,或者干脆限制搜索框只允许中文、字母和数字。pageNo和pageSize的乘积end用int存,页数上百、每页几十条的时候还在安全范围内,但要真做到几万条,就得换成long并考虑游标分页了。

4.4 后台增删改查与级联删除的事务边界

后台最典型的操作是「删除一部动漫」。看起来一条DELETE FROM anime WHERE animeid = ?就完事,实际上catoon、film、catoonpic都通过外键指向它,直接删会报「违反完整性约束」。文档第五章提到的方法是「先删子表数据,再删主表数据」,这个顺序必须包在一个事务里,否则删到一半失败,数据就烂了。

// 删除动漫及其所有关联内容,任何一步失败整体回滚 public boolean deleteAnimeCascade(int animeid) { String delPic = "DELETE FROM catoonpic WHERE catoonid IN " + "(SELECT catoonid FROM catoon WHERE animeid = ?)"; String delCatoon = "DELETE FROM catoon WHERE animeid = ?"; String delFilm = "DELETE FROM film WHERE animeid = ?"; String delAnime = "DELETE FROM anime WHERE animeid = ?"; Connection conn = null; try { conn = DBUtil.getConnection(); conn.setAutoCommit(false); // 关闭自动提交,开启事务 try (PreparedStatement ps = conn.prepareStatement(delPic)) { ps.setInt(1, animeid); ps.executeUpdate(); } try (PreparedStatement ps = conn.prepareStatement(delCatoon)) { ps.setInt(1, animeid); ps.executeUpdate(); } try (PreparedStatement ps = conn.prepareStatement(delFilm)) { ps.setInt(1, animeid); ps.executeUpdate(); } try (PreparedStatement ps = conn.prepareStatement(delAnime)) { ps.setInt(1, animeid); ps.executeUpdate(); } conn.commit(); // 四步都成功才提交 return true; } catch (SQLException e) { if (conn != null) { try { conn.rollback(); } catch (SQLException ex) { ex.printStackTrace(); } } e.printStackTrace(); return false; } finally { if (conn != null) { try { conn.setAutoCommit(true); conn.close(); } catch (SQLException ex) { ex.printStackTrace(); } } } }

四个DELETE的顺序不能换:catoonpic依赖catoon,catoon和film依赖anime,从叶子往根删。setAutoCommit(false)是这一切的前提,不关自动提交的话每条语句独立生效,中间失败就没法回滚了。finally里恢复autoCommit再关闭连接,是为了让连接回到池里时状态是干净的,否则下一个拿到这条连接的人会莫名其妙地处于事务中。

5. 常见问题排查:这套老技术栈上最容易翻车的 5 个点

这套组合的时间跨度大,环境、驱动、字符集、SQL 四个层面的坑几乎都会踩一遍。下面这 5 条是我按发生频率排的,每条都按「现象 → 原因 → 解决」写,对着排查能省不少时间。

5.1 环境与连接类问题

第一条:启动就报ClassNotFoundException: oracle.jdbc.driver.OracleDriver。现象是 Tomcat 启动正常,一访问需要查库的页面就抛异常,控制台堆栈第一行就是这个类找不到。原因几乎都是驱动包只加进了 MyEclipse 的 Build Path,没放进WebRoot/WEB-INF/lib。Build Path 只影响编译,Tomcat 运行时有自己的类加载器,只认WEB-INF/lib和容器的lib目录。解决办法是把ojdbc14.jar复制一份到WEB-INF/lib下,重新部署,如果还报错就检查包名——Oracle 10g 之后驱动类名也可以是oracle.jdbc.OracleDriver,配置里的driver值要跟 jar 里的实际类名对上。

第二条:页面中文全是问号或者乱码。现象是数据库里存进去的中文,在页面上显示成???,或者反过来,页面提交的中文存进库里变成乱码。原因有三个层次,得逐层排:JSP 页面的pageEncoding和contentType里的charset要一致写成UTF-8;Servlet 里读参数前要request.setCharacterEncoding("UTF-8");Oracle 数据库的字符集如果是WE8ISO8859P1这种单字节集,那前两步做得再好也存不了中文,只能建库时选AL32UTF8或ZHS16GBK。排查顺序建议从页面往下走,先在 JSP 上打一个固定中文看显示是否正常,正常了再查请求编码,最后查库。

第三条:照着文档 2.7 节去配 JSP 环境,结果越配越乱。现象是文档里写「JSP 是一套服务器端脚本环境,内含于 IIS 之中」,有人真去装 IIS 配 JSP,怎么都跑不起来。原因是这份资料的这部分描述串了——那段话描述的实际是另一种服务端脚本技术,不是 JSP。JSP 的全称是 JavaServer Pages,跑在 Tomcat 这类 Servlet 容器里,跟 IIS 没有关系。同样的串抄还出现在 2.4 节,把 Oracle 描述成「基于 Windows 的桌面关系数据库管理系统」,那其实是桌面小型库的特征。遇到文档里的技术描述和常识冲突,以官方文档和实际能跑通为准,别硬按文字配。

5.2 数据与页面类问题

第四条:分页翻到第二页,数据和第一页重复,或者少了几条。现象是列表页第一页显示 1 到 10 条,点下一页还是这几条,或者直接跳到 21 条。原因基本都是ORDER BY写在了ROWNUM的外层,导致行号在排序前就分配完了。Oracle 的分页必须严格按「最内层排序 → 中层ROWNUM <= 上界→ 外层RN > 下界」三层结构写。另一个变体是count语句和取数语句的WHERE条件不一致,比如取数加了boardid过滤而计数没加,总页数就会算多,翻到后面全是空页。改的时候把两条 SQL 并排贴在一起逐字对条件,基本一眼就能看出来。

第五条:删完一部动漫,前台列表还在,点进去 500。现象是后台提示删除成功,前台列表刷新后那条记录没了,但直接访问详情页地址还能打开,或者论坛里引用它的帖子点开报错。原因是只删了anime主表,catoon、film这些子表还留着孤儿数据,或者反过来,子表删了主表没删,前台按animeid查不到主记录。这类问题必须用事务包起来按叶子到根的顺序删,也就是 4.4 那段代码的思路。另外后台的「删除」按钮最好加一个二次确认,课设演示的时候手滑点错,恢复数据比重新录入还麻烦,这一点做过的人都懂。

6. 二次开发:把它改成能维护的现代动漫站

拿到这套代码想继续往下做,我的建议是别一上来就重构,先按「跑通 → 收口 → 替换」三步走。第一步保证原始版本能在本机跑起来,登录、发帖、后台删一条数据这三个动作全部走通,这是基线。第二步把散落在 JSP 里的 Java 代码收进 Servlet,页面只留 EL 和 JSTL,这一步做完,后面换前端模板才不会牵一发动全身。第三步才是替换基础设施,把DBUtil里的DriverManager换成连接池,把 Oracle 换成 MySQL 或 PostgreSQL,把明文密码换成加盐哈希。

替换数据库的时候,改动集中在三个地方,可以用下面这张表对照着改:

原实现替换目标需要动的地方
Oracle 序列 +nextvalMySQLAUTO_INCREMENT建表 SQL、所有INSERT语句、实体 Bean 的主键赋值
ROWNUM三层分页LIMIT offset, sizeDAO 里所有分页方法,SQL 从三层压成一层
ojdbc14.jarmysql-connector-javaWEB-INF/lib、db.properties的 driver 和 url
VARCHAR2、NUMBERVARCHAR、INT全部建表脚本、实体 Bean 的字段类型

连接池的引入是最划算的一步改动。原来每次查询都新建连接、用完关闭,一个列表页要发三四条 SQL,高并发下连接数直接打满。换成连接池之后,DBUtil.getConnection()的签名不用变,内部从DataSource取就行,DAO 层一行都不用动:

public class DBUtil { private static DataSource ds; static { try { Properties p = new Properties(); // 配置文件从类路径读,部署后位于 WEB-INF/classes 下 p.load(DBUtil.class.getClassLoader().getResourceAsStream("db.properties")); ComboPooledDataSource cpds = new ComboPooledDataSource(); cpds.setDriverClass(p.getProperty("driver")); cpds.setJdbcUrl(p.getProperty("url")); cpds.setUser(p.getProperty("user")); cpds.setPassword(p.getProperty("password")); cpds.setMaxPoolSize(20); // 最大连接数,按 Tomcat 线程数调 cpds.setMinPoolSize(5); // 最小空闲连接,避免冷启动慢 ds = cpds; } catch (Exception e) { throw new ExceptionInInitializerError(e); } } public static Connection getConnection() throws SQLException { return ds.getConnection(); // 从池里借,close() 时归还而不是真关 } }

maxPoolSize要和 Tomcat 的maxThreads对得上,池子比线程数小太多,请求就会在借连接那一步排队;大太多又会把数据库的连接数吃满。minPoolSize保证有一定的预热连接,页面首次访问不至于卡在建立连接上。这段改完,DAO 层完全无感,这是这套分层结构最值得肯定的地方。

验证改造是否成功,我的习惯是固定跑一条链路:后台登录 → 新增一部动漫 → 前台列表能看到 → 点进详情页封面正常 → 删除它 → 前台列表消失且不报 500。这条链路走通,说明权限、CRUD、外键、页面渲染四块都没问题。从那以后我每次拿到这种年份久远的老项目,都先把驱动包和字符集这两件事钉死,再动任何一行业务代码——因为九成的「跑不起来」都出在这两处,剩下的才是逻辑问题。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询