☰
JavaWeb课设实战:Filter+Session权限控制与文件上传避坑指南
2026/10/19 1:34:12 网站建设 项目流程

简介:这是一套基于JavaWeb技术栈开发的作业管理网站完整源码,面向计算机、通信、人工智能等专业的本科生及课程设计指导教师,用于支撑期末大作业、课程设计或毕业设计实践。系统采用JSP+Servlet+Druid连接池+MySQL架构,涵盖用户登录、作业发布、提交、批阅、成绩统计等核心教学管理功能,代码经调试验证可直接运行。压缩包共54个文件,含9个Java业务类、9个JSP页面(如login.jsp、work.jsp、submit.jsp)、7个CSS样式文件、5个JS脚本(含jQuery 3.2.1)、9个JAR依赖库及web.xml、druid.properties等关键配置,整体7.73MB,结构清晰、模块职责分明。已有237人学习下载,资源附带README说明文档,适合作为JavaWeb入门到进阶的典型项目范例,帮助学习者理解MVC分层实现、前后端交互逻辑与数据库操作全流程。

1. 为什么一个“只管交作业”的网站,反而要花三周重写登录态和权限校验?

这不是一个炫技的后台管理系统,而是一个真实压在学生肩上的课设交付压力:某高校《Java Web程序设计》课程要求——两周内完成可运行、可演示、能答辩的作业管理网站。但现实是,90%的学生卡在第三天:登录成功后跳转到 work.jsp,刷新页面就变回未登录;老师账号能删学生作业,学生账号却能点开 submit.jsp 提交后直接看到所有人的提交记录;更玄学的是,用 Chrome 能正常上传文件,换 Firefox 就报400 Bad Request。这套源码之所以答辩拿了95分,不是因为用了多少高大上技术,而是把「学生真正在意的三个痛点」全打穿了:登录不掉线、角色不越权、提交不丢数据。它适合两类人:一类是刚学完 Servlet/JSP、连 Filter 都没写过几行的初学者,需要一份能直接跑起来、改两行就能交差的参考;另一类是已经搭过 Spring Boot 的进阶者,想反向拆解传统 JavaWeb 的权限链路怎么靠纯 Filter + Session + JSTL 拉通。别被“课程设计”四个字骗了——它本质是一份带完整上下文的 JavaWeb 实战切片。


2. 总体架构设计:三层结构与四个核心模块的职责划分

这套系统没有用 Spring MVC 或 Struts,而是回归 JavaWeb 原生三层:表现层(JSP)、控制层(Servlet)、数据访问层(Druid + JDBC)。这种选择不是为了怀旧,而是课程设计场景下的理性妥协:Tomcat 8.5 自带 JSP 容器,无需额外配置依赖;Druid 连接池比 C3P0 更稳定,且druid.properties里明文写的数据库参数,学生调试时一眼就能改;所有业务逻辑都收在com.gdes.web包下,没有跨包调用,避免初学者陷入类路径地狱。

整个系统围绕「作业生命周期」拆出四个模块:

  • 身份认证模块:负责登录验证、Session 绑定、登出清理;
  • 作业发布模块:教师端创建/修改/删除作业,含截止时间校验;
  • 作业提交模块:学生端上传文件、填写说明、查看历史提交;
  • 作业归档模块:按班级/课程/日期聚合数据,供教师导出统计。

提示:所有模块交互不走 AJAX,全部用同步表单提交。这不是技术倒退,而是降低调试复杂度——当submit.jsp提交失败时,你能在浏览器地址栏直接看到?error=upload_failed,而不是在 F12 的 Network 里翻 17 个 XHR 请求。

2.1 身份认证模块:Filter 拦截 + Session 标记 + JSTL 控制视图

登录态管理是整套系统的地基。它不用 Cookie 加密,也不做 Token 解析,而是用最朴素的方式:用户登录成功后,将User对象存入HttpSession,并在header.jsp中用 JSTL 判断:

<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %> <c:choose> <c:when test="${empty sessionScope.user}"> <a href="login.jsp">登录</a> </c:when> <c:otherwise> <span>欢迎,${sessionScope.user.name}!</span> <a href="logout.do">退出</a> </c:otherwise> </c:choose>

关键在于LoginServlet的实现逻辑:

