
简介这份《动漫网站的设计与实现》毕业论文文档面向计算机相关专业学生与网站开发初学者围绕动漫类网站从需求分析到系统测试的完整开发流程展开可作为课程设计、毕业设计及小型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?animeid1024接着发生的事大致是这么几步浏览器把 HTTP 请求发给 Tomcat 的 8080 端口Tomcat 按web.xml里的映射找到对应的 Servlet 或 JSP。Servlet 从request.getParameter(animeid)拿到参数做一次类型转换和空值判断。调用 DAO 层的findById(int id)DAO 用 JDBC 向 Oracle 发一条select。ResultSet被逐行读出来塞进一个Anime实体 Bean再放进request作用域。请求转发到animeDetail.jsp页面把 Bean 的属性输出成 HTMLTomcat 把这段 HTML 回给浏览器。这条链路里有两个容易忽略的点。一是参数永远不可信animeid可能是空、可能是abc、可能是1 or 11所以 Servlet 里必须做转换和校验DAO 里必须用PreparedStatement。二是作用域要选对详情页用request就够因为一次请求就结束而登录后的用户信息必须放session否则翻个页面就丢了。这个项目里前台列表页、后台管理页大量依赖session判断身份作用域选错就会出现「明明登录了点进后台又被踢出来」这种问题。还有一层是 B/S 结构本身带来的约束。客户端只有浏览器不装任何东西所有状态都保存在服务端这意味着每次请求都是「无状态」的服务端只能靠session或cookie认出你。这个项目里后台权限判断、论坛发帖人身份、积分等级展示全部建立在这一个前提上。2.2 工具与版本矩阵哪些版本能凑到一起这份文档给的是 2012 年前后的组合具体版本和职责对应关系如下表层次选型文档给出的版本在这套系统里的实际作用开发工具MyEclipse8.5JSP/Servlet 编辑、工程管理、一键部署到 TomcatWeb 容器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 建表、序列与分页 SQLOracle 和 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 driveroracle.jdbc.driver.OracleDriver urljdbc:oracle:thin:127.0.0.1:1521:orcl useranime passwordanime123url里的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 ListAnime findByPage(int pageNo, int pageSize) { ListAnime 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 ListAnime searchByPage(String keyword, int pageNo, int pageSize) { ListAnime 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依赖catooncatoon和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-8Servlet 里读参数前要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 和 urlVARCHAR2、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、外键、页面渲染四块都没问题。从那以后我每次拿到这种年份久远的老项目都先把驱动包和字符集这两件事钉死再动任何一行业务代码——因为九成的「跑不起来」都出在这两处剩下的才是逻辑问题。希望帮到你。本文还有配套的精品资源点击获取