简介:面向毕业设计/课程设计的校园二手物品交易系统完整源码,基于 B/S 架构与 SSM(Spring+SpringMVC+MyBatis)框架,采用 JSP、MySQL 和 Maven 构建,帮助开发者快速掌握前后台分离的二手交易平台开发流程。压缩包内共 202 个文件,约 12.63MB,包含 Java 源码、JSP 页面、CSS/JS 前端资源、SQL 脚本及项目配置文件,其中 Java 与 JSP 承载核心业务逻辑,CSS/JS 负责界面交互,SQL 脚本可直接初始化数据库。目前已有 21 人学习,适合正在完成相关课题或希望入门 SSM 整合开发的学习者参考使用。资源包含完整的管理员后台与用户前台功能,如注册登录、物品发布、搜索购买、订单管理、分类管理等,前后台分层清晰;按 Controller、Service、Mapper 三层组织源码,可直接部署运行,也便于二次开发,并附有数据库脚本与说明文档,适合毕业设计答辩和课程实践的重要参考。
1. 为什么选 JSP + MySQL 做校园二手交易:这个组合在今天仍能打的三个理由
如果你正为毕业设计或课程设计找方向,基于JSP和MySQL实现的校园二手物品交易系统会频繁出现在搜索结果里。这套技术栈常被调侃“老”,但它恰恰是学生项目里交付门槛最低、最容易讲清楚业务闭环的搭配:JSP负责把页面动态渲染出来,Servlet处理请求流转,MySQL存用户、商品和订单数据,Tomcat一跑就是一个完整可演示的Web应用。很多同学纠结要不要赶Spring Boot的时髦,我的看法是:毕设评审看的是你对业务和数据库的理解,而不是框架版本号,JSP的MVC结构反而让每一层都暴露在眼前,答辩时不心虚。
这套系统的典型使用场景是校园内的低门槛交易——大四学长出考研资料,毕业生甩卖床垫台灯,同校学生买卖二手自行车。相比闲鱼这类平台,它的优势是“同校信任+线下自提”,所以业务模型里天然带着学号验证、校园分类、站内留言这些功能点。我接下来会用一套可复现的工程结构,把数据库设计、核心页面、Servlet逻辑和部署配置全部拆开讲,并标注清楚最容易让人翻车的坑,比如JDK版本不匹配导致JSP编译失败、MySQL 8的认证插件连不上、表单提交后中文乱码等。这套东西做完,你拿到的不仅是一份能跑的源码,更是一整套能跟评委讲清楚的设计思路。文章面向两类人:一类是时间紧、想要直接改改用的学生,另一类是打算理解JSP全链路、想自己重写一遍的初学者。下面所有代码块都按可复制到IDE直接运行的标准来写。
2. JSP + Servlet + MySQL 的经典三层结构:先理清请求是怎么绕一圈的
2.1 从浏览器输入网址到页面渲染,JSP在这条链路上扮演什么角色
校园二手交易系统看起来功能很多,但拆开就是“浏览商品、登录、发布、留言、下单”这几个动作。每个动作在JSP架构里都遵循同一条路径:浏览器发起HTTP请求,Tomcat根据web.xml里的映射规则找到对应的Servlet,Servlet调用DAO层去MySQL里查数据,拿到结果后把数据塞进request或session,再转发(forward)或重定向(redirect)给JSP页面。JSP负责把Java代码的结果渲染成HTML,最终由浏览器解析展示。这个链路里最容易让新手误解的是JSP和Servlet的关系——JSP本质上是一个被Tomcat编译成Servlet的模板文件,它并不是独立于Servlet之外的另一种技术。很多初学同学以为JSP就是写HTML的地方,把JDBC连接代码也堆在JSP里,结果页面里全是<% ... %>脚本片段,后期根本维护不了。
我一般会建议按“Servlet负责干活,JSP只负责显示”的原则分工,这对应的是Model 2架构(也就是MVC的Web实现)。在这样的结构里,模型是JavaBean和DAO,视图是JSP,控制器是Servlet。二手交易系统需要的Servlet不会太多,按业务划分大概是:用户登录注册一个、商品发布和管理一个、留言和订单各一个,再加上一个首页加载商品列表的。用这种方式写出来的项目,答辩时能清楚画出请求流程图,这是加分项。下面这张表是我做这套系统时习惯分配的三层职责,照着这个边界写代码基本不会乱。
| 层次 | 典型文件 | 职责 | 与数据库关系 |
|---|---|---|---|
| 视图层 | index.jsp、login.jsp、publish.jsp | 渲染HTML、接收表单参数、显示EL表达式结果 | 不直接接触JDBC |
| 控制层 | UserServlet、GoodsServlet、OrderServlet | 解析请求参数、调用业务方法、决定跳转路径 | 通过DAO间接访问数据 |
| 模型层 | UserDao、GoodsDao、Goods、User | 封装数据库访问逻辑、承载表结构对应的字段 | 直接写JDBC或DBUtils |
2.2 建立工程骨架:这里每个文件夹都被Tomcat规定了用途
先建出一个符合Tomcat规范的Web工程。用IDEA新建一个Java Enterprise工程,勾选Web Application模板,它会自动生成src目录和web目录。需要手动补上的文件夹是WEB-INF下的lib(放MySQL驱动包)和classes(放编译后的class文件)。如果你的IDEA里用的不是这种方式,直接手工创建同样的目录结构也可以,关键是把下面的结构保持住:
src/ ├─ com/example/pojo/ # 实体类:User、Goods、Comment ├─ com/example/dao/ # 数据库操作:UserDao、GoodsDao ├─ com/example/servlet/ # 控制器:LoginServlet、GoodsServlet ├─ com/example/util/ # 工具类:DBUtil、StringUtil └─ com/example/filter/ # 编码过滤器,解决中文乱码 web/ ├─ index.jsp # 商品列表首页 ├─ login.jsp # 登录页 ├─ register.jsp # 注册页 ├─ publish.jsp # 发布商品页 ├─ goods_detail.jsp # 商品详情/留言页 └─ WEB-INF/ ├─ web.xml # Servlet映射、欢迎页配置 └─ lib/mysql-connector-java-xxx.jar这里解释一下为什么要单独分出util包。DBUtil是全局数据库连接入口,如果把它放在DAO里,每个DAO都会重复一段“加载驱动、获取连接”的代码。单独提出来之后,连接地址、用户名、密码只在一个类里配置,后期切换本机数据库或部署到服务器时只改一处。项目发布到Tomcat后,WEB-INF目录下的内容浏览器无法直接访问,这正好用来放web.xml,JSP页面不会通过地址栏暴露这个目录的物理路径。
2.3 让项目在Tomcat下跑起来的最小组件
如果你的搜索记录里有“mysql安装教程”“mysql在windows10上怎么安装”“jsp页面加载”这些词,那大概率你遇到过环境层面的卡壳。跑这套系统最朴素的配置是这样:JDK 8(或JDK 11)、Tomcat 8.5或9.0、MySQL 5.7。选择这三个版本的组合是有原因的:JSP在Tomcat 8.5以后对EL表达式的支持更稳定,MySQL 5.7的认证插件是mysql_native_password,和JDBC驱动的兼容性比MySQL 8默认的caching_sha2_password省心得多。如果你装了MySQL 8,连接字符串里必须加上allowPublicKeyRetrieval=true&useSSL=false,否则会报“Public Key Retrieval is not allowed”的错误,这个问题我在第五部分会细说。
一个最容易忽略的细节是MySQL驱动jar包版本要和数据库版本匹配。很多同学下载了最新的mysql-connector-java-8.0.x去连MySQL 5.7,这没问题;但反过来用5.1.x驱动去连MySQL 8就会报CommunicationsException。我的习惯是用5.1.49配MySQL 5.7,或者用8.0.33配MySQL 8,配分清楚动机。部署顺序也有讲究:先启动MySQL,用命令行或Navicat建好数据库和表,再启动Tomcat。如果先启动Tomcat,项目里的连接池初始化阶段会一直报错,日志刷得很快但大家经常忽略。
3. 数据库设计全套SQL:三张核心表与三个极易出错的外键
3.1 用户表、商品表、订单(留言)表怎么定义才经得起答辩追问
校园二手交易系统的数据库设计是答辩重点,评委通常会让解释“为什么这么设计”。我的思路是四张表:user(用户)、goods(商品)、comment(留言)、orders(交易记录)。很多同学觉得订单和留言可以合并成一张,实际拆开后业务边界更清晰——留言是“我想要”,订单是“我买了”,状态流转不同。下面是一套可以直接执行的建表SQL,字段类型和长度都按实际业务需求估过:
CREATE DATABASE IF NOT EXISTS campus_trade DEFAULT CHARACTER SET utf8mb4; USE campus_trade; CREATE TABLE `user` ( `id` INT NOT NULL AUTO_INCREMENT, `student_no` VARCHAR(20) NOT NULL COMMENT '学号,登录账号', `password` VARCHAR(32) NOT NULL COMMENT '存储MD5值,不存明文', `nickname` VARCHAR(30) DEFAULT NULL, `phone` VARCHAR(11) DEFAULT NULL, `avatar` VARCHAR(255) DEFAULT NULL, `create_time` DATETIME DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (`id`), UNIQUE KEY `uk_student_no` (`student_no`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户表'; CREATE TABLE `goods` ( `id` INT NOT NULL AUTO_INCREMENT, `user_id` INT NOT NULL COMMENT '发布者,关联user.id', `title` VARCHAR(50) NOT NULL, `description` TEXT, `price` DECIMAL(10,2) NOT NULL DEFAULT 0, `category` VARCHAR(20) DEFAULT '其他', `image` VARCHAR(255) DEFAULT NULL, `status` TINYINT DEFAULT 1 COMMENT '1上架 0下架 2已售出', `create_time` DATETIME DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (`id`), KEY `idx_user_id` (`user_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='商品表'; CREATE TABLE `orders` ( `id` INT NOT NULL AUTO_INCREMENT, `goods_id` INT NOT NULL, `buyer_id` INT NOT NULL, `seller_id` INT NOT NULL COMMENT '冗余卖家ID,方便查询', `price` DECIMAL(10,2) NOT NULL COMMENT '成交价,下单时快照', `status` TINYINT DEFAULT 0 COMMENT '0待付款 1已付款 2已完成', `create_time` DATETIME DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (`id`), KEY `idx_goods_id` (`goods_id`), KEY `idx_buyer_id` (`buyer_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='订单表';在设计表的时候有一处刻意为之的细节:orders表里同时存了buyer_id和seller_id。理论上卖家可以从goods.user_id查出来,但做“我卖出的”和“我买到的”两个订单列表时,双字段写法能少一次连表查询,这是数据冗余换查询效率的典型案例,答辩时主动提出来反而加分。商品表的status字段我用TINYINT而不是VARCHAR,因为状态只有上架、下架、已售出三种,数字维护比重写中文更不容易出错。注意所有字段都显式指定DEFAULT,否则用框架反向生成实体类时,非空字段会让插入语句直接报错。
3.2 查询语句里的三个高频命中的写法:分页、模糊搜索、连表查昵称
商品列表页不可能一次性把所有商品全查出,用LIMIT做分页是标准动作。我见过不少同学在这里踩坑:他们用LIMIT 10, 20写死了偏移量,结果翻到第三页后数据不对。正确做法是在Servlet里接收page参数,计算出偏移量再传给DAO:
-- 分页查询:page为页码,pageSize为每页条数 SELECT * FROM goods WHERE status = 1 ORDER BY create_time DESC LIMIT #{offset}, #{pageSize}; -- 搜索场景,注意标题和描述同时匹配,用 OR 而不是 AND SELECT * FROM goods WHERE status = 1 AND (title LIKE CONCAT('%', #{keyword}, '%') OR description LIKE CONCAT('%', #{keyword}, '%') OR category LIKE CONCAT('%', #{keyword}, '%')) ORDER BY create_time DESC;模糊搜索里最容易被忽略的是CONCAT('%', ?, '%')这种写法,很多同学会直接写成'%${keyword}%',这在JSP+JDBC里意味着用字符串拼接SQL,存在注入风险。虽然毕设系统不要求绝对安全,但答辩评委很可能会问“你怎么防SQL注入”,用PreparedStatement的占位符配合CONCAT就是标准答案。连表查询在商品详情页会出现一次——查商品信息时把发布者的昵称和手机号带出来,这样页面不用再发一次请求。SQL写法用LEFT JOIN user ON goods.user_id = user.id,注意别用INNER JOIN,万一用户被删了记录,商品详情页会直接空白。
3.3 两处外键约束,到底要不要在数据库层面加
关于外键,不少教程里推荐“物理外键”。我在这个项目里建议建立逻辑关联而不添加FOREIGN KEY约束,原因是毕设系统的数据量级极小,物理外键的维护成本远大于收益。比如发布商品的用户被删除时,如果商品表有FOREIGN KEY (user_id) REFERENCES user(id),删除用户的SQL会直接被数据库拒绝;而逻辑外键模式下,我们可以在删除用户前手动把其商品全部下架或删除。这样业务逻辑更可控,也避免了在代码里处理一堆数据库异常。但要注意,goods.user_id和orders.buyer_id上必须建普通索引(KEY),否则按用户查询商品列表时全表扫描会让页面变慢。虽然是数据的“关系”由代码维持,但索引不能省。
4. 页面与Servlet的约定:登录、发布、列表刷新的完整实现
4.1 用户登录与登录态保持:用session还是cookie
登录模块是整个系统的门面,写得好不好直接影响后续所有页面的权限控制。最常见的设计是:login.jsp提交表单到LoginServlet,Servlet里校验学号和密码是否匹配,成功后把User对象放进session,然后重定向到首页;失败则返回登录页并带一个错误提示。下面这个Servlet代码是完整可用的:
@WebServlet("/login") public class LoginServlet extends HttpServlet { protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { request.setCharacterEncoding("utf-8"); String studentNo = request.getParameter("student_no"); String password = request.getParameter("password"); // 对密码做MD5加密后再比对,避免数据库存明文 String md5Pwd = MD5Util.md5(password); UserDao dao = new UserDao(); User user = dao.findByStudentNoAndPassword(studentNo, md5Pwd); if (user == null) { // 失败时带着错误码回登录页,页面用EL表达式显示 request.setAttribute("error", "学号或密码错误"); request.getRequestDispatcher("login.jsp").forward(request, response); } else { HttpSession session = request.getSession(); session.setAttribute("loginUser", user); session.setMaxInactiveInterval(30 * 60); // 30分钟过期 response.sendRedirect("index.jsp"); } } }这里有两个值得展开的设计决策。第一是密码存MD5值而不是明文,虽然MD5早已不算安全哈希,但毕设系统里它足以防止“数据库被导出后密码裸奔”这类低级问题。答辩时如果能补一句“实际生产会用BCrypt加盐哈希”,评委一般不会再深究。第二是登录成功后用sendRedirect而不是forward,这能避免刷新页面时表单重复提交,是“PRG模式”里的重定向环节。如果你发现刷新登录页会弹“确认重新提交表单”,就是少了这一步。页面侧判断用户有没有登录,用JSP的<c:if>配合session对象就能实现:
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %> <c:choose> <c:when test="${not empty sessionScope.loginUser}"> 欢迎你,${sessionScope.loginUser.nickname} <a href="goods?action=my">我发布的</a> <a href="logout">退出</a> </c:when> <c:otherwise> <a href="login.jsp">登录</a> | <a href="register.jsp">注册</a> </c:otherwise> </c:choose>4.2 发布商品页:表单提交到Servlet再插入数据库的完整链路
发布商品功能是区分“完整项目”和“演示玩具”的分水岭。一个合格的发布页面至少要接收标题、分类、描述、价格、图片五个字段,然后插入数据库并跳转到详情页。这里最容易出问题的地方是图片上传——很多JSP教程停留在“填一个图片URL”的阶段,但实际使用中用户没法手动输入URL。如果要实现真正的文件上传,Servlet要用@MultipartConfig注解配合request.getPart("image"),同时需要引入commons-fileupload和commons-io依赖。这个动作比较长,先看常规版本的GoodsServlet里处理发布的代码:
@MultipartConfig(maxFileSize = 1024 * 1024 * 5) // 限制图片大小5MB @WebServlet("/goods") public class GoodsServlet extends HttpServlet { protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { request.setCharacterEncoding("utf-8"); String action = request.getParameter("action"); if ("publish".equals(action)) { // 从session取当前登录用户,如果为空则踢回登录页 User loginUser = (User) request.getSession().getAttribute("loginUser"); if (loginUser == null) { response.sendRedirect("login.jsp"); return; } String title = request.getParameter("title"); String description = request.getParameter("description"); String category = request.getParameter("category"); String priceStr = request.getParameter("price"); Goods goods = new Goods(); goods.setUserId(loginUser.getId()); goods.setTitle(title); goods.setDescription(description); goods.setCategory(category); goods.setPrice(new BigDecimal(priceStr)); goods.setStatus(1); // 新发布默认上架 // 如果有图片上传,则保存到指定目录 Part part = request.getPart("image"); if (part != null && part.getSize() > 0) { String fileName = UUID.randomUUID().toString().replace("-", "") + getExtension(part.getSubmittedFileName()); String savePath = getServletContext().getRealPath("/uploads") + File.separator + fileName; part.write(savePath); goods.setImage("uploads/" + fileName); } GoodsDao dao = new GoodsDao(); boolean success = dao.insert(goods); if (success) { response.sendRedirect("goods?action=detail&id=" + goods.getId()); } else { request.setAttribute("error", "发布失败"); request.getRequestDispatcher("publish.jsp").forward(request, response); } } } }这段代码里隐含了三个容易被忽略的细节。第一,文件名的生成用了UUID而不是用户原名,否则两个人都传photo.jpg会发生覆盖。第二,保存路径用getServletContext().getRealPath("/uploads"),这是Tomcat下Web应用的物理路径,部署到服务器后会自动指向发布目录,不需要手工改配置。第三,页面里<form>的enctype必须设置为multipart/form-data,否则「取不到请求参数,所有字段都是null」;你如果搜索过“jsp页面表单提交null”,大概率就是漏了这句。对应的JSP表单片段是这样:
<form action="goods" method="post" enctype="multipart/form-data"> <input type="hidden" name="action" value="publish"> 标题:<input type="text" name="title" required><br> 分类: <select name="category"> <option value="学习资料">学习资料</option> <option value="电子产品">电子产品</option> <option value="生活用品">生活用品</option> </select><br> 价格:<input type="number" step="0.01" name="price" required><br> 描述:<textarea name="description" rows="5" cols="30"></textarea><br> 图片:<input type="file" name="image" accept="image/*"><br> <button type="submit">发布商品</button> </form>4.3 首页商品列表与分页展示的JSP写法
首页列表是数据流向视图的最直观示例。我们会在index.jsp里通过<c:forEach>循环渲染request里的商品集合,并用${goods.price}这类EL表达式直接取值。分页组件需要从Servlet传来的总记录数、当前页码、每页条数来计算总页数。这里给一个可用的列表渲染核心片段:
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %> <%@ taglib prefix="fmt" uri="http://java.sun.com/jsp/jstl/fmt" %> <div class="goods-list"> <c:forEach items="${goodsList}" var="goods"> <div class="goods-item"> <a href="goods?action=detail&id=${goods.id}"> <c:choose> <c:when test="${not empty goods.image}"> <img src="${goods.image}" alt="${goods.title}" width="200" height="150"> </c:when> <c:otherwise> <img src="images/default.png" alt="暂无图片" width="200" height="150"> </c:otherwise> </c:choose> <h3>${goods.title}</h3> </a> <p class="price">¥<fmt:formatNumber value="${goods.price}" pattern="#.##"/></p> <p class="meta">${goods.category} · ${goods.createTime}</p> </div> </c:forEach> </div> <c:if test="${totalPages > 1}"> <div class="pagination"> <c:forEach begin="1" end="${totalPages}" var="page"> <a href="goods?action=list&page=${page}">${page}</a> </c:forEach> </div> </c:if>先取出商品集合,再进行循环渲染。这里要注意的是createTime字段类型是DATETIME,JSTL的fmt:formatNumber只能格式化数字,格式化日期必须用fmt:formatDate,两者不要混淆。商品图片路径在4.2里存的是uploads/xxx.jpg这种相对路径,页面渲染时浏览器会相对于当前域名解析,也就是http://localhost:8080/项目名/uploads/xxx.jpg,能正常访问的前提是Tomcat没有拦截/uploads目录的静态资源请求。如果你的项目里静态资源全被404,检查一下web.xml里是否配置了<servlet-mapping>/</servlet-mapping>把默认Servlet覆盖了,这个问题出现了很多次。
5. JSP+MySQL 常见问题与避坑清单:从“跑不起来”到“乱码与连不上库”
5.1 Tomcat启动即报错:Commons DbUtils或驱动类找不到的排查顺序
如果你的context.xml里配置了DriverManager方式获取连接,项目启动后控制台报ClassNotFoundException: com.mysql.jdbc.Driver,排查顺序应该这样走:第一步确认mysql-connector-java-x.x.x.jar确实在WEB-INF/lib目录下,而不是只在IDEA的External Libraries里;第二步看MySQL驱动jar包的版本,如果用的是8.x驱动,注册驱动的类名变成了com.mysql.cj.jdbc.Driver,旧写法com.mysql.jdbc.Driver虽然还能兼容但会打印警告;第三步检查WEB-INF/lib是否被IDEA标记成资源目录。这里有一个血泪经验:IDEA的Artifacts输出设置里,如果lib没有被添加到WEB-INF/lib的对应位置,运行配置会静默跳过这个目录,页面能打开但所有数据库操作都报驱动找不到。
解决后顺手做一件事:在DBUtil的静态代码块里加一段判断,把SQL驱动加载和连接URL从硬编码改成可配置,用Properties读取db.properties文件,这样发布到别的电脑上时只需要改配置文件,不用重新打包。下面这个DBUtil是我在这个项目里一直沿用的写法,可以直接复制改配置:
public class DBUtil { private static String url; private static String username; private static String password; static { try { // 读取src根目录下的db.properties InputStream in = DBUtil.class.getClassLoader() .getResourceAsStream("db.properties"); Properties prop = new Properties(); prop.load(in); Class.forName(prop.getProperty("driver")); url = prop.getProperty("url"); username = prop.getProperty("username"); password = prop.getProperty("password"); } catch (Exception e) { throw new ExceptionInInitializerError(e); } } public static Connection getConnection() throws SQLException { return DriverManager.getConnection(url, username, password); } public static void close(Connection conn, PreparedStatement ps, ResultSet rs) { if (rs != null) try { rs.close(); } catch (SQLException ignored) {} if (ps != null) try { ps.close(); } catch (SQLException ignored) {} if (conn != null) try { conn.close(); } catch (SQLException ignored) {} } }这么做的好处,一是把JDBC4.0自动加载驱动的行为显式化,二是避免在DAO里反复处理try-catch,连接关闭的逻辑集中在一个方法里不容易漏掉。很多项目最后出现“连接数不够,Tomcat卡死”的现象,多半就是某个查询路径里rs或ps没关,长期运行把连接池耗尽。
5.2 MySQL 8 连接报 Public Key Retrieval is not allowed
“mysql安装教程8.0”相关搜索排得很靠前,说明确实有大量同学在用MySQL 8跑老项目。MySQL 8默认的认证插件是caching_sha2_password,而JDBC驱动(尤其是5.x版本)默认不信任服务器的公钥,会报Public Key Retrieval is not allowed。这个错我看到过无数遍。解决办法有两个:一是建用户时指定认证插件为mysql_native_password;二是在JDBC连接URL上追加两个参数。两种都列一下:
-- 方案A:在MySQL里修改已有用户的认证插件 ALTER USER 'root'@'localhost' IDENTIFIED WITH mysql_native_password BY '你的密码'; FLUSH PRIVILEGES;// 方案B:连接URL上追加参数(我推荐这个,因为不用改数据库) String url = "jdbc:mysql://localhost:3306/campus_trade" + "?useSSL=false&allowPublicKeyRetrieval=true" + "&characterEncoding=utf8&serverTimezone=Asia/Shanghai";方案B里的useSSL=false也值得解释一下:MySQL 8的JDBC驱动默认要求SSL连接,本地开发环境没有配置证书,直接连就会因为SSL握手失败而报错,所以必须关闭。serverTimezone参数是因为MySQL 8的时区默认是UTC,和本地时间对不上时,操作DATETIME类型会报The server time zone value '???ú±ê׼ʱ??' is unrecognized。这两个参数写全,基本能解决八成“驱动连不上数据库”的问题。
5.3 中文乱码:解决请求参数和页面显示的双向乱码
JSP页面显示中文出现“???”或者æ ¸¡å这类乱码,原因基本是两个方向:要么是请求参数编码不对,要么是响应输出编码不对。请求方向的解决方法是给每个doPost开头加request.setCharacterEncoding("utf-8"),但如果你有很多Servlet,每个都写一遍太容易漏。更优雅的做法是写一个CharacterEncodingFilter,在web.xml里配置<filter-mapping>让它拦截所有URL:
<filter> <filter-name>encodingFilter</filter-name> <filter-class>com.example.filter.CharacterEncodingFilter</filter-class> <init-param> <param-name>encoding</param-name> <param-value>utf-8</param-value> </init-param> </filter> <filter-mapping> <filter-name>encodingFilter</filter-name> <url-pattern>/*</url-pattern> </filter-mapping>响应方向的问题往往出在JSP页面头部声明上。如果页面顶部漏了<%@ page contentType="text/html; charset=utf-8" %>,Tomcat默认按ISO-8859-1输出HTML,浏览器解析时中文全变乱码。所以每个JSP页面的第一行必须带着这句page指令。另外还要注意MySQL数据库和连接串编码的一致性:建库时用utf8mb4,连接串里写characterEncoding=utf8,页面和过滤器都是UTF-8,三层对齐后基本不会出现乱码。我在实际排查时见过一种隐蔽情况:页面、过滤器和连接串都正常,唯独数据库表在创建时用了latin1,这时JSP一切正常但查询出来的中文全是问号。检查手段是执行SHOW CREATE TABLE goods\G,看DEFAULT CHARSET是不是utf8mb4。
5.4 运行IDEA时端口被占用与404的定位方法
Tomcat默认端口是8080,如果本地开了其他服务占用了8080,启动时会报Port 8080 was already in use。解决方式有两个方向:一是找出占用进程杀掉,Windows下在cmd执行netstat -ano | findstr 8080,拿PID去任务管理器结束;二是直接改Tomcat的端口,在conf/server.xml里把<Connector port="8080">改成8081。我建议新手直接用第二个方案,因为杀进程容易误杀系统服务。改动后访问地址变成http://localhost:8081/项目名/。
另一个高频问题是项目能启动但首页404。确认三件事:web.xml里有没有配置<welcome-file-list>指向index.jsp;index.jsp是否真的放在了web根目录下;浏览器访问的URL里有没有带项目上下文路径。IDEA里Tomcat的Application context默认是/项目名_war_exploded,如果你设置的部署上下文是/就直接访问http://localhost:8080/,否则要拼上路径。很多搜索“基于jsp的毕业论文管理过程系统设计与实现”的同学后来发现404,都是因为URL路径写错,而不是代码有bug。
5.5 数据删除后页面空白:关联查询中LEFT JOIN没留空的预案
这是最后一个经典问题。商品详情页通常会连表查出发布者昵称,但如果管理员在后台把某个用户删掉了,goods.user_id对应的用户不存在时,LEFT JOIN会返回NULL。JSP页面如果直接用${seller.nickname},EL表达式会把null渲染成空字符串,页面不至于报错,但如果用了goods.sellerId再去做一次数据库查询,就可能因为查不到记录而抛出异常。解决的办法是在JSP里给默认值:
<c:choose> <c:when test="${not empty seller.nickname}"> ${seller.nickname} </c:when> <c:otherwise> 匿名用户 </c:otherwise> </c:choose>不要嫌这行代码多余。答辩现场演示时如果恰好在后台删了一个用户再回来看商品详情,页面炸掉会非常尴尬。所有连表查询出来的字段,都要假设它可能为null,页面展示层做兜底。
6. 从能跑到能答辩:验证功能完整性的五个自测步骤与三个扩展方向
当系统已经能运行起来,下一步不是急着写论文,而是按照“用户视角”把整个业务流程走一遍。我建议准备一份测试清单,按顺序执行:第一步注册一个新账号,检查学号重复时是否有错误提示,密码在数据库里是否以MD5密文存储;第二步登录后发布一件商品,上传一张图片,确认图片能在列表页和详情页正常显示;第三步换一个账号登录,搜索刚刚发布的商品,发起留言,再下订单;第四步用卖家账号登录,查看“我卖出的”订单列表,点击“确认交易完成”;第五步退出登录后,尝试直接访问发布页或订单管理页,确认会被重定向到登录页。这五步做完,覆盖了登录态、CRUD、关联查询、权限拦截四类核心逻辑,就算面试官让你现场演示,心里也有底。
验证之外,如果想把这个项目从“课程设计”拔高到“能写进简历的项目”,可以在三个方向里选一个做扩展。方向一是引入Filter做统一的登录拦截,替代在每个Servlet里手动判断session是否为空,这样既能精简代码,又能体现你对“横切关注点”的理解,但要注意把登录页和静态资源排除在拦截范围外。方向二是给商品表加views浏览数字段,每次访问详情页时执行一次UPDATE goods SET views = views + 1 WHERE id = ?,加一个排行榜列表,从“排序”角度展示SQL能力。方向三是把图片上传从本机目录迁移到OSS这种云存储,代码里只存URL,答辩时能引出“分布式部署”的话题,但实现成本会增加不少,时间紧的话跳过。
最后一个建议:把项目里所有资源文件的路径都核对一遍,包括上传图片保存的getRealPath路径和页面引用的相对路径。Tomcat的work目录如果残留了旧的编译文件,修改JSP后不生效,清理Tomcat/work/Catalina目录或重启Tomcat都能解决。当初我在做类似项目时,因为没清work目录,改了index.jsp却总显示旧页面,一度以为是浏览器缓存问题,折腾了整整一个下午;后来养成每次启动前删一次work下项目目录的习惯,这个“玄学”问题就彻底消失了。希望这些经验能帮你把毕设或课设的路走得更顺一点,少踩几个我已经替你踩过的坑。
本文还有配套的精品资源,点击获取