// LoginServlet.java protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { String username = request.getParameter("username"); String password = request.getParameter("password"); // 1. 简单密码校验(实际课设中应替换为 MD5 加盐) User user = userDao.findByUsername(username); if (user != null && user.getPassword().equals(password)) { // 2. 强制清除旧 Session,防止会话固定攻击 HttpSession oldSession = request.getSession(false); if (oldSession != null) oldSession.invalidate(); // 3. 创建新 Session 并绑定用户对象 HttpSession session = request.getSession(true); session.setAttribute("user", user); session.setMaxInactiveInterval(1800); // 30 分钟无操作失效 // 4. 重定向到首页,避免重复提交 response.sendRedirect("index.jsp"); return; } request.setAttribute("error", "用户名或密码错误"); request.getRequestDispatcher("login.jsp").forward(request, response); }

这段代码背后有三层设计意图:第一,session.setMaxInactiveInterval(1800)不是随便写的数字,而是匹配教务系统常见会话策略;第二,oldSession.invalidate()是血泪经验——某次测试发现学生用同一账号在两个浏览器登录,第二个登录会踢掉第一个,但第一个页面刷新后仍显示“已登录”,直到手动清缓存;第三,重定向而非转发,彻底切断表单重复提交链路。

2.2 权限控制模块:基于角色的 URL 拦截与页面级隐藏

权限不是靠注解或 XML 配置,而是用AuthFilter实现硬拦截。它不校验 RBAC 模型,只做两件事:检查 Session 是否存在、检查当前用户角色是否匹配请求路径。

// AuthFilter.java public void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain) throws IOException, ServletException { HttpServletRequest request = (HttpServletRequest) req; HttpServletResponse response = (HttpServletResponse) resp; String path = request.getServletPath(); // 白名单:登录页、静态资源、登出接口 if (path.startsWith("/login.jsp") || path.startsWith("/css/") || path.startsWith("/js/") || path.startsWith("/images/") || path.equals("/logout.do")) { chain.doFilter(req, resp); return; } // 检查 Session HttpSession session = request.getSession(false); if (session == null || session.getAttribute("user") == null) { response.sendRedirect("login.jsp?redirect=" + URLEncoder.encode(path, "UTF-8")); return; } // 角色路由:/teacher/ 开头的路径只允许 teacher 访问 User user = (User) session.getAttribute("user"); if (path.startsWith("/teacher/") && !"teacher".equals(user.getRole())) { response.sendError(HttpServletResponse.SC_FORBIDDEN, "无权访问教师功能"); return; } // 学生不能直接访问 issue.jsp(发布作业页) if (path.equals("/issue.jsp") && !"teacher".equals(user.getRole())) { response.sendError(HttpServletResponse.SC_FORBIDDEN, "请使用教师账号登录"); return; } chain.doFilter(req, resp); }

这个 Filter 在web.xml中全局注册:

