ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

Servlet+JSP+JDBC实战:JavaWeb教务管理系统完整教程

Servlet+JSP+JDBC实战:JavaWeb教务管理系统完整教程 简介面向JavaWeb初学者的教务管理系统完整项目基于MVC设计模式采用Servlet、JSP、Filter与MySQL技术栈按登录系统、学生、教师、教务员、系统管理员五个角色模块划分完整覆盖登录注销、找回密码、成绩查询、选修与考级报名、学籍信息查看修改、教师个人信息维护、教务员成绩管理、管理员用户管理等核心功能业务分层清晰适合作为课程设计或学期作业原型。压缩包共223个文件以82个Java源文件与对应class编译文件为主体配合38个JSP页面、6个jar运行库、XML配置文件及SQL数据库脚本另附Eclipse工程配置、README使用说明和界面示意图整套资源约2.6MB下载部署都较便捷。该资源在CSDN已有11656人学习下载运行环境基于JDK1.8与Tomcat7。读者可对照源码快速掌握DAO模式与前后端交互思路同时继续补全排课管理、公告发布等未实现模块对理解JavaWeb项目整体设计有直接帮助。1. 教务管理系统 JavaWeb 项目为什么值得花一个周末复现教务管理系统是我建议 JavaWeb 新手花精力完整复现的第一个项目。它的技术栈不新——Servlet、JSP、JDBC、MySQL但恰恰是这套东西把 HTTP 请求、Session、Filter、数据库事务这些核心概念全串在了一条真实业务链路上。这套案例工程里包含完整的登录鉴权、学生/课程/成绩管理、分页列表和批量录入做完之后换皮改造成图书馆管理系统、实验室管理系统都只是改表结构的事。适合交课设、准备面试、或者学完框架后回来补底层的人。很多时候你去看 Spring Boot 的拦截器觉得是黑匣子就是因为缺了 Filter 这一层的手工实践——把Servlet/JSP这套跑通后面学任何框架都会顺很多。2. 技术选型与工程骨架Servlet JSP JDBC 凭什么比框架更适合练手2.1 为什么这套案例用 Servlet/JSP 而不是 Spring Boot很多同学拿到项目第一反应是“现在谁还用 JSP不都前后端分离了吗”。这个说法对生产项目成立但对练手项目不成立。教务管理系统选 Servlet JSP JDBC核心原因是把 Tomcat 最原始的机制暴露给你看一个 HTTP 请求进来Tomcat 怎么找到对应的 ServletServlet 里怎么拿参数、怎么存 SessionFilter 在什么时机拦截请求转发和重定向路径写错会出现什么后果。这些内容在 Spring Boot 里全被封装了。你写一个Controller方法上标RequestMapping框架帮你做了路由你要做登录拦截就和HandlerInterceptor打交道底层已经在 Spring 的 DispatcherServlet 之后。一旦环境出问题报错堆栈一层套一层新手根本定位不到 Tomcat 层面。而这个项目依赖极简出问题时的排查链路短要么是 Servlet 没匹配上要么是 JDBC 连接失败要么是 JSP 渲染出错每一层都能单点定位。另外一个很实际的选型理由课设和面试场景里别人用 Spring Boot 包装出来的项目一追问“请求到 Controller 之前经过哪些组件”十有八九答不上来。而你用一个 Servlet 项目能从头到尾把一条请求链路讲完——浏览器发请求、Tomcat 匹配映射、Filter 过滤、Servlet 处理、Service 业务、DAO 查库、forward 到 JSP——每一条都有明确的代码对应面试官挑不出断层。2.2 工程目录结构三层架构在这里不是摆设这个案例的包结构是典型的 Maven Web 工程建议你建项目时也按这个拆法来。我第一次做的时候把所有类都堆在src根包下结果后期想加个 Filter 都找不到该放哪个包改需求时牵连一片翻车翻得很难看。规范的做法是职责分包一眼能看出请求流走到哪一层。src/main/java/com/example/edu/ ├── controller/ # Servlet 层接收 HTTP 请求、调 Service、跳转页面 │ ├── LoginServlet.java │ ├── StudentListServlet.java │ ├── StudentEditServlet.java │ └── ScoreServlet.java ├── service/ # 业务层登录校验、成绩批量录入的事务边界 │ ├── StudentService.java │ └── ScoreService.java ├── dao/ # 数据访问层只写 SQL 和结果集映射 │ ├── BaseDAO.java │ ├── StudentDAO.java │ └── ScoreDAO.java ├── filter/ # 过滤器统一编码 Session 登录拦截 │ └── AuthFilter.java ├── entity/ # 实体类Student, Teacher, Course, Score │ ├── Student.java │ └── Score.java ├── util/ # 工具类连接池初始化与资源关闭 │ └── DBUtil.java └── webapp/ ├── WEB-INF/ │ ├── web.xml │ └── jsp/ # JSP 页面统一放 WEB-INF 下防止浏览器直接访问 ├── login.jsp └── index.jsp这个结构里最值得强调的是WEB-INF/jsp。JSP 文件塞进WEB-INF目录后浏览器直接输 URL 是访问不到的只有通过 Servlet 的forward才能渲染出来。这样登录拦截才真正生效——用户不登录就没法请求到任何页面资源因为你根本没给他一个可以直接访问 JSP 的路径。很多网上的案例把 JSP 跟webapp平铺Filter 拦截做得再好页面文件暴露在外面输入路径照样能打开登录拦截就形同虚设。DAO 层我只写了一个BaseDAO里面放通用的查询、更新方法子类继承后只写自己的 SQL。这样做不是炫技而是为了让每个 DAO 类尽量短看代码时注意力集中在当前业务上。2.3 依赖与运行环境Maven 坐标和 Tomcat 版本怎么配这套项目我建议用 Maven 管理依赖不推荐手工拷 jar 包到lib目录。手工拷 jar 最大的问题是版本冲突看不出来——比如同时拷了旧版的servlet-api和新版 JDBC 驱动启动时 Tomcat 自己带的类和 lib 里的类冲突报的错你根本猜不到是哪来的。用 Maven 的话依赖树一目了然出问题还能mvn dependency:tree查冲突。dependencies !-- Servlet API编译期需要Tomcat 自带所以 scope 一定用 provided -- dependency groupIdjavax.servlet/groupId artifactIdjavax.servlet-api/artifactId version3.1.0/version scopeprovided/scope /dependency !-- JSTLJSP 页面里的 c:forEach / c:if 标签都靠它 -- dependency groupIdjavax.servlet/groupId artifactIdjstl/artifactId version1.2/version /dependency !-- MySQL 驱动5.1.49 兼容 MySQL 5.7MySQL 8.0 也能用 -- dependency groupIdmysql/groupId artifactIdmysql-connector-java/artifactId version5.1.49/version /dependency !-- Druid 连接池比 DriverManager 手动开连接更接近真实项目 -- dependency groupIdcom.alibaba/groupId artifactIddruid/artifactId version1.2.8/version /dependency /dependenciesprovided这个 scope 容易被人忽略但相当关键。Tomcat 8.5 自己带了javax.servlet-api3.1 的实现如果你把依赖 scope 写成默认的compileMaven 打包时会把 servlet-api 也打进去部署到 Tomcat 后就有了两份相同的类启动时要么报ClassNotFoundException相关的奇怪错误要么出现NoSuchMethodError。这不是玄学是类加载器父子关系导致的冲突把 scope 改成provided一劳永逸。Tomcat 版本和 JDK 版本也要匹配。JDK 8 配 Tomcat 8.5 或 9.0 都没问题如果你电脑装的是 JDK 11 以上建议直接用 Tomcat 9因为 Tomcat 8.5 官方声明对高版本 JDK 的支持没那么积极。IDEA 内置的 Tomcat 插件在新版本里叫“Tomcat and TomEE”直接在那里选本地 Tomcat 路径即可。3. 数据库设计与初始化五张核心表把教务业务撑起来3.1 表结构设计登录账号和学生信息是两张表而不是一张教务系统的数据模型本质上是有账号、有角色、有学生档案、有课程、有成绩。很多初学者把username和password直接塞进学生表学生表变成“账号档案”一张大表。这个设计在功能上能跑通但扩展性很差——教师需要登录管理员也需要登录每个人都得在各自表里放一列密码查询逻辑越来越碎后期改密码策略要改好几张表。我的做法是把账号单独拆成sys_user表学生和教师表通过user_id关联过去。这样登录逻辑只面对一张表角色字段role决定登录后跳哪个页面学生表和教师表只存业务属性互不干扰。还有一点要注意学号、教师工号这类字段要用VARCHAR而不是INT因为教务系统的学号经常有前缀字母比如S2023001你建 INT 字段时存不进去。表名核心字段说明sys_userid, username, password, role, real_name登录账号表role 区分 admin/teacher/studentstudentstudent_no, user_id, name, gender, class_name, major学生档案主键是学号teacherteacher_no, user_id, name, title, department教师档案主键是工号coursecourse_id, course_name, credits, teacher_no, capacity课程表credits 用 DECIMAL 存学分scoreid, student_no, course_id, score选课成绩表联合唯一约束防止重复录入主键选择的细节值得说两句student_no用业务主键意味着学号一旦录错改起来要连带改关联表的外键成本高。如果你设计的系统里学号允许变更更稳妥的方案是给student表加一个自增id主键student_no上面只建唯一索引。这个取舍不是对错问题是看你业务里学号到底变不变我一般倾向后者因为自增主键对关联查询更友好。3.2 SQL 初始化脚本建库建表加测试数据一次搞定数据库这块我习惯把所有 DDL 和 INSERT 写进一个init.sql用 Navicat 或命令行一次性执行。这样项目给别人复现时对方只要执行一个脚本就能拥有完整环境不用对着分散的 SQL 到处找。执行时注意utf8mb4字符集用它存中文最稳utf8在某些字符上有坑。CREATE DATABASE edu_db DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci; USE edu_db; -- 登录账号表 CREATE TABLE sys_user ( id INT PRIMARY KEY AUTO_INCREMENT, username VARCHAR(50) UNIQUE NOT NULL, password VARCHAR(50) NOT NULL, role VARCHAR(20) NOT NULL DEFAULT student, real_name VARCHAR(50) NOT NULL ); -- 学生档案表 CREATE TABLE student ( student_no VARCHAR(20) PRIMARY KEY, user_id INT NOT NULL, name VARCHAR(50) NOT NULL, gender VARCHAR(10), class_name VARCHAR(50), major VARCHAR(50), FOREIGN KEY (user_id) REFERENCES sys_user(id) ); -- 教师档案表 CREATE TABLE teacher ( teacher_no VARCHAR(20) PRIMARY KEY, user_id INT NOT NULL, name VARCHAR(50) NOT NULL, title VARCHAR(50), department VARCHAR(80), FOREIGN KEY (user_id) REFERENCES sys_user(id) ); -- 课程表 CREATE TABLE course ( course_id VARCHAR(20) PRIMARY KEY, course_name VARCHAR(100) NOT NULL, credits DECIMAL(3,1), teacher_no VARCHAR(20), capacity INT DEFAULT 60 ); -- 选课成绩表 CREATE TABLE score ( id INT PRIMARY KEY AUTO_INCREMENT, student_no VARCHAR(20) NOT NULL, course_id VARCHAR(20) NOT NULL, score DECIMAL(5,2), UNIQUE KEY uk_stu_course (student_no, course_id) ); -- 初始化三个测试账号密码全部是 123456 INSERT INTO sys_user (username, password, role, real_name) VALUES (admin, 123456, admin, 系统管理员), (T1001, 123456, teacher, 张伟老师), (S2023001, 123456, student, 李小明); -- 关联学生档案 INSERT INTO student (student_no, user_id, name, gender, class_name, major) VALUES (S2023001, 3, 李小明, 男, 计科2301, 计算机科学与技术); -- 关联教师档案 INSERT INTO teacher (teacher_no, user_id, name, title, department) VALUES (T1001, 2, 张伟, 副教授, 计算机系); -- 两条课程 INSERT INTO course (course_id, course_name, credits, teacher_no, capacity) VALUES (C001, Java程序设计, 3.0, T1001, 60), (C002, 数据库原理, 2.5, T1001, 50);SQL 里几个设计点是刻意做的UNIQUE KEY uk_stu_course保证同一个学生对同一门课只能有一条成绩记录防止录成绩时产生重复数据导致统计结果异常DECIMAL(3,1)和DECIMAL(5,2)是学分和成绩的精度控制学分最大 99.9成绩保留两位小数别用 FLOAT 存成绩浮点数的精度问题会让你期末统计平均分时莫名其妙多出小数点后的尾巴。密码字段这里先用明文123456是为了让案例跑起来简单。真实系统里密码必须加密存储至少用 MD5 加盐进阶方向我会在最后单独提。测试账号数量也不用多够跑通登录和列表就行后面要测分页效果再批量插几十条数据用存储过程或复制 INSERT 都很快。3.3 JDBC 连接配置连接池参数与经典配置模板数据库连接这块我直接把 Druid 连接池立好因为真实项目里没人会再用DriverManager.getConnection()每次现开连接——那性能太差。但连接池的配置也带来一个经典坑驱动类名和 URL 参数在不同 MySQL 版本下写法完全不同。这个坑会在第 5 章详细展开这里先把正确配置给出来。# src/main/resources/db.properties jdbc.drivercom.mysql.jdbc.Driver jdbc.urljdbc:mysql://localhost:3306/edu_db?useUnicodetruecharacterEncodingutf8useSSLfalseserverTimezoneAsia/ShanghaiallowPublicKeyRetrievaltrue jdbc.usernameroot jdbc.password123456 # 连接池参数 jdbc.initialSize5 jdbc.maxActive20 jdbc.maxWait60000Druid 的连接池初始化代码固定一套写法封装成工具类放在util包里所有 DAO 都从这里拿连接public class DBUtil { private static DruidDataSource dataSource; static { try (InputStream in DBUtil.class.getClassLoader().getResourceAsStream(db.properties)) { Properties props new Properties(); props.load(in); dataSource (DruidDataSource) DruidDataSourceFactory.createDataSource(props); } catch (Exception e) { throw new ExceptionInInitializerError(数据库连接池初始化失败 e.getMessage()); } } public static Connection getConnection() throws SQLException { return dataSource.getConnection(); } public static void close(Connection conn, PreparedStatement ps, ResultSet rs) { // 关闭顺序rs - ps - conn先关结果集再关语句最后关连接 try { if (rs ! null) rs.close(); } catch (SQLException ignored) {} try { if (ps ! null) ps.close(); } catch (SQLException ignored) {} try { if (conn ! null) conn.close(); } catch (SQLException ignored) {} } }这段代码里有一个值得留意的理念close方法里关闭连接并不是真的断开数据库而是把连接归还给连接池。所以你在业务代码里用完连接后一定要记得调用DBUtil.close(conn, ps, rs)否则连接池的连接迟早被耗尽程序运行一段时间后就开始报“连接超时”。参数方面serverTimezoneAsia/Shanghai不加的话MySQL 8 的驱动会因为时区差报错allowPublicKeyRetrievaltrue是应对 MySQL 8 的加密认证要求这个参数在 MySQL 5.7 下加了也不会报错所以建议直接统一写上。4. 核心功能模块实现登录鉴权、分页列表与批量录入4.1 登录与 Session 拦截一个 Filter 管住全部内部页面登录功能是所有教务模块的入口它的核心不只是“比对用户名密码”而是登录成功后怎么让其他页面知道你已登录。这里用的是 HttpSession 方案登录成功把用户对象存进 Session后续每次请求由 Filter 检查 Session 里有没有这个人。WebServlet(/login) public class LoginServlet extends HttpServlet { private UserDAO userDAO new UserDAO(); Override protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { request.setCharacterEncoding(UTF-8); String username request.getParameter(username); String password request.getParameter(password); User user userDAO.findByUsernameAndPassword(username, password); if (user null) { request.setAttribute(error, 用户名或密码错误); request.getRequestDispatcher(/login.jsp).forward(request, response); return; } // 登录成功把用户放进 Session后续 Filter 就看这个属性 HttpSession session request.getSession(); session.setAttribute(loginUser, user); // 按角色分发到不同的首页 String target admin.equals(user.getRole()) ? /admin/home.jsp : teacher.equals(user.getRole()) ? /teacher/home.jsp : /student/home.jsp; response.sendRedirect(request.getContextPath() target); } }登录用sendRedirect而不是forward背后的逻辑要搞清楚forward是服务器内部跳转浏览器地址栏不变用户刷新页面时会重新提交表单导致重复登录sendRedirect让浏览器发起全新 GET 请求地址栏变成目标首页刷新也不会重复提交。这是 Web 开发一个老生常谈但新手必踩的坑记住一句话——登录成功后的跳转用重定向不用转发。再来看 Filter 的写法。这个 Filter 配了/*意思是拦下所有请求但登录请求和静态资源必须放行否则会出现“登录都进不去”的死循环。WebFilter(/*) 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; String uri request.getRequestURI(); // 放行登录接口、登录页面和静态资源其他的都要检查登录状态 if (uri.endsWith(/login) || uri.endsWith(/login.jsp) || uri.endsWith(.css) || uri.endsWith(.js) || uri.endsWith(.png) || uri.endsWith(.jpg)) { chain.doFilter(request, response); return; } HttpSession session request.getSession(); User loginUser (User) session.getAttribute(loginUser); if (loginUser null) { // 未登录重定向到登录页不要转发 response.sendRedirect(request.getContextPath() /login.jsp); return; } // 已登录放行到后续的 Servlet 或 JSP chain.doFilter(request, response); } }这个 Filter 拦截的是“未登录”这个状态但有一个边界情况要说明白角色权限控制不能只靠 Session 里有没有人。学生登录后如果直接手动输入 URL 访问教师的管理页面Session 里是有用户的Filter 会放行页面照样能打开。所以角色级别的控制需要在业务层或者 Filter 里再判断一次loginUser.getRole()这属于系统设计上的边界在验收和面试时它反而是你展示思考深度的机会——你可以明确说出“当前 Filter 管会话角色路由需要进一步细化”这句话。4.2 分页查询与列表渲染LIMIT 参数计算与查询条件保持分页是教务管理系统最常被问到细节的功能点。它的难点不在 SQL 本身而在于两点页码参数非法时怎么兜底翻页之后搜索条件怎么保持。先说分页参数的边界处理再给你看完整的 Servlet 代码。WebServlet(/student/list) public class StudentListServlet extends HttpServlet { private StudentService studentService new StudentService(); Override protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { request.setCharacterEncoding(UTF-8); int page 1; int pageSize 10; String pageParam request.getParameter(page); if (pageParam ! null !pageParam.isEmpty()) { try { page Integer.parseInt(pageParam); } catch (NumberFormatException e) { // 非法参数直接回退到第 1 页 page 1; } } if (page 1) page 1; String keyword request.getParameter(keyword); if (keyword null) keyword ; // DAO 里执行带条件的 count 和 limit 查询 int total studentService.count(keyword); ListStudent list studentService.findPage(page, pageSize, keyword); int totalPage (total pageSize - 1) / pageSize; if (page totalPage) page totalPage; request.setAttribute(list, list); request.setAttribute(total, total); request.setAttribute(totalPage, totalPage); request.setAttribute(currentPage, page); request.setAttribute(keyword, keyword); request.getRequestDispatcher(/WEB-INF/jsp/student/list.jsp) .forward(request, response); } }totalPage的计算式(total pageSize - 1) / pageSize是一个经典写法目的是向上取整。比如总记录数是 101每页 10 条用这个公式得到 11 而不是 10避免了101 / 10 10导致最后一页丢 1 条数据的尴尬。DAO 里分页 SQL 配合 LIMIT注意参数顺序public ListStudent findPage(int page, int pageSize, String keyword) { String sql SELECT * FROM student WHERE name LIKE ? OR student_no LIKE ? ORDER BY student_no LIMIT ?, ?; int offset (page - 1) * pageSize; // 参数顺序第一个 LIKE 是姓名第二个 LIKE 是学号然后才是 offset 和 pageSize ListStudent list ...; return list; }LIMIT的offset从 0 开始所以第 2 页的数据要从第 10 条开始取公式是(page - 1) * pageSize。这个计算逻辑本身不难但在 JSP 里翻页时很容易出现“点第 2 页结果和第 1 页一样”的情况——多半是 SQL 里offset算错了或者LIMIT 10, 10写成了LIMIT 10 OFFSET 1搞混了两者的语义。JSP 页面里翻页链接的写法也有一坑keyword 必须原样拼接回去否则点下一页搜索条件就丢了c:forEach varstu items${list} tr td${stu.studentNo}/td td${stu.name}/td td${stu.gender}/td td${stu.className}/td td${stu.major}/td /tr /c:forEach %-- 分页导航当前页码用 URL 参数传keyword 保持住 --% c:if test${currentPage 1} a hreflist?page${currentPage - 1}keyword${keyword}上一页/a /c:if span第 ${currentPage} / ${totalPage} 页共 ${total} 条/span c:if test${currentPage totalPage} a hreflist?page${currentPage 1}keyword${encodeURIComponent(keyword)}下一页/a /c:ifJSP 里直接拼${keyword}到 URL 上在搜索词是中文的时候会出现乱码问题。因为 URL 里的中文没有经过编码部分 Tomcat 版本对 URL 中的非 ASCII 字符处理不一致。建议在 Servlet 跳转前用URLEncoder.encode(keyword, UTF-8)编码一次或者在前端用 JavaScript 的encodeURIComponent处理。这个问题在本地测试时可能看不出来因为有些浏览器会自动编码但在别的环境跑就是翻车现场。4.3 成绩批量录入事务边界与 PreparedStatement 批量提交成绩录入是教务系统的核心写操作。场景是教师在一个页面里填多行成绩提交后一次性写入数据库。这时候两步操作不能拆开——要么全部写入成功要么全部回滚。这里牵扯到事务的边界问题必须放在 Service 层而不是 DAO 层。public class ScoreService { public void batchAddScore(ListScore scoreList) throws SQLException { if (scoreList null || scoreList.isEmpty()) { throw new IllegalArgumentException(成绩列表不能为空); } Connection conn DBUtil.getConnection(); PreparedStatement ps null; try { // 关闭自动提交整个批量过程作为一个事务 conn.setAutoCommit(false); String sql INSERT INTO score (student_no, course_id, score) VALUES (?, ?, ?) ON DUPLICATE KEY UPDATE score VALUES(score); ps conn.prepareStatement(sql); for (Score s : scoreList) { ps.setString(1, s.getStudentNo()); ps.setString(2, s.getCourseId()); ps.setBigDecimal(3, s.getScore()); ps.addBatch(); // 累积到批处理 } ps.executeBatch(); // 一次性发送给 MySQL conn.commit(); // 全部成功才提交 } catch (SQLException e) { conn.rollback(); // 任何一条失败全部回滚 throw e; } finally { conn.setAutoCommit(true); DBUtil.close(conn, ps, null); } } }这段代码有三个细节值得展开。第一个是ON DUPLICATE KEY UPDATE它的作用是如果(student_no, course_id)已经存在就把成绩更新成新值而不是报主键冲突。这个语法把“新增选课成绩”和“教师修改成绩”两个操作合并成了一条 SQL省掉了先查再插的额外逻辑。第二个是addBatch() executeBatch()它把多条 INSERT 放到同一个网络请求里发给 MySQL而不是一条条发性能差距在数据量大的时候特别明显。第三个是事务必须依赖同一个 Connection 对象如果每次循环都重新拿连接事务就失效了这里 Service 层拿一次连接、循环内共用正是为了守住事务边界。Service 层把事务控制在这里DAO 层只管单条 SQL 执行这个职责划分在使用时会非常清晰。你在测试时可以在循环里故意放一条非法学号的数据观察前面合法数据是否被回滚——正确结果是所有数据都没入库这就是事务的原子性在起作用。5. 部署运行与 IDEA 配置避坑从 500 到 404 的完整排查记录5.1 IDEA 里 Tomcat 运行配置Artifacts 没配好等于白搭这个项目拿到手里第一个卡点通常是 IDEA 里跑不起来。IDE 报错五花八门但九成根源在一个地方Artifacts 没配置完整。我在这里直接给一套固定流程按步骤走大概率一次通过。第一步打开项目结构路径是File - Project Structure - Artifacts。点加号新建Web Application: Exploded把输出目录指到项目的target/edu-web。第二步在这个 Artifact 里展开WEB-INF/lib节点右键点“Put into Output Root”把 Maven 依赖mysql 驱动、jstl、druid全部打进去。第三步配置 TomcatRun - Edit Configurations - - Tomcat Server - Local在Deployment页签把刚才建的 Artifact 添加进去Application context填/edu。第四步确认 JRE 和 Tomcat 版本匹配直接点启动。如果你发现启动后访问http://localhost:8080/edu/login.jsp报 404先不要怀疑代码去target目录看看有没有生成edu-web这个目录结构。没有的话就是 Artifact 没建对。这个问题的特点是日志里不报任何异常只有浏览器端 404最容易让人误判成 Servlet 映射写错实际上请求根本没到 Tomcat 的处理逻辑。5.2 数据库连接失败的三个高频报错数据库连接失败是另一个重灾区这里的现象、原因和解决方式是固定的我直接列清单。第一条启动项目时控制台报java.lang.ClassNotFoundException: com.mysql.jdbc.Driver。原因是 MySQL 8 的驱动类名改成了com.mysql.cj.jdbc.Driver或者驱动 jar 根本没打包进 Artifact。解决方法是先确认db.properties里驱动类名MySQL 8 必须写com.mysql.cj.jdbc.Driver再检查 Artifact 里WEB-INF/lib下有没有mysql-connector-java。第二条驱动能找到但报Communications link failure。原因是 MySQL 服务没启动或连接串端口写错最常见的是本机装了多个 MySQL 实例3306 被占用。解决方法是先用netstat -an | findstr 3306查端口占用情况用 Navicat 或命令行测试一下能否用同一套账号密码连上把问题限定在“程序配置”还是“数据库环境”。第三条报Public Key Retrieval is not allowed。原因只在 MySQL 8 出现在默认认证插件caching_sha2_password要求客户端先取公钥而 JDBC 驱动出于安全考虑默认禁止。解决方法是连接串加allowPublicKeyRetrievaltrue如果加了还报再配合useSSLfalse一起挂上去。这些报错每个都有一堆网上教程让你改配置但没有人告诉你是为什么。这三个问题全部根源于 MySQL 版本与驱动版本、认证方式之间的错位只要你先把db.properties里的四要素驱动类名、URL、用户名、密码和数据库真实环境对一遍90% 的连接问题都能在 5 分钟内收敛。5.3 Servlet 路径映射与页面跳转的 404/405这个项目里 404 除了 Artifact 问题还有一类是 Servlet 映射路径写错。现象是控制台完全没有报错浏览器显示 404而 JSP 文件明明存在。 WebServlet 注解和 JSP 里 form 的 action 路径必须严格一致每个字符都不能差。最常见的坑是 form 提交路径漏了上下文路径。假如项目部署后上下文是/edu你的 Servlet 映射是/student/add那 form 的 action 必须写action/edu/student/add。如果你在 JSP 里直接写action/student/add浏览器会请求http://localhost:8080/student/add跳过了/edu这一层Tomcat 根本找不到对应的 Web 应用直接 404。解决方法是永远不要在 JSP 里硬编码上下文路径用 JSTL 动态拼接form action${pageContext.request.contextPath}/student/add methodpost${pageContext.request.contextPath}在运行时动态输出当前应用的上下文路径不管部署成/edu还是/这个写法都不会错。除此之外还有一种 405 情况浏览器报HTTP 405 - Method Not Allowed多半是 HTML 表单用了methodget而 Servlet 只重写了doPost没写doGet。解决办法是别在两个方法里复制代码直接都在service()方法里处理这个场景或者让doGet和doPost互转调用这样以后换请求方式不会踩漏一半。5.4 JSP 页面 EL 不解析与中文乱码页面渲染层面的问题一个典型现象是 JSP 在浏览器里把${student.name}原样打印出来。EL 表达式没被解析原因基本锁定在两个方向第一是web.xml的版本声明太低。如果你新建项目时生成的是 2.5 或 2.3 头Servlet 容器默认按老规范处理 JSPEL 默认关闭。第二是 JSP 页面头部写了% page isELIgnoredtrue %某些老教材为了让 JSP 显示原样 EL 学习用的写法但实际项目里没理由开。解决方法是把web.xml头部改成 3.1 版本web-app xmlnshttp://xmlns.jcp.org/xml/ns/javaee xmlns:xsihttp://www.w3.org/2001/XMLSchema-instance xsi:schemaLocationhttp://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_3_1.xsd version3.1第二个大坑是中文乱码。这个项目里中文涉及三个环节每个环节都要正确设置编码。第一环是 JSP 页面本身文件顶部必须有% page contentTypetext/html;charsetUTF-8 %同时 IDE 的 File Encoding 要为 UTF-8第二环是请求参数因为 Tomcat 8 默认 GET 请求 URI 编码是 UTF-8但 POST 请求必须靠request.setCharacterEncoding(UTF-8)指定这段代码最好写在 Filter 里保证所有 POST 都过一遍第三环是数据库连接串必须有characterEncodingutf8建库时字符集用utf8mb4。这三环只要断一环中文要么在数据库中变问号要么从库里查出来乱码。我调试乱码问题时有个固定流程先用浏览器 F12 看请求头里的Content-Type是不是UTF-8再在 Service 层打印接口到的参数值最后直接连 MySQL 用 SQL 查库里的数据。这样做能快速定位是传输层、服务层还是存储层出了问题不用瞎蒙。6. 验证与进阶从“能跑”到“能讲清楚”的三个方向6.1 一条从登录到成绩录入的完整自测链项目跑通之后你得有一套自测流程来验证功能完整性而不是随机点点页面就认为完成了。我按业务链路顺序列一张验收清单每一条对应一个模块是否真正工作。编号操作步骤预期结果1管理员用 admin 登录跳到管理员首页Session 中能取到 loginUser2管理员打开学生列表第 2 页URL 带 page2展示不同的 10 条记录3教师账号登录后访问学生管理页页面正常打开但写入操作被 Service 拒绝4教师进入成绩录入页提交 3 行成绩3 行全部写入重复提交同一学生同一课程时变为更新5学生账号登录后访问教师首页被 Filter 拦截或跳转到学生首页6手动清除浏览器 Cookie 后刷新页面被重定向到登录页不能看到任何内部页面第 3 条和第 5 条是检验权限边界的关键。很多课设项目登录没问题但换账号一测就露馅——所有页面都能访问只是菜单不同。如果你发现权限拦截确实没做可以在 Filter 里加一行角色判断比如学生角色访问/teacher/前缀的路径时直接返回 403。这个改动不大但面试时的价值非常大因为你主动暴露了设计缺口并给出了修复方案。6.2 三个值得深挖的验证场景第一个是 Session 超时验证。Tomcat 默认 Session 超时时间是 30 分钟测试时你可以在web.xml里临时改成 1 分钟登录后等 60 秒再刷新页面观察 Filter 是否正确拦截。这个测试能验证 Session 生命周期不是你写在代码里的固定值而是容器管理的。第二个是事务回滚验证。在batchAddScore方法里故意构造一条非法学号比如字符串长度超过字段长度然后提交整个批次。如果事务控制正确前面合法数据的插入也会被撤销。测试完后记得把故意构造的数据删掉。第三个是连接池回收验证。把initialSize设成 1maxActive设成 2开三个浏览器标签同时操作页面观察超出连接池上限后的报错文案和等待时间。这个场景能直观理解maxWait参数的含义——等待时间超时就抛异常不超时则排队等连接。6.3 进阶改造方向把 DAO 换成 Spring JDBC接口不动如果你想在现有代码基础上做进阶改造最顺手的升级路径是把 DAO 层的原生 JDBC 换成 Spring JDBC 的 JdbcTemplateService 层和 Controller 层可以完全不动。这里的关键思路是 DAO 层的接口先保持不变只替换实现类里的 JDBC 调用这样你能深刻理解 Spring 容器“面向接口编程”的收益。Repository public class StudentDAOJdbcImpl implements StudentDAO { Autowired private JdbcTemplate jdbcTemplate; Override public ListStudent findPage(int page, int pageSize, String keyword) { String sql SELECT * FROM student WHERE name LIKE ? OR student_no LIKE ? ORDER BY student_no LIMIT ?, ?; Object[] args new Object[]{ % keyword %, % keyword %, (page - 1) * pageSize, pageSize }; return jdbcTemplate.query(sql, new BeanPropertyRowMapper(Student.class), args); } }这个改造完成后你再回头看原来手写的DBUtil.close和PreparedStatement那一套就能体会到框架帮你做了什么资源关闭、异常封装、参数绑定全部由 JdbcTemplate 接管你只需要关心 SQL 本身。但从面试角度来说原生 JDBC 的经历是你的底牌因为理解了手动管连接和事务你讲 Spring 的Transactional时才不会只是背概念。密码加密也是一个值得顺手做的改造方向。当前项目里密码是明文存储的你可以把sys_user表加一列salt注册时随机生成盐值登录时用MD5(password salt)比对。这个改造能让你讲清楚一个安全常识为什么不能直接对密码做一次 MD5 存储。但注意别在生产系统里用 MD5 做密码哈希至少要换 SHA-256 加盐更安全的方案是 BCrypt。这里分享我自己的一个教训最早做课设时我把密码直接明文存了答辩时老师问了一句“数据库泄露了怎么办”当场答不上来。从那以后我每次拿到一个 JavaWeb 老项目都会强制先检查密码存储方式、Filter 拦截范围、以及分页 SQL 的注入风险这三件事再把项目跑起来。这套检查习惯帮我在面试里躲过了很多追问。希望帮到你。本文还有配套的精品资源点击获取
返回列表