Java校园二手平台部署避坑指南:Servlet+JSP全链路实操

发布时间:2026/10/27 23:11:25
Java校园二手平台部署避坑指南:Servlet+JSP全链路实操

简介:这是一套面向Java初学者与高校课程设计者的校园二手交易平台实战项目,基于SpringBoot快速开发框架构建,聚焦Web全栈开发能力训练,解决校园场景下二手商品信息发布、浏览、交易闭环等实际需求。资源包共232个文件,含53个核心Java业务逻辑与控制器代码、26个Thymeleaf模板HTML页面、24个交互式JS脚本、10个CSS样式文件(含Layui及自定义主题如login.css、goods_main.css等),以及29个JPG商品图和75个GIF操作示意动图,整体压缩后仅5.58MB,轻量易部署。已有616人学习下载,适合用于Java Web课程实训、毕业设计参考或SpringBoot+Mybatis+Thymeleaf技术栈入门实践。读者可直接运行jar包体验完整MVC架构下的用户管理、商品发布、购物车、订单交易及安全加密等六大功能模块,并通过源码快速掌握前后端协同开发流程与Layui前端集成技巧。

1. Java校园二手交易平台系统:不是Demo,是能跑通「发布-浏览-下单-确认」闭环的完整工程

你手头那份标着“Java校园二手交易平台系统”的压缩包,大概率不是教学PPT里的UML图,也不是IDEA里新建项目后敲了三行System.out.println的空壳。它是一套真实跑在Tomcat上的、数据库连着MySQL、前端用JSP+Bootstrap、后端用Servlet+JDBC(或Spring Boot+MyBatis)的可部署系统——学生能发闲置教材,管理员能审核上架,买家能加购物车、生成订单、状态流转到“已签收”。我去年帮三个高校社团部署过同类系统,最常被问的问题不是“怎么编译”,而是“为什么登录后首页空白”“为什么上传图片报错500”“为什么订单状态死在‘待付款’不动”。这些问题背后,90%不是代码写错了,而是环境没对齐:JDK版本卡在8还是17?MySQL驱动jar包漏放WEB-INF/lib?Tomcat的server.xml里URIEncoding没设UTF-8导致中文乱码?这篇笔记不讲Java基础语法,也不画类图,就拆开这个资源包,告诉你从解压到上线每一步踩什么坑、怎么绕过去、参数改哪一行——尤其当你只有Windows笔记本、没Linux服务器、连Docker都不熟的时候,怎么用最朴素的方式把它跑起来。


2. 系统架构与技术栈解析:为什么选Servlet+JSP而不是Spring Boot?

2.1 从源码结构反推技术选型逻辑

打开下载包里的src/目录(或java/目录),你会看到典型的三层分包结构:

com.school.secondhand.dao/ → 封装JDBC操作,如UserDao.java、GoodsDao.java com.school.secondhand.service/ → 业务逻辑,含事务控制(常见用synchronized或手动commit/rollback) com.school.secondhand.servlet/ → Servlet入口,如LoginServlet.java、PublishServlet.java

这明确指向传统Java Web老派架构:无Spring IoC容器,依赖web.xml配置URL映射,数据库连接靠DBUtil.java单例管理Connection。这种设计不是落后,而是刻意为之——它规避了Spring Boot自动配置带来的黑匣子问题。比如学生用Eclipse+Tomcat 8.5本地调试时,遇到ClassNotFoundException: org.springframework.web.servlet.DispatcherServlet,根本不用查starter版本冲突,因为压根没Spring。同理,pom.xml(如果存在)里只会看到mysql-connector-java:5.1.47和servlet-api:3.1.0两个核心依赖,而非一长串starter。这种“裸写”方式,让新手能看清HTTP请求如何从doPost()进、经DAO层查库、再request.setAttribute()传给JSP渲染的全链路,调试时打断点一跟到底。

2.2 前端交互的关键约束:JSP内置对象与表单提交陷阱

系统前端大量使用JSP内置对象,这是理解交互逻辑的钥匙:

  • request.getParameter("username")获取表单字段,但注意:若表单method="get"且含中文,必须在Tomcat的conf/server.xml中为Connector添加URIEncoding="UTF-8",否则getParameter()返回乱码;
  • session.setAttribute("user", user)存储登录态,但Session超时默认30分钟,若测试时等太久发现自动登出,别急着改代码,先查web.xml里的<session-config><session-timeout>30</session-timeout></session-config>;
  • application.getAttribute("onlineCount")统计在线人数,这类全局变量在多用户并发时需加synchronized块保护,否则出现负数或跳变——我在某校部署时就因漏加锁,导致后台统计页面显示“在线-5人”。

