ARTICLE DETAIL

资讯详情

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

JSP连接MySQL增删改查实战:从JDBC到Servlet的完整流程

JSP连接MySQL增删改查实战:从JDBC到Servlet的完整流程 做Java Web课设的朋友十有七八会在第一步就卡住代码照着教程敲完了Tomcat也启动了浏览器却给你一个500或者干脆报ClassNotFoundException: com.mysql.cj.jdbc.Driver。我见过太多人把时间耗在环境问题上最后真正用来理解增删改查业务逻辑的时间反而没多少。这个标题看起来简单——JSP连接MySQL数据库并实现增删改查功能但把它真正跑通涉及JDK版本、Tomcat版本、MySQL版本、JDBC驱动、IDEA部署配置、Servlet与JSP配合方式再到数据表的增删改查SQL语句一整条链路。这篇博文就按我实际做课设和带项目时的顺序来写把从零到跑通的每个关键点都拆开讲清楚。内容虽然是用学生信息管理系统当例子但换到任何业务表思路完全一致。1. 为什么还在用JSPMySQL这套技术栈的真实定位1.1 什么场景下它依然是合理选择JSP属于Java EE的遗留技术但注意遗留不等于过时。在数据库课程设计、校内实训、毕业设计这类场景里JSPServletMySQL的组合依然是出现频率最高的方案之一。原因很实际它够底层、够直观能把HTTP请求如何到达服务器、服务器如何操作数据库、结果又如何渲染回页面的完整链路清清楚楚地摆在你面前。相比之下如果直接用Spring BootMyBatis Plus很多东西被框架封装掉了你反而看不懂数据到底是怎么流到页面的。所以我的建议是如果你是在做课程设计或者刚学完Java SE想入门Web开发JSPServlet是理解Web本质的最好路径。如果你是在准备生产环境项目那显然应该直接学主流框架。搞清楚自己处在哪个阶段再决定要不要用这套技术栈。还有一个非常现实的原因很多学校的数据库课程设计题目要求里明确写着使用JSPServletMySQL或者Java Web技术。这时候与其纠结这个组合老不老不如把它吃透至少能顺利过答辩。1.2 版本搭配与安装清单最容易翻车的地方版本问题是我每次都要强调的因为大部分连不上数据库的报错根源不在代码而在版本不匹配。网上老教程用的是MySQL 5.xmysql-connector-java 5.x你装的是MySQL 8.0驱动类名和URL写法都对不上自然连不上。我建议的稳定搭配如下这套组合我踩过坑后固定下来一直很稳组件推荐版本说明JDK1.8 或 11JDK 8最稳Tomcat 9以下必需JDK 11也能配Tomcat 9Tomcat9.0.x对应Servlet 4.0规范类名是javax.servletMySQL8.0.x使用com.mysql.cj.jdbc.Driver驱动类JDBC驱动mysql-connector-j 8.0.x不要用老版的5.x驱动连MySQL 8IDEA2021以上社区版即可课设功能完全够用这里有个关键细节如果你用了Tomcat 10以上版本Servlet包名已经变成了jakarta.servlet而不是javax.servlet。网上大量JSP教程还是基于javax写的直接用Tomcat 10会连编译都过不去报package javax.servlet does not exist。所以新手老老实实用Tomcat 9等你明白这两个包的差别后再升级不迟。MySQL 8.0安装时要注意认证插件的选择。8.0默认的caching_sha2_password插件在旧版JDBC驱动下会报Unable to load authentication plugin。解决办法有两个一是驱动依赖升级到8.x版本推荐二是在创建用户时指定mysql_native_password。我建议直接升级驱动别改认证插件因为新版驱动和默认插件才是未来的方向。2. 数据库设计与JDBC连接层先稳住地基2.1 建库建表用学生信息管理系统的表结构说话增删改查的第一步不是写代码而是把表设计好。我见过太多人上来就写Servlet写到一半发现字段对不上又回头改数据库来回折腾。正确顺序是先确定页面要展示哪些字段再建表再写代码。以网上最常见的学生信息管理系统为例我设计一张student表字段如下CREATE DATABASE IF NOT EXISTS student_db DEFAULT CHARACTER SET utf8mb4; USE student_db; DROP TABLE IF EXISTS student; CREATE TABLE student ( id INT PRIMARY KEY AUTO_INCREMENT COMMENT 主键ID, student_no VARCHAR(20) NOT NULL UNIQUE COMMENT 学号, name VARCHAR(50) NOT NULL COMMENT 姓名, gender CHAR(1) DEFAULT 男 COMMENT 性别, age INT COMMENT 年龄, major VARCHAR(50) COMMENT 专业, create_time DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT 创建时间 ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT学生信息表;注意几个细节字符集统一用utf8mb4而不是utf8因为utf8mb4才是完整的UTF-8能存下emoji和生僻字id用自增主键student_no加唯一约束防止重复数据create_time让数据库自动填充当前时间不用在代码里手动set。这些设计不是花架子数据库设计直接影响后面代码的复杂度和系统健壮性。字段类型选错、没加唯一约束后面做新增和修改时各种奇葩数据都会冒出来。建议做完CREATE后顺手插入三条测试数据方便后面验证查询功能INSERT INTO student (student_no, name, gender, age, major) VALUES (2024001, 张三, 男, 20, 计算机科学与技术), (2024002, 李四, 女, 19, 软件工程), (2024003, 王五, 男, 21, 数据科学与大数据);2.2 写一个能用的JDBC连接工具类附连接池升级方案表建好后接下来是Java和MySQL之间的桥——JDBC连接工具类。最基础的写法是DriverManager.getConnection()简单直接适合初学阶段。工具类如下package com.student.util; import java.sql.Connection; import java.sql.DriverManager; import java.sql.SQLException; public class DBUtil { private static final String URL jdbc:mysql://localhost:3306/student_db?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/Shanghai; private static final String USER root; private static final String PASSWORD 你自己的密码; static { try { Class.forName(com.mysql.cj.jdbc.Driver); } catch (ClassNotFoundException e) { e.printStackTrace(); throw new ExceptionInInitializerError(MySQL驱动加载失败检查jar包是否导入); } } public static Connection getConnection() throws SQLException { return DriverManager.getConnection(URL, USER, PASSWORD); } public static void close(AutoCloseable... resources) { for (AutoCloseable r : resources) { if (r ! null) { try { r.close(); } catch (Exception e) { e.printStackTrace(); } } } } }URL里的serverTimezoneAsia/Shanghai是MySQL 8必须加的不加会报时区错误characterEncodingutf8保证中文字符正常存取。连接工具类算是个半成品——每个教程都这么写但基本没人告诉你这种方法每次getConnection()都会新建物理连接性能很差真实项目不会这么干。所以我再给出一个用HikariCP连接池的进阶版本这是生产级的做法。先在WEB-INF/lib下引入HikariCP和slf4j-api的jar包然后写配置类package com.student.util; import com.zaxxer.hikari.HikariConfig; import com.zaxxer.hikari.HikariDataSource; import javax.sql.DataSource; import java.sql.Connection; import java.sql.SQLException; public class DBUtil { private static final HikariDataSource dataSource; static { HikariConfig config new HikariConfig(); config.setJdbcUrl(jdbc:mysql://localhost:3306/student_db?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/Shanghai); config.setUsername(root); config.setPassword(你自己的密码); config.setMaximumPoolSize(10); config.setMinimumIdle(2); config.setConnectionTimeout(30000); dataSource new HikariDataSource(config); } public static Connection getConnection() throws SQLException { return dataSource.getConnection(); } }池化连接的好处是应用启动时先建立一批连接放在池里每次getConnection()只是从池里借用完还回去省去了反复建连的开销。对于课设来说用量不大但理解了这层逻辑以后看Spring Boot的HikariCP默认配置就完全不陌生。顺带提醒使用完连接后一定要关闭。最稳妥的方式是Java 7引入的try-with-resources写法代码里不需要手动finally关闭try (Connection conn DBUtil.getConnection(); PreparedStatement ps conn.prepareStatement(sql); ResultSet rs ps.executeQuery()) { // 处理结果集 } catch (SQLException e) { e.printStackTrace(); }这种写法会自动调用close()比手动写finally干净得多。3. 增删改查的完整实现JavaBean DAO Servlet JSP四层配合3.1 JavaBean与DAO层把SQL从页面里赶出去很多新手刚开始写课设时喜欢把% ... %直接塞进JSP里面写一堆JDBC代码。这种写法当时确实跑通了但后面改需求时会让人崩溃。正确的做法是拆成四层JSP只负责展示Servlet只负责接收请求和跳转DAO只负责数据库操作JavaBean负责封装数据行。先说JavaBean它就是一张表的对象化映射。字段名和表字段保持一致提供无参构造和getter/setterpackage com.student.bean; import java.time.LocalDateTime; public class Student { private int id; private String studentNo; private String name; private String gender; private int age; private String major; private LocalDateTime createTime; public Student() {} public Student(String studentNo, String name, String gender, int age, String major) { this.studentNo studentNo; this.name name; this.gender gender; this.age age; this.major major; } // 省略所有getter和setter // IDEA快捷键 AltInsert 可以快速生成 }接着是DAO层。以StudentDAO为例把增删改查的SQL集中管理。注意一个原则DAO里只负责数据库操作不处理业务逻辑更不能输出HTML。代码如下package com.student.dao; import com.student.bean.Student; import com.student.util.DBUtil; import java.sql.*; import java.util.ArrayList; import java.util.List; public class StudentDAO { // 新增 public boolean addStudent(Student stu) { String sql INSERT INTO student (student_no, name, gender, age, major) VALUES (?, ?, ?, ?, ?); try (Connection conn DBUtil.getConnection(); PreparedStatement ps conn.prepareStatement(sql)) { ps.setString(1, stu.getStudentNo()); ps.setString(2, stu.getName()); ps.setString(3, stu.getGender()); ps.setInt(4, stu.getAge()); ps.setString(5, stu.getMajor()); return ps.executeUpdate() 0; } catch (SQLException e) { e.printStackTrace(); return false; } } // 按ID删除 public boolean deleteStudent(int id) { String sql DELETE FROM student WHERE id ?; try (Connection conn DBUtil.getConnection(); PreparedStatement ps conn.prepareStatement(sql)) { ps.setInt(1, id); return ps.executeUpdate() 0; } catch (SQLException e) { e.printStackTrace(); return false; } } // 更新 public boolean updateStudent(Student stu) { String sql UPDATE student SET student_no ?, name ?, gender ?, age ?, major ? WHERE id ?; try (Connection conn DBUtil.getConnection(); PreparedStatement ps conn.prepareStatement(sql)) { ps.setString(1, stu.getStudentNo()); ps.setString(2, stu.getName()); ps.setString(3, stu.getGender()); ps.setInt(4, stu.getAge()); ps.setString(5, stu.getMajor()); ps.setInt(6, stu.getId()); return ps.executeUpdate() 0; } catch (SQLException e) { e.printStackTrace(); return false; } } // 按ID查询单条 public Student getStudentById(int id) { String sql SELECT * FROM student WHERE id ?; try (Connection conn DBUtil.getConnection(); PreparedStatement ps conn.prepareStatement(sql)) { ps.setInt(1, id); try (ResultSet rs ps.executeQuery()) { if (rs.next()) { return mapRow(rs); } } } catch (SQLException e) { e.printStackTrace(); } return null; } // 查询全部 public ListStudent getAllStudents() { ListStudent list new ArrayList(); String sql SELECT * FROM student ORDER BY id; try (Connection conn DBUtil.getConnection(); PreparedStatement ps conn.prepareStatement(sql); ResultSet rs ps.executeQuery()) { while (rs.next()) { list.add(mapRow(rs)); } } catch (SQLException e) { e.printStackTrace(); } return list; } private Student mapRow(ResultSet rs) throws SQLException { Student s new Student(); s.setId(rs.getInt(id)); s.setStudentNo(rs.getString(student_no)); s.setName(rs.getString(name)); s.setGender(rs.getString(gender)); s.setAge(rs.getInt(age)); s.setMajor(rs.getString(major)); s.setCreateTime(rs.getTimestamp(create_time).toLocalDateTime()); return s; } }这里全部使用了PreparedStatement而不是Statement这是有意为之。PreparedStatement有两大优势一是预编译多次执行同一条SQL时性能更好二是参数占位符?会自动处理特殊字符从根本上防住了SQL注入。网上很多老教程用字符串拼接SQL比如SELECT * FROM student WHERE id id这种做法一旦用户传入1 OR 11整张表数据就全被查出来了这是安全红线必须从一开始就养成用PreparedStatement的习惯。3.2 Servlet控制器GET与POST的职责划分DAO层做完后需要一个调度员来接收页面请求、调用DAO方法、决定跳转到哪个页面。这个角色由Servlet担任。在Servlet的doGet和doPost方法里我用一个action参数来控制不同的业务操作。这样写的好处是一个StudentServlet就能覆盖所有学生相关的增删改查操作不需要为每个功能建一个Servlet对于课设规模来说结构更清爽package com.student.servlet; import com.student.bean.Student; import com.student.dao.StudentDAO; import javax.servlet.ServletException; import javax.servlet.annotation.WebServlet; import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException; import java.util.List; WebServlet(/student) public class StudentServlet extends HttpServlet { private final StudentDAO studentDAO new StudentDAO(); Override protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { // GET请求统一交给doPost处理避免重复代码 doPost(req, resp); } Override protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { String action req.getParameter(action); if (action null) { action list; } switch (action) { case add: addStudent(req, resp); break; case delete: deleteStudent(req, resp); break; case edit: showEditForm(req, resp); break; case update: updateStudent(req, resp); break; default: listStudents(req, resp); } } private void listStudents(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { ListStudent students studentDAO.getAllStudents(); req.setAttribute(students, students); req.getRequestDispatcher(/list.jsp).forward(req, resp); } private void addStudent(HttpServletRequest req, HttpServletResponse resp) throws IOException { // 注意新增和修改都需要处理中文乱码 req.setCharacterEncoding(UTF-8); String studentNo req.getParameter(studentNo); String name req.getParameter(name); String gender req.getParameter(gender); int age Integer.parseInt(req.getParameter(age)); String major req.getParameter(major); Student stu new Student(studentNo, name, gender, age, major); studentDAO.addStudent(stu); // 重定向到列表页防止表单重复提交 resp.sendRedirect(req.getContextPath() /student?actionlist); } private void deleteStudent(HttpServletRequest req, HttpServletResponse resp) throws IOException { int id Integer.parseInt(req.getParameter(id)); studentDAO.deleteStudent(id); resp.sendRedirect(req.getContextPath() /student?actionlist); } private void showEditForm(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { int id Integer.parseInt(req.getParameter(id)); Student stu studentDAO.getStudentById(id); req.setAttribute(student, stu); req.getRequestDispatcher(/edit.jsp).forward(req, resp); } private void updateStudent(HttpServletRequest req, HttpServletResponse resp) throws IOException { req.setCharacterEncoding(UTF-8); int id Integer.parseInt(req.getParameter(id)); String studentNo req.getParameter(studentNo); String name req.getParameter(name); String gender req.getParameter(gender); int age Integer.parseInt(req.getParameter(age)); String major req.getParameter(major); Student stu new Student(studentNo, name, gender, age, major); stu.setId(id); studentDAO.updateStudent(stu); resp.sendRedirect(req.getContextPath() /student?actionlist); } }这里有一个容易被忽略的设计细节为什么写操作用POST而查询和删除放在GET后面GET请求的参数会出现在URL里有长度限制也容易被浏览器收藏夹记录下来。删除操作如果用GET搜索引擎的爬虫爬到这个链接时可能直接把记录删了。所以规范做法是新增和修改用POST删除在页面里做一个隐藏表单用POST提交查询列表偶尔用GET无妨但最好也是POST。我上面的delete为了简化示例用了GET实际项目里我会用POST包一层form表单再提交。3.3 JSP页面EL表达式和JSTL代替Java脚本片段JSP页面这一层的核心原则是不要出现一行Java代码。早期JSP支持直接在% %里写Java这种方式导致页面逻辑混乱、难以维护所以才有了EL表达式和JSTL标签库来替代脚本片段。列表页list.jsp是整套系统的门面它接收Servlet塞进来的students集合循环渲染成表格% page contentTypetext/html;charsetUTF-8 languagejava % % taglib prefixc urihttp://java.sun.com/jsp/jstl/core % html head title学生信息列表/title style body { font-family: Microsoft YaHei, sans-serif; margin: 40px; } table { border-collapse: collapse; width: 100%; margin-top: 20px; } th, td { border: 1px solid #ddd; padding: 8px 12px; text-align: center; } th { background-color: #f2f2f2; } .btn { text-decoration: none; padding: 4px 10px; border-radius: 4px; color: #fff; } .btn-add { background: #28a745; } .btn-edit { background: #ffc107; } .btn-del { background: #dc3545; } /style /head body h2学生信息列表/h2 a classbtn btn-add href${pageContext.request.contextPath}/add.jsp新增学生/a table tr thID/th th学号/th th姓名/th th性别/th th年龄/th th专业/th th操作/th /tr c:forEach items${students} varstu tr td${stu.id}/td td${stu.studentNo}/td td${stu.name}/td td${stu.gender}/td td${stu.age}/td td${stu.major}/td td a classbtn btn-edit href${pageContext.request.contextPath}/student?actioneditid${stu.id}编辑/a a classbtn btn-del href${pageContext.request.contextPath}/student?actiondeleteid${stu.id} onclickreturn confirm(确定删除该学生吗);删除/a /td /tr /c:forEach /table /body /html这段代码里${stu.name}就是EL表达式它自动调用Student类的getName()方法c:forEach是JSTL的循环标签作用等同于Java里的for (Student stu : students)。注意顶点引入的taglib指令——不引入JSTL库的话c:forEach是没法用的你要记得把jstl-1.2.jar放到WEB-INF/lib目录下。删除确认弹窗onclickreturn confirm(...)虽然是个很小的细节但我强烈建议加上。学生信息删错了可以再录入但如果不小心误删整张表的数据且没有备份那就真没了。哪怕只是课设也应该在删除操作前加一道确认。新增和编辑页面结构类似我都使用表单POST提交。区别在于新增提交到/student?actionadd编辑提交到/student?actionupdate。两个页面的form写法基本一样顺便说明一下编辑页需要把当前记录的ID放进隐藏域input typehidden nameid value${student.id}这样才能在提交时定位到要更新的记录。% page contentTypetext/html;charsetUTF-8 languagejava % html headtitle新增学生/title/head body h2新增学生/h2 form action${pageContext.request.contextPath}/student?actionadd methodpost 学号input typetext namestudentNo requiredbr 姓名input typetext namename requiredbr 性别 select namegender option value男男/option option value女女/option /selectbr 年龄input typenumber nameage min10 max60 requiredbr 专业input typetext namemajorbr input typesubmit value保存 a href${pageContext.request.contextPath}/student?actionlist返回列表/a /form /body /html4. 部署到Tomcat后如何验证与排查4.1 IDEA里配置Tomcat部署写好的项目如何跑起来代码写完了接下来是让它在Tomcat里跑起来。我用IDEA举例无论社区版还是旗舰版操作基本一致点击右上角Add Configuration选择Tomcat Server - Local。在Application server里选择本地的Tomcat安装目录。切到Deployment标签页点击加号选择Artifact - 你的项目名:war exploded。这里用war exploded而不是war因为war exploded是解压目录部署IDEA能把修改后的文件实时同步到Tomcat不用每次都重新打包。Application context填/student或留空这个就是你访问项目的根路径。点击运行IDEA会自动启动Tomcat并打开浏览器。启动成功后访问http://localhost:8080/student?actionlist能看到测试数据的列表说明查询链路已经通了。如果IDEA的Run面板里报错Error during artifact deployment最可能的原因是WEB-INF/lib下缺少驱动包或JSTL包。检查方式展开Project Structure里的Artifacts确认WEB-INF/lib包含了你添加的jar包。IDEA的war exploded部署不会自动带上外部jar这是新手最容易困惑的一个点需要在Project Structure - Artifacts - Output Layout里手动把lib目录拉进去。我再补充一个不一定每个人都会遇到但值得知道的操作把mysql-connector-j和jstl的jar包放到Tomcat/lib目录下也能解决依赖问题但这会污染所有部署在同一个Tomcat上的项目不推荐。更规范的做法是让依赖跟随项目走也就是放在WEB-INF/lib里。4.2 从ClassNotFoundException到404高频报错的排查顺序我在帮人调课设代码时最常遇到的报错就那几种列成表格对照排查基本不会走弯路报错信息原因解决办法ClassNotFoundException: com.mysql.cj.jdbc.Driver缺少MySQL驱动jar包把驱动jar放进WEB-INF/lib重启TomcatAccess denied for user rootlocalhost数据库密码错误或用户不存在核对DBUtil里的账号密码Communications link failureMySQL服务没启动或端口不是3306启动MySQL服务检查URL端口Table student_db.student doesnt exist没执行建表SQL或连错库检查数据库名和表名Connection refusedTomcat里部署的应用尝试连接MySQL但MySQL不在同一机器检查MySQL是否远程可用页面404访问路径拼错检查WebServlet(/student)和表单提交地址JSP页面报找不到属性JavaBean的getter/setter没写全检查字段名和getter方法名是否一致有一个比较隐蔽的情况有时改了Java类Tomcat重启后仍然是旧的Class。原因是IDEA的Build产生了旧资源缓存。解决办法是Build - Rebuild Project然后把Tomcat完全停掉再启动不要用Redeploy因为Redeploy有时不刷新类加载器。热词里有一条web项目配置tomcat后查看jsp编译后的java类这里一并解释JSP本质是一个Servlet。Tomcat会把JSP翻译成一个.java文件再编译成.class。在Tomcat/work/Catalina/localhost/你的项目名/org/apache/jsp/目录下你能看到每个JSP对应的_jsp.java和_jsp.class。遇到页面报500但不知道JSP哪一行写错了时打开这个生成的Java文件定位到报错行号通常能找到JSP里的问题在哪。比如EL表达式写错属性名生成的Java代码就会有明确的编译错误提示。另外热词里提到的屏蔽jsp离开页面提示指的是在表单页加一个onbeforeunload事件用户点击链接离开页面时浏览器会弹出确定离开吗的提示。如果你在课设演示时觉得这个弹窗很烦可以在页面的script里临时注释或移除响应的绑定代码或者使用body onbeforeunload覆盖掉默认行为。更优雅的做法是只在表单内容有改动时才启用离开提醒用JavaScript监听input事件加标记没有改动就不触发。4.3 验证真实数据和中文乱码功能跑通后不要只看页面效果建议同时打开MySQL Workbench或命令行客户端验证数据真实落库。执行SELECT * FROM student;如果插入的中文在数据库里正常显示说明编码链路没问题。中文乱码是个高频问题而且一乱就是一套家族病——URL乱码、请求乱码、响应乱码、数据库乱码每一环都可能导致中文变成问号或方框。我的排查口诀是先看数据库再看响应最后看请求。数据库层面建库时指定utf8mb4建表时也指定utf8mb4连接URL带characterEncodingutf8这三步不遗漏数据库基本不会乱码。请求层面如果是POST提交在Servlet里调用req.setCharacterEncoding(UTF-8)必须在读取参数之前调用如果是GET提交还需要在Tomcat的conf/server.xml连接器配置加上URIEncodingUTF-8。响应层面JSP页面第一行% page contentTypetext/html;charsetUTF-8 languagejava %里的UTF-8要写对HTML的meta charsetUTF-8保持和JSP声明的编码一致。两层编码不一致时浏览器会优先认HTTP响应头里的charset。如果你用的是IDEA还要确保Settings - File Encodings里Global Encoding、Project Encoding、Properties Files都是UTF-8否则源码文件本身可能以GBK编码保存编译后乱码会让你排查得怀疑人生。5. 踩坑笔记与后续可以深挖的方向5.1 一个典型的新增成功但列表页乱码排查实录有一次我帮学弟调一个新增功能现象很怪打开列表页中文显示正常但新增一条记录后列表页里新记录的名字变成了??。已有数据不乱码新数据乱码——这说明问题只出在写入环节。第一反应是检查Servlet里的setCharacterEncoding发现确实写了排除。又检查了JSP表单的accept-charset也没问题。最后在DBUtil的URL里发现少了一个参数characterEncodingutf8。原来已有数据是安装MySQL时默认字符集为utf8的情况下导入的而新数据连接串没指定编码默认用了latin1插入时自然乱码。加上characterEncodingutf8后重启Tomcat问题消失。这个案例说明排查要按新增链路一步步走不要上来就怀疑数据库配置。先看连接串、再看Servlet、再看JSP错开排查顺序反而容易漏掉真正的问题。5.2 为什么坚持用PreparedStatement上面代码里我用的是PreparedStatement这里再展开说一次。有人可能觉得我直接拼字符串SQL多省事。但你仔细看一下这两种写法在用户输入恶意值时的表现假设登录页面有一个搜索框用户输入 OR 11如果用字符串拼接String sql SELECT * FROM student WHERE name name ;最终SQL变成SELECT * FROM student WHERE name OR 11这个条件恒为真会把整张表的数据全部查出来。数据库课程设计可能只是演示环节翻车但放到真实业务里就是严重数据泄露漏洞。用PreparedStatement后同样的输入只会被当作一个字符串参数而不是SQL关键字攻击彻底失效。养成这个习惯的成本极低收益极高没有理由不用。5.3 增删改查之后还能学什么如果这套增删改查你已经写完了、跑通了恭喜你你已经掌握了Web开发最核心的增删改查闭环。但这个项目还有大量的进阶空间按难度排个序增加模糊查询、分页功能在getAllStudents基础上加WHERE条件和LIMIT/OFFSET前端加搜索框和页码。增加登录功能设计user表用session保存登录状态通过Filter做登录拦截。将DAO层改成MyBatis或JDBC Template换掉手写JDBC的样板代码学习ORM思想。引入Maven管理依赖彻底告别手动复制jar包的痛苦然后顺理成章接入Spring Boot。数据库连接池换成Druid或HikariCP对比不同连接池的参数与性能表现写进项目文档作为加分项。这些方向每一条都能让你的课设从能跑进化到像样答辩时讲出来也会更有底气。最后分享一个我调课设时最常看到的教训写代码前先花二十分钟把表结构和页面跳转关系画出来哪怕只是手画在纸上。增删改查本身不难难点在于理清页面→Servlet→DAO→数据库之间的数据流向。表结构错了后面所有代码都要跟着返工表结构对了剩下的只是机械劳动。这种先规划再动手的习惯比任何具体API都值得你养成本身。
返回列表