简介这是一份面向Java初学者与编程练习者的简易宠物商店源代码项目围绕宠物信息管理、库存控制与交易处理等业务场景帮助读者理解面向对象编程、JDBC数据库操作及Swing图形界面设计等核心知识点。压缩包为rar格式共包含7个文件以class字节码、java源文件、project与classpath工程配置、prefs偏好设置及txt说明文档为主整体约8KB体积轻巧便于在Eclipse中直接导入运行与调试。目前已有827人学习下载适合作为课程设计或自学练手素材。项目结构清晰读者可借此观察Pet类等实体如何封装name、species、age属性及buy、sell方法进而掌握类间继承、封装与多态的组织方式同时可结合异常处理、文件读写与设计模式等角度进行重构练习巩固Java编程技能并积累软件工程实践经验。1. 简易宠物商店Java源代码从课程设计到能跑通的最小闭环很多同学搜「简易宠物商店Java源代码」真实诉求并不是要一份能上线的电商系统而是想找一个结构清晰、能编译、能演示、能改的课程设计级项目。它通常出现在 Java 面向对象、数据库应用、Web 开发这几门课的期末大作业里核心是「宠物信息管理 用户下单 后台维护」这条主线。我见过太多人下载了一堆源码结果卡在数据库连不上、JDK 版本对不上、Servlet 容器跑不起来最后交作业前一天通宵改配置。这篇笔记就按一线做法把一份简易宠物商店 Java 源码该有的模块、怎么在本地跑通、参数怎么设、坑在哪一层层拆开讲清楚。适合刚学完 Java 基础、准备做第一个完整小项目的同学也适合需要快速搭一个可演示 Demo 的开发者。2. 先定架构再写代码简易宠物商店该拆成哪几层2.1 为什么课程设计级项目优先选 Servlet JDBC 而不是 Spring Boot很多人一上来就想用 Spring Boot觉得「现代」。但对于「简易宠物商店 Java 源代码」这个场景Spring Boot 反而会掩盖掉你真正需要展示的东西面向对象设计、请求流转、SQL 手写能力。课程设计考察的是你对基础链路的理解不是框架熟练度。常见做法是三层结构表现层用 JSP/Servlet业务层用 Service 类数据访问层用 JDBC 直连数据库。这样每一层职责清晰代码量可控一个宠物商店的核心表也就四张宠物表、分类表、用户表、订单表。用 Spring Boot 的话自动配置会把数据源、事务、MVC 全包掉答辩时老师问你「一个请求怎么走到数据库的」你反而说不清。我一般会建议如果时间只有一周选 Servlet JDBC如果时间充裕且想顺便学框架可以在跑通基础版之后再把 DAO 层替换成 MyBatis 做对比。这样既保底又有加分项。2.2 四张核心表的设计与建表 SQL数据库设计决定了后面代码好不好写。宠物商店最小可用模型如下表名作用关键字段pet宠物商品id, name, category_id, price, stock, imagecategory宠物分类id, nameuser用户id, username, password, phoneorders订单id, user_id, pet_id, quantity, total, create_time建表语句直接给一份可执行的-- 分类表 CREATE TABLE category ( id INT PRIMARY KEY AUTO_INCREMENT, name VARCHAR(50) NOT NULL ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; -- 宠物表category_id 外键关联分类 CREATE TABLE pet ( id INT PRIMARY KEY AUTO_INCREMENT, name VARCHAR(100) NOT NULL, category_id INT, price DECIMAL(10,2) NOT NULL DEFAULT 0.00, stock INT NOT NULL DEFAULT 0, image VARCHAR(255), FOREIGN KEY (category_id) REFERENCES category(id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; -- 用户表密码存明文仅用于教学演示 CREATE TABLE user ( id INT PRIMARY KEY AUTO_INCREMENT, username VARCHAR(50) UNIQUE NOT NULL, password VARCHAR(100) NOT NULL, phone VARCHAR(20) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; -- 订单表 CREATE TABLE orders ( id INT PRIMARY KEY AUTO_INCREMENT, user_id INT NOT NULL, pet_id INT NOT NULL, quantity INT NOT NULL DEFAULT 1, total DECIMAL(10,2) NOT NULL, create_time DATETIME DEFAULT CURRENT_TIMESTAMP ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;这里有几个参数要说明。utf8mb4而不是utf8是因为宠物名字里可能出现生僻字或表情符号utf8在三字节以上字符会截断。DECIMAL(10,2)用于金额绝不能用FLOAT否则 0.10.2 会出现精度误差订单金额对不上。stock默认 0 而不是允许 NULL是为了后面扣库存时不用额外判空。2.3 项目目录结构与依赖清单一个能直接导入 IDE 的目录结构长这样petstore/ ├── src/ │ ├── main/ │ │ ├── java/ │ │ │ └── com/example/petstore/ │ │ │ ├── dao/ # 数据访问 │ │ │ ├── service/ # 业务逻辑 │ │ │ ├── servlet/ # 控制器 │ │ │ └── util/ # 数据库连接工具 │ │ ├── resources/ │ │ │ └── db.properties # 数据库配置 │ │ └── webapp/ │ │ ├── WEB-INF/web.xml │ │ └── *.jsp └── pom.xml依赖只需要三个javax.servlet-apiprovided 范围、mysql-connector-java、jstl。用 Maven 管理避免手动拷 jar 包导致版本冲突。注意 Servlet API 的 scope 要设成provided因为 Tomcat 容器自带打进 war 包反而会冲突。3. 把源码跑起来数据库连接与第一个 Servlet3.1 db.properties 与 JDBC 工具类的写法数据库配置单独抽成文件不要硬编码在 Java 里。src/main/resources/db.propertiesjdbc.drivercom.mysql.cj.jdbc.Driver jdbc.urljdbc:mysql://localhost:3306/petstore?useSSLfalseserverTimezoneAsia/ShanghaicharacterEncodingutf8mb4 jdbc.usernameroot jdbc.passwordyour_password注意serverTimezone必须显式指定MySQL 8 驱动不设会报时区错误useSSLfalse在本地开发避免证书警告。接着写连接工具类package com.example.petstore.util; import java.io.InputStream; import java.sql.Connection; import java.sql.DriverManager; import java.util.Properties; public class DBUtil { private static String url; private static String username; 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); username props.getProperty(jdbc.username); password props.getProperty(jdbc.password); } catch (Exception e) { throw new RuntimeException(数据库配置加载失败, e); } } public static Connection getConnection() throws Exception { return DriverManager.getConnection(url, username, password); } }逻辑说明静态块保证驱动只注册一次用getResourceAsStream从 classpath 读配置打成 war 后依然能找到。参数上Class.forName在 MySQL 8 驱动里其实可以省略但保留它兼容旧环境更稳。失败时抛RuntimeException而不是吞掉是为了让启动阶段就暴露问题而不是等到第一次查询才报错。3.2 宠物列表 Servlet 的完整实现这是整个项目最核心的一个 Servlet把数据库里的宠物查出来转发给 JSPpackage com.example.petstore.servlet; import com.example.petstore.util.DBUtil; import javax.servlet.ServletException; import javax.servlet.annotation.WebServlet; import javax.servlet.http.*; import java.io.IOException; import java.sql.*; import java.util.*; WebServlet(/pet/list) public class PetListServlet extends HttpServlet { Override protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { ListMapString, Object pets new ArrayList(); String sql SELECT p.id, p.name, p.price, p.stock, c.name AS category FROM pet p LEFT JOIN category c ON p.category_id c.id ORDER BY p.id DESC; try (Connection conn DBUtil.getConnection(); PreparedStatement ps conn.prepareStatement(sql); ResultSet rs ps.executeQuery()) { while (rs.next()) { MapString, Object row new HashMap(); row.put(id, rs.getInt(id)); row.put(name, rs.getString(name)); row.put(price, rs.getBigDecimal(price)); row.put(stock, rs.getInt(stock)); row.put(category, rs.getString(category)); pets.add(row); } } catch (Exception e) { // 把异常带到页面方便定位而不是白屏 req.setAttribute(error, e.getMessage()); } req.setAttribute(pets, pets); req.getRequestDispatcher(/pet_list.jsp).forward(req, resp); } }逻辑说明用LEFT JOIN而不是INNER JOIN是为了分类被删掉时宠物仍能显示只是分类为空。用try-with-resources自动关闭连接、语句、结果集避免连接泄漏——这是新手最常见的翻车点跑几十次请求后数据库连接池就满了。getBigDecimal而不是getDouble取价格保持精度。异常不抛出而是塞进 request是为了页面上能看到具体错误而不是一片 500 白屏。3.3 用 Tomcat 部署并验证第一个请求打包和部署步骤在项目根目录执行mvn clean package生成target/petstore.war。把 war 包复制到 Tomcat 的webapps目录。启动 Tomcatbin/startup.shLinux/Mac或bin/startup.batWindows。浏览器访问http://localhost:8080/petstore/pet/list。如果页面报 404先看 Tomcat 日志里 war 是否解压成功如果报 500 且提示驱动找不到检查mysql-connector-java是否真的打进了WEB-INF/lib。验证数据库连通性最直接的办法是在DBUtil静态块里加一行System.out.println(DB connected: url)启动时看控制台有没有输出。提示Tomcat 10 及以上版本把javax.servlet改成了jakarta.servlet如果你的依赖还是 javax会报 ClassNotFound。要么降 Tomcat 到 9要么把源码里的包名整体替换成 jakarta。4. 下单与库存扣减业务层最容易写错的地方4.1 订单创建的 Service 层与事务边界下单不是简单插一条订单记录它包含「查库存 → 扣库存 → 写订单」三个动作必须在一个事务里。Service 层写法package com.example.petstore.service; import com.example.petstore.util.DBUtil; import java.sql.Connection; import java.sql.PreparedStatement; public class OrderService { public boolean createOrder(int userId, int petId, int quantity) throws Exception { Connection conn null; try { conn DBUtil.getConnection(); conn.setAutoCommit(false); // 开启事务 // 1. 扣库存条件里带 stock quantity 防止超卖 String deduct UPDATE pet SET stock stock - ? WHERE id ? AND stock ?; try (PreparedStatement ps conn.prepareStatement(deduct)) { ps.setInt(1, quantity); ps.setInt(2, petId); ps.setInt(3, quantity); if (ps.executeUpdate() 0) { conn.rollback(); return false; // 库存不足 } } // 2. 查单价算总价 double price; try (PreparedStatement ps conn.prepareStatement( SELECT price FROM pet WHERE id ?)) { ps.setInt(1, petId); var rs ps.executeQuery(); if (!rs.next()) { conn.rollback(); return false; } price rs.getDouble(price); } // 3. 写订单 String insert INSERT INTO orders(user_id, pet_id, quantity, total) VALUES(?,?,?,?); try (PreparedStatement ps conn.prepareStatement(insert)) { ps.setInt(1, userId); ps.setInt(2, petId); ps.setInt(3, quantity); ps.setDouble(4, price * quantity); ps.executeUpdate(); } conn.commit(); return true; } catch (Exception e) { if (conn ! null) conn.rollback(); throw e; } finally { if (conn ! null) { conn.setAutoCommit(true); conn.close(); } } } }逻辑说明setAutoCommit(false)是事务的起点任何一步失败都rollback。扣库存的 SQL 里AND stock ?是关键它把「判断库存」和「扣减」合并成一条原子语句避免先查后扣之间被其他请求插队导致超卖。参数上quantity传了两次一次用于扣减、一次用于条件判断不能省。finally里恢复自动提交再关闭连接是因为连接可能来自连接池不恢复会影响下一次复用。4.2 库存超卖的三种错误写法与正确对照错误写法问题正确做法先 SELECT stock 再 UPDATE并发下两个请求都读到 1都扣成功UPDATE 带stock ?条件用stock ?直接赋值覆盖其他请求的扣减结果用stock stock - ?不开事务三步独立提交扣了库存但订单没写进去setAutoCommit(false) 统一提交这三种错误在课程设计里非常常见尤其是第一种单机测试永远发现不了一上并发就出问题。答辩时如果老师问「怎么防止超卖」把这张表讲清楚就是加分项。4.3 订单列表的分页查询参数怎么设订单多了必须分页否则一次查全表会拖慢页面。分页 SQLSELECT o.id, u.username, p.name AS pet_name, o.quantity, o.total, o.create_time FROM orders o JOIN user u ON o.user_id u.id JOIN pet p ON o.pet_id p.id ORDER BY o.create_time DESC LIMIT ?, ?;两个占位符分别是offset和pageSize。offset (pageNum - 1) * pageSize。pageSize一般设 10 或 20课程设计演示用 10 足够。注意 MySQL 的LIMIT在数据量大时深分页会慢但订单表几千条以内无感。如果要做总数显示再补一条SELECT COUNT(*) FROM orders两条查询分开执行不要用子查询硬凑。5. 避坑与排查源码跑不起来的五个高频问题5.1 中文乱码从请求到响应要设三处现象宠物名字在页面显示成问号或方块。原因请求体、响应头、数据库连接三处编码不一致。解决Servlet 里req.setCharacterEncoding(UTF-8)和resp.setContentType(text/html;charsetUTF-8)都要设数据库 URL 加characterEncodingutf8mb4JSP 页面顶部% page contentTypetext/html;charsetUTF-8 %。三处缺一处都可能乱码这是血泪经验。5.2 驱动加载失败ClassNotFound 的两种根因现象启动报java.lang.ClassNotFoundException: com.mysql.cj.jdbc.Driver。原因一依赖没打进WEB-INF/libMaven 里 scope 写成了provided或test。原因二MySQL 5 的驱动类名是com.mysql.jdbc.Driver8 才是com.mysql.cj.jdbc.Driver版本和类名对不上。解决确认依赖 scope 是默认的compile并核对驱动版本与类名。5.3 连接泄漏为什么跑一会儿就卡死现象连续点几十次页面后所有请求都卡住不返回。原因某个 DAO 方法里Connection没关连接池耗尽。解决所有 JDBC 操作统一用try-with-resources或者把关闭逻辑放进finally。排查办法是在DBUtil.getConnection()里打印当前连接数看是否只增不减。5.4 外键约束导致删分类失败现象删除一个分类时报Cannot delete or update a parent row。原因pet表有外键指向category分类下还有宠物时不允许删。解决要么先删该分类下的宠物要么把外键改成ON DELETE SET NULL。课程设计里建议先查再删给用户一个明确提示而不是直接抛数据库异常。5.5 JSP 里取不到 Servlet 传的值现象${pets}在页面上是空白。原因JSTL 标签库没引入或者 EL 表达式被禁用。解决JSP 顶部加% taglib prefixc urihttp://java.sun.com/jsp/jstl/core %并确认jstl依赖已引入。如果用的是 Tomcat 10taglib 的 uri 也要换成 jakarta 版本。6. 进阶技巧把简易源码改造成可演示的加分项跑通基础版之后想让这份「简易宠物商店 Java 源代码」在答辩或面试里更有说服力可以加三个小改造成本低但效果明显。第一个是给宠物列表加图片上传。在pet表已有image字段的基础上写一个UploadServlet用Part接口接收文件存到webapp/uploads目录数据库只存相对路径。关键参数是文件大小限制在web.xml里配multipart-configmax-file-size2097152/max-file-size/multipart-config即 2MB避免有人传大图把磁盘塞满。注意上传目录不能放在WEB-INF下否则浏览器访问不到。第二个是把密码明文改成加盐哈希。教学项目里存明文很常见但答辩时被问到就是减分项。用MessageDigest做 SHA-256 加随机盐即可不需要引入额外库public static String hash(String password, String salt) throws Exception { var md java.security.MessageDigest.getInstance(SHA-256); md.update((salt password).getBytes(UTF-8)); byte[] digest md.digest(); var sb new StringBuilder(); for (byte b : digest) sb.append(String.format(%02x, b)); return sb.toString(); }盐值每个用户随机生成和哈希一起存进user表。登录时用同样的盐再算一次比对。这样即使数据库被看到也拿不到原始密码。第三个是加一个简单的登录拦截器。用Filter判断 session 里有没有user没有就跳登录页。这样后台管理页面不会被人直接输 URL 访问。Filter 的urlPatterns设成/admin/*只拦后台前台浏览不受影响。验证改造是否成功最直接的办法是写一个main方法跑一遍 Service 层或者用 Postman 发请求看返回。我自己的习惯是每加一个功能先在数据库里手动插一条边界数据——比如库存为 0 的宠物、名字带空格的宠物——看页面会不会崩。很多 bug 不是逻辑错而是没考虑空值和边界。这套源码的价值不在于功能多全而在于你能把每一条链路讲清楚、改得动、测得住。希望帮到你。本文还有配套的精品资源点击获取
阅读完成 · 觉得有帮助?