提示:所有JSP页面顶部必须有<%@ page contentType="text/html;charset=UTF-8" %>,否则中文标题、商品描述全变成方框。这不是可选项,是硬性要求。

2.3 数据库设计的现实妥协:为什么用VARCHAR(255)存图片路径而非BLOB?

goods表结构通常长这样:

CREATE TABLE goods ( id INT PRIMARY KEY AUTO_INCREMENT, title VARCHAR(100) NOT NULL, description TEXT, price DECIMAL(10,2), image_path VARCHAR(255), -- 关键!存相对路径,如 "upload/20240512/abc.jpg" status TINYINT DEFAULT 0, -- 0:待审核, 1:已上架, 2:已售出 create_time DATETIME DEFAULT CURRENT_TIMESTAMP );

这里刻意避开BLOB存储图片,原因很实际:

  • 学生用笔记本跑Tomcat,内存有限,BLOB读取易OOM;
  • 图片文件直接放在WebContent/upload/目录下,Nginx/Apache可直接静态代理,减轻Java层压力;
  • 调试时删错文件?只要image_path字段值还在,重传一次就行,比修复损坏的BLOB字段简单十倍。
    所以,当你看到UploadServlet.java里用DiskFileItemFactory解析文件时,重点看它是否把文件保存到request.getServletContext().getRealPath("/upload/")——这是Tomcat运行时的真实物理路径,不是项目根目录。

2.4 配置文件的隐藏战场:web.xml与db.properties的联动

web.xml不仅是URL映射,更是整个系统的启动开关:

<!-- 指定初始化参数 --> <context-param> <param-name>dbConfig</param-name> <param-value>/WEB-INF/db.properties</param-value> </context-param> <!-- 加载监听器,读取db.properties创建连接池 --> <listener> <listener-class>com.school.secondhand.listener.ContextLoaderListener</listener-class> </listener>

而db.properties内容看似简单:

driver=com.mysql.jdbc.Driver url=jdbc:mysql://localhost:3306/secondhand?useUnicode=true&characterEncoding=utf8 username=root password=123456

但致命细节在url参数:

  • useUnicode=true&characterEncoding=utf8必须存在,否则中文插入MySQL报错Incorrect string value;
  • 若你的MySQL是8.0+,driver要改成com.mysql.cj.jdbc.Driver,且url追加&serverTimezone=Asia/Shanghai,否则时间字段存入全是0000-00-00;
  • password明文写在这里是教学系统常态,但上线前务必改成JNDI或环境变量注入——这点我会在最后一章细说。

3. 本地部署实操:从解压到首页显示的七步流程

3.1 环境准备:JDK、Tomcat、MySQL的版本锁链

别跳过这步!我见过太多人卡在第一步:

  • JDK:必须用JDK 8u202或JDK 11(非JDK 17)。原因:JSP编译器jasper.jar在JDK 17+被移除,Tomcat 9+虽支持,但本系统web.xml声明的是Servlet 3.1,对应JDK 8兼容性最佳;
  • Tomcat:推荐Tomcat 8.5.99(非9.x或10.x)。Tomcat 9开始强制HTTPS重定向,而本系统无SSL配置,会导致http://localhost:8080自动跳https://然后失败;
  • MySQL:5.7.32是黄金版本。MySQL 8.0的caching_sha2_password认证插件会让老版JDBC驱动连不上,报错Unknown initial character set index '255'。

提示:检查JDK版本用java -version,输出含1.8.0_202即可;Tomcat解压后运行bin/startup.bat,浏览器访问http://localhost:8080看到猫图标才算成功;MySQL用mysql -V确认版本。

3.2 数据库导入:字符集与SQL模式的双重校准

解压包里必有db/secondhand.sql文件。导入前执行两步校准:

# 进入MySQL命令行 mysql -u root -p # 创建数据库并指定字符集 CREATE DATABASE secondhand CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; # 设置客户端默认字符集(关键!) SET NAMES utf8mb4; # 再导入 source D:/download/secondhand.sql;

为什么用utf8mb4?因为学生发的商品标题可能含emoji(如“📚考研资料🔥包邮”),utf8在MySQL里实际是utf8mb3,不支持4字节字符。若跳过SET NAMES,即使数据库建对了,INSERT语句仍会截断emoji成?。

