简介:基于 JSP+Servlet+JDBC+MySQL 的学生管理系统,面向 Java Web 方向的初、中级学习者和需要完成毕业设计、期末设计的高校学生,定位为一套可直接运行、便于二次开发的典型 Web 管理项目。压缩包共 341 个文件、约 6.99MB,以 84 个 Java 源文件、84 个 Class 文件、36 个 JSP 页面、61 个 JS 脚本和 28 个 CSS 样式为主体,同时包含 SQL 初始化脚本、项目配置文件和少量图片资源,整体目录结构规范,导入 IDE 后即可进行数据库还原与项目部署。工程按分层思想组织代码,DAO 层基于 JDBC 完成用户、教师、学生、课程、成绩等实体的数据库操作,Service/JavaBean 负责业务逻辑,Servlet 与 JSP 实现前台控制和页面展示,包含登录校验、信息增删改查等常见功能;作者上传前已在本机测试通过,代码可顺利运行。目前已有 3320 人学习或下载,对想掌握 JSP+Servlet 分层开发、SQL 联表查询,或者准备课程设计、毕业论文的学生来说,是一份不错的参考资料。 每个学Java Web的人,大概率都会在某个阶段碰上一个“基于jsp+servlet+jdbc+MySQL的学生管理系统”。这个项目看起来朴素,甚至被很多人说“技术栈太老”,但这么多年过去,它依然是课程设计、毕业设计和自学练手最常见的题目,没有之一。原因很简单:它不是最潮的架构,却把Web开发最底层的请求-响应模型、会话管理、数据库交互和页面渲染串成了一条完整链路,是理解后续所有框架的基石。
这篇文章就围绕这个经典项目展开,从整体设计、数据库与JDBC连接、Servlet控制层、JSP页面交互,到分页、上传、Excel导出等常见扩展功能,再到我最想分享的排查技巧。不管你是正在做课设的学生,还是想补Java Web基础的自学者,这里面的东西都值得花十分钟看完。
1. 项目整体设计思路拆解
1.1 为什么jsp+servlet+jdbc+MySQL这套组合依然值得写
先回答一个很多人纠结的问题:现在Spring Boot都卷到不行了,为什么还要写jsp+servlet+jdbc?
我的看法是,这个技术栈最大的价值在于“没有魔法”。使用Spring Boot时,一个@RequestMapping注解进去,请求怎么到Controller、事务怎么开启、连接池怎么管理,很多新手是说不清的。而在原生的Servlet工程里,用户点了按钮,表单数据通过HTTP协议发到Tomcat,Tomcat找到web.xml里注册的Servlet映射,doPost()方法里一行行拿到参数、调用JDBC、拼装结果、request.getRequestDispatcher().forward()转发回JSP渲染成HTML——每一步都是透明的,没有任何框架掩盖细节。把这些原理吃透了,后面学Spring MVC基本是降维打击。
从另一个角度说,这个项目的业务边界也非常适合练手。学生管理涉及登录认证、增删改查、条件搜索、分页、批量删除、文件上传(头像)、数据导出(Excel),甚至视频播放(个人信息页放个MP4),几乎覆盖了Java Web日常开发的高频场景,每个功能既能独立实现,又能组合成完整系统,非常适合作为综合练习。
1.2 经典三层结构在项目里怎么落地
参考常见的课程设计要求,我习惯把这个项目按三层结构拆开:
- 表示层(JSP页面):负责展示数据和收集用户输入。jsp页面放在
webapp目录下,通过${}EL表达式和JSTL标签库简化Java代码的嵌入,避免满屏<%%>脚本片段。 - 业务控制层(Servlet):接收请求、校验参数、调用Service(或直接调用DAO,课设规模不必强行分层)、决定跳转路径。一个Servlet类最好只处理一类功能模块的职责,比如
StudentServlet负责学生相关的请求,通过action参数区分增删改查操作。 - 数据访问层(DAO/JDBC):封装所有SQL操作,对外提供
findById、findAll、insert、update、delete等方法。这一层要负责连接的获取与关闭、PreparedStatement的创建和结果集的转换。
这样做的好处非常明显:JSP里不会出现一大堆Java操作数据库的代码,Servlet不会变成几千行的巨无霸,日后想更换数据库或改造业务逻辑时,只需要动DAO层而不必触碰页面代码。虽然项目规模小,但规范一旦建立,扩展起来会省非常多的事。
2. 数据库设计与JDBC连接层实现
2.1 MySQL建表与字段设计要点
学生管理系统的核心表基本逃不出几个:学生表、用户表(管理员/教师登录)、可能还有班级表或专业表。以学生表为例,字段建议这样设计:
CREATE TABLE `student` ( `id` INT(11) NOT NULL AUTO_INCREMENT, `stu_no` VARCHAR(20) NOT NULL COMMENT '学号', `name` VARCHAR(50) NOT NULL COMMENT '姓名', `gender` CHAR(1) DEFAULT '男' COMMENT '性别', `age` INT(3) DEFAULT NULL, `major` VARCHAR(100) DEFAULT NULL COMMENT '专业', `phone` VARCHAR(20) DEFAULT NULL, `email` VARCHAR(100) DEFAULT NULL, `avatar` VARCHAR(255) DEFAULT NULL COMMENT '头像路径', `create_time` DATETIME DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (`id`), UNIQUE KEY `uk_stu_no` (`stu_no`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;几个设计上的细节值得注意。第一,主键用自增id,学号单独加唯一索引,避免用学号当主键导致后续修改麻烦。第二,create_time用DEFAULT CURRENT_TIMESTAMP,插入数据时不用手动填时间。第三,字符集务必用utf8mb4而不是utf8,否则存emoji或者部分生僻字会出现乱码。第四,avatar字段存的是文件保存后的相对路径(比如uploads/avatar_001.jpg),而不是二进制数据,这点后面讲文件上传时还会再提。
2.2 JDBC连接工具类的标准写法
连接管理是整个项目最基础也最容易出错的部分。我先给出一个比较稳妥的工具类写法,然后用它说明几个关键点。
package com.demo.util; import java.sql.Connection; import java.sql.DriverManager; import java.sql.ResultSet; import java.sql.SQLException; import java.sql.Statement; public class DBUtil { private static final String URL = "jdbc:mysql://localhost:3306/student_db?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true"; private static final String USER = "root"; private static final String PASSWORD = "123456"; static { try { Class.forName("com.mysql.cj.jdbc.Driver"); } catch (ClassNotFoundException e) { e.printStackTrace(); } } public static Connection getConnection() throws SQLException { return DriverManager.getConnection(URL, USER, PASSWORD); } public static void close(Connection conn, Statement stmt, ResultSet rs) { try { if (rs != null) rs.close(); } catch (SQLException e) { e.printStackTrace(); } try { if (stmt != null) stmt.close(); } catch (SQLException e) { e.printStackTrace(); } try { if (conn != null) conn.close(); } catch (SQLException e) { e.printStackTrace(); } } }连接URL里面参数非常多,每一样都有讲究。useUnicode=true&characterEncoding=utf8保证中文和参数正常传输;useSSL=false关掉SSL警告;serverTimezone=Asia/Shanghai是为了解决MySQL 8.x和驱动之间的时区差异;allowPublicKeyRetrieval=true是因为MySQL 8.0默认的caching_sha2_password认证方式,在本地非SSL连接时会报Public Key Retrieval is not allowed,加上它才能正常连上。
静态代码块里的Class.forName("com.mysql.cj.jdbc.Driver")是传统做法,虽然JDBC 4.0之后可以通过SPI机制自动加载驱动,但保险起见还是显式加载。close方法必须分别处理ResultSet、Statement、Connection,顺序不能乱,先结果集后语句最后连接。如果业务代码中忘关连接,本地开发时可能感觉不到问题,跑上一段时间就会出现Too many connections。
2.3 要不要用连接池
说实话,课程设计这种并发量极低的应用,直接DriverManager.getConnection()完全够用。但如果你想在项目里体现一点工程意识,或者老师要求“数据库连接要优化”,建议引入Druid连接池,也就多几十行配置。
Druid的配置其实就三块:druid.properties配置文件、初始化连接池的工具类、在DAO中从池子拿连接。配置文件放在src目录下,内容大致是:
driverClassName=com.mysql.cj.jdbc.Driver url=jdbc:mysql://localhost:3306/student_db?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=Asia/Shanghai username=root password=123456 initialSize=5 maxActive=10 maxWait=3000工具类使用Druid的DruidDataSourceFactory来加载配置:
private static DataSource dataSource; static { try (InputStream in = DBUtil.class.getClassLoader().getResourceAsStream("druid.properties")) { dataSource = DruidDataSourceFactory.createDataSource(in); } catch (Exception e) { e.printStackTrace(); } } public static Connection getConnection() throws SQLException { return dataSource.getConnection(); }引入连接池后,DAO层的代码不用改太多,不用再写Class.forName,连接从池子里借、用完后close()其实是还回池子里。这里有个容易踩的坑:Druid连接池默认对空连接检测有testWhileIdle机制,如果MySQL服务端wait_timeout过短导致连接失效,报错往往诡异,比如各种奇怪的通信异常,排查时可以先把validationQuery=SELECT 1加上。
3. Servlet控制层与JSP页面实现要点
3.1 用web.xml写Servlet还是注解
“使用web.xml方式编写servlet的完整示例”是很多人搜索过的需求,原因在于很多教材和课程设计要求用web.xml配置映射。虽然@WebServlet注解从Servlet 3.0开始支持且非常方便,但web.xml的方式能更直观地展示Servlet的注册过程,也是一些老旧系统的标准做法。
web.xml放到WEB-INF目录下,核心片段如下:
<servlet> <servlet-name>StudentServlet</servlet-name> <servlet-class>com.demo.servlet.StudentServlet</servlet-class> <load-on-startup>1</load-on-startup> </servlet> <servlet-mapping> <servlet-name>StudentServlet</servlet-name> <url-pattern>/student</url-pattern> </servlet-mapping>load-on-startup填整数,表示容器启动时创建的优先级,数字越小越先加载。这个配置常用来预加载全局初始化的Servlet。url-pattern是浏览器访问的路径映射,请求到/student的URL都会交给StudentServlet处理。
在实际项目中,一个项目通常有多个Servlet,所以我常用的做法是所有*.do结尾的请求统一走某个入口Servlet,内部再分发给不同方法,类似一个简单的前端控制器。但这属于设计偏好了,课设里按模块各建一个Servlet也完全没问题。
3.2 doGet与doPost的取舍和乱码处理
Servlet最核心的两个方法是doGet和doPost。GET请求参数会拼在URL后面,适合查询和跳转;POST请求参数在请求体里,适合提交数据、上传文件这类操作。我习惯在Servlet里重写service方法统一处理编码,或者在doGet里调用doPost,这样无论表单用什么方式提交都不会漏掉。
中文乱码是新手最常见的拦路虎,而且原因不止一个。Servlet接收参数时,要判断请求方式:POST请求通过request.setCharacterEncoding("UTF-8")设置请求体的编码,GET请求参数在URL中,Tomcat默认按ISO-8859-1解码,所以还要在配置文件里加上连接器编码:
<Connector port="8080" protocol="HTTP/1.1" connectionTimeout="20000" redirectPort="8443" URIEncoding="UTF-8" />输出响应时要设置response.setContentType("text/html;charset=UTF-8"),JSP页面顶部还要写上<%@ page contentType="text/html;charset=UTF-8" language="java" %>。这三处缺一不可。MySQL连接URL里的characterEncoding=utf8也已经处理了数据库这层,四层编码对齐了,中文才会真正稳定不乱码。
3.3 请求转发与重定向的选择
Servlet处理完业务后要跳转页面,有两条路:request.getRequestDispatcher("list.jsp").forward(request, response)和response.sendRedirect("list.jsp")。这两者的区别非常基本但极其重要。
转发发生在服务器内部,浏览器地址栏不变,request对象里保存的属性(比如查询出来的学生列表)在目标页面还能用;重定向则是一次新的请求,浏览器地址栏会变化,之前request里的属性全部丢失。所以需要携带数据渲染页面时,必须用转发。而新增数据、删除数据这类操作,为了防止用户刷新页面导致重复提交,应该redirect到另一个查询的URL。
我见过不少项目的删除操作是用转发方式跳转回列表页,结果用户按一下F5,同一个删除请求被重复执行。如果数据库没有做关联约束,数据可能被重复删除或报错。这一点在课设答辩时经常被老师问,搞清楚原理,回答起来会很从容。
3.4 JSP页面的EL表达式和JSTL
JSP页面里最忌讳的就是满屏<% %>脚本片段,既难看又难维护。建议引入EL表达式和JSTL标签库,让页面专注展示。
比如学生列表页面,用c:forEach循环渲染表格:
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %> <table> <tr><th>学号</th><th>姓名</th><th>专业</th><th>操作</th></tr> <c:forEach items="${studentList}" var="stu"> <tr> <td>${stu.stuNo}</td> <td>${stu.name}</td> <td>${stu.major}</td> <td> <a href="student?action=edit&id=${stu.id}">编辑</a> <a href="student?action=delete&id=${stu.id}" onclick="return confirm('确定删除吗?')">删除</a> </td> </tr> </c:forEach> </table>条件判断用c:if,比如性别显示:<c:if test="${stu.gender == '男'}">男</c:if>。分页导航可以用c:forEach配合begin和end生成页码。这些标签语法简单,但能显著减少Java代码在页面中的比重,也是老师比较喜欢的代码风格。
注意JSTL需要引入两个jar包,jstl.jar和standard.jar,版本选择上建议用1.2版本,Java 8以上环境都能兼容。如果页面报错说找不到taglib描述符,检查一下WEB-INF/lib下这两个jar是否存在。
4. 核心功能模块的完整实现路径
4.1 登录认证与会话管理
登录模块看上去简单,但牵扯到Session生命周期、密码安全、页面拦截三个知识点。常见的实现逻辑是:用户提交用户名和密码,Servlet从数据库查询比对,成功后把用户信息放入Session中:
User user = userDao.findByUsernameAndPwd(username, password); if (user != null) { request.getSession().setAttribute("loginUser", user); response.sendRedirect("student?action=list"); } else { request.setAttribute("errorMsg", "用户名或密码错误"); request.getRequestDispatcher("login.jsp").forward(request, response); }Session的默认超时时间是30分钟,可以在web.xml中配置:
<session-config> <session-timeout>30</session-timeout> </session-config>页面拦截有两种实现方式。最简单的是写一个LoginFilter,实现javax.servlet.Filter接口,在doFilter里检查Session中是否存在登录标记,没有则跳回登录页。另一种是每个页面用<% if(session.getAttribute("loginUser")==null) response.sendRedirect("login.jsp"); %>做判断,代码冗余且容易遗漏。Filter更优雅,也是标准做法。
密码存储方面,课程设计里明文存库的很多,但我建议至少用MD5加盐处理一下。DigestUtils.md5DigestAsHex((password + salt).getBytes())几行代码就能搞定,在数据库里加个salt字段,注册时生成随机盐,登录时再拼起来加密比对。不加密的后果就是数据库一旦泄露,所有密码裸奔,这个习惯从课设开始养成没有坏处。
4.2 学生信息的增删改查与分页
学生模块的CRUD相对直接,重点在分页查询和条件查询。分页的核心SQL是LIMIT ? , ?,第一页返回起始行,第二页返回每页大小。Service层要计算出总页数,并把当前页、总页数、每页大小、查询结果集一起封装成PageBean对象传给JSP。
public class PageBean<T> { private int pageNum; // 当前页码 private int pageSize; // 每页条数 private int totalCount; // 总记录数 private int totalPages; // 总页数 private List<T> list; // 当前页数据 }计算逻辑是:
int totalPages = totalCount % pageSize == 0 ? totalCount / pageSize : totalCount / pageSize + 1;条件查询时要动态拼接SQL,这里务必使用PreparedStatement占位符,千万不能字符串拼SQL,否则SQL注入的风险极大。例如:
String sql = "SELECT * FROM student WHERE 1=1"; List<Object> paramList = new ArrayList<>(); if (name != null && !"".equals(name.trim())) { sql += " AND name LIKE ?"; paramList.add("%" + name.trim() + "%"); } if (major != null && !"".equals(major.trim())) { sql += " AND major = ?"; paramList.add(major.trim()); } if (pageNum > 0) { sql += " LIMIT ?, ?"; paramList.add((pageNum - 1) * pageSize); paramList.add(pageSize); } pstmt = conn.prepareStatement(sql); for (int i = 0; i < paramList.size(); i++) { pstmt.setObject(i + 1, paramList.get(i)); }WHERE 1=1是纯为了方便拼接条件的技巧,条件恒真,后面的AND可以直接拼,不用判断是不是第一段。这种做法在生成动态SQL时很常见,性能影响可以忽略,但代码简洁不少。
修改和删除操作按id操作,前端传id参数,DAO层用PreparedStatement绑定参数,流程上没有太多玄机,主要注意前端表单的action地址和方法要正确,以及删除时用confirm弹窗确认,避免误删。
4.3 图片上传与MP4视频播放
课设中“jsp个人信息展示页面”“jsp实现mp4视频播放”这两个需求经常被一起提出。实际上照片和视频的展示模式是相同的:文件不要存数据库,而是存到服务器磁盘的某个目录,数据库只存路径,页面通过相对路径去访问。
文件上传如果只用原生Servlet,表单要设enctype="multipart/form-data",然后借助Apache Commons FileUpload组件解析。引入commons-fileupload-1.4.jar和commons-io-2.11.0.jar两个依赖后,Servlet里可以用ServletFileUpload解析request,拿到FileItem列表:
String uploadPath = getServletContext().getRealPath("/uploads"); File uploadDir = new File(uploadPath); if (!uploadDir.exists()) uploadDir.mkdirs(); DiskFileItemFactory factory = new DiskFileItemFactory(); ServletFileUpload upload = new ServletFileUpload(factory); List<FileItem> items = upload.parseRequest(request); for (FileItem item : items) { if (!item.isFormField()) { String fileName = UUID.randomUUID().toString().replace("-", "") + "_" + new File(item.getName()).getName(); File file = new File(uploadPath, fileName); item.write(file); // 将 fileName 存到数据库的 avatar 字段 } }文件名用UUID重命名,是为了防止不同用户上传同名文件互相覆盖,也是基本的规范做法。getRealPath("/uploads")会把文件保存到项目部署后的webapp目录下,这样浏览器直接访问/uploads/文件名就能读到。
视频播放更简单,JSP里放一个<video>标签,src指向uploads/xxx.mp4即可,后端不需要额外处理。需要注意两点:一是webapps目录下文件会被容器直接访问,但要确认uploads目录没有放在WEB-INF内部,否则外部访问不到;二是Tomcat默认对http的POST大小有限制,maxPostSize默认2MB,如果上传的视频较大,需要去server.xml里调大maxPostSize="-1"或者单独限制上传文件大小。
4.4 数据导出为Excel
“jsp实现数据导出为excel”也是常见扩展点。最简单的实现方式是POI生成.xls文件并响应给浏览器,浏览器端会触发下载。
HSSFWorkbook workbook = new HSSFWorkbook(); HSSFSheet sheet = workbook.createSheet("学生信息"); HSSFRow headerRow = sheet.createRow(0); headerRow.createCell(0).setCellValue("学号"); headerRow.createCell(1).setCellValue("姓名"); headerRow.createCell(2).setCellValue("专业"); int rowNum = 1; for (Student stu : list) { HSSFRow row = sheet.createRow(rowNum++); row.createCell(0).setCellValue(stu.getStuNo()); row.createCell(1).setCellValue(stu.getName()); row.createCell(2).setCellValue(stu.getMajor()); } response.setContentType("application/vnd.ms-excel"); response.setHeader("Content-Disposition", "attachment; filename=students.xls"); workbook.write(response.getOutputStream()); workbook.close();需要引入poi相关jar包,建议用4.1.2版,同时需要poi-ooxml和poi核心包。导出的Excel按钮放列表页,点击后请求一个单独的ExportServlet,不走转发也不重定向到其他页面,因为浏览器要接收输出流下载文件。
如果文件名里有中文,Content-Disposition需要对中文做URL编码,否则下载时文件名乱码:filename*=UTF-8''+ URLEncoder.encode(name, "UTF-8")。这个坑比较隐蔽,很多人导出的内容没问题,但文件名是乱码,多半就出在这里。
5. 常见问题与排查技巧实录
5.1 JDBC连接报错的常见场景
项目中遇到最多的报错,整理成一个速查表,下次碰到可以直接对照排查。
| 报错信息 | 原因 | 解决办法 |
|---|---|---|
No suitable driver found for jdbc:mysql://... | MySQL驱动jar没有导入或URL格式错误 | 检查WEB-INF/lib下驱动jar;检查URL前缀是否为jdbc:mysql://,8.x驱动类为com.mysql.cj.jdbc.Driver |
Public Key Retrieval is not allowed | MySQL 8.0默认认证插件是caching_sha2_password | 连接URL拼接allowPublicKeyRetrieval=true |
Access denied for user 'root'@'localhost' | 用户名密码错误或远程连接权限问题 | 检查账号密码,本地连接确认host为localhost |
Too many connections | 连接数溢出,通常是连接未关闭 | 检查DAO中close是否都执行;引入连接池限制最大连接数 |
name JDBC is not bound in this context | 代码里用JNDI方式查找DataSource,但环境中未配置 | 改用本地Druid连接池或DriverManager方式,或者检查context.xml数据源配置 |
其中name JDBC is not bound in this context比较特殊,它是JNDI数据源相关错误。用Tomcat配置JNDI数据源时,代码要从InitialContext查找java:comp/env/jdbc/studentDB,如果context.xml没配置或者配置名称对不上,就会报这个错。课设场景我建议直接用Druid连接池,少踩一层配置的坑。
5.2 页面改动不生效、乱码和日期显示问题
JSP改了不生效,九成原因是浏览器缓存或Tomcat没有重新编译。开发阶段可以在web.xml中给JSP的Servlet配置修改检查参数:development=true。如果用的是IDEA的Artifact部署模式,改了JSP有时需要重新构建或者重启Tomcat,这是因为IDE没有同步更新到target目录。遇到这种问题,先mvn clean再重新部署,或者直接Rebuild项目。
乱码问题前面提过四个层面的编码,这里再给一个快速定位的方法:先看页面显示乱码还是数据中文乱码。页面乱码通常是JSP的pageEncoding没设;数据显示成?通常是数据库字符集不对,检查建表语句的DEFAULT CHARSET=utf8mb4;参数传过去乱码则是请求编码问题,检查Servlet里的setCharacterEncoding和Tomcat的URIEncoding。
日期类型的处理也容易翻车。数据库DATETIME类型在JDBC中对应java.sql.Timestamp,从ResultSet取值后用SimpleDateFormat格式化输出:
Date createTime = rs.getTimestamp("create_time"); String createTimeStr = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(createTime);如果你在JavaBean里用的是java.util.Date,直接用rs.getTimestamp赋值是可以的,但不要用rs.getDate,它会丢掉时分秒。另一方表单里如果涉及日期字符串转数据库日期,用ps.setTimestamp(1, Timestamp.valueOf(dateStr + " 00:00:00")),注意格式必须严格匹配,否则会抛IllegalArgumentException。
5.3 我踩过的几个典型坑
写这个项目时,我印象最深的是文件上传路径问题。用getRealPath("/uploads")拿到的路径在IDEA部署时指向的是target/项目名/uploads,开发环境下文件是能访问的,但每次mvn clean一执行,整个target目录被清空,上传的文件也没了。如果是一个长期运行的系统,更稳妥的做法是把上传目录配置成外部绝对路径,比如/home/user/uploads或Windows下的D:/uploads,然后写一个虚拟目录映射,或者用自定义Servlet把文件流读出来返回。课程设计的话用getRealPath就能跑通,但答辩时如果能主动说清楚这个局限,并提出外部存储的改进方案,会是非常好的加分项。
另一个坑是MySQL 8.0驱动版本不一致导致时区异常。有次项目用的驱动是5.1.47,连MySQL 8.0数据库,报Unknown system variable 'query_cache_size',搜了很久才明白是驱动太老。解决就是统一用8.0.x的mysql-connector-java,并加上serverTimezone=Asia/Shanghai参数。
再说一个关于删除功能的小设计:删除操作最好用POST请求而不是GET链接。很多人写着<a href="student?action=delete&id=1">删除</a>,这样的链接会被搜索引擎爬虫、浏览器预加载插件触发,也可能被用户误点。更合理的方式是用一个表单包裹,按钮用type="submit",提交方式为POST。课设虽然不会有人恶意刷,但养成了GET只做查询的好习惯,以后工作会受益。
最后分享一个小技巧:调试SQL时,在DAO层把即将执行的PreparedStatement的SQL语句和参数打印到控制台。最简单的方式就是写一个log变量拼接占位符替换后的完整SQL,或者临时用com.mysql.cj.jdbc.interceptors.SQLExceptionInterceptor这类插件,但项目里直接打日志已经很直观了。打印出的SQL拷贝到Navicat里手动执行一遍,很多问题瞬间就能定位到底是SQL写错还是代码传参有误。
踩过几次坑之后你会发现,这个项目的所有问题几乎都集中在编码、驱动、路径、连接管理这四个方向。把这四个问题控住了,整个系统的稳定性会大大提高,答辩时也能更有底气地和老师聊设计思路,而不是只说“网上找的模板能跑”。
本文还有配套的精品资源,点击获取