JavaEE影视创作论坛课程设计:Servlet+JSP+JDBC实现注册登录与文件上传 简介基于Java EE的影视创作论坛设计与实现资源包面向计算机专业课程设计、毕业设计及系统开发学习者用于解决论坛类Web项目从需求分析到编码落地的完整流程问题。压缩包共142个文件容量54.95MB文件构成以XML配置与数据文件、JPEG/PNG界面图片、WDP图像资源为主其中XML与rels常用于项目配置及文档关联图片直观展示系统运行界面与设计成果。目前已有487人学习下载资源热度良好。资源涵盖程序源代码、数据库文件与论文教程围绕影视资讯、幕后创作心得、观众观影体验及心得交流等模块展开可帮助理解需求分析、数据库建模、Java EE分层架构和前端页面设计的实际工程方法。数据库脚本包含论坛用户、帖子、评论等核心数据表论文教程说明设计思路与系统测试完整源码配合教程既可直接用于课程设计答辩也可作为影视类社区项目的改造基础。1. 影视创作论坛一个JavaEE课程设计把注册、发帖、上传到权限校验一次讲透“影视创作论坛”不是商业产品它是一份典型得不能再典型的JavaEE课程设计源码Servlet接请求、JSP出页面、JDBC连MySQL从用户注册登录到发布影视创作主题帖、回帖讨论、上传海报剧照最后管理员进后台删帖封号整条链路没有用Spring却五脏俱全。有不少人拿到这种系统开发课设项目第一反应是“这年头谁还写JSP”但恰恰是这种无框架项目能让计算机专业的学生在答辩时把HTTP会话、SQL预编译、文件上传原理讲得清清楚楚。它适合两类人需要课程设计交付物和答辩素材的学生以及想把Servlet JSP这套老链路重新捡起来的人。2. 架构与工程结构MVC分层、五张表设计与war包部署方式2.1 技术选型Servlet JSP JDBC不靠框架也能跑这套项目里没有Struts、没有Spring、更没有Spring Boot顶层是Servlet 3.1规范下的业务控制器视图层是JSP EL JSTL模型层是普通JavaBean。选这套组合不是技术落后而是课程设计阶段需要让每个请求的路径肉眼可见浏览器发来login.doweb.xml把请求映射到LoginServletServlet调UserServiceService再调UserDaoDao用JDBC拼出SQL发给MySQL最后forward到index.jsp。换成Spring之后这条链路会被DispatcherServlet和Autowired藏起来答辩时反而不容易讲清楚。运行环境按JDK 1.8 Tomcat 8.5 MySQL 5.7来组织。Tomcat 8.5对应Servlet 3.1支持注解配置和异步处理但又保留了对web.xml的完整解析不会因为版本太新出现行为变化。MySQL 5.7在课程设计里最常见sql脚本直接导入即可不需要额外改方言。JDK用1.8是因为javax.*包体系稳定Commons-FileUpload这类老库在JDK 8上从未出过坑JDK 11之后反而可能出现模块化导致的反射告警。课程设计阶段我一般不建议上来就套Spring Boot一来Tomcat和JSP的原生机制能帮助你理解HTTP请求-响应模型二来很多课设答辩老师会追问“请求从进入Tomcat到返回用户经历了哪些环节”如果全程是RestController MyBatis-Plus你甚至说不清session是在哪里被创建的这个问题在无框架项目里反而是送分题。2.2 目录结构war包里每一层的位置拿到源码后先把目录摊开看这套工程是标准Eclipse Dynamic Web Project结构打出来是一个war包。我在本地习惯把它拆成两个视角一个看src下按分层建的包另一个看WebContent下按访问路径建的页面目录。src/main/javaEclipse里就是src ├─ com.filmforum.controller # Servlet只做参数接收、调用、跳转 ├─ com.filmforum.service # 业务逻辑层处理事务和校验 ├─ com.filmforum.dao # JDBC数据访问层只写SQL ├─ com.filmforum.entity # 用户、帖子、回复等JavaBean ├─ com.filmforum.filter # 登录校验、管理员拦截、编码处理 ├─ com.filmforum.util # DBUtil、分页工具类 └─ com.filmforum.listener # 启动时初始化数据源、加载分类 WebContent ├─ admin/ # 后台管理页面 ├─ css/ js/ images/ # 静态资源 ├─ upload/ # 上传后的图片落地目录 ├─ WEB-INF/ │ ├─ web.xml # 路由映射、全局参数配置 │ └─ lib/ # mysql驱动、jstl、fileupload等jar ├─ index.jsp ├─ login.jsp register.jsp └─ post_list.jsp post_detail.jspcontroller层只干三件事从request取参数、调用service、选择forward还是redirect。service层处理事务边界和过滤条件比如发帖时把HTML标签和敏感词提前洗掉。dao层只写SQL和参数绑定不再拼业务判断。entity层是纯字段JavaBean对应数据库表和页面表单。util里的DBUtil统一提供Connection这是排查后面“连接泄漏”问题的关键位置。注意WebContent根目录下没有把JSP再包一层而是按访问角色分了admin和普通页面两套。这样做的理由很直接登录页、注册页、帖子列表页都是公开资源不需要进入WEB-INF而后台管理页面在admin目录下用URL前缀区分正好让Filter按照路径匹配做权限控制。如果你看到某一版源码把JSP全部塞进WEB-INF那通常是为了强制走Servlet转发属于另一个风格。2.3 数据库设计五张表撑起内容型社区影视创作论坛的核心业务是内容有人发帖、有人回帖、帖子里要带图片管理员要能管人。基于这个需求需要五张表用户表、帖子分类表、帖子表、回帖表、用户状态记录表。帖子和用户是核心。CREATE TABLE tb_user ( uid INT PRIMARY KEY AUTO_INCREMENT, username VARCHAR(30) NOT NULL UNIQUE, password CHAR(32) NOT NULL, nickname VARCHAR(30), role TINYINT DEFAULT 1 COMMENT 1普通用户 2管理员, status TINYINT DEFAULT 1 COMMENT 1正常 0封禁, created_at DATETIME ); CREATE TABLE tb_category ( cid INT PRIMARY KEY AUTO_INCREMENT, cname VARCHAR(20) NOT NULL ); CREATE TABLE tb_post ( pid INT PRIMARY KEY AUTO_INCREMENT, uid INT NOT NULL, cid INT NOT NULL, title VARCHAR(80) NOT NULL, content TEXT, cover_img VARCHAR(100), view_count INT DEFAULT 0, reply_count INT DEFAULT 0, created_at DATETIME, CONSTRAINT fk_post_user FOREIGN KEY (uid) REFERENCES tb_user(uid), CONSTRAINT fk_post_cate FOREIGN KEY (cid) REFERENCES tb_category(cid) );tb_user里的password设计成CHAR(32)是存MD5摘要课程设计阶段不需要上BCrypt也能演示加密存储。注意加了一个status字段被管理员封禁的用户登录时会在这里被拦下来比直接删用户好调试。帖子表与用户表、分类表用外键关联外键在中小系统里能挡掉一批脏数据但删除用户时得先删帖子或做逻辑禁用我的习惯是用status做逻辑禁用不用物理外键级联删帖否则后台管理很容易误操作。这套表结构背后一个容易被忽略的细节是cover_img冗余字段。影视类帖子在列表页要显示封面图如果不冗余发帖时上传的图片路径就要单独建一张素材表列表页每次都要多一次join。课程设计阶段把封面路径直接存在帖子里查询效率高逻辑也直观以后要扩展“一个帖子多张剧照”再拆一张attachment表也不迟。3. 核心模块拆解注册登录、分页列表与文件上传的实现细节3.1 登录与会话校验从LoginServlet到Filter登录是所有业务入口。先看Servlet侧的核心代码WebServlet(/login) public class LoginServlet extends HttpServlet { protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { req.setCharacterEncoding(UTF-8); String username req.getParameter(username); String password DigestUtils.md5Hex(req.getParameter(password)); UserService userService new UserService(); User user userService.login(username, password); if (user null) { req.setAttribute(error, 用户名或密码错误); req.getRequestDispatcher(/login.jsp).forward(req, resp); return; } if (user.getStatus() 0) { req.setAttribute(error, 账号已被封禁); req.getRequestDispatcher(/login.jsp).forward(req, resp); return; } req.getSession().setAttribute(loginUser, user); resp.sendRedirect(req.getContextPath() /index.jsp); } }这里有两个容易被新手看漏的设计点。一是password在Servlet层就做了一次MD5这意味着UserDao里拿到已经是摘要串不会再出现“数据库存明文、页面上却能直接看到密码”的问题注册时也必须用同一个md5Hex方法处理否则登录永远失败。二是登录成功用的是sendRedirect而不是forward为了防止用户按F5时浏览器重复提交表单这种做法值得沿用。登录之后的会话校验放在Filter里这样可以避免在每个Servlet里重复判断sessionWebFilter(/admin/*) public class AdminAuthFilter implements Filter { 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); User user session null ? null : (User) session.getAttribute(loginUser); if (user null || user.getRole() ! 2) { response.sendRedirect(request.getContextPath() /login.jsp); return; } chain.doFilter(req, resp); } }注意两处关键参数getSession(false)在会话不存在时返回null而不是创建一个新会话这样避免恶意请求频繁制造空sessionrole ! 2的判断直接把普通用户挡在admin目录之外。Filter无法直接拿到HttpSession必须先强转HttpServletRequest这是很多新手照着网上的过滤器模板抄完报ClassCastException的原因。3.2 帖子分页PageBean写法和SQL参数绑定论坛列表页最忌讳一次查全表数据课程设计里分页是必问点。先定义一个PageBean封装分页状态再让Dao返回PageBean而不是Listpublic class PageBean { private int pageNum; // 当前页码 private int pageSize; // 每页条数 private int totalCount; // 总记录数 private int totalPages; // 总页数 private ListPost list; // 当前页数据 }Dao层方法如下public PageBean findPage(int pageNum, int pageSize, int cid) { PageBean pb new PageBean(); pb.setPageNum(pageNum); pb.setPageSize(pageSize); StringBuilder countSql new StringBuilder(SELECT COUNT(*) FROM tb_post); StringBuilder listSql new StringBuilder( SELECT * FROM tb_post ORDER BY created_at DESC LIMIT ?, ?); ListObject args new ArrayList(); if (cid 0) { countSql.append( WHERE cid ?); listSql.insert(listSql.indexOf(ORDER BY), WHERE cid ? ); args.add(cid); } // count查询 pb.setTotalCount(queryCount(countSql.toString(), args)); pb.setTotalPages((pb.getTotalCount() pageSize - 1) / pageSize); // 当前页数据 args.add((pageNum - 1) * pageSize); args.add(pageSize); pb.setList(queryList(listSql.toString(), args)); return pb; }反转页数公式(totalCount pageSize - 1) / pageSize是向上取整避免最后一页数据丢失。偏移量(pageNum - 1) * pageSize从0开始对应SQL里LIMIT ?, ?的第一个参数。所有拼接的条件值都走args列表再绑定到PreparedStatement目的就是防SQL注入。有一点要说明ORDER BY后面的列名和DESC关键字不能作为占位符参数只能写在SQL字符串里所以这里的cid过滤能参数化但排序字段是从代码常量里带入的不能直接拼用户传入的字段名。3.3 影视作品封面上传Commons-FileUpload上传模块是最容易出错的环节。课程设计通常引入Commons-FileUpload组件核心代码精简后是这样boolean isMultipart ServletFileUpload.isMultipartContent(request); if (!isMultipart) { throw new BusinessException(表单必须使用multipart/form-data); } DiskFileItemFactory factory new DiskFileItemFactory(); ServletFileUpload upload new ServletFileUpload(factory); upload.setFileSizeMax(2 * 1024 * 1024); // 单个文件2MB upload.setSizeMax(5 * 1024 * 1024); // 整个请求5MB ListFileItem items upload.parseRequest(request); for (FileItem item : items) { if (item.isFormField()) { // 普通表单字段比如标题、分类id } else { String ext FilenameUtils.getExtension(item.getName()); ListString allow Arrays.asList(jpg, png, webp, jpeg); if (!allow.contains(ext.toLowerCase())) { throw new BusinessException(不支持的图片格式); } String fileName UUID.randomUUID().toString().replace(-, ) . ext; item.write(new File(uploadDir File.separator fileName)); } }setFileSizeMax限制单个文件大小setSizeMax限制整次上传体积单位都是字节两者都要设置否则攻击者可以拆成很多小文件把磁盘占满。扩展名判断不要信任前端传的MimeType前端可以伪造用FilenameUtils.getExtension拿到原始文件名后缀再和允许列表比对。文件名用UUID重命名一是避免中文文件名在Tomcat不同版本下乱码二是防止用户上传一个和服务器已有文件同名的文件把别人的封面覆盖掉。这里要特别提醒一个不兼容问题如果用了Commons-FileUpload的parseRequestServlet上就不需要再加MultipartConfig注解也不需要调用req.getPart()两个方案混用会抛出FileUploadException报错信息显示“the request was rejected because no multipart boundary was found”。两种方案任选其一即可。3.4 发帖与回帖的事务边界发帖的请求处理相对直接把表单字段封装进Post对象调用PostService.save然后重定向到帖子详情。真正值得展开的是回帖这个动作因为它要同时改两张表插入一条回帖记录和更新帖子表的reply_count。Connection conn DBUtil.getConnection(); try { conn.setAutoCommit(false); PreparedStatement ps conn.prepareStatement( INSERT INTO tb_reply(pid, uid, content, created_at) VALUES(?,?,?,NOW())); ps.setInt(1, pid); ps.setInt(2, uid); ps.setString(3, content); ps.executeUpdate(); ps conn.prepareStatement( UPDATE tb_post SET reply_count reply_count 1 WHERE pid ?); ps.setInt(1, pid); ps.executeUpdate(); conn.commit(); } catch (SQLException e) { conn.rollback(); throw e; } finally { conn.setAutoCommit(true); DBUtil.close(conn, ps, null); }事务边界必须放在Service层而不是Dao层因为一个业务动作涉及多条SQL才能称为事务如果把commit写在单个Dao方法里第二个Dao执行失败时第一个Dao已经提交回帖数和回复内容就对不上了。finally里把autoCommit恢复成true也很关键因为连接下一步会归还给连接池上一次事务状态会污染下一个使用者。nodeNum参数把正文里可能存在的