<filter> <filter-name>AuthFilter</filter-name> <filter-class>com.gdes.web.filter.AuthFilter</filter-class> </filter> <filter-mapping> <filter-name>AuthFilter</filter-name> <url-pattern>/*</url-pattern> </filter-mapping>

注意:AuthFilter不处理/work.jsp这类通用页面的按钮级权限。按钮显隐交给 JSTL 在页面层控制,比如在work.jsp中:

<c:if test="${sessionScope.user.role == 'teacher'}"> <button onclick="location.href='issue.jsp'">发布新作业</button> </c:if> <c:if test="${sessionScope.user.role == 'student'}"> <button onclick="location.href='submit.jsp?workId=${work.id}'">提交作业</button> </c:if>

这种「Filter 拦截 URL + JSTL 控制 UI」的双保险,比单靠前端隐藏按钮更可靠——毕竟学生自己改 HTML 也能绕过按钮隐藏,但改不了 Filter 的 403 错误。

2.3 数据访问层:Druid 连接池与手写 DAO 的取舍逻辑

druid.properties文件内容极简,但每行都有讲究:

# druid.properties driverClassName=com.mysql.cj.jdbc.Driver url=jdbc:mysql://localhost:3306/homework_db?useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true username=root password=123456 initialSize=5 maxActive=20 minIdle=5 maxWait=60000 timeBetweenEvictionRunsMillis=60000 minEvictableIdleTimeMillis=300000 validationQuery=SELECT 1 testWhileIdle=true testOnBorrow=false testOnReturn=false poolPreparedStatements=true maxPoolPreparedStatementPerConnectionSize=20 filters=stat,wall

重点看三处:

  • serverTimezone=Asia/Shanghai:MySQL 8.0+ 必加,否则java.sql.SQLException: The server time zone value 'XXX' is unrecognized;
  • allowPublicKeyRetrieval=true:解决 MySQL 8.0 默认禁用公钥检索导致的连接失败;
  • filters=stat,wall:开启 Druid 内置监控(/druid/index.html)和防火墙(防 SQL 注入),课设答辩时打开监控页,能直观展示连接池健康状态。

DAO 层完全手写,不引入 MyBatis。以WorkDao为例:

// WorkDao.java public class WorkDao { private static final String INSERT_SQL = "INSERT INTO t_work (title, description, deadline, teacher_id, create_time) VALUES (?, ?, ?, ?, ?)"; public int insert(Work work) throws SQLException { QueryRunner qr = new QueryRunner(JdbcUtils.getDataSource()); Object[] params = { work.getTitle(), work.getDescription(), work.getDeadline(), work.getTeacherId(), new Timestamp(System.currentTimeMillis()) }; return qr.update(INSERT_SQL, params); } public List<Work> findAllByTeacher(int teacherId) throws SQLException { QueryRunner qr = new QueryRunner(JdbcUtils.getDataSource()); String sql = "SELECT * FROM t_work WHERE teacher_id = ? ORDER BY create_time DESC"; return qr.query(sql, new BeanListHandler<>(Work.class), teacherId); } }

这里用QueryRunner而非原始PreparedStatement,是因为它自动处理资源释放,避免学生忘记close()导致连接泄漏。JdbcUtils.getDataSource()返回的就是 Druid 的DataSource实例,封装在工具类里:

// JdbcUtils.java public class JdbcUtils { private static DataSource dataSource; static { try { Properties props = new Properties(); props.load(JdbcUtils.class.getClassLoader().getResourceAsStream("druid.properties")); dataSource = DruidDataSourceFactory.createDataSource(props); } catch (Exception e) { throw new RuntimeException("初始化 Druid 连接池失败", e); } } public static DataSource getDataSource() { return dataSource; } }

这种写法牺牲了 ORM 的便捷性,但换来的是——学生能看清每一行 SQL 怎么拼、参数怎么绑、异常怎么抛。答辩时被问「如果数据库挂了,你的 DAO 怎么体现?」,答「QueryRunner.update()抛SQLException,上层 Servlet 捕获后跳转 error.jsp」,比说「MyBatis 会自动重试」更有说服力。


3. 核心功能实现:从作业发布到文件提交的完整链路

作业管理网站的核心价值不在界面美观,而在「教师发得准、学生交得稳、数据看得清」。本节聚焦三个高频操作的真实实现细节:教师发布作业时如何校验截止时间、学生提交作业时如何处理文件上传、归档页面如何避免 N+1 查询。

3.1 作业发布模块:表单校验 + 服务端时间校验 + 数据库约束

issue.jsp是教师发布作业的入口,表单字段包括标题、描述、截止日期、附件(可选)。前端用 HTML5 原生校验:

<form action="IssueWorkServlet" method="post"> <input type="text" name="title" required placeholder="作业标题" maxlength="100"> <textarea name="description" required placeholder="作业要求"></textarea> <input type="datetime-local" name="deadline" required> <input type="file" name="attachment"> <button type="submit">发布作业</button> </form>

但仅靠前端校验远远不够。IssueWorkServlet做三重防护:

// IssueWorkServlet.java protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { request.setCharacterEncoding("UTF-8"); String title = request.getParameter("title"); String description = request.getParameter("description"); String deadlineStr = request.getParameter("deadline"); // 1. 空值校验(防御空字符串) if (StringUtils.isBlank(title) || StringUtils.isBlank(description) || StringUtils.isBlank(deadlineStr)) { request.setAttribute("error", "标题、描述、截止时间不能为空"); request.getRequestDispatcher("issue.jsp").forward(request, response); return; } // 2. 时间格式校验(ISO 8601 格式:yyyy-MM-dd'T'HH:mm) LocalDateTime deadline; try { deadline = LocalDateTime.parse(deadlineStr, DateTimeFormatter.ISO_LOCAL_DATE_TIME); } catch (DateTimeParseException e) { request.setAttribute("error", "截止时间格式错误,请选择有效日期时间"); request.getRequestDispatcher("issue.jsp").forward(request, response); return; } // 3. 业务规则校验:截止时间不能早于当前时间 LocalDateTime now = LocalDateTime.now(); if (deadline.isBefore(now)) { request.setAttribute("error", "截止时间不能早于当前时间"); request.getRequestDispatcher("issue.jsp").forward(request, response); return; } // 4. 构建 Work 对象并保存 Work work = new Work(); work.setTitle(title); work.setDescription(description); work.setDeadline(deadline); work.setTeacherId(((User) request.getSession().getAttribute("user")).getId()); try { int result = workDao.insert(work); if (result > 0) { // 5. 附件处理(见 3.2 节) handleAttachment(request, work.getId()); response.sendRedirect("index.jsp?msg=作业发布成功"); } else { request.setAttribute("error", "作业发布失败,请重试"); request.getRequestDispatcher("issue.jsp").forward(request, response); } } catch (SQLException e) { request.setAttribute("error", "数据库操作异常:" + e.getMessage()); request.getRequestDispatcher("issue.jsp").forward(request, response); } }

这里的关键设计点是:时间校验必须在服务端做,且用LocalDateTime而非Date。Date类在时区处理上容易翻车,而LocalDateTime.parse()明确指定格式,配合DateTimeFormatter.ISO_LOCAL_DATE_TIME,能精准捕获2024-05-20T14:30这类标准输入。更重要的是,isBefore(now)校验堵死了「教师手输 2020-01-01」这类低级错误。

3.2 作业提交模块:Apache Commons FileUpload 的安全配置

学生提交作业的核心是文件上传。本项目用commons-fileupload-1.5.jar(lib目录下),而非 Tomcat 自带的PartAPI,原因有二:一是Part在 Tomcat 8.5 下需额外配置@MultipartConfig,学生容易漏;二是FileUpload的流式处理更可控,能实时校验文件大小和类型。

SubmitServlet的上传逻辑如下:

// SubmitServlet.java protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { // 1. 创建 DiskFileItemFactory,设置临时目录和内存阈值 DiskFileItemFactory factory = new DiskFileItemFactory(); factory.setSizeThreshold(1024 * 1024); // 1MB 内存缓冲 String tempPath = request.getServletContext().getRealPath("/WEB-INF/temp"); File tempDir = new File(tempPath); if (!tempDir.exists()) tempDir.mkdirs(); factory.setRepository(tempDir); // 2. 创建 ServletFileUpload 实例,设置总大小限制(20MB) ServletFileUpload upload = new ServletFileUpload(factory); upload.setFileSizeMax(10 * 1024 * 1024); // 单文件 ≤10MB upload.setSizeMax(20 * 1024 * 1024); // 总请求 ≤20MB try { List<FileItem> items = upload.parseRequest(request); Map<String, String> params = new HashMap<>(); FileItem attachmentItem = null; // 3. 解析表单字段和文件项 for (FileItem item : items) { if (item.isFormField()) { params.put(item.getFieldName(), item.getString("UTF-8")); } else if (item.getFieldName().equals("attachment")) { attachmentItem = item; } } // 4. 校验必填字段 String workIdStr = params.get("workId"); String content = params.get("content"); if (StringUtils.isBlank(workIdStr) || StringUtils.isBlank(content)) { throw new IllegalArgumentException("作业ID和提交内容不能为空"); } // 5. 校验文件类型(只允许 .pdf .doc .docx .txt) if (attachmentItem != null && !attachmentItem.getName().isEmpty()) { String fileName = attachmentItem.getName(); String ext = FilenameUtils.getExtension(fileName).toLowerCase(); if (!Arrays.asList("pdf", "doc", "docx", "txt").contains(ext)) { throw new IllegalArgumentException("不支持的文件类型:" + ext); } // 6. 生成唯一文件名,防止覆盖 String uniqueName = UUID.randomUUID().toString() + "." + ext; String savePath = request.getServletContext().getRealPath("/uploads/" + uniqueName); File saveFile = new File(savePath); File parentDir = saveFile.getParentFile(); if (!parentDir.exists()) parentDir.mkdirs(); attachmentItem.write(saveFile); // 7. 保存到数据库(含文件路径) Submission submission = new Submission(); submission.setWorkId(Integer.parseInt(workIdStr)); submission.setStudentId(((User) request.getSession().getAttribute("user")).getId()); submission.setContent(content); submission.setFilePath("/uploads/" + uniqueName); submission.setSubmitTime(new Timestamp(System.currentTimeMillis())); submissionDao.insert(submission); } response.sendRedirect("submit.jsp?workId=" + workIdStr + "&success=1"); } catch (FileSizeLimitExceededException e) { request.setAttribute("error", "文件大小超过限制(单文件≤10MB)"); request.getRequestDispatcher("submit.jsp").forward(request, response); } catch (SizeLimitExceededException e) { request.setAttribute("error", "总上传大小超过限制(≤20MB)"); request.getRequestDispatcher("submit.jsp").forward(request, response); } catch (Exception e) { request.setAttribute("error", "提交失败:" + e.getMessage()); request.getRequestDispatcher("submit.jsp").forward(request, response); } }

这段代码的避坑点藏在注释里:factory.setRepository(tempDir)必须显式指定临时目录,否则默认用系统临时目录,不同操作系统路径不一致;FilenameUtils.getExtension()来自commons-io,它比String.substring()更安全,能正确处理archive.tar.gz这类多点扩展名;UUID.randomUUID()生成唯一文件名,彻底杜绝同名覆盖风险——某次测试发现两个学生同时交report.docx,后交的直接覆盖前交的,导致教师下载错文件。

3.3 作业归档模块:分页查询 + 手动关联 + 缓存优化

gather.jsp是教师查看所有作业提交情况的汇总页。它不依赖任何分页插件,而是用原生 SQLLIMIT和OFFSET实现:

-- 获取总记录数(用于计算页数) SELECT COUNT(*) FROM t_submission s JOIN t_work w ON s.work_id = w.id WHERE w.teacher_id = ? AND s.submit_time >= ? -- 获取当前页数据 SELECT s.*, w.title as work_title, u.name as student_name FROM t_submission s JOIN t_work w ON s.work_id = w.id JOIN t_user u ON s.student_id = u.id WHERE w.teacher_id = ? AND s.submit_time >= ? ORDER BY s.submit_time DESC LIMIT ? OFFSET ?

对应的GatherServlet:

// GatherServlet.java protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { int currentPage = Integer.parseInt(request.getParameter("page") != null ? request.getParameter("page") : "1"); int pageSize = 10; int offset = (currentPage - 1) * pageSize; User user = (User) request.getSession().getAttribute("user"); long startTime = System.currentTimeMillis() - 30 * 24 * 60 * 60 * 1000L; // 近30天 try { // 1. 查询总条数 long totalCount = gatherDao.countByTeacherAndTime(user.getId(), startTime); int totalPage = (int) Math.ceil((double) totalCount / pageSize); // 2. 查询当前页数据 List<GatherItem> items = gatherDao.findByTeacherAndTime( user.getId(), startTime, pageSize, offset); request.setAttribute("items", items); request.setAttribute("currentPage", currentPage); request.setAttribute("totalPage", totalPage); request.setAttribute("totalCount", totalCount); request.getRequestDispatcher("gather.jsp").forward(request, response); } catch (SQLException e) { request.setAttribute("error", "数据加载失败:" + e.getMessage()); request.getRequestDispatcher("gather.jsp").forward(request, response); } }

GatherItem是一个手动组装的 VO 类,包含work_title、student_name等字段,避免在 JSP 中写 N+1 查询:

// GatherItem.java public class GatherItem { private int id; private String workTitle; // 作业标题 private String studentName; // 学生姓名 private String content; // 提交内容摘要(截取前50字) private String filePath; // 文件路径 private Timestamp submitTime; // 提交时间 // getter/setter 省略 }

在gather.jsp中,用 JSTL 循环渲染:

<c:forEach items="${items}" var="item"> <tr> <td>${item.workTitle}</td> <td>${item.studentName}</td> <td>${fn:substring(item.content, 0, 50)}${fn:length(item.content) > 50 ? '...' : ''}</td> <td> <c:if test="${not empty item.filePath}"> <a href="${item.filePath}" target="_blank">下载附件</a> </c:if> </td> <td>${item.submitTime}</td> </tr> </c:forEach>

分页导航栏用纯 HTML 实现,不依赖 JS:

<div class="pagination"> <c:if test="${currentPage > 1}"> <a href="?page=${currentPage - 1}">&laquo; 上一页</a> </c:if> <span>第 ${currentPage} 页,共 ${totalPage} 页</span> <c:if test="${currentPage < totalPage}"> <a href="?page=${currentPage + 1}">下一页 &raquo;</a> </c:if> </div>

这种写法看似笨重,但胜在稳定——学生改 CSS 样式不影响分页逻辑,换浏览器也不用担心 JS 兼容性。答辩时被问「为什么不用 PageHelper?」,答「PageHelper 依赖 MyBatis,而本项目用原生 JDBC,保持技术栈纯粹」,比强行套用更显思考深度。


4. 常见问题排查:五个让课设答辩翻车的高频陷阱

这套源码虽经调试,但学生在本地部署时仍极易踩坑。以下是我在指导某高校 12 个小组课设时,统计出的最高频、最致命的五个问题。每个问题都按「现象 → 原因 → 解决」给出可立即执行的方案,不讲原理,只给解法。

4.1 现象:登录成功后跳转到index.jsp,但页面右上角仍显示「登录」,F12 查看 Network 发现index.jsp返回 200 但 Session 为空

原因:web.xml中<session-config>缺失或cookie被浏览器拦截。Tomcat 默认启用 Session Cookie,但若浏览器隐私模式或禁用 Cookie,Session ID 无法传递。
解决:

  1. 在web.xml中强制启用 Cookie 并设置路径:
<session-config> <cookie-config> <http-only>true</http-only> <secure>false</secure> <!-- 本地开发用 HTTP --> <path>/</path> </cookie-config> </session-config>
  1. 浏览器访问http://localhost:8080/yourapp/login.jsp(不要用127.0.0.1),确保域名一致;
  2. 清除浏览器所有localhost相关 Cookie 后重试。

4.2 现象:submit.jsp提交文件时,request.getParameter("content")返回null,但文件能正常上传

原因:FileItem解析顺序错误。当表单同时含普通字段和文件字段时,upload.parseRequest(request)返回的List<FileItem>中,普通字段和文件字段混排,若遍历顺序不对,可能先处理了文件项再处理文本项,导致paramsMap 未填充。
解决:严格按「先收集所有普通字段,再单独处理文件项」的顺序写代码:

for (FileItem item : items) { if (item.isFormField()) { // ✅ 必须先处理所有 isFormField() 项 params.put(item.getFieldName(), item.getString("UTF-8")); } } // ❌ 禁止在此处 break 或 return for (FileItem item : items) { if (!item.isFormField() && item.getFieldName().equals("attachment")) { // ✅ 再处理文件项 attachmentItem = item; } }

4.3 现象:MySQL 连接报错The server time zone value 'XXX' is unrecognized,或插入时间字段全是0000-00-00 00:00:00

原因:MySQL 服务器时区与 Java 应用时区不一致,且 JDBC URL 未显式声明serverTimezone。
解决:

  1. 登录 MySQL 执行SELECT @@global.time_zone, @@session.time_zone;,确认服务器时区(如SYSTEM或+08:00);
  2. 修改druid.properties中的url,强制指定时区:
url=jdbc:mysql://localhost:3306/homework_db?useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true
  1. 若服务器时区是SYSTEM,在 MySQL 配置文件my.cnf中添加:
[mysqld] default-time-zone = '+08:00'

4.4 现象:gather.jsp页面空白,查看 Tomcat 日志出现java.lang.ClassNotFoundException: org.apache.commons.fileupload.FileItem

原因:commons-fileupload-1.5.jar和commons-io-2.8.0.jar未放入WEB-INF/lib,或版本不匹配(fileupload 1.5必须配io 2.8.0,低版本会报NoClassDefFoundError)。
解决:

  1. 确认lib目录下存在以下两个 JAR:
    • commons-fileupload-1.5.jar(1.4MB)
    • commons-io-2.8.0.jar(320KB)
  2. 删除lib中所有commons-*的旧版本 JAR(如commons-fileupload-1.3.jar);
  3. 重启 Tomcat,观察日志是否还有ClassNotFoundException。

4.5 现象:work.jsp列表显示「暂无作业」,但数据库t_work表中有数据,且teacher_id字段值正确

原因:WorkDao.findAllByTeacher()方法中 SQL 的WHERE条件字段名写错,比如写成teacherid(少下划线)或teacher_idd(多字母),导致查询返回空集合。
解决:

  1. 打开WorkDao.java,定位findAllByTeacher方法;
  2. 复制 SQL 语句到 MySQL 客户端执行,替换?为真实teacher_id值(如1),确认是否返回数据;
  3. 若客户端能查到,说明 Java 代码中QueryRunner.query()的参数顺序错了——检查new BeanListHandler<>(Work.class)后的参数是否与 SQL 中?位置一一对应;
  4. 最保险做法:在findAllByTeacher方法开头加日志:
System.out.println("查询教师ID:" + teacherId); // 确认传入值

5. 答辩验收指南:从环境准备到演示话术的全流程 checklist

答辩不是代码秀,而是需求闭环验证。评审老师最关心三件事:能不能跑、有没有用、靠不靠谱。我带过的 23 个课设小组中,95 分以上的共同点不是功能多炫,而是验收环节零卡顿、问题回答有依据、演示过程有节奏。以下是我给学生定制的答辩前 48 小时 checklist,按时间倒序排列,每一步都对应一个答辩得分点。

5.1 答辩前 2 小时:环境快照与故障预案

此时不该再改代码,而是固化环境、预演故障。打开命令行,依次执行:

# 1. 检查端口占用(避免 Tomcat 启动失败) netstat -ano | findstr :8080 # 2. 清理 Tomcat 工作目录(防止旧编译类干扰) del /q "C:\apache-tomcat-8.5.94\work\Catalina\localhost\*" # 3. 导出当前数据库(生成 backup.sql,答辩时若被问「数据丢了怎么办」可立刻展示) mysqldump -u root -p homework_db > backup.sql # 4. 截图三张关键页面(存桌面,备用): # - http://localhost:8080/yourapp/druid/index.html (Druid 监控页,证明连接池健康) # - http://localhost:8080/yourapp/index.jsp (首页,显示登录态) # - http://localhost:8080/yourapp/gather.jsp?page=1 (归档页,证明数据可查)

注意:druid/index.html默认账号密码是admin/admin,答辩时打开此页,指着「ActiveCount」「PoolingCount」指标说:「连接池当前活跃连接 3 个,空闲 2 个,说明并发处理正常」,比空讲「我用了 Druid」有力十倍。

5.2 答辩前 30 分钟:演示脚本与话术设计

把 10 分钟演示拆成 4 个 2 分钟模块,每个模块配一句「价值话术」,避免陷入技术细节:

模块操作步骤价值话术
身份可信用 teacher/123456 登录 → 点击「发布作业」→ 填写标题「JavaWeb 课设」、截止时间选明天 → 点击发布「系统强制校验截止时间不能早于当前,从源头杜绝教师误操作」
提交可靠切换 student/123456 登录 → 在「我的作业」列表找到刚发布的作业 → 点击「提交」→ 上传report.pdf→ 填写说明「已完成」→ 提交「文件上传限制 10MB 且只允许 PDF/DOC,保障服务器安全与教学规范」
数据可视教师账号登录 → 点击「作业归档」→ 查看近 30 天提交列表 → 点击「下载附件」→ 打开report.pdf「归档页按教师维度聚合数据,支持一键下载,满足教学管理刚需」
权限可控学生账号尝试访问http://localhost:8080/yourapp/issue.jsp→ 触发 403 Forbidden「URL 级权限由 Filter 硬拦截,非前端隐藏,真正保障数据隔离」

演示时禁止说「这个功能还没做完」「这部分是同学写的」。如果某个按钮没实现,提前在header.jsp中注释掉对应<c:if>块,让界面看起来就是「本就不该有」。

5.3 答辩中高频问题应答模板

评审老师的问题往往围绕「为什么」和「怎么改」。以下是 5 个必问问题的标准答案,全部基于本项目真实设计:

Q1:为什么不用 Spring Boot?
A:课程设计要求体现 JavaWeb 核心技术栈,Spring Boot 的自动配置会掩盖 Servlet 生命周期、Filter 链、JSP 编译等底层机制。本项目用原生 Filter 实现权限控制,用 JSTL 控制视图,能让评审老师清晰看到每个技术点的落地。

Q2:文件上传怎么保证不被恶意文件攻击?
A:三重防护:第一,FileUpload设置setFileSizeMax(10*1024*1024)限制单文件大小;第二,解析文件名后用FilenameUtils.getExtension()提取扩展名,只允许.pdf/.doc/.docx/.txt;第三

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

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

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

立即咨询