3.3 项目部署:WAR包生成与路径映射的生死线

不要直接把文件夹拖进webapps/!正确流程:

  1. 用Eclipse/IDEA打开项目,右键→Export → WAR file,导出为secondhand.war;
  2. 将secondhand.war复制到Tomcat/webapps/目录下;
  3. 启动Tomcat,等待自动解压(观察webapps/secondhand/目录生成);
  4. 浏览器访问http://localhost:8080/secondhand/(注意末尾斜杠!缺了会404)。

若首页空白,立刻检查:

  • webapps/secondhand/WEB-INF/web.xml是否存在?缺失则WAR包导出失败;
  • webapps/secondhand/WEB-INF/lib/下是否有mysql-connector-java-5.1.47.jar?没有则数据库连不上,首页加载超时;
  • webapps/secondhand/WEB-INF/classes/下是否有编译后的.class文件?若只有.java源码,说明没Build Project。

3.4 登录验证:绕过验证码与密码加密的临时方案

系统通常带验证码(VerifyCodeServlet),但本地调试时极难通过。临时方案:

  1. 打开LoginServlet.java,注释掉验证码校验段:
// String inputCode = request.getParameter("code"); // String sessionCode = (String) request.getSession().getAttribute("verifyCode"); // if (!inputCode.equalsIgnoreCase(sessionCode)) { // request.setAttribute("msg", "验证码错误"); // request.getRequestDispatcher("login.jsp").forward(request, response); // return; // }
  1. 密码加密通常是MD5(PasswordUtil.md5("123456")生成e10adc3949ba59abbe56e057f20f883e),但数据库里存的可能是明文。查user表:
SELECT username, password FROM user WHERE username='admin';

若password字段值是123456,说明未加密,直接用admin/123456登录;若是MD5值,则用在线工具反查原密码,或临时改代码跳过校验。

3.5 图片上传测试:upload目录权限与路径拼接的玄学

上传功能失败90%因路径问题。UploadServlet.java关键代码:

String uploadPath = request.getServletContext().getRealPath("/upload"); // 注意:无结尾斜杠! File uploadDir = new File(uploadPath); if (!uploadDir.exists()) uploadDir.mkdirs(); // 必须创建目录 String fileName = System.currentTimeMillis() + "_" + item.getName(); File uploadedFile = new File(uploadDir, fileName); item.write(uploadedFile); // 写入文件 // 保存到数据库的路径是相对路径 String dbPath = "upload/" + fileName; // 注意:此处加"upload/"前缀

所以,uploadPath在Windows上生成类似D:\apache-tomcat-8.5.99\webapps\secondhand\upload,而数据库存的是upload/20240512_xxx.jpg。若你看到图片不显示,检查:

  • webapps/secondhand/upload/目录是否存在?不存在则mkdirs()没执行;
  • 浏览器F12看图片<img src="upload/xxx.jpg">,路径是否404?若是,说明upload目录不在secondhand应用根目录下,需确认getRealPath()返回值是否正确。

4. 避坑指南:五个血泪经验换来的高频故障排查清单

4.1 现象:首页加载一半卡住,控制台报java.lang.NoClassDefFoundError: javax/servlet/http/HttpServletRequest

原因:Tomcat的servlet-api.jar未被项目引用,或IDEA里勾选了“Use module compile output path”导致重复加载。
解决:

  • Eclipse:右键项目→Properties→Java Build Path→Libraries→Add Library→Server Runtime→选择Tomcat;
  • IDEA:File→Project Structure→Modules→Dependencies→点击+号→JARs or directories→选择Tomcat/lib/servlet-api.jar;
  • 删除WEB-INF/lib/下所有servlet-api*.jar,Tomcat自身提供即可。

4.2 现象:登录成功后跳转到空白页,URL变成http://localhost:8080/secondhand/null

原因:LoginServlet.java里response.sendRedirect()参数为空字符串或null,常见于request.getParameter("redirect")未传值时未设默认值。
解决:

