简介:本资源是一个基于Java原生技术栈(JSP + Servlet)实现的轻量级二手物品交易网站源码,面向Java Web初学者与课程设计实践者,解决校园或小型社区场景下二手商品发布、浏览与交易的基础需求。压缩包共108个文件,总计5.55MB,涵盖33个Java后端逻辑类、21个JSP前端页面、19张商品展示图片,以及6个CSS样式文件和5个JavaScript交互脚本,完整支撑用户注册登录、商品管理、购物车及订单流程等核心功能。已有296人学习下载,说明其结构清晰、代码规范,适合作为JSP/Servlet入门项目进行调试、二次开发或教学演示。资源中包含Bootstrap相关样式文件(如bootstrap.min.css)、SQL数据库脚本及META-INF配置,目录组织合理,便于快速理解MVC分层逻辑与前后端交互机制,是掌握传统Java Web开发全流程的典型参考案例。
1. 一个能跑起来的二手交易网站,为什么 Java + Bootstrap 是新手最稳的起点?
你不需要部署云服务器、不用配 Nginx 反向代理、甚至不碰 Docker,就能在本地 Windows 或 macOS 上双击启动一个带登录、发布商品、搜索浏览功能的二手物品交易网站——这就是“基于 Java 的简单二手物品交易网站设计源码”真正落地的样子。它不是教学 Demo,而是用真实 Web 开发逻辑组织的最小可行系统:后端用 Servlet + JDBC 直连 H2 内存数据库(零安装),前端用 Bootstrap 5.3 的bootstrap.min.css实现响应式布局,所有页面通过 JSP 渲染,没有 Spring Boot 自动配置的黑盒,也没有 Vue/React 的构建链路干扰。适合刚学完 Java 基础、JDBC 和 HTML/CSS 的开发者,在 2 小时内完成从解压到访问http://localhost:8080的全过程。它不追求高并发或微服务架构,但每行代码都暴露在你眼皮底下:用户注册怎么校验手机号格式、商品图片如何存路径而非二进制、搜索关键词怎么防 SQL 注入、分页查询 LIMIT OFFSET 怎么和前端页码联动——这些正是 Java 面试题里高频出现的“手写 DAO 层”“JSP 表单处理”“Servlet 生命周期”等考点的实战场。如果你正卡在“学了一堆语法却写不出完整 Web 应用”的阶段,这个源码就是那根可拆解、可调试、可改出自己简历项目的脚手架。
2. 用 Servlet + JSP 搭建核心流程:从用户注册到商品发布的最小闭环
2.1 为什么选原生 Servlet 而非 Spring Boot?——看清技术栈的“呼吸感”
很多初学者一上来就啃 Spring Boot,结果连DispatcherServlet怎么被web.xml加载都不清楚。本项目坚持用web.xml显式配置 Servlet 映射,目的就是让你看清请求从浏览器发出到页面返回的每一层穿透:HTTP 请求 → Tomcat 容器 →web.xml匹配 URL → 对应 Servlet 类 →doPost()方法处理表单 → JDBC 执行 INSERT → 转发到 JSP 成果页。这种“慢速但透明”的路径,恰恰是理解 Java Web 基础的关键。比如RegisterServlet.java中关键代码段:
// RegisterServlet.java protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { String username = request.getParameter("username").trim(); String phone = request.getParameter("phone"); String password = request.getParameter("password"); // 1. 前端传来的 phone 是字符串,这里做基础格式校验(面试常考正则) if (!phone.matches("^1[3-9]\\d{9}$")) { request.setAttribute("error", "手机号格式不正确"); request.getRequestDispatcher("/register.jsp").forward(request, response); return; } // 2. 使用 PreparedStatement 防 SQL 注入(Java 面试八股文必答点) String sql = "INSERT INTO users (username, phone, password, create_time) VALUES (?, ?, ?, ?)"; try (Connection conn = DBUtil.getConnection(); PreparedStatement ps = conn.prepareStatement(sql)) { ps.setString(1, username); ps.setString(2, phone); ps.setString(3, DigestUtils.md5Hex(password)); // 密码 MD5 加密,非明文存储 ps.setTimestamp(4, new Timestamp(System.currentTimeMillis())); int rows = ps.executeUpdate(); if (rows > 0) { response.sendRedirect("login.jsp?msg=注册成功,请登录"); } } catch (SQLException e) { e.printStackTrace(); request.setAttribute("error", "注册失败:" + e.getMessage()); request.getRequestDispatcher("/register.jsp").forward(request, response); } }提示:这段代码直接对应 Java 面试题中“如何防止 SQL 注入”“Servlet 如何处理 POST 表单”“JDBC 连接池是否必要”三个高频问题。注意
DBUtil.getConnection()返回的是 H2 内存数据库连接,无需额外安装 MySQL;DigestUtils.md5Hex()来自 Apache Commons Codec,jar 包已包含在lib/目录下,避免新手陷入 Maven 依赖冲突。
2.2 Bootstrap 5.3 的 CSS 如何驱动整个前端骨架?
项目前端不写一行 CSS,全部依赖bootstrap.min.css提供的类名系统。关键不是“用没用 Bootstrap”,而是怎么用得精准且不冗余。例如商品列表页index.jsp中的商品卡片结构:
<!-- index.jsp 片段 --> <div class="row g-4"> <c:forEach items="${goodsList}" var="g"> <div class="col-12 col-md-6 col-lg-4"> <div class="card h-100 border-0 shadow-sm"> <img src="${g.imagePath}" class="card-img-top object-fit-cover" style="height: 200px;" alt="${g.title}"> <div class="card-body d-flex flex-column"> <h5 class="card-title text-truncate">${g.title}</h5> <p class="card-text text-muted flex-grow-1 mb-2">${g.description}</p> <div class="d-flex justify-content-between align-items-center"> <span class="h5 text-primary fw-bold">¥${g.price}</span> <a href="detail.jsp?id=${g.id}" class="btn btn-sm btn-outline-primary">查看详情</a> </div> </div> </div> </div> </c:forEach> </div>参数说明:
row g-4:Bootstrap 5 的栅格系统,g-4表示列间距 1.5rem(16px × 0.9375);col-12 col-md-6 col-lg-4:响应式断点,手机单列、平板双列、桌面三列;object-fit-cover+style="height:200px":强制图片等比缩放填充容器,解决二手图尺寸不一问题;text-truncate:单行文本溢出显示省略号,避免长标题撑破卡片;flex-column+flex-grow-1:让描述文字区域自动撑满剩余空间,价格和按钮固定在底部。
这些不是炫技,而是真实二手场景需求:用户扫一眼要快速识别价格、图片、标题,Bootstrap 类名就是你的“CSS 速查字典”。
2.3 数据库设计直击二手业务本质:三张表搞定核心关系
项目仅用三张表支撑全部功能,结构精简但覆盖关键业务约束:
| 表名 | 字段 | 说明 |
|---|---|---|
users | id(PK), username, phone(UNIQUE), password, create_time | 用户表,手机号唯一索引,防止重复注册 |
goods | id(PK), title, description, price(DECIMAL), image_path, user_id(FK), status(TINYINT) | 商品表,status=1表示上架,0表示已售出/下架 |
orders | id(PK), goods_id(FK), buyer_id(FK), seller_id(FK), create_time | 订单表,记录谁买了谁的商品(简化版,无支付状态) |
H2 数据库初始化脚本src/main/resources/init.sql在应用首次启动时自动执行:
-- init.sql CREATE TABLE IF NOT EXISTS users ( id BIGINT AUTO_INCREMENT PRIMARY KEY, username VARCHAR(50) NOT NULL, phone VARCHAR(11) NOT NULL UNIQUE, password VARCHAR(32) NOT NULL, create_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP ); CREATE TABLE IF NOT EXISTS goods ( id BIGINT AUTO_INCREMENT PRIMARY KEY, title VARCHAR(100) NOT NULL, description TEXT, price DECIMAL(10,2) NOT NULL, image_path VARCHAR(200) DEFAULT '/images/default.jpg', user_id BIGINT NOT NULL, status TINYINT DEFAULT 1, create_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE );注意:
ON DELETE CASCADE是关键设计——当用户注销时,其发布的所有商品自动删除,避免孤儿数据。这比在 Java 代码里手动遍历删除更可靠,也是数据库设计面试题常考点。
3. 本地运行四步法:从解压到访问 localhost:8080 的完整命令链
3.1 环境准备:JDK 8+ 与 Tomcat 9 的最小化安装验证
本项目兼容 JDK 8 至 JDK 17,但必须使用 Tomcat 9.x(Tomcat 10+ 默认启用 Jakarta EE 9 命名空间,javax.servlet.*包名变为jakarta.servlet.*,会导致编译报错)。验证环境是否就绪:
# 检查 JDK 版本(需 1.8+) java -version # 输出应类似:openjdk version "17.0.1" 2021-10-19 # 检查 Tomcat 版本(需 9.0.x) $CATALINA_HOME/bin/version.sh # Linux/macOS %CATALINA_HOME%\bin\version.bat # Windows # 输出应含 "Apache Tomcat/9.0.xx"提示:若未安装 Tomcat,去官网下载
apache-tomcat-9.0.83.zip(最新稳定版),解压后设置环境变量CATALINA_HOME指向解压目录。Windows 用户注意路径不要含中文或空格。
3.2 源码部署:将项目放入 Tomcat webapps 并启动服务
项目结构为标准 Java Web Archive(WAR)布局,无需 Maven 构建,直接复制即可:
# 假设源码解压在 ~/project/secondhand/ # Linux/macOS 执行: cp -r ~/project/secondhand/ $CATALINA_HOME/webapps/secondhand # Windows 执行(PowerShell): Copy-Item -Path "C:\project\secondhand" -Destination "$env:CATALINA_HOME\webapps\secondhand" -Recurse # 启动 Tomcat(后台运行,方便查看日志) $CATALINA_HOME/bin/startup.sh # Linux/macOS %CATALINA_HOME%\bin\startup.bat # Windows启动后观察$CATALINA_HOME/logs/catalina.out日志,确认无ClassNotFoundException或SQLException错误。正常启动末尾会输出:
INFO [main] org.apache.catalina.startup.Catalina.start Server startup in [1234] milliseconds此时访问http://localhost:8080/secondhand/即可看到首页。
3.3 关键配置文件解析:web.xml 与 DBUtil.java 的联动逻辑
WEB-INF/web.xml是整个应用的“中枢神经”,定义了 Servlet 映射和过滤器:
<!-- web.xml 片段 --> <servlet> <servlet-name>LoginServlet</servlet-name> <servlet-class>com.example.servlet.LoginServlet</servlet-class> </servlet> <servlet-mapping> <servlet-name>LoginServlet</servlet-name> <url-pattern>/login</url-pattern> <!-- 注意:JSP 页面提交 form action="/login" --> </servlet-mapping> <!-- 全局字符编码过滤器,解决中文乱码 --> <filter> <filter-name>EncodingFilter</filter-name> <filter-class>com.example.filter.EncodingFilter</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>DBUtil.java则封装了 H2 数据库连接逻辑,关键在于内存模式 URL 和自动建表:
// DBUtil.java private static final String URL = "jdbc:h2:mem:secondhand;DB_CLOSE_DELAY=-1;DB_CLOSE_ON_EXIT=FALSE"; private static final String USER = "sa"; private static final String PASSWORD = ""; public static Connection getConnection() throws SQLException { // 第一次调用时触发 init.sql 执行 try (Connection conn = DriverManager.getConnection(URL, USER, PASSWORD)) { conn.createStatement().execute("RUNSCRIPT FROM 'src/main/resources/init.sql'"); return conn; } }参数说明:
mem:secondhand:H2 内存数据库,重启 Tomcat 后数据丢失,适合开发调试;DB_CLOSE_DELAY=-1:允许连接池复用,避免频繁创建销毁连接;RUNSCRIPT FROM '...':在每次获取连接时执行初始化脚本,确保表结构存在。
这种设计让新手免于手动建库建表,但必须确保init.sql文件路径在类路径下(即src/main/resources/编译后位于WEB-INF/classes/)。
3.4 常见启动失败排查表:按错误日志快速定位根因
| 错误日志关键词 | 根本原因 | 解决方案 |
|---|---|---|
java.lang.ClassNotFoundException: javax.servlet.http.HttpServlet | Tomcat 版本过高(10+),包名已变 | 降级到 Tomcat 9.0.x,或修改所有import javax.servlet.*为jakarta.servlet.*(不推荐新手) |
org.h2.jdbc.JdbcSQLNonTransientException: Database "mem:secondhand" not found | init.sql路径错误或未被加载 | 检查DBUtil.java中RUNSCRIPT路径是否为src/main/resources/init.sql,确认该文件存在于WEB-INF/classes/下 |
HTTP Status 404 – /secondhand/ | 项目未正确放入webapps/目录,或目录名不匹配 | 确认$CATALINA_HOME/webapps/secondhand/下存在WEB-INF/和index.jsp,URL 必须带/secondhand/ |
java.sql.SQLException: Unique constraint violation | 用户注册时手机号重复 | 清空 H2 内存数据库:停止 Tomcat,删除$CATALINA_HOME/work/Catalina/localhost/secondhand/目录 |
4. 商品搜索与分页的实战优化:从模糊匹配到用户体验细节
4.1 模糊搜索的 SQL 实现与性能边界
二手交易的核心操作是“找东西”,搜索不能只靠LIKE '%关键词%'。项目采用组合策略提升准确率:
// SearchServlet.java String keyword = request.getParameter("q").trim(); if (keyword.isEmpty()) { // 空搜索返回全部上架商品 sql = "SELECT * FROM goods WHERE status = 1 ORDER BY create_time DESC LIMIT ? OFFSET ?"; } else { // 分词搜索:标题 AND 描述 OR 标题 OR 描述(模拟搜索引擎逻辑) sql = "SELECT * FROM goods WHERE status = 1 AND " + "(title LIKE ? OR description LIKE ? OR title LIKE ?) " + "ORDER BY create_time DESC LIMIT ? OFFSET ?"; ps.setString(1, "%" + keyword + "%"); // 标题包含 ps.setString(2, "%" + keyword + "%"); // 描述包含 ps.setString(3, keyword + "%"); // 标题前缀匹配(提高首字母输入体验) }为什么这样写?
title LIKE ?和description LIKE ?覆盖大部分用户输入习惯;title LIKE ?(前缀匹配)让“iphone”能命中 “iPhone 12 Pro”,而'%iphone%'无法利用索引;- H2 内存数据库对小数据量(<1000 条)全表扫描无压力,无需建全文索引。
这正是 Java 面试题中“如何优化模糊搜索”的务实答案:不盲目上 Elasticsearch,先用 SQL 层级策略解决问题。
4.2 前端分页组件的 Bootstrap 5 实现与后端参数协同
分页不是简单循环<li>,而是前后端严格约定参数。index.jsp中分页 HTML:
<!-- 分页导航 --> <nav aria-label="商品列表分页"> <ul class="pagination justify-content-center"> <li class="page-item ${pageNo <= 1 ? 'disabled' : ''}"> <a class="page-link" href="?page=${pageNo-1}&q=${param.q}">上一页</a> </li> <c:forEach begin="1" end="${totalPages}" var="i"> <li class="page-item ${i == pageNo ? 'active' : ''}"> <a class="page-link" href="?page=${i}&q=${param.q}">${i}</a> </li> </c:forEach> <li class="page-item ${pageNo >= totalPages ? 'disabled' : ''}"> <a class="page-link" href="?page=${pageNo+1}&q=${param.q}">下一页</a> </li> </ul> </nav>后端GoodsServlet.java计算逻辑:
int pageSize = 9; // 每页 9 条,适配 Bootstrap 卡片三列布局 int pageNo = Integer.parseInt(request.getParameter("page") != null ? request.getParameter("page") : "1"); int offset = (pageNo - 1) * pageSize; // 查询总条数(用于计算总页数) String countSql = "SELECT COUNT(*) FROM goods WHERE status = 1 " + (keyword.isEmpty() ? "" : "AND (title LIKE ? OR description LIKE ?)"); // ... 执行 countSql 获取 totalCount ... int totalPages = (int) Math.ceil((double) totalCount / pageSize); request.setAttribute("totalPages", totalPages); request.setAttribute("pageNo", pageNo);关键协同点:
- 前端
href="?page=${pageNo-1}&q=${param.q}"保证搜索关键词q在翻页时保留;Math.ceil()计算总页数,避免totalCount % pageSize == 0时少算一页;disabled类由 EL 表达式动态控制,禁用无效翻页链接,这是 Bootstrap 5 推荐的无障碍实践。
4.3 图片上传的极简实现:不依赖第三方库的 JSP 文件处理
二手网站必须支持用户上传商品图。项目采用cos.jar(O'Reilly 的 classic upload)实现,因其轻量(仅 100KB)且无需 Servlet 3.0+ 注解:
<!-- publish.jsp 表单 --> <form action="publish" method="post" enctype="multipart/form-data"> <div class="mb-3"> <label class="form-label">商品图片</label> <input type="file" name="image" class="form-control" accept="image/*"> </div> <!-- 其他字段 --> <button type="submit" class="btn btn-primary">发布商品</button> </form>PublishServlet.java处理逻辑:
// PublishServlet.java MultipartRequest multi = new MultipartRequest( request, getServletContext().getRealPath("/images/"), // 保存到 webapps/secondhand/images/ 5 * 1024 * 1024, // 5MB 限制 "UTF-8", new DefaultFileRenamePolicy() ); String title = multi.getParameter("title"); String imagePath = "/images/" + multi.getFilesystemName("image"); // 路径存入数据库注意:
cos.jar已预置在WEB-INF/lib/下,无需额外下载。getRealPath("/images/")返回绝对路径,确保文件写入webapps/secondhand/images/目录,浏览器可通过http://localhost:8080/secondhand/images/filename.jpg直接访问。这是 Java Web 开发中“文件上传”考点的标准解法。
5. 从源码到面试:三个可直接复用的 Java 面试高频问题应答模板
5.1 “请手写一个 JDBC 查询方法”——以 GoodsDAO.findAll() 为蓝本
面试官要的不是完美 ORM,而是你对资源管理、异常处理、SQL 注入防护的理解。直接复用项目中的GoodsDAO.java:
public List<Goods> findAll(int offset, int limit) { List<Goods> list = new ArrayList<>(); String sql = "SELECT id, title, price, image_path, create_time FROM goods " + "WHERE status = 1 ORDER BY create_time DESC LIMIT ? OFFSET ?"; try (Connection conn = DBUtil.getConnection(); PreparedStatement ps = conn.prepareStatement(sql)) { ps.setInt(1, limit); // 防止恶意传入超大数字 ps.setInt(2, offset); try (ResultSet rs = ps.executeQuery()) { while (rs.next()) { Goods g = new Goods(); g.setId(rs.getLong("id")); g.setTitle(rs.getString("title")); g.setPrice(rs.getBigDecimal("price")); g.setImagePath(rs.getString("image_path")); g.setCreateTime(rs.getTimestamp("create_time")); list.add(g); } } } catch (SQLException e) { throw new RuntimeException("查询商品列表失败", e); // 包装为运行时异常,上层统一处理 } return list; }应答要点:
- 强调
try-with-resources自动关闭连接,避免连接泄漏;- 指出
LIMIT ? OFFSET ?参数化防止注入,且offset由前端传入需校验范围(如offset >= 0 && offset < 10000);- 说明
RuntimeException包装是为了让 Controller 层用@ExceptionHandler统一返回 JSON 错误,符合 RESTful 设计。
5.2 “JSP 和 Servlet 如何配合处理用户登录?”——用 LoginServlet + login.jsp 拆解生命周期
这不是背概念,而是讲清数据流向:
- 用户在
login.jsp输入账号密码,表单action="login"提交到LoginServlet; LoginServlet.doPost()从request.getParameter()获取参数,用PreparedStatement查询数据库;- 若密码匹配,将
User对象存入HttpSession:request.getSession().setAttribute("user", user); - 重定向到
index.jsp,该页面通过<c:if test="${not empty sessionScope.user}">判断登录态,显示“欢迎,${sessionScope.user.username}”; - 退出登录时
LogoutServlet调用session.invalidate(),清除所有 session 属性。
面试加分项:指出
request.getSession(true)默认创建新会话,而request.getSession(false)仅获取不创建,避免无意义会话膨胀。
5.3 “Bootstrap 如何实现响应式商品网格?”——用 index.jsp 的 col-* 类名链回答
不要说“用了栅格系统”,要具体到类名作用链:
class="row g-4":定义弹性盒子容器,g-4设置列间距为1.5rem(Bootstrap 5 的 spacing scale);class="col-12 col-md-6 col-lg-4":col-12在超小屏(<576px)占满整行,col-md-6在中屏(≥768px)占半行(2 列),col-lg-4在大屏(≥992px)占 1/3 行(3 列);class="card h-100":h-100让卡片高度继承父容器,解决不同商品标题长度导致卡片高度不一的问题;class="object-fit-cover":CSS 属性object-fit: cover强制图片等比缩放并裁剪,保持视觉一致性。
这个回答把“响应式”从抽象概念落到具体类名、像素值和 CSS 属性,证明你真正在项目里调过这些样式,而不是复制粘贴。
本文还有配套的精品资源,点击获取