JavaEE影视创作论坛课程设计:Servlet+JSP+JDBC全链路拆解
简介面向计算机专业课程设计与JavaEE入门进阶学习者的影视创作论坛完整项目包围绕影视资讯、幕后创作心得、观众观影体验与交流讨论等模块展开可用于课程设计选题、毕业设计参考或JavaEE综合实践练习。压缩包共142个文件、约54.95MB以xml配置与rels关系文件为主体配合png、jpeg界面截图与wdp素材图覆盖工程配置、页面资源与图文素材等层面。另含源代码、数据库脚本与论文教程便于按模块对照理解系统结构与实现流程。目前已有488人学习下载。读者可据此获得一套较完整的论坛系统实现方案从需求分析、数据库表设计到前后端交互与页面呈现配合论文文档梳理设计思路与关键技术点适合需要快速搭建项目骨架、补足文档材料或对照学习JavaEE开发流程的同学参考。1. 解压之后先别急着打开 IDE解压一个课程设计压缩包最怕看到的是满屏 JSP 和 Servlet没有 README连数据库有几张表都得从代码里反推。这份基于 JavaEE 的影视创作论坛就属于这一类没有花哨的微服务是一套典型的 Java Web 系统开发骨架——注册登录、按板块发帖、作品展示、评论回复、后台审核功能闭环是完整的。它最适合两类人正在做计算机专业课程设计、需要一个能跑通又改得动的参考工程的同学以及刚学完 Servlet 想把 MVC 串一遍的初学者。下面按我拆包的顺序从选型、表结构、核心链路一路讲到部署和二次开发。2. 技术栈拆解Servlet JSP JDBC 这套“老三样”为什么够用拿到一个陌生工程我第一件事不是运行而是看它用了什么、为什么这么用。影视创作论坛这类内容型站点核心动作只有几个读列表、看详情、写内容、管内容。请求量小、业务规则简单、数据关系清楚这三条决定了它不需要上 Spring Cloud 那一套Servlet 容器加一层薄薄的 MVC 就足够。2.1 选型判断为什么是 JSP 而不是前后端分离这类课程设计工程的典型组合是 JDK 8 Servlet 3.1 JSP 2.3 JSTL 1.2 JDBC MySQL 5.7 Tomcat 8.5/9。前端多半是 Bootstrap 3 加 jQuery页面跳转靠服务端渲染表单同步提交。这么选的理由很实在JSP 可以直接在页面上写${post.title}和c:forEach不用额外起一个 Node 服务也不用配跨域Tomcat 一个进程就把静态资源和接口全包了答辩时插上 U 盘、拷个 war 包就能演示。代价是页面和 Java 代码耦合度高改样式要动 JSP改逻辑要动 Servlet。对课程设计来说这个代价可以接受。真正需要留意的分界点在“内容编辑器”上。影视创作论坛发帖经常要贴剧本、贴分镜说明纯 textarea 体验很差。常见做法是引一个轻量富文本编辑器如 wangEditor 早期版本把 HTML 内容存进content字段展示时用c:out value${post.content} escapeXmlfalse/输出。这一步会顺带引出一个 XSS 问题后面避坑章节会讲。2.2 四个包的分层约定entity / dao / service / servlet工程里的 Java 源码一般按职责分包看清包名基本就看懂了架构包名职责典型类com.forum.entity与表一一对应的 POJOUser、Post、Reply、Boardcom.forum.dao只做 SQL返回实体或集合UserDao、PostDaocom.forum.service业务规则、事务边界、参数校验UserService、PostServicecom.forum.servlet收参数、调 service、转发或重定向LoginServlet、PostListServletcom.forum.filter编码、登录态、权限EncodingFilter、AuthFiltercom.forum.util连接池、分页对象、字符串工具DBUtil、PageBean需要说明的是很多课程设计工程会把 service 层省掉Servlet 里直接 new 一个 Dao 就开始查库。这种写法短期省事一旦要加“发帖同时给用户加积分”这类跨表操作事务就没地方放了。我在拆这类项目时倾向于至少在帖子发布、回复、删除这三处保留 service 层其余查询可以让 Servlet 直连 Dao不必为了分层而分层。2.3 依赖与环境把 jar 包和版本先对齐工程一般不是 Maven 结构WEB-INF/lib下直接躺着 jar。要跑起来先确认这几样JDK 1.8Servlet 3.1 与 Tomcat 9 的组合最稳JDK 17 会因为模块化限制在编译期报javax找不到Tomcat 8.5 或 9.0不要用 Tomcat 10它的包名从javax.servlet改成了jakarta.servlet整份代码会全红MySQL 5.75.8/8.0 也能用但驱动类名和连接串要改mysql-connector-java-5.1.x.jar、jstl-1.2.jar、standard-1.1.2.jar这三个缺一个页面就会抛 500数据源不引连接池也能跑但每次请求都DriverManager.getConnection在演示时明显卡顿。我一般会写一个静态工具类把驱动加载和连接获取封起来package com.forum.util; import java.io.InputStream; import java.sql.*; import java.util.Properties; public class DBUtil { private static String url; private static String username; private static String password; // 类加载时读一次配置避免每次取连接都读文件 static { try (InputStream in DBUtil.class.getClassLoader() .getResourceAsStream(jdbc.properties)) { Properties p new Properties(); p.load(in); Class.forName(p.getProperty(jdbc.driver)); url p.getProperty(jdbc.url); username p.getProperty(jdbc.username); password p.getProperty(jdbc.password); } catch (Exception e) { // 静态块里抛异常必须包成 Error否则会被吞掉 throw new ExceptionInInitializerError(数据源初始化失败: e.getMessage()); } } public static Connection getConn() throws SQLException { return DriverManager.getConnection(url, username, password); } public static void close(ResultSet rs, Statement st, Connection conn) { // 关闭顺序与打开顺序相反且每个都要单独 try前一个抛异常不能影响后面的 try { if (rs ! null) rs.close(); } catch (SQLException ignored) {} try { if (st ! null) st.close(); } catch (SQLException ignored) {} try { if (conn ! null) conn.close(); } catch (SQLException ignored) {} } }配套的jdbc.properties放在src根目录编译后会自动落到WEB-INF/classes下jdbc.drivercom.mysql.jdbc.Driver jdbc.urljdbc:mysql://127.0.0.1:3306/film_forum?useUnicodetruecharacterEncodingutf8useSSLfalse jdbc.usernameroot jdbc.password123456这里有两个参数值得单独说。useUnicodetruecharacterEncodingutf8少了中文标题入库就是问号useSSLfalse少了MySQL 5.7 以上会在控制台刷一堆警告虽然不影响功能但演示时不好看。驱动类名在 MySQL 8 的驱动里要改成com.mysql.cj.jdbc.Driver连接串再加serverTimezoneAsia/Shanghai否则时间字段会差 8 小时——这是我从一个“发帖时间总是显示凌晨”的 bug 里换来的经验。3. 工程结构与数据表5 张表 4 个包撑起整个论坛搞清楚分层之后下一步是把“代码长在哪”和“数据存成什么样”对齐。论坛类系统的表设计有个共同点实体少、关系清楚、读多写少。这份工程的核心就是五张表。3.1 目录结构从 webapp 往下看典型的 Eclipse 动态 Web 工程结构如下如果你拿到的是 IDEA 工程只是.idea和src/main的差异业务代码位置一致路径内容src/com/forum/entityUser.java、Board.java、Post.java、Reply.javasrc/com/forum/daoUserDao.java、PostDao.java、ReplyDao.javasrc/com/forum/serviceUserService.java、PostService.javasrc/com/forum/servletLoginServlet.java、RegisterServlet.java、PostListServlet.java、PostDetailServlet.java、PostAddServlet.javasrc/com/forum/filterEncodingFilter.java、AuthFilter.javaWebContent/jspindex.jsp、login.jsp、post_list.jsp、post_detail.jsp、post_add.jspWebContent/adminboard_manage.jsp、post_audit.jspWebContent/staticcss/、js/、images/含用户上传的作品封面WebContent/WEB-INFweb.xml、lib/、classes/web.xml里除了 welcome-file主要就是 Filter 和 Servlet 的映射。如果工程用的是 Servlet 3.0 的WebServlet注解web.xml会很干净只留一个EncodingFilter两种写法都常见看web.xml的metadata-complete是不是true就能判断。3.2 五张表的关系与建表语句用户发帖、帖子归属板块、帖子下面挂回复、管理员改状态这四条关系决定了表结构。下面是我按这类工程常见字段整理的建表脚本字段名与实体类的属性一一对应CREATE DATABASE IF NOT EXISTS film_forum DEFAULT CHARSET utf8mb4; USE film_forum; -- 用户表role 区分普通会员(0)和管理员(1)status 用于封禁 CREATE TABLE t_user ( id INT PRIMARY KEY AUTO_INCREMENT, username VARCHAR(32) NOT NULL UNIQUE, password VARCHAR(64) NOT NULL, -- 存 MD5 摘要不存明文 nickname VARCHAR(32), email VARCHAR(64), avatar VARCHAR(255), role TINYINT DEFAULT 0, status TINYINT DEFAULT 1, reg_time DATETIME DEFAULT CURRENT_TIMESTAMP ); -- 板块表影视创作论坛按内容分区如剧本、短片、影评、器材 CREATE TABLE t_board ( id INT PRIMARY KEY AUTO_INCREMENT, name VARCHAR(32) NOT NULL, description VARCHAR(255), sort INT DEFAULT 0, post_count INT DEFAULT 0 ); -- 帖子表is_top 置顶、is_essence 加精、status 控制是否已审核 CREATE TABLE t_post ( id INT PRIMARY KEY AUTO_INCREMENT, board_id INT NOT NULL, user_id INT NOT NULL, title VARCHAR(128) NOT NULL, content TEXT, cover VARCHAR(255), -- 作品封面图路径 views INT DEFAULT 0, reply_count INT DEFAULT 0, is_top TINYINT DEFAULT 0, is_essence TINYINT DEFAULT 0, status TINYINT DEFAULT 1, -- 0 待审核 1 已发布 2 已删除 create_time DATETIME DEFAULT CURRENT_TIMESTAMP, KEY idx_board_time (board_id, create_time) ); -- 回复表parent_id 支持楼中楼不需要就把该字段留空 CREATE TABLE t_reply ( id INT PRIMARY KEY AUTO_INCREMENT, post_id INT NOT NULL, user_id INT NOT NULL, content VARCHAR(1000) NOT NULL, parent_id INT DEFAULT 0, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, KEY idx_post (post_id, create_time) );几点设计上的取舍值得说清楚。t_post上的idx_board_time联合索引是为板块列表页准备的列表查询永远是where board_id? order by is_top desc, create_time desc走这个索引能避免全表扫描。reply_count和post_count是冗余字段每次回复时update ... set reply_count reply_count 1读列表时就不用count(*)子查询了——这是论坛类系统最划算的一处反范式。status字段是审核功能的关键。影视创作论坛里用户上传的是原创剧本、短片链接管理员多半要过一眼再放出来。默认值设成 0待审核还是 1直接发布取决于课程设计要不要演示审核流程。如果答辩时需要展示“后台审核”这个功能点就把它设成 0前台列表查询统一加and status 1。3.3 初始化数据与首次导入脚本一般随工程一起放在db/film_forum.sql用命令行导入最快# 登录 MySQL 并执行脚本注意路径换成你解压后的实际位置 mysql -uroot -p123456 --default-character-setutf8mb4 db/film_forum.sql # 验证应该能看到 5 张表和至少一条管理员账号 mysql -uroot -p123456 -e use film_forum; show tables; select id,username,role from t_user;初始化数据至少要有一个管理员账号role1和四个板块剧本创作、短片分享、影评杂谈、器材交流。如果脚本里没写手工补一条INSERT INTO t_user(username, password, nickname, role) VALUES (admin, MD5(123456), 管理员, 1);密码用MD5()函数直接生成登录时 Java 侧对用户输入做同样的摘要再比对。注意MD5 在今天不算安全方案但课程设计里够用如果这是要真正上线的项目换成 BCrypt 并加盐改动点只在UserService.login()一处。4. 核心链路一个帖子列表请求从 Filter 走到 JSP 的 5 个节点功能跑通与否看一条完整链路就够。我选“打开某个板块的帖子列表”这条路径因为它串起了编码过滤、登录校验、分页查询、JSP 渲染四个环节几乎覆盖了这个工程的全部技术要点。4.1 第一站EncodingFilter 解决中文乱码所有请求先进编码过滤器。这一步放在最前面是因为 POST 表单里的中文一旦在getParameter之前没设编码后面怎么补救都没用package com.forum.filter; import javax.servlet.*; import javax.servlet.annotation.WebFilter; import java.io.IOException; WebFilter(/*) public class EncodingFilter implements Filter { Override public void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain) throws IOException, ServletException { // 请求体编码只对 POST 有效GET 的编码由 Tomcat 的 URIEncoding 决定 req.setCharacterEncoding(UTF-8); resp.setContentType(text/html;charsetUTF-8); chain.doFilter(req, resp); } }req.setCharacterEncoding只影响请求体GET 请求的参数在 Tomcat 8 之后默认按 UTF-8 解析但如果拿到的是 Tomcat 7需要在server.xml的 Connector 上加URIEncodingUTF-8。这就是为什么同样的代码在别人机器上好好的在你这里搜索关键词全是问号。4.2 第二站AuthFilter 判断哪些路径要登录论坛的规则是列表和详情页游客可看发帖、回复、后台必须登录。这个判断写在一个 Filter 里比在每个 Servlet 里写if (session.getAttribute(user) null)要干净得多WebFilter(/*) public class AuthFilter implements Filter { // 白名单静态资源、登录注册、帖子浏览不拦 private static final String[] WHITE { /login, /register, /post/list, /post/detail, /static/, /jsp/login.jsp, /jsp/register.jsp }; Override public void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain) throws IOException, ServletException { HttpServletRequest request (HttpServletRequest) req; HttpServletResponse response (HttpServletResponse) resp; String uri request.getRequestURI().substring(request.getContextPath().length()); for (String w : WHITE) { if (uri.startsWith(w)) { chain.doFilter(req, resp); return; } } // 后台路径额外校验角色普通会员访问 /admin/ 一律打回首页 Object user request.getSession().getAttribute(loginUser); if (user null) { response.sendRedirect(request.getContextPath() /jsp/login.jsp); return; } if (uri.startsWith(/admin/) ((com.forum.entity.User) user).getRole() ! 1) { response.sendRedirect(request.getContextPath() /post/list); return; } chain.doFilter(req, resp); } }两个 Filter 的执行顺序由web.xml里filter-mapping的声明顺序或注解的类名字母序决定。注意如果AuthFilter排在EncodingFilter前面登录失败跳转时写入 session 的提示语会是乱码。稳妥做法是在web.xml里显式声明把编码过滤器放第一个。4.3 第三站PostListServlet 收参数列表页要处理三个参数板块 id、页码、可选的关键词。Servlet 的职责很薄——解析参数、调 service、把结果塞进 request、转发 JSPWebServlet(/post/list) public class PostListServlet extends HttpServlet { private final PostService postService new PostService(); Override protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { // 参数缺失时给默认值避免 NumberFormatException 直接把页面打崩 int boardId parseInt(req.getParameter(boardId), 0); int pageNo parseInt(req.getParameter(page), 1); String kw req.getParameter(kw); PageBeanPost page postService.listByBoard(boardId, kw, pageNo, 10); req.setAttribute(page, page); req.setAttribute(boards, postService.listAllBoards()); req.getRequestDispatcher(/jsp/post_list.jsp).forward(req, resp); } private int parseInt(String s, int def) { try { return Integer.parseInt(s); } catch (Exception e) { return def; } } }parseInt这个私有方法看着不起眼却是很多工程翻车的地方——地址栏被人手改成?pageabc服务端直接 500答辩现场很尴尬。给默认值是最省事的防御。4.4 第四站分页查询与 PageBean分页是论坛列表的核心。两条 SQL一条count拿总数算总页数一条limit拿当前页数据。封装成一个通用PageBean后所有列表页都能复用public class PageBeanT { private int pageNo; // 当前页从 1 开始 private int pageSize; // 每页条数 private int total; // 总记录数 private ListT list; // 当前页数据 public int getTotalPage() { return total % pageSize 0 ? total / pageSize : total / pageSize 1; } public boolean isHasPrev() { return pageNo 1; } public boolean isHasNext() { return pageNo getTotalPage(); } // getter / setter 省略 }对应的 DAO 方法注意limit的两个参数是偏移量和条数偏移量从 0 开始public PageBeanPost listByBoard(int boardId, String kw, int pageNo, int pageSize) { PageBeanPost page new PageBean(); page.setPageNo(pageNo); page.setPageSize(pageSize); StringBuilder where new StringBuilder( where status 1 ); ListObject args new ArrayList(); if (boardId 0) { where.append( and board_id ? ); args.add(boardId); } if (kw ! null !kw.trim().isEmpty()) { where.append( and title like ? ); args.add(% kw.trim() %); } String countSql select count(*) from t_post where; String dataSql select * from t_post where order by is_top desc, create_time desc limit ?, ?; // ... 执行 countSql 得到 total再补两个分页参数执行 dataSql映射成 ListPost return page; }这里拼接的是where条件串参数一律走?占位符。不要把kw直接拼进 SQL—— or 11 --这种输入会让整个帖子库裸奔是课程设计里最常见的安全扣分项。另外order by is_top desc, create_time desc的顺序不能反否则置顶帖会被新帖淹没。4.5 第五站JSP 渲染与分页条数据到了 JSP用 JSTL 遍历即可不需要写 Java 脚本片段c:forEach items${page.list} varpost div classpost-item a href${pageContext.request.contextPath}/post/detail?id${post.id}${post.title}/a span${post.nickname} · fmt:formatDate value${post.createTime} patternyyyy-MM-dd HH:mm//span span浏览 ${post.views} / 回复 ${post.replyCount}/span /div /c:forEach %-- 分页条上一页/下一页用 PageBean 的布尔方法控制避免页码越界 --% c:if test${page.hasPrev} a href?boardId${param.boardId}page${page.pageNo - 1}上一页/a /c:if c:if test${page.hasNext} a href?boardId${param.boardId}page${page.pageNo 1}下一页/a /c:iffmt:formatDate需要页面顶部% taglib prefixfmt urihttp://java.sun.com/jsp/jstl/fmt %少这一行时间会直接打印成Tue Mar 12 10:20:31 CST 2024这种 Java 默认格式。列表页做完详情页、发帖页、回复页基本是同一套模式复制理解了这条链路其余页面看一眼就懂。5. 部署与避坑从解压到首页打开要过的六道关代码看懂不等于能跑起来。这一章是我每次拿到类似工程必定会走的部署顺序以及几个最容易翻车的地方。5.1 六步部署流程# 1. 建库导数据脚本在 db 目录下 mysql -uroot -p --default-character-setutf8mb4 db/film_forum.sql # 2. 改 jdbc.properties 里的账号密码务必和本机一致 # 3. 确认 WEB-INF/lib 下有 mysql-connector、jstl、standard 三个 jar # 4. IDE 中把工程加入 TomcatDeployment 里确认 context path如 /forum # 5. 启动 Tomcat先看控制台有没有 ClassNotFoundException # 6. 浏览器访问 http://localhost:8080/forum/post/list 验证列表页先访问列表页而不是首页是有讲究的列表页依赖数据库连通性能打开就说明连接串、驱动、建表三件事全对首页往往只有静态内容打开了也证明不了什么。5.2 六个高频翻车点现象一页面报 404控制台无异常。原因多半是WebServlet(/post/list)这类注解没生效工程web.xml的metadata-completetrue会关闭注解扫描。解决办法是把该属性删掉或改成false或者改在web.xml里显式配置servlet-mapping。现象二所有中文变问号或乱码。三个地方要逐个排查连接串有没有characterEncodingutf8、Filter 有没有在getParameter之前执行、数据库和表的字符集是不是utf8mb4。我遇到过一次建库时用了默认的 latin1改连接串怎么都没用最后alter database film_forum character set utf8mb4才解决。现象三登录成功但一刷新就掉线。通常是 JSP 页面里写死了sessionfalse或者 Filter 里用request.getSession()时又 new 了一个 session和登录时存的不是同一个。检查登录 Servlet 里存的是request.getSession().setAttribute(loginUser, user)取的时候也用request.getSession()不要用getSession(true)重复创建。现象四图片上传成功但页面显示裂图。上传目录如果放在工程根目录下的upload/重新部署 war 包时整个目录会被覆盖清空。稳妥做法是把上传路径配到工程外部如D:/forum_upload/再在 Tomcat 里配一个虚拟目录映射或者接受“演示前重新传一次”的妥协。现象五SQL 报Unknown column xxx in field list。实体类加了字段、数据库没加列或者反过来的情况在二次开发时特别常见。这类错误的排查顺序是先看报错里的列名去表结构里desc t_post确认再回实体类对属性名。现象六发帖提交后浏览器提示重复提交表单。刷新页面会重复插数据这是转发和重定向没分清。写操作发帖、回复、删除处理完必须response.sendRedirect()不能forward()。改一行就能解决但很多工程里就是错的。5.3 出问题先看哪三处第一看 Tomcat 控制台的catalina.out或 IDE 控制台异常栈的第一行Caused by通常就是根因第二看浏览器 F12 的 Network请求状态码是 404 还是 500 决定了排查方向完全不同第三看 SQL 日志如果工程没配 log4j最土的办法是在 DAO 方法里临时System.out.println(sql)打一下拼好的语句复制到 MySQL 客户端跑一遍问题立刻现形。这套三板斧用完九成问题都能定位。6. 二次开发在现有骨架上加“作品集 审核流”的两处改法这份工程最值钱的地方不是功能多而是骨架完整、改动面小。我在它上面做过两次扩展一次是加“个人作品集”一次是把审核流从“管理员手动放行”改成“积分自动放行 抽检”两次改动都控制在半天以内。第一处作品集。思路是复用t_post而不是新建表给t_user加works_count冗余字段个人主页直接查select * from t_post where user_id? and status1 order by create_time desc前台加一个/user/works的 Servlet 即可。真正要动的是权限作品集页面的删除按钮必须校验session里的用户 id 与帖子的user_id一致否则改个 URL 参数就能删别人的帖子。这段校验我建议写在 service 里别放在 JSP 用c:if隐藏按钮——隐藏不等于拦截。第二处审核流。原逻辑是status从 0 到 1 由管理员逐条点。改成自动放行的关键是在PostService.add()里加一个分支public int add(Post post, User user) { // 老用户发帖数 3 且未被封禁直接发布新用户进待审核队列 boolean trusted user.getStatus() 1 postDao.countByUser(user.getId()) 3; post.setStatus(trusted ? 1 : 0); int id postDao.insert(post); if (trusted) { // 只有真正发布的帖子才更新板块计数避免待审核内容把统计撑虚 boardDao.incrPostCount(post.getBoardId()); } return id; }改动只有三行但它把“谁能直接发、谁要等审核”这条业务规则从散落在各处的 if 收敛到了一处。验证方法是准备两个账号一个发满三条、一个刚注册分别发帖后查t_post.status能稳定得到 1 和 0 就说明分支正确。还有一处容易被忽略的改造点帖子详情页的浏览量。update t_post set views views 1 where id ?放在每次详情查询里压力不大时没问题但同一个人反复刷新会刷出虚高数字。我一般的做法是用 session 记一个已访问帖子 id 的 Set同一个会话内只加一次代码不到十行演示时数据好看很多。顺便给个改造顺序的建议按依赖关系来最省事顺序改动影响文件1加数据库字段film_forum.sql、对应实体类2改 service 业务规则PostService、UserService3加或改 Servlet 路由web.xml或注解4最后动 JSP页面展示与表单从后往前改必翻车——先改 JSP 再改表结构中间那段时间页面永远报列不存在改一次错一次。我头一回拆这类课程设计图省事直接从 JSP 下手结果字段名对不上、分页参数丢、编码又乱了一遍折腾到凌晨才发现是改的顺序反了。从那以后我每次动这类 JavaEE 老工程都强制先跑通列表页、再动数据库、最后碰页面一步一验证。希望帮到你。本文还有配套的精品资源点击获取