简介:基于Java Web的超市管理系统设计与实现,是一套含数据库与完整源码的毕业设计项目,面向计算机、通信、人工智能、自动化等相关专业的学生、老师及从业者,既适合作为期末课程设计、课程大作业或毕业设计的参考原型,也适合Java Web初学者从零学习项目搭建与功能扩展。系统代码均经过调试测试,答辩成绩98分,整体采用分层设计,商品、会员、供应商、结账等模块的DAO与Servlet实现清晰,可以直观呈现请求处理、数据访问及页面交互的完整链路。资源包为ZIP格式,共386个文件、约10.36MB,核心内容包含49个Java源文件、56个JSP页面、1个SQL数据库脚本,以及CSS、JS、LESS等前端辅助资源,便于从后端到前端对照理解。目前已有156人浏览学习,基础扎实的读者可在源代码基础上继续改造,按需增加或调整业务功能。
1. 拿到这套超市管理系统源码,先想清楚毕业设计到底要你拿出什么
每年毕业季都能看到一批人对着“基于javaweb的超市管理系统”这套经典课题发愁:源码包解压了,数据库脚本也有了,但双击启动就是黑匣子,Tomcat 起不来,页面 404,评委问两句就露怯。这个课题能经久不衰,是因为它把 JavaWeb 最核心的东西全占了——前端页面、Servlet/Controller 层、DAO 层、MySQL 数据库设计、增删改查,还有最让学生头疼的环境配置。它不是什么高并发高可用的企业系统,但它是最标准的“能跑起来的完整闭环”。这篇文章不跟你谈虚的,就讲清楚这套系统的技术栈是什么、怎么在 IDEA 里跑通、代码和数据库怎么配合、以及有哪些坑是你大概率会踩的。适合谁看?正在做 JavaWeb 课程设计或毕业设计、手里有源码和数据库脚本但没跑起来的同学,以及想快速搞懂这类系统架构、准备答辩的人。
2. 拿到源码先别急着跑:先看懂这套超市系统的技术栈与表结构
2.1 经典技术选型:SSM 还是 Servlet+JSP,一眼分辨
超市管理系统在毕业设计里最常见的两套技术栈是纯 Servlet+JSP 和 SSM(Spring+SpringMVC+MyBatis)。也有少数用 SSH(Struts2+Spring+Hibernate)的老项目,但近五年新做的课题基本不会再选它。你拿到源码包后,第一步不是找 README,而是翻目录结构判断是哪种,这决定了后面所有配置方式。
纯 Servlet+JSP 的项目特征很明显:src下没有applicationContext.xml,web.xml里写满了<servlet>和<servlet-mapping>,lib目录下有servlet-api.jar、jstl.jar。运行时不依赖 Maven 下载依赖,直接把整个 webapp 目录丢给 Tomcat 就能部署。SSM 项目则一定有pom.xml或至少一个spring相关 jar 包,web.xml里配置的是DispatcherServlet,src/main/resources下放着applicationContext.xml、spring-mvc.xml和mybatis-config.xml。
怎么判断?看一个文件就够:src或src/main/java根目录下有没有ApplicationContext的痕迹,或者看web.xml第一行<display-name>下面紧跟的监听器是ContextLoaderListener还是老老实实的<servlet>。我一般会让对方直接发我web.xml的截图,五秒钟就能判断。另一个偷懒的办法是看有没有.classpath文件里的 Maven 标记,或者src目录层级——src/main/java这种 Maven 标准结构是 SSM 项目跑不掉的特征。
2.2 数据库脚本里的业务设计:从建表 SQL 反推模块边界
超市管理系统的核心业务模块基本固定在七八张表以内:用户表(管理员登录)、商品表、商品分类表、供应商表、进货记录表、销售记录表,有的还会加会员表和库存表。拿到.sql脚本后,先将建表语句过一遍,每张表都要能回答三个问题:主键是什么、外键关联哪张表、哪些字段是业务状态位而不是数据字段。
一张典型的商品表建表语句长这样:
CREATE TABLE `t_product` ( `id` int(11) NOT NULL AUTO_INCREMENT, `product_name` varchar(100) NOT NULL COMMENT '商品名称', `category_id` int(11) DEFAULT NULL COMMENT '分类ID,关联t_category表', `price` decimal(10,2) NOT NULL DEFAULT '0.00' COMMENT '销售单价', `stock` int(11) NOT NULL DEFAULT '0' COMMENT '当前库存', `supplier_id` int(11) DEFAULT NULL COMMENT '供应商ID', `status` tinyint(1) NOT NULL DEFAULT '1' COMMENT '1上架 0下架', `create_time` datetime DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (`id`), KEY `idx_category` (`category_id`), KEY `idx_supplier` (`supplier_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;注意看这条 SQL 里的几个设计意图:AUTO_INCREMENT是自增主键,说明新增商品的 SQL 不需要传 id;decimal(10,2)是金额字段的标配,千万不要为了图省事把价格存成double,答辩时老师问到精度问题会很难看;status是典型的软删除或上下架标记位,说明这套系统的“删除商品”大概率是 UPDATE 不是 DELETE;外键没有用FOREIGN KEY约束,而是通过KEY索引建立逻辑关联,这是很多 JavaWeb 项目的常见做法——省去物理外键在插入时的校验开销,也避免删数据时被外键卡住。
判断表结构是否存在问题,可以参考两条经验。第一,所有表是否都有主键;没有主键的表在 MyBatis 逆向工程里会报错,而且分页插件用不了。第二,字符集是否统一成utf8mb4;如果建表脚本里混着utf8和latin1,后面做模糊查询时中文数据极容易出现乱码或者查询无结果。数据库脚本是这套系统的地基,有条件的先把脚本在本地 MySQL 里跑一遍,再对照applicationContext.xml里配置的连接库名,确保连的是同一个库。
2.3 启动前的一小时准备:JDK、Tomcat、Maven 版本怎么配
这一节只讲本地运行时的版本兼容,不讲原理。前面判断出技术栈后,照着下面这张表用对应的版本,能省掉大量莫名其妙的报错。
| 技术栈 | JDK 建议 | Tomcat 建议 | 额外说明 |
|---|---|---|---|
| Servlet+JSP | JDK 8 | Tomcat 8.5 或 9.0 | 不需要 Maven |
| SSM(Maven) | JDK 8 或 11 | Tomcat 8.5 或 9.0 | Maven 3.6 以上 |
| SSH 老项目 | JDK 7 或 8 | Tomcat 7 或 8.0 | 慎用 Tomcat 9,拦截器配置改动大 |
JDK 8 系统性价比最高,几乎所有 JavaWeb 毕业设计代码都能在它上面跑起来。Tomcat 版本选 8.5 最稳,它对web.xml4.0 的兼容和 Servlet 3.1 的支持足够覆盖这套系统的全部特性。MySQL 的话,老项目多用 5.7,新写的一般是 8.0;8.0 需要注意驱动类名是com.mysql.cj.jdbc.Driver,URL 还要带上serverTimezone=Asia/Shanghai,这两个细节在第 5 章避坑清单里详细说。
环境准备的动作可以固化成一个固定顺序:先装 JDK 并配好JAVA_HOME,再装 MySQL 并导入.sql脚本,接着装 IDEA(社区版够用)或 Eclipse,最后配置 Tomcat。如果项目带pom.xml,IDEA 会自动识别并触发 Maven 导入,第一次导入会下载大量依赖,网络差的时候等十分钟很正常。那段时间不要手动关 IDEA,也不要反复点刷新,否则maven-compiler-plugin的target/classes目录会留下半截文件,编译报一堆找不到符号的错,还以为源码有问题。
3. 用 IDEA 跑通 JavaWeb 项目的完整操作:从导入到看到登录页
3.1 IDEA 运行 JavaWeb 项目配置的四步:Facets、Artifacts 与 Tomcat
这一步是整个课题里最容易让人崩溃的环节。IDEA 不像 Eclipse 那样把 webapp 目录自动识别为可部署项目,你需要手动告诉它三件事:这是一个 Web 项目、Web 根目录在哪、打成什么形式的包。这四步按顺序做,做错一步就重来一遍。
第一步,配置 Facets。打开File -> Project Structure -> Facets,点加号选Web,选择你的 module。右侧有个Web Resource Directories,默认指向src/main/webapp或web目录,这里必须确认路径无误。Deployment Descriptor 那一行指向WEB-INF/web.xml,如果项目是纯 Servlet 的,这个文件一定存在;如果是注解版(Servlet 3.0+),这里可能提示没有描述符,也不用慌,不影响部署。
<!-- web.xml 头部,确认版本和命名空间,是判断项目类型的关键 --> <web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_4_0.xsd" version="4.0"> <display-name>SuperMarket</display-name> </web-app>web.xml的version字段直接决定 Tomcat 用哪种模式加载项目,4.0 对应 Tomcat 9,3.1 对应 Tomcat 8.5,低版本强行放在高版本 Tomcat 上也能跑,但高版本的配置放在低版本 Tomcat 上会直接启动失败。看到version="4.0"就老实配 Tomcat 9,不要用 8.0 硬顶。
第二步,配置 Artifacts。在Project Structure -> Artifacts里新增一个Web Application Exploded,名字随意,Output directory 会自动指向 Tomcat 的webapps目录的同名文件夹。这一步的关键是把 Facets 里认到的 web 根目录和编译后的target/classes都打包进这个 artifact。如果之前配过 Facets,这里直接点Create from Facets就能自动带出,不需要手动加目录,手动加的反而容易重复。
第三步,配置 Run Configuration。点顶部运行下拉框,选Edit Configurations,加一个Tomcat Server -> Local。Application server 里选你本机的 Tomcat 安装目录,JRE 选 JDK 8。切换到Deployment页签,点加号选Artifact,把刚才建好的那个war exploded加进去。这里有个细节:Application context 一栏,默认是/项目名,建议改成/或/supermarket,改成/后访问地址就是http://localhost:8080/index.jsp,省得每次启动还要记项目路径。
第四步,检查 classpath。进入Run -> Edit Configurations,找到刚才的 Tomcat 配置,切到Server页签,下面有Open browser和JMX port,这两项不用管。真正需要确认的是Before launch里的构建步骤,默认是Build Artifact。如果你用的是 Maven 项目,建议换成Build -> Build Project,这样每次启动前会自动执行 Maven 编译,避免改了 Java 代码但 Tomcat 加载的还是旧 class。
四步下来,启动 Tomcat,控制台出现Connected to server和类似Deployment of web application archive has finished的日志,说明部署成功。接着浏览器访问登录页,如果页面出来了但样式全乱,那是静态资源路径写死成/项目名/导致的,这种问题放到第 5 章讲。
3.2 JavaWeb 连接 MySQL 数据库:驱动、URL 与账号密码的落点
跑通了 Tomcat,下一步就是数据库连接。JavaWeb 项目连接 MySQL 的配置集中在三个位置:驱动包、连接配置文件、DAO 层获取连接的代码。任何一个位置出错,启动不会报错,但一登录就抛Cannot create PoolableConnectionFactory或Access denied for user。
先看驱动包。纯 Servlet 项目把mysql-connector-java-5.1.x.jar丢进WEB-INF/lib即可;Maven 项目在pom.xml里配依赖。MySQL 5.7 对应5.1.46版本驱动,MySQL 8.0 对应8.0.x版本驱动,两者不通用。
# db.properties -- 注意 MySQL 8 和 5.7 的 URL 写法差异 jdbc.driver=com.mysql.cj.jdbc.Driver jdbc.url=jdbc:mysql://localhost:3306/supermarket?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true jdbc.username=root jdbc.password=123456这个配置文件的三个参数是血泪经验:useSSL=false必须加,MySQL 8 默认开启 SSL 握手,本地开发环境会拖慢连接速度甚至报错;serverTimezone=Asia/Shanghai必须加,不加报The server time zone value异常,这是 MySQL 8 的强制要求;allowPublicKeyRetrieval=true只在用账号密码登录且遇到公钥检索问题时需要,加上无害。MySQL 5.7 的连接串就简单很多,驱动类名是com.mysql.jdbc.Driver,URL 不需要时区参数。
连接配置文件写好后,实际读取它的代码通常在DBUtil或BaseDao里,核心逻辑是用java.sql.DriverManager按配置动态加载驱动。遇到中文乱码时,回来看这个文件的characterEncoding=utf8参数,同时确认数据库本身的字符集也是utf8mb4,两个层面都对了,乱码问题基本能消除。
// DBUtil.java -- 基于类加载机制读取配置,避免硬编码 public class DBUtil { private static String url; private static String user; private static String password; static { try { InputStream in = DBUtil.class.getClassLoader() .getResourceAsStream("db.properties"); Properties props = new Properties(); props.load(in); Class.forName(props.getProperty("jdbc.driver")); url = props.getProperty("jdbc.url"); user = props.getProperty("jdbc.username"); password = props.getProperty("jdbc.password"); } catch (Exception e) { e.printStackTrace(); } } public static Connection getConnection() throws SQLException { return DriverManager.getConnection(url, user, password); } }这段静态块在类加载时执行一次,把配置读进内存,getConnection()每次调用时创建新连接。注意Class.forName这一步,它是让DriverManager能识别驱动类的关键,省略不写会导致No suitable driver found。新手常犯的错是把db.properties放在src根目录之外,导致getResourceAsStream返回 null,props.load(in)抛空指针。解决办法是确认文件路径在类编译输出的根目录下,IDEA 里就是src/main/resources或项目源码根目录。
3.3 第一次启动大概率翻车:按顺序排查三个报错
跑通这套系统,一次成功的概率不大,关键是报错时要能快速定位。按出现频率排序,新手遇到最多的三类问题如下。
第一个是端口占用。错误日志里会有一行Port 8080 required by Tomcat 8.5.xx Server at localhost is already in use。原因很简单,上一个 Tomcat 实例没关干净,或者别的程序占用了 8080。解决方式:netstat -ano | findstr 8080找到 PID,然后taskkill /PID 进程号 /F。也可以直接改 IDEA 里的 Tomcat 端口,Edit Configurations -> HTTP port改成 8081,但要记得数据库连接 URL 里的端口是 3306,两者不要混淆。
第二个是 404 或者 NoClassDefFoundError。404 的原因百分之八十是 Artifacts 没配好,部署产物里没有WEB-INF/classes目录,或者 JSP 页面放在了WEB-INF外面。NoClassDefFoundError通常是 jar 包冲突,最常见的是servlet-api.jar重复引入——Tomcatlib目录本身有,项目的WEB-INF/lib又放了一份,启动时类加载器晕掉。解决方式:检查WEB-INF/lib,删掉系统自带的servlet-api.jar和jsp-api.jar。
第三个是登录时直接页面崩溃,Tomcat 日志里抛Exception in thread "http-nio-8080-exec-1"。这时候别慌,往下翻栈信息找Caused by,这行才是真正的病根。如果Caused by是Communications link failure,说明 MySQL 没启动,或者 URL 里的 IP 端口写错;如果是Access denied for user 'root'@'localhost',是密码和配置文件对不上。这两种都属于连接问题,把第 3.2 节的内容再核对一遍即可。
4. 把增删改查讲清楚:超市管理系统的核心代码与 SQL 是怎么配合的
4.1 商品管理的后台逻辑:从 DAO 到 Servlet 到 JSP 的完整链路
看清一条业务请求的完整流向,是答辩时最加分的部分。以“新增商品”为例,完整链路是:JSP 页面表单提交 → Servlet 接收参数并封装到实体类 → 调用 Service 层 → 调用 DAO 层执行 INSERT 语句 → 返回结果跳转列表页。每层各干各的活,这也是 JavaWeb 分层设计的核心思想。
// ProductDao.java -- 基于 JDBC 的增删改查,注意 PreparedStatement 的使用 public class ProductDao { // 新增商品:使用 PreparedStatement 防止 SQL 注入 public int insert(Product product) throws SQLException { String sql = "INSERT INTO t_product (product_name, category_id, price, stock, supplier_id, status) " + "VALUES (?, ?, ?, ?, ?, ?)"; try (Connection conn = DBUtil.getConnection(); PreparedStatement ps = conn.prepareStatement(sql)) { ps.setString(1, product.getProductName()); ps.setInt(2, product.getCategoryId()); ps.setBigDecimal(3, product.getPrice()); ps.setInt(4, product.getStock()); ps.setInt(5, product.getSupplierId()); ps.setInt(6, product.getStatus()); return ps.executeUpdate(); } } // 根据关键字模糊查询:注意 LIKE 语句的拼接方式 public List<Product> search(String keyword) throws SQLException { List<Product> list = new ArrayList<>(); String sql = "SELECT * FROM t_product WHERE product_name LIKE ?"; try (Connection conn = DBUtil.getConnection(); PreparedStatement ps = conn.prepareStatement(sql)) { ps.setString(1, "%" + keyword + "%"); try (ResultSet rs = ps.executeQuery()) { while (rs.next()) { Product p = new Product(); p.setId(rs.getInt("id")); p.setProductName(rs.getString("product_name")); p.setPrice(rs.getBigDecimal("price")); p.setStock(rs.getInt("stock")); list.add(p); } } } return list; } }这段代码里有四个细节答辩时会被问到。第一,为什么用PreparedStatement而不是Statement?因为前者会将参数与 SQL 语句分离,通过预编译机制让' or '1'='1这类输入变成纯粹的字符串参数,而不是拼进 SQL 里的执行代码。第二,setBigDecimal对应数据库的decimal类型,如果用setString往里传价格,MySQL 会做隐式转换,精度可能丢失。第三,try-with-resources语法保证了Connection、PreparedStatement、ResultSet在退出时自动关闭,这是 JDK 7 以后的规范写法,比在finally里手动关闭更简洁且不容易漏。第四,模糊查询的%是加在参数里的而不是拼在 SQL 字符串里的,这样即使输入包含%或_也不会被当作通配符转义。
Servlet 层的职责是把 HTTP 请求里的字符串参数转换成 Java 对象,再调用 DAO。这里不直接 newProductDao,而是通过 Service 层中转,其实背后的目的是让上层代码不依赖具体 DAO 实现,之后想换成 MyBatis 或 Spring 管理,只需要改 Service 以下的部分。
// ProductServlet.java -- 统一入口,通过 action 参数分发请求 @WebServlet("/admin/product") public class ProductServlet extends HttpServlet { private ProductService productService = new ProductService(); protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { String action = request.getParameter("action"); if ("list".equals(action)) { List<Product> products = productService.findAll(); request.setAttribute("products", products); request.getRequestDispatcher("/WEB-INF/jsp/product_list.jsp") .forward(request, response); } else if ("delete".equals(action)) { int id = Integer.parseInt(request.getParameter("id")); productService.deleteById(id); response.sendRedirect("admin/product?action=list"); } } protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { doGet(request, response); } }看到doPost里调用doGet,这种写法在简单系统里很常见,能统一处理逻辑避免重复代码。分发时如果用的是@WebServlet注解,就不需要在web.xml里再写<servlet-mapping>——两种写法只能选一种,同时存在会导致映射混乱,这是容易踩的坑之一。跳转列表页用的是forward,删除成功后用的是sendRedirect,区别在于前者地址栏 URL 不变,刷新会重复提交表单;后者是重定向,刷新只重新请求列表,不会重复执行删除操作。这个细节回答“为什么删除要用重定向”时很管用。
4.2 数据库增删改查的边界:事务、连接释放与 SQL 注入
增删改查四个字虽然简单,但落在数据库上就有三个必须守住的边界。第一是事务,第二是连接释放,第三是 SQL 注入。超市系统的“销售出库”业务是测试这三个边界的标准场景——先扣库存,再插入销售记录,两步必须同时成功或同时失败。
// SaleService.java -- 事务的经典写法,两步操作在一个连接里完成 public void sellProduct(int productId, int quantity) throws SQLException { String deductStock = "UPDATE t_product SET stock = stock - ? WHERE id = ? AND stock >= ?"; String insertSale = "INSERT INTO t_sale_record (product_id, quantity, sale_time) VALUES (?, ?, NOW())"; Connection conn = null; try { conn = DBUtil.getConnection(); conn.setAutoCommit(false); // 关闭自动提交,开启事务 try (PreparedStatement ps1 = conn.prepareStatement(deductStock)) { ps1.setInt(1, quantity); ps1.setInt(2, productId); ps1.setInt(3, quantity); int rows = ps1.executeUpdate(); if (rows == 0) { throw new RuntimeException("库存不足"); } } try (PreparedStatement ps2 = conn.prepareStatement(insertSale)) { ps2.setInt(1, productId); ps2.setInt(2, quantity); ps2.executeUpdate(); } conn.commit(); } catch (Exception e) { conn.rollback(); throw e; } finally { conn.setAutoCommit(true); conn.close(); } }这段代码演示了事务的最小完整形态:setAutoCommit(false)之后,两次executeUpdate的结果要么一起提交,要么一起回滚。看到UPDATE ... WHERE stock >= ?这种写法,是一个典型的乐观锁思想——把库存判断和扣减放进同一条 SQL,让数据库在原子操作里完成校验,避免多个线程同时扣减时库存变成负数。这是比“先查库存再更新”更稳的做法,也是面试题里常说的并发安全边界。finally里的setAutoCommit(true)和conn.close()顺序不能颠倒,先把事务状态复位再归还连接,否则连接池里的连接会带着未提交事务状态,下次取出来时行为异常。
连接释放这块,很多教科书代码喜欢在finally里逐个关闭rs、ps、conn,写起来啰嗦还容易漏。第 4.1 节已经展示了try-with-resources的写法,它按逆序自动关闭,连Connection也能托管,前提是conn在try()里创建,而不是在外部创建再传进来。还有一种容易忽略的泄漏场景:DAO 里getConnection()成功但后续 SQL 抛异常,如果连接是普通DriverManager创建的,没被关闭就会一直占着 MySQL 的连接数,直到wait_timeout到点被服务端踢掉。症状很隐蔽,系统跑几天后突然全部请求变慢,重启 Tomcat 就好了——这就是典型的连接泄漏。
SQL 注入的防线就是PreparedStatement,没有任何理由用字符串拼接 SQL。除了注入风险,拼接 SQL 还会让 MySQL 无法复用执行计划,每次都要重新解析,批量插入时性能差异非常明显。答辩时如果老师问“你这个系统安全吗”,回答里能带上“所有 SQL 都走 PreparedStatement 预编译,不存在拼接注入点”这句话,比吹嘘加密算法实在得多。
4.3 改一个业务需求的实战:给商品表加“库存预警”字段
答辩时老师最喜欢问的一句话是:“如果我要加一个库存预警功能,你怎么改?”这题考察的是你对自己代码的熟悉程度,以及是否理解数据库增删改查的延伸。完整的改动涉及四层:数据库层加字段、实体类加属性、DAO 层加查询条件、JSP 层加展示。
-- 第一步:数据库层,修改表结构 ALTER TABLE `t_product` ADD COLUMN `min_stock` int(11) NOT NULL DEFAULT 0 COMMENT '库存预警阈值' AFTER `stock`;这条ALTER TABLE是开发里很常用的mysql 数据库修改结构操作。AFTER stock控制新列的位置,不写也行,默认加到末尾。注意修改表结构操作在生产环境是要谨慎的,但这是毕业设计,直接执行即可。改完后用DESC t_product;验证字段已添加,再用SHOW CREATE TABLE t_product;查看完整的表定义,确认COMMENT注释写对了——注释是给人看的,组内同学合作时没有注释的字段就像黑匣子。
// Product.java -- 第二步:实体类增加对应属性,注意包装类型 private Integer minStock; public Integer getMinStock() { return minStock; } public void setMinStock(Integer minStock) { this.minStock = minStock; }实体类字段的类型用Integer而不是int,原因在于int默认值为 0,无法区分“阈值设为了 0”和“没设过阈值”;Integer默认是null,在查询结果映射时能保留数据库的原始语义,这是 JavaWeb 项目里的一个容易忽视的细节。
// ProductDao.java -- 第三步:新增查询方法,筛选库存不足的商品 public List<Product> findLowStockProducts() throws SQLException { String sql = "SELECT * FROM t_product WHERE status = 1 AND stock <= min_stock"; try (Connection conn = DBUtil.getConnection(); PreparedStatement ps = conn.prepareStatement(sql); ResultSet rs = ps.executeQuery()) { List<Product> list = new ArrayList<>(); while (rs.next()) { Product p = new Product(); p.setId(rs.getInt("id")); p.setProductName(rs.getString("product_name")); p.setStock(rs.getInt("stock")); p.setMinStock(rs.getInt("min_stock")); list.add(p); } return list; } }这个查询条件的逻辑值得好好讲:stock <= min_stock能抓住“刚好等于阈值”的商品,如果你只想抓“低于阈值”,就改成<。边界怎么说,直接决定这个功能在答辩时的严谨程度。还有,SQL 里status = 1过滤掉下架商品,避免把不再卖的商品也列为预警项,这也是业务上的一个常见考量。到这里,四层改动前后逻辑贯通,一套完整的库存预警功能就落地了。
JSP 层的改动最简单也最容易出错:在商品列表表格里加一列“预警阈值”,再在数值上加个颜色判断。新手容易在 JSP 里写复杂 Java 代码,这种写法在老项目里很常见,但答辩时会被扣分。更好的做法是在 Servlet 层提前计算好一个boolean isLowStock放进Product对象的扩展属性里,JSP 只做if判断。把逻辑留在 Java 层、展示留给 JSP,这也符合分层设计的初衷。
5. 答辩和验收前必看的避坑清单:现象、原因与解决
5.1 MySQL 8 与 5.7 的驱动差异:连接慢、拒绝连接、时区报错
现象一连串三连:Tomcat 启动没报错,但点登录等很久后报Communications link failure,控制台还有The server time zone value 'Öйú±ê׼ʱ¼ä' is unrecognized。这个是 MySQL 8 最经典的坑,原因在于驱动版本和 URL 参数不匹配。MySQL 8 要求驱动类是com.mysql.cj.jdbc.Driver(中间多了cj),URL 必须带serverTimezone参数,而老项目里拿来的代码往往还是 5.7 时代的写法。解决方式是照第 3.2 节的完整 URL 字符串替换,不要只加时区参数——useSSL=false和allowPublicKeyRetrieval=true在 8.0 下都建议带上,否则还有概率遇到 SSL 握手协商导致的启动缓慢。
5.2 Tomcat 端口被占用和启动后 404:先看 Deploy 日志再改配置
现象是控制台输出一大段红色,然后被一行Port 8080 required by Tomcat ... is already in use打断。原因不一定是你上次的 Tomcat 没关,有可能是 IDEA 的 Tomcat 集成模式残留了进程。解决方式:IDEA 里File -> Settings -> Build, Execution, Deployment -> Application Servers,把 Tomcat 配置里的After disconnect设为Shutdown,避免 IDEA 退出后 Tomcat 变孤儿进程。如果已经在生产环境上,别乱杀进程,先确认是哪个项目在占用,catalina.bat stop优雅关闭。启动后访问显示 404 的,跑去看 IDEA 控制台最底部Deployment of web application archive ... has finished行,确认部署目录里有没有WEB-INF。一个很隐蔽的原因:Facets里 Web Resource 路径配错了,导致 JSP 页面没被复制到 artifact 里,但WEB-INF/classes却正常出来了——此时页面 404、接口正常,容易被误判成路由问题,实际上路径指错了。
5.3 页面中文乱码:三层字符集逐一排除,别只改一处
现象是页面显示出温æ±这类问号乱码,或者??一片问号。原因是字符集在这条链路上断了好几层:JSP 文件本身编码、服务器响应编码、数据库连接编码、数据库表字段字符集,任何一层不对都会乱。我的习惯是三层排查:第一层看 JSP 文件头部,要求是pageEncoding="UTF-8"且文件本身用 UTF-8 保存;第二层看 response 的 content type,request.setCharacterEncoding("UTF-8")要放在getParameter之前才生效;第三层看数据库连接串里是否带了characterEncoding=utf8,以及表字符集是不是utf8mb4。注意 Tomcat 8.5 以上默认 URI 编码是 UTF-8,和低版本默认 ISO-8859-1 不同,如果你把老项目直接升级到高版本 Tomcat,GET 请求的中文参数也要关注这个差异。
检查db.properties连接串里是否有characterEncoding=utf8,没有就补上。三条链路都统一后,重启 Tomcat 再看效果,不要改一层就重启一次,浪费时间而且容易漏判是哪里生效的。
5.4 万能的重启大法也有边界:修改数据库结构、更换驱动、调整配置文件
这套系统的调试过程中,有一个血泪经验:Tomcat 重启能解决 80% 的运行时问题,但有三类改动必须“冷重启”——即重启前先做 Maven 的clean或手动删除target目录。第一类是改了数据库脚本或配置文件,不少同学改了db.properties里的密码后直接点重启,结果连接信息没变,因为配置已经被类加载进 JVM 缓存,static {}块只执行一次。第二类是换了 JDBC 驱动版本,只替换WEB-INF/lib下的 jar 不够,需要重新 Build Artifact 让新 jar 进到部署目录。第三类是改了web.xml的映射或 servlet 配置,这时比重启更彻底的做法是用mvn clean compile package重新打包,再把新的 war 部署。
5.5 运行时改了代码不生效:IDEA 的 update 资源与 JSP 热部署的错觉
最后一坑属于 IDEA 用户专属。改完 JSP 后刷新页面,改动没出来,以为 devtools 没生效或者 Tomcat 配置有问题。其实 IDEA 里对 JSP 资源的更新默认不会自动同步到 Tomcat 的部署目录,你需要手动Build -> Rebuild Artifact或按快捷键Ctrl+Shift+F10触发。Java 代码的改动默认也不会热替换,只有加了JRebel这类插件才支持真正的热部署。避免误解的办法是:把 JSP 页面放在web目录下,同时检查Edit Configurations -> On frame deactivation设为Update resources,这样切回 IDEA 窗口时会自动把改过的 JSP 同步过去。Java 代码改动就老老实实重启,一次重启十秒左右,换来的是确定性的结果,不值得为了省那几秒赌 IDEA 的玄学热部署。
6. 让这套系统在答辩时更值钱:3 个能讲深又不费力的增强点
6.1 加一个登录验证过滤器:五张代码能讲明白的拦截逻辑
答辩时最尴尬的事是主页面没有登录就能直接访问。评委点开product_list.jsp,发现绕开登录页也能看到数据,第一印象就扣分了。加一个Filter是最小成本的补救。
// LoginFilter.java -- 注册为 /* 拦截所有请求,白名单放行登录页 @WebFilter("/*") public class LoginFilter implements Filter { @Override public void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain) throws IOException, ServletException { HttpServletRequest request = (HttpServletRequest) req; HttpServletResponse response = (HttpServletResponse) resp; // 静态资源和登录接口放行,其余请求必须校验 session String uri = request.getRequestURI(); if (uri.contains("/login") || uri.endsWith(".js") || uri.endsWith(".css") || uri.endsWith(".png") || uri.endsWith(".jpg")) { chain.doFilter(req, resp); return; } Object user = request.getSession().getAttribute("loginUser"); if (user == null) { response.sendRedirect(request.getContextPath() + "/login.jsp"); return; } chain.doFilter(req, resp); } }这段代码的逻辑一句话就能讲完:除了登录页和静态资源,其他请求必须带着 session 里的loginUser才放行。换成 SpringMVC 项目则用Interceptor实现,配置路径在spring-mvc.xml里。能把这个过滤器讲清楚并能现场改放行路径的同学,在“项目安全”这个问题上基本就算过关了。
6.2 打开 SQL 日志:让评委看到你的代码真的是在操作数据库
另一个很加分的细节是把执行的 SQL 打印出来。JDBC 项目在DBUtil.getConnection()里打开mysql驱动的日志即可,SSM 项目在mybatis-config.xml里加一行配置。
<!-- mybatis-config.xml -- 打印 SQL 日志,注意使用 slf4j 输出层级 --> <configuration> <settings> <setting name="logImpl" value="STDOUT_LOGGING"/> </settings> </configuration>STDOUT_LOGGING会把每一条 SQL 和查询结果输出到控制台。答辩演示时,你操作一次查询,控制台里滚出对应的 SELECT 语句,评委一眼就能看出你的项目是真实跑了数据库操作,而不是 mock 数据。演示结束记得把这个配置关掉,STDOUT_LOGGING在并发场景下对性能有影响,生产环境不会开。
6.3 导出一份数据字典放进论文附录:用表格证明你懂表结构
论文附录里放一张数据字典表是传统操作,但大多数同学直接截图数据库设计工具,一股脑导出一大页,评委根本看不懂。正确的做法是手工整理,只挑核心表和关键字段,做成一张「表名 — 字段 — 类型 — 说明」的表格。
| 表名 | 字段 | 类型 | 说明 |
|---|---|---|---|
| t_user | username | varchar(50) | 登录账号,唯一索引 |
| t_product | price | decimal(10,2) | 销售单价,不允许负数 |
| t_sale_record | quantity | int | 销售数量,触发库存联动扣减 |
做这张表的过程也是重新理解自己系统的过程。你会发现当初建表时哪些字段是多余的,哪些字段类型选得不好,答辩被问到“为什么用 decimal 不用 double”时,也能顺手把精度问题答上。数据字典不需要覆盖所有表,覆盖核心业务的三到五张表,每张表列五个字段左右,就足够展现你的设计能力了。
这 3 个增强点全部加完,系统从功能上依旧是超市管理系统的底子,但答辩时讲述的深度会明显不同。我的习惯是每个增强点准备一个“为什么这样做”的故事,比如过滤器选/*而不是/admin/*是因为直接访问 JSP 文件不受路径过滤保护,这个解释比代码本身更让评委信服。希望这些经验能帮你把毕业设计从“能跑”做“能讲”,答辩顺利。
本文还有配套的精品资源,点击获取