String redirect = request.getParameter("redirect"); if (redirect == null || redirect.trim().isEmpty()) { redirect = "index.jsp"; // 强制设默认页 } response.sendRedirect(redirect);

4.3 现象:发布商品时图片上传成功,但商品列表页图片不显示,F12看<img>标签src是/upload/xxx.jpg(开头多了一个斜杠)

原因:getRealPath()返回绝对路径,但JSP里写<img src="${goods.imagePath}">时,imagePath存的是upload/xxx.jpg,而浏览器解析/upload为根路径,实际应为/secondhand/upload/xxx.jpg。
解决:

  • 方案A(推荐):数据库存相对路径upload/xxx.jpg,JSP里写<img src="${pageContext.request.contextPath}/${goods.imagePath}">;
  • 方案B:修改UploadServlet.java,存路径时加应用名:"secondhand/upload/xxx.jpg"。

4.4 现象:MySQL插入中文报错java.sql.SQLException: Incorrect string value: '\xE7\x94\xB5\xE5\xAD\x90...' for column 'title' at row 1

原因:数据库、表、字段三级字符集未统一为utf8mb4,或JDBC URL缺characterEncoding=utf8mb4。
解决:

-- 逐级检查并修正 ALTER DATABASE secondhand CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci; ALTER TABLE goods CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; -- 修改JDBC URL jdbc:mysql://localhost:3306/secondhand?useUnicode=true&characterEncoding=utf8mb4&serverTimezone=Asia/Shanghai

4.5 现象:管理员后台审核商品后,前台商品状态仍是“待审核”,刷新也不变

原因:GoodsService.java里更新状态后未调用conn.commit(),或事务被异常吞掉。
解决:

  • 检查DAO层方法是否在try-catch里吞了异常:
try { pstmt.executeUpdate(); conn.commit(); // 必须显式提交 } catch (SQLException e) { conn.rollback(); // 回滚 e.printStackTrace(); // 至少打印日志! }
  • 若用DBUtil.getConnection()获取连接,确认DBUtil类里conn.setAutoCommit(false)后,所有业务方法都配对commit()。

5. 安全加固与生产化改造:从校园Demo到可用系统的三道关

5.1 密码存储升级:从明文/MD5到BCrypt的平滑迁移

系统原始密码存MD5(不可逆但易撞库),生产环境必须升级。步骤:

  1. 添加BCrypt依赖(Maven):
<dependency> <groupId>org.springframework.security</groupId> <artifactId>spring-security-crypto</artifactId> <version>5.7.8</version> </dependency>
  1. 修改UserService.java登录逻辑:
// 验证密码(旧MD5兼容) String dbPassword = userDao.getPasswordByUsername(username); if (dbPassword.length() == 32) { // MD5长度 if (dbPassword.equals(PasswordUtil.md5(password))) { // 兼容旧密码,登录成功后异步更新为BCrypt new Thread(() -> { String bcrypt = new BCryptPasswordEncoder().encode(password); userDao.updatePasswordToBcrypt(username, bcrypt); }).start(); } } else { // BCrypt格式(以$2a$开头) if (new BCryptPasswordEncoder().matches(password, dbPassword)) { // 直接验证 } }
  1. 数据库user表password字段改为VARCHAR(100),足够存BCrypt哈希值。

5.2 SQL注入防御:从Statement到PreparedStatement的强制切换

搜索所有dao/下的Java文件,将Statement stmt = conn.createStatement()替换为:

String sql = "SELECT * FROM goods WHERE title LIKE ? AND status = ?"; PreparedStatement pstmt = conn.prepareStatement(sql); pstmt.setString(1, "%" + keyword + "%"); pstmt.setInt(2, status); ResultSet rs = pstmt.executeQuery();

为什么必须改?原始代码若写"SELECT * FROM goods WHERE title LIKE '%" + keyword + "%'",用户搜'; DROP TABLE user; --直接删库。PreparedStatement预编译机制天然免疫。

5.3 XSS过滤:JSP页面的EL表达式安全边界

所有用户输入内容(商品标题、描述、评论)在JSP中必须用fn:escapeXml()过滤:

<%@ taglib prefix="fn" uri="http://java.sun.com/jsp/jstl/functions" %> <h3>${fn:escapeXml(goods.title)}</h3> <p>${fn:escapeXml(goods.description)}</p>

不这样做会怎样?用户发商品标题<script>alert('xss')</script>,所有浏览者打开页面就弹窗,甚至窃取Cookie。

5.4 文件上传防护:白名单校验与大小限制的硬编码

UploadServlet.java里必须加两道锁:

// 1. 文件类型白名单 String contentType = item.getContentType(); if (!contentType.equals("image/jpeg") && !contentType.equals("image/png")) { request.setAttribute("msg", "只允许上传JPG/PNG图片"); request.getRequestDispatcher("publish.jsp").forward(request, response); return; } // 2. 文件大小限制(2MB) if (item.getSize() > 2 * 1024 * 1024) { request.setAttribute("msg", "文件不能超过2MB"); request.getRequestDispatcher("publish.jsp").forward(request, response); return; }

为什么不用MIME类型判断?item.getContentType()可被伪造,必须结合文件头检测(本系统简化处理,生产环境需读取前几个字节校验)。

5.5 日志审计:记录关键操作的最小可行方案

在OrderService.java的createOrder()方法开头加:

// 记录订单创建日志 String log = String.format("[%s] 用户%s创建订单ID:%d, 商品:%s, 金额:%.2f", new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date()), currentUser.getUsername(), orderId, goods.getTitle(), order.getAmount() ); // 写入文件(生产环境建议用Log4j) try (FileWriter fw = new FileWriter("logs/order.log", true)) { fw.write(log + "\n"); } catch (IOException e) { e.printStackTrace(); }

价值在哪?当学生投诉“我付了钱但没发货”,查order.log就能确认支付时间、金额、商品ID,避免扯皮。


6. 最后一道防线:上线前必须做的五项验证与我的个人习惯

6.1 验证清单:用真实场景跑通核心链路

别只测登录注册!按学生真实行为走一遍:

步骤操作预期结果检查点
1学生A发布教材《Java编程思想》,上传封面图goods表新增记录,image_path字段值为upload/xxx.jpg,webapps/secondhand/upload/下存在该文件
2学生B搜索“Java”,找到该教材并加入购物车cart表新增记录,关联goods_id和user_id
3学生B提交订单,支付成功order表状态为1(已支付),order_item表关联商品,goods表status变为2(已售出)
4管理员登录后台,审核该商品(若需审核)goods.status从0变为1,前台立即可见
5学生A查看“我的出售”,状态显示“已售出”user_goods视图或查询逻辑正确关联订单状态

提示:每步执行后,直接查MySQL确认数据变更,比看页面更可靠。

6.2 性能临界点测试:模拟并发下的连接池泄漏

用Apache JMeter模拟100用户同时登录:

  • 线程组:100线程,Ramp-up 10秒,循环1次;
  • HTTP请求:POST/secondhand/LoginServlet,参数username=admin&password=123456;
  • 查看结果树:若出现大量Connection refused或响应时间>5s,说明DBUtil.java里的连接未关闭。
    修复代码模板:
Connection conn = null; PreparedStatement pstmt = null; ResultSet rs = null; try { conn = DBUtil.getConnection(); pstmt = conn.prepareStatement(sql); rs = pstmt.executeQuery(); // 处理结果... } finally { // 必须按顺序关闭,且判空 if (rs != null) try { rs.close(); } catch (SQLException e) {} if (pstmt != null) try { pstmt.close(); } catch (SQLException e) {} if (conn != null) try { conn.close(); } catch (SQLException e) {} // 关键! }

6.3 安全扫描:用OWASP ZAP快速揪出漏洞

下载OWASP ZAP(免费),配置代理127.0.0.1:8080,浏览器访问http://localhost:8080/secondhand/,ZAP自动爬取所有链接。重点关注:

  • Active Scan结果中的Reflected XSS(搜索框未过滤);
  • SQL Injection(商品详情页?id=1参数未预编译);
  • Insecure Direct Object References(订单页?orderId=123可被篡改ID查他人订单)。
    修复优先级:XSS和SQLi必须当天解决,IDOR可加权限校验(if(!order.getUserId().equals(currentUser.getId())){throw new SecurityException();})。

6.4 备份策略:数据库与上传文件的双保险

  • 数据库:每天凌晨3点用mysqldump备份:
mysqldump -u root -p123456 secondhand > /backup/secondhand_$(date +%Y%m%d).sql
  • 上传文件:webapps/secondhand/upload/目录同步到NAS或云存储,用robocopy(Windows)或rsync(Linux)增量备份。
    血泪教训:去年某校系统硬盘损坏,因只备份了数据库,200G学生上传的教材图片全丢,重传耗时两周。从那以后我每次部署新系统,第一件事就是写好这两行备份脚本,并设置Windows任务计划或Linux crontab,确保它真在跑。

希望帮到你。

本文还有配套的精品资源,点击获取