ARTICLE DETAIL

资讯详情

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

JSP+Servlet+JDBC+MySQL学生管理系统:从零搭建Java Web全栈项目

JSP+Servlet+JDBC+MySQL学生管理系统:从零搭建Java Web全栈项目 简介基于jspservletjdbcMySQL开发的学生管理系统面向计算机相关专业需要完成课程设计或毕业设计的在校生。系统涵盖学生信息、课程成绩、用户管理等常见模块采用经典的MVC分层结构配有可直接运行的完整工程。压缩包共341个文件包含84个Java源码、84个编译后class文件、36个JSP页面、61个JS脚本及28个CSS样式另有SQL脚本和项目配置文件大小仅6.99MB便于快速部署学习。资源已由作者提前测试确保能完美运行并附有项目说明链接供查看显示效果遇到问题可留言或私信交流。目前已有3320人学习下载适合用来理解Servlet与JDBC交互、掌握JavaBean封装及前后端数据流转的完整实现思路。 最近在整理硬盘的时候翻出一个大学时期的Java Web课设项目基于JSPServletJDBCMySQL的学生管理系统。说实话这套技术栈现在看着确实有点复古但我在实际开发和带新人的过程中发现它依然是理解Java Web后端运行机制最直观的组合之一。很多新手直接上手Spring Boot往往搞不清请求是怎么被处理的、Session是怎么维持的、数据库连接为什么会泄漏而这些在JSPServlet时代都是必须亲手解决的问题。这个项目包含了学生信息管理系统中非常典型的功能模块登录认证、CRUD操作、分页查询、模糊搜索、会话管理。虽然不是微服务也不是分布式但它五脏俱全特别适合用来打通前端页面→Servlet控制器→Service业务层→DAO数据访问→MySQL存储这条完整链路。如果你正在学Java Web或者马上要交课程设计又或者想回头补一补基础这篇文章应该能帮你少走不少弯路。1. 这套老古董技术栈为什么还值得写先说一个很多人会问的问题现在企业里都是Spring Boot、MyBatis Plus学JSPServlet还有意义吗我的看法是有意义而且意义比你想的要大。Servlet是Java Web的基石Spring MVC的前端控制器DispatcherServlet本质上就是一个Servlet只是它帮你封装了太多东西。当你在Spring Boot里写一个RequestMapping注解时背后发生的URL匹配、请求分发、参数绑定底层逻辑和手写Servlet是一模一样的。只是框架把这些过程自动化了所以你感受不到。如果你不懂Servlet规范遇到404、405、请求参数丢失这类问题排查起来会比较痛苦。另外JSP也有它的价值。虽然现在主流是前后端分离但JSP里那种Java代码直接嵌入HTML的方式对于理解服务端渲染SSR的概念非常有帮助。你亲手在%%标签里写一段循环把数据库里的学生列表渲染成表格比看十篇前后端分离架构优势的文章都管用。等你理解了渲染发生在服务端和客户端到底有什么区别再去看Thymeleaf、FreeMarker甚至Vue的SSR方案都会觉得顺理成章。实际开发这套系统的过程中我还发现一个很现实的好处它对机器配置要求极低。一个Tomcat 8.5一个JDK 1.8一个MySQL 5.7加起来不到300MB的部署环境随便一台老旧笔记本都能跑起来。不像现在的前端工程动辄npm install装出几个GB的node_modules这套东西轻量、直观、没有黑魔法。2. 学生管理系统的功能拆解与数据库设计写任何项目之前先把功能边界划清楚。我这个系统的用户角色只有一种管理员。功能没有做得很花哨专注解决学生信息管理的几个核心场景。2.1 功能模块清单我最终实现的功能模块如下登录模块管理员输入账号密码登录使用Session保存登录状态未登录用户访问受限页面时自动跳转到登录页。学生信息列表分页展示学生基本信息每页10条底部有页码导航。新增学生表单录入学号、姓名、性别、年龄、班级、联系方式提交后写入数据库。编辑学生点击编辑按钮表单回显当前学生的既有数据修改后提交更新。删除学生点击删除按钮根据学号主键删除对应记录。模糊查询支持按学生姓名或班级进行模糊匹配查询结果同样支持分页。退出登录销毁Session并跳转回登录页。看起来功能不算多但增删改查登录分页已经覆盖了Java Web课设的绝大多数要求。如果你需要交作业在这个基础上加个成绩管理或选课管理模块架构是完全兼容的。2.2 数据库表结构设计数据库我取名为student_db核心表只有一张t_student。设计上有一个细节值得注意我用stu_no学号作为业务主键同时设置了唯一索引。为什么不直接用自增ID因为学号本身就是唯一标识学生的业务字段用它做查询条件的频率远高于自增ID。但自增ID也保留了一份作为物理主键防止业务主键将来发生变化比如学号规则调整时牵一发动全身。CREATE DATABASE IF NOT EXISTS student_db DEFAULT CHARSET utf8mb4; USE student_db; CREATE TABLE t_user ( id INT PRIMARY KEY AUTO_INCREMENT, username VARCHAR(50) NOT NULL UNIQUE, password VARCHAR(100) NOT NULL ); CREATE TABLE t_student ( id INT PRIMARY KEY AUTO_INCREMENT, stu_no VARCHAR(20) NOT NULL, name VARCHAR(50) NOT NULL, gender CHAR(1), age INT, class_name VARCHAR(50), phone VARCHAR(20), create_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP, UNIQUE KEY uk_stu_no (stu_no) ); INSERT INTO t_user (username, password) VALUES (admin, 123456);这里有个很常见的坑MySQL 5.7默认的字符集是latin1如果你建表时没指定utf8mb4插入中文姓名时就会变成???。而且utf8mb4和utf8还不一样utf8mb4是真正的四字节UTF-8可以存储emoji等特殊字符。在这个项目中直接用utf8mb4是更稳妥的选择。另外关于年龄字段我的表里直接用INT存储。如果你要做得更规范可以用TINYINT UNSIGNED因为年龄不可能超过255也不可能为负。虽然对这个小项目来说差别不大但这种字段设计的思考习惯会在你写更复杂的业务时派上用场。3. 后端核心实现JDBC连接管理的两种姿势JDBC是这套系统里最容易写错的部分也是最能体现一个开发者经验的地方。我在写这个项目时一开始把获取连接的代码写在每个DAO方法里后来重构时发现重复代码太多而且连接管理极度混乱。这里我把两种写法的对比展开讲一下。3.1 第一种写法每个DAO方法内获取连接反面教材最初的学生DAO是这样的public ListStudent findAll() { Connection conn null; PreparedStatement ps null; ResultSet rs null; ListStudent list new ArrayList(); try { Class.forName(com.mysql.jdbc.Driver); conn DriverManager.getConnection( jdbc:mysql://localhost:3306/student_db?useUnicodetruecharacterEncodingutf8, root, 123456); String sql SELECT * FROM t_student; ps conn.prepareStatement(sql); rs ps.executeQuery(); while (rs.next()) { // 封装对象... } } catch (Exception e) { e.printStackTrace(); } finally { // 关闭 rs、ps、conn... } return list; }这段代码有两个问题。第一Class.forName这行其实可以省略因为MySQL 5.0以上的JDBC驱动在加载时已经通过SPI机制自动注册了驱动类你直接DriverManager.getConnection是能连上的。但这行代码在面试里经常被问到所以保留它作为显式加载驱动的示范也无妨。第二每个方法都写一遍完整的加载驱动→建立连接→执行SQL→关闭资源流程代码冗余度极高。一旦数据库连接信息变化比如密码改了你得把所有DAO方法都改一遍极易遗漏。3.2 第二种写法抽取DBUtil工具类管理连接后来的重构版本中我把连接管理抽取成一个独立的DBUtil类public class DBUtil { private static final String URL jdbc:mysql://localhost:3306/student_db?useUnicodetruecharacterEncodingutf8useSSLfalseserverTimezoneAsia/Shanghai; private static final String USERNAME root; private static final String PASSWORD 123456; static { try { Class.forName(com.mysql.jdbc.Driver); } catch (ClassNotFoundException e) { e.printStackTrace(); } } public static Connection getConnection() throws SQLException { return DriverManager.getConnection(URL, USERNAME, PASSWORD); } public static void close(Connection conn, PreparedStatement ps, ResultSet rs) { try { if (rs ! null) rs.close(); } catch (SQLException e) { e.printStackTrace(); } try { if (ps ! null) ps.close(); } catch (SQLException e) { e.printStackTrace(); } try { if (conn ! null) conn.close(); } catch (SQLException e) { e.printStackTrace(); } } }这里有几个连接参数的细节非常关键。useSSLfalse是MySQL 5.7的新要求不设置的话每次连接都会在控制台打印一大段SSL警告。serverTimezoneAsia/Shanghai是解决时区问题必须加的否则会报The server time zone value Öйú±ê׼ʱ¼ä的乱码时区错误。这些参数看着不起眼但少了任何一个都可能在运行时踩坑。JDBC的六步走流程加载驱动、获取连接、创建Statement、执行SQL、处理结果集、关闭资源。这套流程在任何Java数据库操作里都不会变只是框架帮你封装了。所以在DBUtil里我做的是第一步和第二步的统一封装而第六步也做了一个静态的close方法让调用方的finally块简洁很多。3.3 数据库连接池的补充说明这个项目为了保持浅显易懂用的是DriverManager直接获取连接。但在实际的Java Web项目里这种写法只适合学习和课设不适合生产环境。因为每次请求都要经历建立TCP连接→身份验证→断开连接的完整过程而一次SQL执行可能只需要几毫秒连接建立的消耗反而成了主要瓶颈。生产环境应该使用连接池如Druid、HikariCP原理很简单启动时预先创建一批连接放在池子里谁要用就借出去用完还回来而不是销毁。连接池还自带超时回收、空闲检测、SQL防注入过滤等功能。如果你在这个项目基础上做毕业设计强烈建议引入Druid连接池代码改动量不大但在技术面试时可以理直气壮地说自己熟悉数据库连接池的原理和使用。4. Servlet层设计URL映射与控制器的编写思路有了数据库访问层接下来就是Servlet层。这一层的核心职责是接收HTTP请求、解析参数、调用Service层方法、决定跳转到哪个JSP页面。我在这个项目里没有引入任何MVC框架所有的请求分发都通过Servlet手动完成。4.1 使用web.xml方式配置Servlet映射这里我特意用了web.xml方式而不是WebServlet注解方式原因是课设阶段老师往往要求掌握配置文件方式。另外web.xml方式可以更直观地看到请求URL和Servlet类的对应关系对理解映射机制有帮助。?xml version1.0 encodingUTF-8? 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_4_0.xsd version4.0 display-nameStudent Management System/display-name welcome-file-list welcome-filelogin.jsp/welcome-file /welcome-file-list servlet servlet-nameLoginServlet/servlet-name servlet-classcom.student.servlet.LoginServlet/servlet-class /servlet servlet-mapping servlet-nameLoginServlet/servlet-name url-pattern/login/url-pattern /servlet-mapping servlet servlet-nameStudentListServlet/servlet-name servlet-classcom.student.servlet.StudentListServlet/servlet-class /servlet servlet-mapping servlet-nameStudentListServlet/servlet-name url-pattern/list/url-pattern /servlet-mapping !-- 其他Servlet映射类似省略 -- /web-app注意一个细节url-pattern的写法。/login这种精确匹配只有URL完全是/login时才会命中。如果你写的是/login/*那/login/anything也会命中。还有种写法是*.do表示以.do结尾的URL都命中比如Struts时代的约定。不同写法的匹配优先级在Servlet规范里有明确规定精确匹配 最长路径匹配 扩展名匹配 默认匹配。如果你发现请求被莫名其妙的Servlet拦截了优先检查这个优先级顺序。4.2 典型的LoginServlet实现登录逻辑是整个系统的入口也是Session管理的关键。我给出核心代码WebServlet(/login) // 如果你用的Servlet 3.0也可以这样替代web.xml public class LoginServlet extends HttpServlet { private UserService userService new UserService(); 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); // 简单的前端校验 if (username null || username.trim().isEmpty() || password null || password.trim().isEmpty()) { request.setAttribute(msg, 用户名和密码不能为空); request.getRequestDispatcher(/login.jsp).forward(request, response); return; } User user userService.login(username, password); if (user ! null) { // 登录成功保存会话信息 HttpSession session request.getSession(); session.setAttribute(loginUser, user); session.setMaxInactiveInterval(30 * 60); // 30分钟过期 response.sendRedirect(request.getContextPath() /list); } else { request.setAttribute(msg, 用户名或密码错误); request.getRequestDispatcher(/login.jsp).forward(request, response); } } Override protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { doPost(request, response); } }关于doGet和doPost的处理这里我直接让doGet也调用doPost是为了防止用户直接在浏览器地址栏输入/login时出现405错误。但要注意登录操作严格来说应该只接受POST请求因为GET请求的URL会携带密码参数既有安全风险又会污染访问日志。这里做成两种方式都支持是出于演示方便生产环境建议在doGet里直接返回405或重定向到登录页。另一个关键点是request.setCharacterEncoding(UTF-8)的位置。这行代码必须放在获取任何参数之前否则POST提交的中文会出现乱码。GET请求的乱码问题则是另一套逻辑Tomcat 8及以上版本默认的GET请求URI编码已经是UTF-8但如果你用的Tomcat 7还需要在server.xml里配置URIEncodingUTF-8。这算是一个经典的历史坑。4.3 登录验证过滤器可选但推荐在学生管理系统的最终版本里我没有加登录过滤器而是简单地在每个受保护Servlet内部检查Session。但如果你想让代码更规范应该用一个Filter统一处理未登录的拦截WebFilter(/*) public class LoginFilter implements Filter { Override public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { HttpServletRequest req (HttpServletRequest) request; HttpServletResponse resp (HttpServletResponse) response; HttpSession session req.getSession(false); // 不创建新Session String uri req.getRequestURI(); if (session ! null session.getAttribute(loginUser) ! null || uri.endsWith(login.jsp) || uri.contains(/login)) { chain.doFilter(request, response); } else { resp.sendRedirect(req.getContextPath() /login.jsp); } } }注意我用了req.getSession(false)这表示如果当前没有Session就返回null而不是创建一个新的。很多新手会写成getSession()这会带来一个安全隐患即使你只是打开登录页服务器也会为你的浏览器创建一次Session。虽然在这个小项目里影响不大但养成getSession(false)的习惯是好的尤其在开发高并发系统时无意义的Session创建会占用内存。5. JSP页面渲染与表单回显的细节JSP页面是整个系统的最上层也是很多初学者写起来最容易给自己挖坑的地方。核心问题集中在如何正确使用EL表达式与JSTL标签、表单回显时怎么处理空值、页面之间跳转路径怎么找准。5.1 使用EL表达式与JSTL渲染学生列表我最终的学生列表面板使用JSTL标签库替代了原始的%%脚本片段。如果你去翻很多老项目会发现页面上写满了% for(int i0; ilist.size(); i){ %这种脚本片段。这种写法的缺点是页面维护困难而且脚本片段里出现的异常会导致整个页面报错错误信息还极其难定位。而JSTLEL是JSP官方推荐的替代方案逻辑更清晰、页面更干净。% page contentTypetext/html;charsetUTF-8 languagejava % % taglib prefixc urihttp://java.sun.com/jsp/jstl/core % html head title学生列表/title link relstylesheet typetext/css hrefcss/style.css /head body div classcontainer h2学生信息列表/h2 div classtoolbar a hrefadd.jsp classbtn-add新增学生/a form actionlist methodget classsearch-form input typetext namekeyword value${keyword} placeholder输入姓名或班级 button typesubmit查询/button /form /div table border1 cellpadding10 cellspacing0 tr th学号/thth姓名/thth性别/thth年龄/thth班级/thth联系方式/thth操作/th /tr c:forEach items${page.list} varstu tr td${stu.stuNo}/td td${stu.name}/td td${stu.gender}/td td${stu.age}/td td${stu.className}/td td${stu.phone}/td td a hrefedit?stuNo${stu.stuNo}编辑/a a hrefdelete?stuNo${stu.stuNo} onclickreturn confirm(确定删除该学生吗);删除/a /td /tr /c:forEach /table div classpagination c:if test${page.currentPage 1} a hreflist?currentPage${page.currentPage - 1}keyword${keyword}上一页/a /c:if span第 ${page.currentPage} / ${page.totalPage} 页/span c:if test${page.currentPage page.totalPage} a hreflist?currentPage${page.currentPage 1}keyword${keyword}下一页/a /c:if /div /div /body /html一个重要的踩坑点EL表达式中访问的是JavaBean的getXxx()方法去掉get后首字母小写的属性名。如果数据库字段是stu_no我在Java对象里定义为stuNo那么在JSP里就要用${stu.stuNo}。有些人习惯把数据库字段的下划线照搬到Java属性里stu_no这样EL表达式就要写成${stu.getStu_no()}既不美观也容易出错。所以在设计JavaBean时属性名要遵循驼峰命名规则与数据库字段的映射关系在DAO层的ResultSet封装中处理好。5.2 编辑页面的表单回显空值防御编辑页面的核心需求是页面加载时将数据库中已有的学生信息填充到表单的各个输入框中。实现的逻辑是在EditServlet中根据stuNo查出学生对象放入request域然后forward到edit.jsp在JSP中通过EL表达式输出到value属性里。tr td学号/td tdinput typetext namestuNo value${student.stuNo} readonly/td /tr tr td姓名/td tdinput typetext namename value${student.name}/td /tr这里有一个很多新手必踩的坑如果${student.name}的值是null浏览器显示时输入框不会为空而是会出现一行null字面量。原因是EL表达式对null值的输出结果确实是空字符串但如果你在Servlet中初始化对象时不小心把某个字段设成了字符串null比如从request参数直接用request.getParameter(name)得到的是null字符串而不是null页面上就会显示出来。解决方法是value${empty student.name ? : student.name}这样做的好处是当字段为null或空字符串时输入框显示空白不会出现null字样的尴尬情况。这个细节在你做其他任何Web项目时都会用到建议形成条件反射。5.3 JSP页面之间的路径问题JSP里写href和src时最安全的写法是使用c:url标签或者${pageContext.request.contextPath}拼接绝对路径。比如link relstylesheet typetext/css href${pageContext.request.contextPath}/css/style.css a href${pageContext.request.contextPath}/list学生列表/a为什么不直接写hreflist因为在转发forward的情况下浏览器的地址栏URL不会改变。比如你在/login页面通过request.getRequestDispatcher(/list).forward()跳转后地址栏仍然是/login但页面内容已经是列表页了。此时如果你在列表页里写了一个相对路径hrefedit?stuNo001浏览器解析时会基于/login目录去拼路径得到/login/edit然后404。而加上${pageContext.request.contextPath}前缀后永远是/student_db/list、/student_db/edit不会因为页面来源不同而错乱。6. 分页和模糊查询的组合实现一个经典业务场景分页和模糊查询是两个独立的功能但放到一起实现时非常考察代码组织的严密性。尤其是查询条件的持久化——用户在第一页搜索张翻到第二页时搜索条件张应该仍然保留。这个细节很多课设项目都做得不对。6.1 PageBean的设计我定义了一个PageBeanT泛型类来封装分页相关的所有数据public class PageBeanT { private int currentPage; // 当前页码从1开始 private int pageSize; // 每页显示条数 private int totalCount; // 总记录数 private int totalPage; // 总页数 private ListT list; // 当前页的数据集合 private String keyword; // 查询关键字 // getters and setters... }为什么把这些字段封装成一个对象而不是在Servlet里散乱地传递多个参数因为这样可以一次性从request中获取、放置页面渲染时只需从${page.xxx}取数据逻辑非常集中。6.2 DAO层的动态SQL拼接模糊查询的关键在于SQL语句里使用LIKE关键字。但如果你直接拼接字符串会引入SQL注入的风险。正确的做法是使用PreparedStatement的占位符?机制public ListStudent findByPage(int currentPage, int pageSize, String keyword) { ListStudent list new ArrayList(); String sql SELECT * FROM t_student; StringBuilder sb new StringBuilder(sql); ListObject params new ArrayList(); if (keyword ! null !keyword.trim().isEmpty()) { sb.append( WHERE name LIKE ? OR class_name LIKE ?); params.add(% keyword.trim() %); params.add(% keyword.trim() %); } sb.append( ORDER BY id DESC LIMIT ?, ?); params.add((currentPage - 1) * pageSize); params.add(pageSize); try (Connection conn DBUtil.getConnection(); PreparedStatement ps conn.prepareStatement(sb.toString())) { for (int i 0; i params.size(); i) { ps.setObject(i 1, params.get(i)); } ResultSet rs ps.executeQuery(); while (rs.next()) { // 封装Student对象... } } catch (SQLException e) { e.printStackTrace(); } return list; }这里我特别注意了两个地方。第一LIMIT后面的偏移量计算第1页的偏移量是0第2页是10第3页是20。公式是(currentPage - 1) * pageSize因为数据库的偏移量是从0开始的。第二PreparedStatement的下标从1开始而不是从0开始这个和ResultSet的下标是一样的新手容易在这里越界。关于防SQL注入PreparedStatement的预编译机制会在数据库端将参数转义处理比如用户输入 OR 11时它会把整个字符串当成一个字面量匹配而不是当成SQL语句的一部分。如果你用字符串拼接的方式写SQL用户输入; DROP TABLE t_student;--可能导致灾难性的后果。所以无论项目大小只要写JDBC操作就用PreparedStatement不要用Statement。6.3 查询条件的跨页面保留查询条件丢失是模糊查询分页最常见的bug。用户在第一页输入张三点击查询后正确显示了结果。翻到第二页时如果请求里没有携带keyword张三查询条件就丢失了分页变成了全表数据的第二页而不是查询结果的第二页。解决思路是在分页导航的链接里面把keyword也作为参数传递。我上面的JSP代码里已经体现了这一点a hreflist?currentPage${page.currentPage - 1}keyword${keyword}上一页/a而在Servlet端每次处理请求时都要把keyword从request参数中重新取出来存入PageBean对象中再放入request域。这样无论用户怎么翻页关键词都不会丢。一个要注意的小细节当页码超过总页数时怎么处理。比如总页数只有3页用户手动把URL里的currentPage改成99DAO层查询结果会是空。更好的做法是在Servlet里加一个校验int currentPage 1; if (request.getParameter(currentPage) ! null) { currentPage Integer.parseInt(request.getParameter(currentPage)); } if (currentPage 1) currentPage 1;这里Integer.parseInt如果遇到非数字参数会抛出NumberFormatException导致500错误。更加健壮的做法是包一层try-catch解析失败时默认使用1。7. 项目打包、部署与踩坑记录最后聊聊整个项目的部署流程和我在这个过程中踩过的坑。这部分内容比较零散但每一条都是真实经历写出来帮你省点时间。7.1 从IDE导出war包并部署到Tomcat开发完成后项目需要部署到Tomcat运行。我使用的开发环境是IntelliJ IDEA但直接依赖IDEA的Tomcat集成插件运行项目时偶尔会出现一些诡异的类加载问题。所以最终测试时我选择手动打包部署这样也更能理解Web应用的标准目录结构。在IDEA中操作路径是File → Project Structure → Artifacts → 点 → Web Application: Exploded → 在Output Layout里检查是否有lib目录并添加所有依赖的JDBC驱动JAR包。然后通过Build → Build Artifacts → Build来生成war包。war包的本质是一个zip压缩文件它的目录结构必须符合Java Web规范student-management.war ├── META-INF/ │ └── MANIFEST.MF ├── WEB-INF/ │ ├── classes/ (编译后的.class文件) │ │ └── com/student/... │ ├── lib/ (第三方jar包如mysql-connector-java.jar) │ └── web.xml ├── css/ ├── js/ ├── login.jsp ├── list.jsp └── add.jsp把war包直接扔到Tomcat的webapps目录下启动Tomcat它就会自动解压并部署。访问路径是http://localhost:8080/student-management/注意这个路径名默认与war包的文件名一致。7.2 部署过程中的三大经典报错第一个报错java.lang.ClassNotFoundException: com.mysql.jdbc.Driver这个错误几乎每个初学者都遇到过原因多半是JDBC驱动JAR包没有正确放到WEB-INF/lib目录下。注意是WEB-INF/lib不是WEB-INF/classes也不是Tomcat的lib目录。启动Tomcat时WEB-INF/lib下的jar包只对当前应用可见不会污染其他应用。判断jar包是否加载成功可以在Tomcat启动日志里看到类似Deploying web application archive的信息。第二个报错The server time zone value Öйú±ê׼ʱ¼ä is unrecognized这个我在前面提到过MySQL 8.0以上的驱动对时区敏感URL里必须加serverTimezoneAsia/Shanghai。另外还有一个变体报错是java.sql.SQLException: Connection is not available如果你用了连接池通常是连接没有正确归还导致的连接池耗尽。第三个报错Error during artifact deployment. See server log for details这是IDEA部署时的通用错误具体原因要看日志。最常见的原因有两个一个是web.xml的web-app版本号与Tomcat版本不匹配比如Tomcat 9不支持web-app的3.0版本另一个是某个Servlet类漏写了WebServlet或web.xml中的servlet-class写错导致类加载失败。7.3 MySQL 8.0驱动兼容性这个项目的开发经历了MySQL 5.7和8.0两个版本其中一个很隐蔽的问题是驱动类名的变化。MySQL 5.x时代驱动类是com.mysql.jdbc.DriverMySQL 8.x时代驱动类改成了com.mysql.cj.jdbc.Driver。旧类名的兼容性在8.0中被保留了一段时间但会在启动时打印警告日志。另一个是我在实际运行中遇到的MySQL 8.0的密码加密方式变了默认使用caching_sha2_password而某些老版本的JDBC驱动5.1.x系列不支持这种认证方式会报Unable to load authentication plugin caching_sha2_password。解决方法是使用较新的mysql-connector-java-8.0.x.jar驱动或者在MySQL里把用户的认证方式改回mysql_native_passwordALTER USER rootlocalhost IDENTIFIED WITH mysql_native_password BY 你的密码;如果你是在本地练手更推荐第二种方案因为很多老教程默认root密码是123456改认证方式不影响已有代码。7.4 JSP文件修改后不生效的检查链很多人在修改JSP后遇到改了没反应的问题。这个问题的检查链是浏览器缓存按CtrlF5强制刷新排除浏览器缓存干扰。IDEA中JSP的编译缓存IDEA有时会缓存JSP的翻译结果尝试Rebuild Project。Tomcat的work目录Tomcat会把JSP翻译成Java文件再编译成class文件存在Tomcat安装目录/work/Catalina/localhost/下。如果你直接在Tomcat的webapps里改了JSP而没重启Tomcat正常情况下Tomcat会检测到文件变化自动重编译但有时文件时间戳没变就不会触发。手动删除work目录下的对应文件夹重启Tomcat即可。文件编码问题JSP文件必须保存为UTF-8编码且在页面头部声明% page contentTypetext/html;charsetUTF-8 %。如果你用的IDE默认编码是GBK而Tomcat按UTF-8解析页面中文就会乱码看起来就像没有正确修改。8. 这套代码的后续扩展方向如果这篇博文里的项目是你用来交课设或起步学习的你可以在这个基础上做几个有趣的扩展。这里我根据自己的经验给三个方向方向一引入Druid连接池。改动量不大Dao层所有DBUtil.getConnection()换成DruidDataSource.getConnection()即可但随之带来的是连接池配置、监控、防注入等功能能写进简历的技术点更多。方向二把Servlet精简成BaseServlet。通过反射机制将/list、/add、/edit、/delete映射到同一个StudentServlet的不同方法上避免创建大量Servlet类。这其实就是MVC框架中路由分发器的雏形。方向三增加成绩管理模块。设计t_score表加一个班级平均分统计的SQL查询用JSP展示统计结果。这个扩展虽然听着普通但在课设答辩时老师非常喜欢问你是怎么统计平均分的如果数据量大了怎么优化回答好这些问题展示的不只是CRUD能力。我在实现这些扩展时最大的感受是这个项目就像一套乐高积木的底座功能模块之间耦合度低替换和添加都相对容易。这也是它作为课设项目被广泛使用的原因——结构简单清晰每一层都能独立修改而不影响其他层。本文还有配套的精品资源点击获取
返回列表