尧图精选

从零手搓Java+JSP+Servlet+MySQL宠物管理系统:老技术栈实战指南

🕒 发布时间:2026/10/1 5:42:51 📁 来源:尧图网络
简介这份资源是一套基于Java Web技术栈的简单宠物管理系统源码采用JSPServletMySQL实现面向刚接触Java Web开发、希望用完整小项目练手的小白学习者。系统围绕宠物信息管理展开包含按分类查询宠物以及添加、编辑、删除宠物等操作在原有基础上补齐了编辑与删除功能形成一套完整的增删改查模块适合用来理解Servlet请求处理、JSP页面展示与数据库交互的基本流程。压缩包共41个文件约6.37MB其中7个java源文件与7个class文件构成核心业务代码4个jsp页面负责前端展示4个jar包提供依赖支持另附sql建库脚本、配置说明文档、项目说明pdf以及一段实操演示视频便于对照理解项目结构与运行方式。目前已有1580人学习下载可作为课程设计、入门练习或自学Java Web的参考案例帮助读者快速掌握基础CRUD开发思路。1. 从零手搓 javajspservletmysql 宠物管理系统这套老技术栈为什么还值得写一遍如果你正在搜「javajspservletmysql 简答宠物管理系统源码」大概率不是想听人劝你换 Spring Boot。你可能是要交一份课程设计可能是面试前想拿一个能讲清楚 MVC 全链路的项目练手也可能是带学生做实训需要一个不依赖任何脚手架、每一行都能看懂的完整案例。这套组合确实老但它把「浏览器发请求 → Servlet 接住 → 业务处理 → JDBC 落库 → JSP 渲染」这条链路完整暴露在你面前没有自动配置帮你藏东西。宠物管理系统这个题材又刚好够用宠物档案、主人信息、领养记录三张表就能撑起增删改查加关联查询。这篇文章不贴一份不存在的源码包而是把从建库、建工程、写 DAO、写 Servlet、写 JSP 到打包部署的每一步拆开讲参数怎么设、坑在哪、为什么这么选都落到能直接抄的程度。新手能跟着跑通熟手能对照检查自己的实现边界。2. 建库建表与工程骨架把数据模型和目录结构先定死2.1 三张表撑起宠物管理系统的数据模型宠物管理系统的核心实体其实就三个宠物pet、主人owner、领养记录adoption。很多人一上来就堆十几张表结果字段关系自己都理不清。我一般先用最小可用模型跑通再按需扩展。下面这套建表语句在 MySQL 5.7 和 8.0 上都能直接执行字符集统一用 utf8mb4避免中文和 emoji 存进去变问号。-- 建库字符集必须显式指定否则默认 latin1 会坑中文 CREATE DATABASE pet_manager DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci; USE pet_manager; -- 主人表一个主人可以有多只宠物 CREATE TABLE owner ( id INT PRIMARY KEY AUTO_INCREMENT, name VARCHAR(50) NOT NULL COMMENT 主人姓名, phone VARCHAR(20) NOT NULL COMMENT 联系电话, address VARCHAR(200) DEFAULT NULL, create_time DATETIME DEFAULT CURRENT_TIMESTAMP ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; -- 宠物表owner_id 外键关联主人允许为空表示暂未登记主人 CREATE TABLE pet ( id INT PRIMARY KEY AUTO_INCREMENT, name VARCHAR(50) NOT NULL COMMENT 宠物名, species VARCHAR(20) NOT NULL COMMENT 种类猫/狗/其他, age INT DEFAULT 0 COMMENT 年龄默认0, owner_id INT DEFAULT NULL, status TINYINT DEFAULT 1 COMMENT 1在养 0已领养, CONSTRAINT fk_pet_owner FOREIGN KEY (owner_id) REFERENCES owner(id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; -- 领养记录表记录某只宠物被谁领养 CREATE TABLE adoption ( id INT PRIMARY KEY AUTO_INCREMENT, pet_id INT NOT NULL, adopter VARCHAR(50) NOT NULL COMMENT 领养人, adopt_time DATETIME DEFAULT CURRENT_TIMESTAMP, CONSTRAINT fk_adopt_pet FOREIGN KEY (pet_id) REFERENCES pet(id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;逻辑说明owner 和 pet 是一对多pet 和 adoption 是一对多。外键约束放在数据库层能防止你代码写错时插入脏数据。参数上age INT DEFAULT 0对应热搜里「mysql 设置默认值为 0」这个高频需求注意默认值只在插入时不写该字段才生效显式传 NULL 不会走默认值。status用 TINYINT 而不是布尔是为了以后扩展更多状态留余地。提示如果执行建表报ERROR 2002 (HY000): Cant connect to local MySQL server through socket说明 MySQL 服务没起来先确认服务状态再回来建表这跟建表语句本身无关。2.2 传统 JSP 项目的目录结构怎么摆不用 Maven 也能跑但用 Maven 管依赖更省心尤其是 mysql-connector-java 这个 jar。标准 webapp 目录结构如下这是 IDEA 新建 JSP 项目后你应该整理成的样子pet-manager/ ├── pom.xml └── src/main/ ├── java/com/pet/ │ ├── dao/ PetDao.java OwnerDao.java │ ├── entity/ Pet.java Owner.java │ ├── servlet/ PetServlet.java OwnerServlet.java │ └── util/ DBUtil.java └── webapp/ ├── WEB-INF/web.xml ├── index.jsp └── pet/list.jsp pet/edit.jsppom.xml 里只需要三个依赖servlet-apiscope 设 provided因为 Tomcat 自带、jsp-api同样 provided、mysql-connector-java。这里有个血泪经验servlet-api 如果打成 compile 放进 war部署时会和容器自带的冲突报 ClassCastException 或者类加载异常排查半天找不到原因。dependencies dependency groupIdjavax.servlet/groupId artifactIdjavax.servlet-api/artifactId version4.0.1/version scopeprovided/scope /dependency dependency groupIdmysql/groupId artifactIdmysql-connector-java/artifactId version8.0.28/version /dependency /dependencies参数说明scopeprovided表示编译时需要、打包时不带。mysql 驱动版本要和你的 MySQL 服务端匹配8.0 的驱动连 5.7 也能用但 URL 里要加时区和 SSL 参数下一节细说。3. JDBC 工具类与 DAO 层连接、参数、异常一个都不能含糊3.1 DBUtil把连接参数和关闭逻辑收口DAO 层最容易写乱的地方就是每个方法都自己 getConnection、自己 close最后连接泄漏。正确做法是抽一个 DBUtil把 URL、用户名、密码集中管理关闭逻辑也统一。下面这个实现用 DriverManager 直连够简单适合教学和课程设计生产环境该上连接池后面会讲。package com.pet.util; import java.sql.*; public class DBUtil { // 8.0 驱动必须带 serverTimezone否则时间字段可能差 8 小时 private static final String URL jdbc:mysql://localhost:3306/pet_manager?useUnicodetruecharacterEncodingutf8 useSSLfalseserverTimezoneAsia/ShanghaiallowPublicKeyRetrievaltrue; private static final String USER root; private static final String PASSWORD 你的密码; static { try { Class.forName(com.mysql.cj.jdbc.Driver); } catch (ClassNotFoundException e) { throw new RuntimeException(MySQL 驱动未加载, e); } } public static Connection getConnection() throws SQLException { return DriverManager.getConnection(URL, USER, PASSWORD); } // 统一关闭顺序必须是 ResultSet - Statement - Connection public static void close(Connection conn, Statement stmt, ResultSet rs) { try { if (rs ! null) rs.close(); } catch (SQLException ignored) {} try { if (stmt ! null) stmt.close(); } catch (SQLException ignored) {} try { if (conn ! null) conn.close(); } catch (SQLException ignored) {} } }逻辑说明静态代码块只执行一次负责注册驱动。URL 里useSSLfalse是为了避开「mysql ssl 连接错误」这个高频报错本地开发不需要 SSL。serverTimezoneAsia/Shanghai解决 8.0 驱动时区问题不加的话插入时间可能比实际早或晚 8 小时。allowPublicKeyRetrievaltrue是 8.0 驱动用 caching_sha2_password 认证时的常见补充参数。注意密码不要硬编码提交到公开仓库教学项目里至少抽到配置文件用 Properties 读取。3.2 PetDao增删改查与关联查询的写法DAO 层只干一件事把 SQL 执行结果映射成实体对象。下面以宠物查询为例包含按 id 查、查全部、带主人名的关联查询三种典型场景。package com.pet.dao; import com.pet.entity.Pet; import com.pet.util.DBUtil; import java.sql.*; import java.util.*; public class PetDao { // 查询全部宠物带主人姓名左连接没主人的也要查出来 public ListPet findAll() { ListPet list new ArrayList(); String sql SELECT p.id, p.name, p.species, p.age, p.status, o.name AS ownerName FROM pet p LEFT JOIN owner o ON p.owner_id o.id ORDER BY p.id DESC; Connection conn null; Statement stmt null; ResultSet rs null; try { conn DBUtil.getConnection(); stmt conn.createStatement(); rs stmt.executeQuery(sql); while (rs.next()) { Pet pet new Pet(); pet.setId(rs.getInt(id)); pet.setName(rs.getString(name)); pet.setSpecies(rs.getString(species)); pet.setAge(rs.getInt(age)); pet.setStatus(rs.getInt(status)); pet.setOwnerName(rs.getString(ownerName)); list.add(pet); } } catch (SQLException e) { e.printStackTrace(); } finally { DBUtil.close(conn, stmt, rs); } return list; } // 新增宠物用 PreparedStatement 防注入 public int insert(Pet pet) { String sql INSERT INTO pet(name, species, age, owner_id, status) VALUES(?,?,?,?,?); Connection conn null; PreparedStatement ps null; try { conn DBUtil.getConnection(); ps conn.prepareStatement(sql); ps.setString(1, pet.getName()); ps.setString(2, pet.getSpecies()); ps.setInt(3, pet.getAge()); if (pet.getOwnerId() null) { ps.setNull(4, Types.INTEGER); } else { ps.setInt(4, pet.getOwnerId()); } ps.setInt(5, pet.getStatus()); return ps.executeUpdate(); } catch (SQLException e) { e.printStackTrace(); return 0; } finally { DBUtil.close(conn, ps, null); } } }逻辑说明findAll用 LEFT JOIN 保证没有主人的宠物也能显示ownerName是关联出来的字段实体类里要额外加这个属性。insert用 PreparedStatement 占位符这是防 SQL 注入的基本功别用字符串拼接。setNull(4, Types.INTEGER)处理 owner_id 为空的情况直接 setInt 传 null 会报错。参数说明ORDER BY p.id DESC让最新录入的排前面符合管理后台习惯。ps.executeUpdate()返回受影响行数大于 0 表示成功Servlet 层据此判断跳转。3.3 连接池什么时候该从 DriverManager 换掉DriverManager 每次 getConnection 都新建物理连接并发一高就顶不住。热搜里「mysql 的数据库连接池」正是这个痛点。教学项目用 DriverManager 没问题但只要你想让它稍微像样点就换成 Druid 或 HikariCP。以 Druid 为例加依赖后把 DBUtil 改成从 DataSource 取连接private static DruidDataSource dataSource new DruidDataSource(); static { dataSource.setUrl(URL); dataSource.setUsername(USER); dataSource.setPassword(PASSWORD); dataSource.setInitialSize(5); // 初始连接数 dataSource.setMaxActive(20); // 最大连接数 dataSource.setMinIdle(5); // 最小空闲 } public static Connection getConnection() throws SQLException { return dataSource.getConnection(); }参数说明initialSize是启动时预建连接数maxActive是并发上限minIdle保证空闲时也有连接可复用。这三个值按你机器和并发量调课程设计设 5/20/5 足够。注意换成连接池后close 方法里的 conn.close() 实际是归还连接而不是真关闭逻辑不用改。4. Servlet 与 JSP 打通请求链路生命周期、转发与中文乱码4.1 Servlet 生命周期决定了你的初始化代码放哪很多人把数据库初始化写在 doGet 里每次请求都执行一遍这是没搞懂 servlet 生命周期。Servlet 的生命周期就三个阶段init 只调用一次servicedoGet/doPost每次请求调用destroy 在容器关闭时调用一次。所以加载配置、建连接池这类一次性动作应该放 init。package com.pet.servlet; import com.pet.dao.PetDao; import com.pet.entity.Pet; import javax.servlet.ServletException; import javax.servlet.annotation.WebServlet; import javax.servlet.http.*; import java.io.IOException; import java.util.List; WebServlet(/pet) public class PetServlet extends HttpServlet { private PetDao petDao; Override public void init() throws ServletException { petDao new PetDao(); // 只初始化一次 } Override protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { req.setCharacterEncoding(UTF-8); resp.setContentType(text/html;charsetUTF-8); String action req.getParameter(action); if (action null) action list; switch (action) { case list: ListPet pets petDao.findAll(); req.setAttribute(pets, pets); req.getRequestDispatcher(/pet/list.jsp).forward(req, resp); break; case delete: int id Integer.parseInt(req.getParameter(id)); petDao.deleteById(id); resp.sendRedirect(req.getContextPath() /pet?actionlist); break; default: resp.sendError(404); } } Override protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { req.setCharacterEncoding(UTF-8); // POST 中文必须设且要在取参数之前 Pet pet new Pet(); pet.setName(req.getParameter(name)); pet.setSpecies(req.getParameter(species)); pet.setAge(Integer.parseInt(req.getParameter(age))); pet.setStatus(1); petDao.insert(pet); resp.sendRedirect(req.getContextPath() /pet?actionlist); } }逻辑说明WebServlet(/pet)是注解方式注册不用再写 web.xml。doGet 里用 action 参数区分操作这是传统 JSP 项目最常见的单 Servlet 多动作模式。查询用 forward转发URL 不变能带 request 属性增删改用 sendRedirect重定向防止刷新重复提交。参数说明req.setCharacterEncoding(UTF-8)对 POST 有效且必须在第一次 getParameter 之前调用否则无效。GET 请求的中文乱码要靠 Tomcat 的 server.xml 里 Connector 加URIEncodingUTF-8这是两个不同的坑。4.2 JSP 页面用 JSTL 循环渲染别写 Java 脚本片段JSP 里写% %脚本片段是能跑但可读性差、难维护。正确做法是用 JSTL 的 c:forEach 渲染列表。需要引入 jstl 依赖页面顶部加 taglib 声明。% page contentTypetext/html;charsetUTF-8 languagejava % % taglib prefixc urihttp://java.sun.com/jsp/jstl/core % html headtitle宠物列表/title/head body h2宠物管理系统/h2 a hrefpet?actionadd新增宠物/a table border1 cellpadding6 trthID/thth名字/thth种类/thth年龄/thth主人/thth操作/th/tr c:forEach items${pets} varp tr td${p.id}/td td${p.name}/td td${p.species}/td td${p.age}/td td${empty p.ownerName ? 未登记 : p.ownerName}/td tda hrefpet?actiondeleteid${p.id} onclickreturn confirm(确认删除?)删除/a/td /tr /c:forEach /table /body /html逻辑说明${pets}是 Servlet 里 setAttribute 放进去的JSTL 表达式语言直接取。${empty p.ownerName ? 未登记 : p.ownerName}处理关联字段为空的情况比在页面里写 if 判断干净。删除链接加 confirm 是基本交互防止误点。参数说明items是要遍历的集合var是每次循环的变量名。JSTL 依赖记得加进 pom否则 taglib 报错找不到 URI。5. 避坑与排查这套老技术栈最容易翻车的五个地方5.1 中文乱码POST 和 GET 是两套解法现象表单提交的中文存进数据库变成问号或者页面显示乱码。原因POST 乱码是请求体编码问题GET 乱码是 URL 编码问题两者机制不同。解决POST 在 Servlet 里req.setCharacterEncoding(UTF-8)且必须在取参数前GET 要在 Tomcat 的 server.xml Connector 上加URIEncodingUTF-8数据库连接 URL 加characterEncodingutf8建表用 utf8mb4。四处都对齐才彻底不乱。5.2 驱动加载失败或时区报错现象启动报No suitable driver found或时间字段差 8 小时。原因8.0 驱动类名是com.mysql.cj.jdbc.Driver不是老的com.mysql.jdbc.Driver且必须带 serverTimezone。解决确认驱动类名URL 加serverTimezoneAsia/ShanghaiuseSSLfalse避开 SSL 警告。5.3 外键约束导致删除失败现象删除宠物时报Cannot delete or update a parent row。原因adoption 表有外键指向 pet直接删 pet 违反约束。解决先删关联的领养记录再删宠物或者建表时外键加ON DELETE CASCADE。我一般选前者逻辑更显式避免误删。5.4 war 包部署后 404现象本地 IDEA 跑得好好的打成 war 丢进 Tomcat 就 404。原因多半是 context path 变了或者 servlet-api 被打进 war 导致类冲突。解决确认访问路径带上了 war 包名作为 context path检查 pom 里 servlet-api 的 scope 是 provided看 Tomcat 日志有没有类加载异常。5.5 JSP 修改后不生效现象改了 JSP 刷新页面还是旧的。原因Tomcat 缓存了编译后的 JSP。解决清 work 目录或者配置 Tomcat 的 development 模式。生产环境 JSP 预编译是好事开发阶段嫌烦就清缓存。6. 进阶技巧把项目从「能跑」推到「能讲清楚」课程设计交上去只是及格线真正拉开差距的是你能不能讲清楚每个技术选型的理由。这里给三个我常用的进阶动作。第一把 DAO 层的重复代码抽成泛型基类用反射做结果集映射这样加一张表不用重写一遍增删改查面试时讲「我怎么消除重复」比讲「我写了几个 CRUD」有分量。第二给关键操作加一层简单的日志用 java.util.logging 或 log4j记录谁在什么时候删了哪条数据这是「java 怎么保证数据一致性」这类面试题的落地答案——不是背概念而是你真的做了可追溯。第三把事务控制显式写出来领养操作要同时更新 pet 状态和插入 adoption 记录这两步必须在一个事务里Connection conn null; try { conn DBUtil.getConnection(); conn.setAutoCommit(false); // 关闭自动提交 petDao.updateStatus(conn, petId, 0); adoptionDao.insert(conn, petId, adopter); conn.commit(); // 两步都成功才提交 } catch (SQLException e) { if (conn ! null) conn.rollback(); // 任一步失败全部回滚 } finally { if (conn ! null) try { conn.setAutoCommit(true); conn.close(); } catch (SQLException ignored) {} }注意这里 DAO 方法要接收外部传入的 Connection而不是自己 getConnection否则事务管不到。这是很多人写事务时翻车的地方方法内部各自开连接setAutoCommit(false) 根本不起作用。参数上setAutoCommit(false)之后所有操作都在同一事务commit提交、rollback回滚finally 里恢复自动提交再关闭避免连接归还池时状态污染。验证方法很简单故意在插入 adoption 后抛一个异常看 pet 的状态有没有被回滚。如果状态没变说明事务生效如果状态改了说明你的 Connection 没传对。这个自测我每次带新人都让他们做一遍比看十篇文章管用。最后一个习惯别急着上框架。这套 JSPServletJDBC 你亲手写一遍把请求怎么进来、参数怎么取、SQL 怎么执行、结果怎么渲染这条链路走通再去学 Spring MVC你会发现它只是把你写的东西封装了一层。我当年跳过这一步直接上框架结果出了问题连从哪查都不知道后来回头补这一课才真正踏实。希望帮到你。本文还有配套的精品资源点击获取
上一篇/下一篇内容由系统自动关联 返回资讯列表 →