简介这是一套面向高校计算机相关专业毕业设计场景的学生作品展示系统完整源码采用Java作为后端开发语言、JSP负责动态页面展示、MySQL承担数据存储适合正在准备毕设或希望练习Java Web三层架构的开发者参考。压缩包共825个文件约11.47MB涵盖jsp页面、java类、js脚本、css样式、html页面、xml配置、jar依赖及数据库脚本等基本覆盖表现层、业务逻辑层与数据访问层的完整结构。系统包含用户注册登录与权限管理、作品上传、分类展示与关键词搜索、评分评论互动以及管理员审核、删除、修改和账号管理等模块可帮助读者理解Java Bean、Servlet与JDBC协同工作的实现方式。目前已有211人学习下载适合作为毕设选题参考、课程设计模板或Web开发入门练手项目便于快速梳理项目目录与核心代码逻辑。1. JavaJSPMySQL 学生作品展示系统从建库到部署一套能跑通的毕设落地路径毕业设计选题里学生作品展示系统是出现频率极高的一类。它不追求算法深度但对 Java Web 全链路能力要求完整数据库设计、后端业务、页面渲染、文件上传、权限控制、部署上线一个都不能少。很多同学卡在“能写单个功能但串不成一个系统”这一步。这篇笔记就围绕 JavaJSPMySQL 这套经典组合把学生作品展示系统从建库、写 DAO、做 JSP 页面到打包部署的完整路径拆开讲。适合正在做计算机毕业设计、需要一套可复现 Java Web 项目的同学也适合想用 JSP 入门 Web 开发、把 Java 基础落到真实项目里的开发者。源码结构我会按常见做法给出你照着改就能跑。2. 数据库先立住作品表、用户表、分类表怎么设计才不返工学生作品展示系统的核心数据就三块谁提交作品、作品属于什么类别、作品本身长什么样。很多同学一上来就写 JSP 页面写到一半发现字段不够、关联对不上回头改库血泪经验。正确顺序是先定表结构再写 Java 实体类最后才是页面。2.1 三张核心表与字段取舍最小可用模型需要三张表user用户、category作品分类、work作品。用户表区分学生和管理员用role字段控制分类表独立出来方便首页按类别筛选作品表通过外键关联用户和分类。CREATE DATABASE student_work_db DEFAULT CHARACTER SET utf8mb4; USE student_work_db; -- 用户表学生和管理员共用role 区分 CREATE TABLE user ( id INT PRIMARY KEY AUTO_INCREMENT, username VARCHAR(50) NOT NULL UNIQUE, password VARCHAR(64) NOT NULL, -- 存 MD5 或 SHA-256不存明文 real_name VARCHAR(50), role TINYINT DEFAULT 0, -- 0 学生 1 管理员 create_time DATETIME DEFAULT CURRENT_TIMESTAMP ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; -- 分类表 CREATE TABLE category ( id INT PRIMARY KEY AUTO_INCREMENT, name VARCHAR(50) NOT NULL ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; -- 作品表外键关联用户和分类 CREATE TABLE work ( id INT PRIMARY KEY AUTO_INCREMENT, title VARCHAR(100) NOT NULL, description TEXT, file_path VARCHAR(255), -- 上传文件的相对路径 cover_img VARCHAR(255), -- 封面图路径 user_id INT NOT NULL, category_id INT, status TINYINT DEFAULT 0, -- 0 待审核 1 已通过 2 驳回 create_time DATETIME DEFAULT CURRENT_TIMESTAMP, CONSTRAINT fk_work_user FOREIGN KEY (user_id) REFERENCES user(id), CONSTRAINT fk_work_category FOREIGN KEY (category_id) REFERENCES category(id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;字段说明几个容易踩的点。password长度给 64 是为了放 SHA-256 十六进制串如果只给 32 后面换加密方式就得改表。status用 TINYINT 而不是布尔因为审核状态未来可能扩展成多级。file_path存相对路径不存绝对路径换服务器时不用批量改数据。字符集统一utf8mb4否则学生姓名里的生僻字或作品标题里的 emoji 会插入失败。2.2 连接池配置与 JDBC 工具类不要在每个 Servlet 里DriverManager.getConnection连接开销大且容易泄漏。常见做法是用 Druid 或 HikariCP。这里给一个基于 Druid 的DBUtil把连接池初始化收口到一个类里。public class DBUtil { private static DataSource dataSource; static { try { Properties props new Properties(); // db.properties 放在 src 根目录打包后位于 classes 下 props.load(DBUtil.class.getClassLoader() .getResourceAsStream(db.properties)); dataSource DruidDataSourceFactory.createDataSource(props); } catch (Exception e) { throw new ExceptionInInitializerError(数据库连接池初始化失败: e.getMessage()); } } public static Connection getConnection() throws SQLException { return dataSource.getConnection(); } // 统一关闭资源顺序ResultSet - PreparedStatement - Connection public static void close(Connection conn, Statement stmt, ResultSet rs) { try { if (rs ! null) rs.close(); } catch (SQLException ignored) {} try { if (stmt ! null) stmt.close(); } catch (SQLException ignored) {} try { if (conn ! null) conn.close(); } catch (SQLException ignored) {} } }db.properties里至少配四项driverClassNamecom.mysql.cj.jdbc.Driver、urljdbc:mysql://localhost:3306/student_work_db?useSSLfalseserverTimezoneAsia/ShanghaicharacterEncodingutf8、username、password。注意 MySQL 8 的驱动类是com.mysql.cj.jdbc.Driver不是老的com.mysql.jdbc.Driver写错会报ClassNotFoundException。serverTimezone不配插入时间可能差 8 小时这是经典玄学问题。2.3 用 DAO 模式隔离 SQL 与业务JSP 里直接写 SQL 是毕设里最常见的翻车点页面和数据库耦合改一个字段要动好几个文件。正确做法是每个实体一个 DAO 类SQL 只出现在 DAO 里。以WorkDao为例public class WorkDao { // 查询已审核通过的作品支持按分类过滤 public ListWork findApproved(Integer categoryId) { ListWork list new ArrayList(); StringBuilder sql new StringBuilder( SELECT w.*, u.real_name, c.name AS category_name FROM work w JOIN user u ON w.user_id u.id LEFT JOIN category c ON w.category_id c.id WHERE w.status 1 ); if (categoryId ! null) sql.append(AND w.category_id ? ); sql.append(ORDER BY w.create_time DESC); try (Connection conn DBUtil.getConnection(); PreparedStatement ps conn.prepareStatement(sql.toString())) { if (categoryId ! null) ps.setInt(1, categoryId); try (ResultSet rs ps.executeQuery()) { while (rs.next()) { Work w new Work(); w.setId(rs.getInt(id)); w.setTitle(rs.getString(title)); w.setDescription(rs.getString(description)); w.setFilePath(rs.getString(file_path)); w.setCoverImg(rs.getString(cover_img)); w.setRealName(rs.getString(real_name)); w.setCategoryName(rs.getString(category_name)); list.add(w); } } } catch (SQLException e) { e.printStackTrace(); } return list; } }这里用JOIN一次查出作者名和分类名避免在 JSP 里循环查库。LEFT JOIN category是因为分类可能为空用内连接会漏数据。try-with-resources自动关连接比手动 close 更安全。参数用PreparedStatement占位符不要字符串拼接否则标题里带单引号就 SQL 报错也是注入风险。3. JSP 页面与 Servlet 分工把展示逻辑写对JSP 的本质是 Servlet但把 Java 代码和 HTML 混在一起写后期维护成本极高。合理分工是Servlet 负责取数据、做判断、转发JSP 只负责用 EL 和 JSTL 把数据渲染出来。这样页面里几乎看不到% %脚本片段。3.1 首页作品列表的 Servlet JSP 写法先写一个IndexServlet处理首页请求把作品列表和分类列表放进 request 域转发到index.jsp。WebServlet(/index) public class IndexServlet extends HttpServlet { private WorkDao workDao new WorkDao(); private CategoryDao categoryDao new CategoryDao(); Override protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { // 分类参数可能为空为空则查全部 String cid req.getParameter(cid); Integer categoryId (cid null || cid.isEmpty()) ? null : Integer.parseInt(cid); req.setAttribute(works, workDao.findApproved(categoryId)); req.setAttribute(categories, categoryDao.findAll()); req.setAttribute(currentCid, categoryId); req.getRequestDispatcher(/index.jsp).forward(req, resp); } }WebServlet注解要求 Servlet 3.0Tomcat 7 以上都支持不用再配web.xml。forward而不是redirect因为要把 request 域数据带到 JSP。如果这里用重定向页面拿不到works列表就是空的。对应的index.jsp用 JSTL 遍历% page contentTypetext/html;charsetUTF-8 % % taglib prefixc urihttp://java.sun.com/jsp/jstl/core % html headtitle学生作品展示/title/head body div classfilter a href${pageContext.request.contextPath}/index全部/a c:forEach items${categories} varcat a href${pageContext.request.contextPath}/index?cid${cat.id}${cat.name}/a /c:forEach /div div classwork-list c:forEach items${works} varw div classcard img src${pageContext.request.contextPath}/${w.coverImg} alt封面/ h3${w.title}/h3 p作者${w.realName} | 分类${w.categoryName}/p p${w.description}/p /div /c:forEach c:if test${empty works} p暂无作品/p /c:if /div /body /html${pageContext.request.contextPath}是拿应用上下文路径部署到 Tomcat 后如果应用名不是 ROOT直接写/index会 404。c:if test${empty works}处理空列表比在 Java 里判断更直观。JSTL 需要在WEB-INF/lib下放jstl.jar和standard.jar只加依赖不放进 libTomcat 启动会报NoClassDefFoundError。3.2 文件上传作品附件与封面图处理学生作品展示系统绕不开文件上传。Servlet 3.0 自带PartAPI不用引第三方包。关键点表单enctype必须是multipart/form-dataServlet 上要加MultipartConfig。WebServlet(/work/upload) MultipartConfig(maxFileSize 10 * 1024 * 1024, // 单文件 10MB maxRequestSize 20 * 1024 * 1024) // 整个请求 20MB public class UploadServlet extends HttpServlet { // 上传目录建议放在应用外部避免重新部署被清空 private static final String UPLOAD_DIR D:/upload/; Override protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { req.setCharacterEncoding(UTF-8); String title req.getParameter(title); String desc req.getParameter(description); Integer categoryId Integer.parseInt(req.getParameter(categoryId)); Integer userId (Integer) req.getSession().getAttribute(userId); Part filePart req.getPart(file); String fileName System.currentTimeMillis() _ filePart.getSubmittedFileName(); filePart.write(UPLOAD_DIR fileName); Work w new Work(); w.setTitle(title); w.setDescription(desc); w.setFilePath(upload/ fileName); w.setUserId(userId); w.setCategoryId(categoryId); new WorkDao().insert(w); resp.sendRedirect(req.getContextPath() /my-works); } }maxFileSize不设上传大文件会抛IllegalStateException。文件名用时间戳前缀避免同名覆盖。filePart.write的路径是绝对路径目录必须提前存在否则报FileNotFoundException。存库时存相对路径upload/xxx页面拼接上下文路径访问。上传目录放应用外部是因为重新部署 war 包时 Tomcat 会清空应用目录学生交的作品就没了这个坑每年都有人踩。3.3 登录与权限过滤器的正确姿势学生和管理员看到的内容不同用 Filter 统一拦截比在每个 Servlet 里判断更可靠。WebFilter(urlPatterns {/admin/*, /work/upload}) public class AuthFilter 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); if (session null || session.getAttribute(userId) null) { response.sendRedirect(request.getContextPath() /login.jsp); return; } // /admin/* 额外要求管理员角色 String uri request.getRequestURI(); if (uri.contains(/admin/)) { Integer role (Integer) session.getAttribute(role); if (role null || role ! 1) { response.sendError(403, 无权限访问); return; } } chain.doFilter(req, resp); } }getSession(false)表示没有会话时不新建避免给未登录用户创建空 session。sendRedirect后必须return否则请求继续往下走过滤器形同虚设。管理员路径判断用uri.contains是简化写法生产环境应该用更精确的匹配。4. 避坑与排查JSPMySQL 项目最容易翻车的五个地方这一章集中说几个高频问题每个都按现象、原因、解决来写。这些问题在本地跑没事一换环境或一部署就冒出来提前知道能省很多时间。4.1 中文乱码页面、请求、数据库三处都要管现象页面显示问号或乱码或者插入数据库后查出来是乱码。原因通常有三层JSP 页面pageEncoding没设、请求体编码没设、数据库连接字符集没设。解决要三处同时改。JSP 顶部写% page contentTypetext/html;charsetUTF-8 pageEncodingUTF-8 %Servlet 里req.setCharacterEncoding(UTF-8)要在取参数之前调用JDBC url 加characterEncodingutf8建库建表用utf8mb4。只改一处往往还是乱码因为链路里任何一环丢了编码都会出问题。4.2 连接池报错驱动类名和时区是重灾区现象启动时报ClassNotFoundException: com.mysql.jdbc.Driver或者报时区错误The server time zone value is unrecognized。原因是 MySQL 8 的驱动类改成了com.mysql.cj.jdbc.Driver老教程还在用旧类名时区问题是因为 url 没指定serverTimezone。解决驱动类名换成com.mysql.cj.jdbc.Driverurl 加serverTimezoneAsia/Shanghai。如果还报 SSL 警告加useSSLfalse本地开发不需要 SSL。4.3 上传文件丢失部署后目录被清空现象本地测试上传正常重新部署 war 包后之前上传的文件全没了。原因是上传目录放在了 webapp 应用目录下Tomcat 重新部署会删除整个应用目录再解压。解决上传目录放到应用外部比如D:/upload/或/data/upload/数据库只存相对路径页面通过一个下载 Servlet 读取外部文件返回。这样重新部署不影响已上传文件。4.4 JSTL 标签不生效jar 包没放进 WEB-INF/lib现象JSP 里c:forEach直接原样输出或者报The absolute uri http://java.sun.com/jsp/jstl/core cannot be resolved。原因是只在 IDE 里加了依赖没有把jstl.jar和standard.jar复制到WEB-INF/lib。Maven 项目要确认 scope 不是provided否则打包时不会进 war。解决检查WEB-INF/lib下是否有这两个 jarMaven 项目把 scope 设为默认或compile。4.5 外键约束导致删除失败现象删除用户或分类时报Cannot delete or update a parent row: a foreign key constraint fails。原因是作品表通过外键引用了用户或分类直接删父表记录会被拒绝。解决有两种一是先删子表记录再删父表适合数据量小的场景二是把外键设为ON DELETE CASCADE删父表时自动删子记录。毕设里推荐第一种逻辑清晰避免误删。如果确实要级联建表时写ON DELETE CASCADE。5. 打包部署与一个提效技巧让系统真正跑起来项目写完最后一步是打包成 war 部署到 Tomcat。IDEA 里用 Maven 的话pom.xml的packaging设为war执行mvn clean packagetarget 下生成 war 包丢进 Tomcat 的webapps目录启动即可。非 Maven 项目用 IDEA 的 Artifacts 功能选 Web Application: Archive输出 war。注意WEB-INF/lib下要有所有运行时依赖缺一个就NoClassDefFoundError。部署后访问路径是http://localhost:8080/应用名/index。如果 404先看 Tomcat 日志有没有启动异常再看应用名对不对。如果 500看catalina.out或 IDEA 控制台堆栈通常是数据库连不上或 SQL 写错。这里给一个提效技巧把常用查询封装成带分页的方法避免首页数据多了以后一次性查全表。分页在 MySQL 里用LIMIT offset, sizeoffset 用(pageNum - 1) * pageSize算。下面是一个通用分页查询public ListWork findByPage(int pageNum, int pageSize, Integer categoryId) { int offset (pageNum - 1) * pageSize; String sql SELECT w.*, u.real_name, c.name AS category_name FROM work w JOIN user u ON w.user_id u.id LEFT JOIN category c ON w.category_id c.id WHERE w.status 1 (categoryId ! null ? AND w.category_id ? : ) ORDER BY w.create_time DESC LIMIT ?, ?; // 后续 setInt 时注意参数顺序categoryId 在前offset 和 pageSize 在后 // ... }参数顺序容易错如果 categoryId 不为空它是第 1 个占位符offset 是第 2 个pageSize 是第 3 个如果为空offset 是第 1 个。写的时候建议用ListObject收集参数再统一 set避免手数占位符。分页总数用SELECT COUNT(*)单独查别用List.size()数据量大时不准也慢。我自己的习惯是每加一个功能先在数据库里把 SQL 跑通再写 DAO最后写 Servlet 和 JSP。顺序反了页面调半天发现是 SQL 写错白费功夫。还有密码一定不要明文存毕设答辩时老师看到明文密码会扣分用MessageDigest做一次 SHA-256 就行不用上复杂的加密框架。希望帮到你。本文还有配套的精品资源点击获取