JSP+SQL宠物诊所系统拆解:从JDBC事务到Tomcat部署
发布时间:2026/10/9 12:33:31 锦皓数字建站

简介这份基于JSPSQL的宠物诊所系统设计与实现毕业设计资料包面向正在完成Web课程设计或毕业设计的计算机专业学生可用于理解JSPServletJDBC的传统Java Web开发流程也适合作为答辩演示与文档撰写参考。资源共445个文件核心包括83个class编译文件、38个java源文件、26个jsp页面另有较多bak备份文件及xml配置、jar依赖库等能清晰看到从JavaBean、DAO到Servlet、JSP视图的完整分层实现压缩包仅2.77MB。系统覆盖用户注册登录、宠物信息管理、预约服务、诊疗记录等模块内容预览中的addownerServlet、addpetServlet及DAO类等文件恰好对应管理员录入主人、宠物建档、预约处理等关键业务逻辑。已有96人学习下载适合需要参考项目报告、阅读源码并快速跑通项目的学习者可以借此深入掌握MVC模式、JDBC事务处理和SQL语句的实际运用。1. 从 JSPSQL 毕设源码包里能拆出什么一个能跑通增删改查的宠物诊所系统这篇笔记拆的是一份《基于 JSPSQL 的宠物诊所系统设计与实现》毕业设计资源包包里有项目报告、源代码和部分编译产物。JSP 负责动态页面渲染SQL 负责数据落库两者通过 JDBC 搭桥跑在 Tomcat 上是一个典型的 Java Web 三层架构。对正在做毕设、或者刚学完 Servlet/JSP 想找一个完整项目练手的人来说这份资源的价值在于它把用户管理、宠物档案、预约、诊疗这几条核心业务线都串了起来代码量不算大但足够你理解一个 Web 项目从建表到页面展示的完整链路。后面我会按实际拆包的顺序把目录结构、运行环境、关键代码、坑点和打包部署逐一讲清楚。2. 先把包拆开看结构从 .bak、.class 和 JSP 文件名逆推工程布局拿到资源包后别急着右键解压就完事。第一步是看清楚里面到底有什么。我解压后扫了一眼文件清单里面有一批*.java.bak、*.jsp.bak和*.class文件这其实是两个信息一是原项目用的是 Eclipse 动态 Web 工程二是打包的人没有做 maven clean把备份文件和编译产物一起塞进来了。2.1 能直接从文件名读出的模块线索从清单里能明确看到这些业务模块的存在addownerservlet.java.bak—— 新增饲主Owner的 Servlet对应业务里的用户注册/饲主建档addpetservlet.java.bak—— 新增宠物Pet的 ServletaddownerDAO.java.bak—— 饲主数据访问对象负责和数据库交互addowner.jsp.bak/addpet.jsp.bak—— 新增饲主和新增宠物的表单页面petsearch.jsp.bak—— 宠物搜索页面验证了资源里有查询逻辑addpet_jsp.class/petview_005fname_jsp.class—— JSP 编译后的 class说明之前的 Tomcat 版本编译过这些页面我一般拿到这种包会先建一个干净的目录把.bak后缀去掉再逐个看代码。.class文件属于旧的编译产物不建议直接拿到新环境里用。mkdir -p pet-clinic/src/main/webapp unzip 基于JSPSQL的宠物诊所系统设计与实现毕业设计*.zip -d pet-clinic/ cd pet-clinic # 去掉备份后缀恢复成可编译的 Java/JSP 源文件 find . -name *.bak -exec sh -c mv $1 ${1%.bak} _ {} \; find . -name *.class -delete去掉.bak后工程结构基本就是 Eclipse 动态 Web 项目的标准布局src/下放 Java 源码WebContent/下放 JSP 和WEB-INF/web.xml。如果原包目录有缺失我会自己补一个web.xml来声明 Servlet 映射常见写法如下?xml version1.0 encodingUTF-8? web-app xmlnshttp://xmlns.jcp.org/xml/ns/javaee xmlns:xsihttp://www.w3.org/2001/XMLSchema-instance xsi:schemaLocationhttp://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_3_1.xsd version3.1 display-namePetClinic/display-name welcome-file-list welcome-filelogin.jsp/welcome-file welcome-fileindex.jsp/welcome-file /welcome-file-list /web-app这个 web.xml 做的事不多设定欢迎页声明 Servlet 容器版本。Tomcat 8.5 及以上用 3.1 的 schema 比较稳如果你用的是 Tomcat 7把 version 改成 3.0 即可。2.2 环境版本选型和第一个 Hello 级别的启动验证这个项目是 JSPServletJDBC 的组合不依赖 Spring所以环境要求很低。我实测下来最稳的组合是组件推荐版本说明JDK1.8Servlet 3.1 和 JSP 2.3 的基线版本Tomcat8.5.x不要用 Tomcat 10包名从 javax 变 jakarta这套代码会直接翻车Eclipse2020 之后任意版本自带 Dynamic Web Project 模板MySQL5.7 或 8.0注意 8.0 需要换驱动类和连接参数把工程导入 Eclipse 后先检查build path里有没有servlet-api.jar。这个 jar 在 Tomcat 的lib/目录下导入方式是在 Project Properties → Targeted Runtimes 里勾选 Tomcat 8.5而不是手动把 jar 塞进 WEB-INF/lib。import java.sql.Connection; import java.sql.DriverManager; public class DBUtil { private static final String URL jdbc:mysql://localhost:3306/petclinic?useSSLfalseserverTimezoneAsia/ShanghaicharacterEncodingutf8; private static final String USER root; private static final String PASSWORD 123456; static { try { Class.forName(com.mysql.cj.jdbc.Driver); } catch (ClassNotFoundException e) { e.printStackTrace(); } } public static Connection getConnection() throws Exception { return DriverManager.getConnection(URL, USER, PASSWORD); } }这里有两个参数要特别说明。serverTimezoneAsia/Shanghai是 MySQL 8.0 的强制要求不加会报时区错误characterEncodingutf8是中文乱流的后悔药前后端和数据库三层都得统一用 UTF-8。数据库连接串里的petclinic是库名你需要先在 MySQL 里手动建库资源包的报告部分一般会附带建表脚本如果没有我下一章会给一份可以直接执行的 SQL。启动 Tomcat 后访问http://localhost:8080/petclinic/能看到欢迎页就算环境通了。这一步走通之后整个项目的学习路径才会顺。3. 把 JSP、Servlet 和 DAO 串起来从注册表单到数据库落地的完整链路毕设资源包里最容易让人晕的地方是 JSP、Servlet、DAO、Service 这些类之间的关系。很多新手看代码时每个文件都认识合在一起就不知道数据是怎么流动的。这里我用两个核心场景给你拆开讲。3.1 用户注册的请求路径到底走了几步用户注册这条链路是理解这个项目的第一把钥匙。流程是这样的用户在addowner.jsp表单里填名字、电话、地址点提交后浏览器发一个 POST 请求到addownerservletServlet 拿到 request 参数后做基础校验调用addownerDAO的插入方法DAO 通过 JDBC 执行 INSERT 语句执行成功后再由 Servlet 把页面重定向到成功页。addowner.jsp的表单部分长这样% page contentTypetext/html;charsetUTF-8 languagejava % html headtitle新增饲主/title/head body form actionAddOwnerServlet methodpost label饲主姓名/labelinput typetext nameownerName required/br/ label联系电话/labelinput typetext nameownerPhone pattern\d{11} title请输入11位手机号/br/ label联系地址/labelinput typetext nameownerAddress size30/br/ input typesubmit value保存饲主/ /form /body /html注意 action 里的AddOwnerServlet对应 web.xml 或注解配置的 Servlet 映射名。pattern\d{11}是 HTML5 的前端校验如果电话格式不对表单根本不会提交这可以减轻后端压力但后端仍然要再校验一次。Servlet 端的代码逻辑如下WebServlet(/AddOwnerServlet) public class AddOwnerServlet extends HttpServlet { private static final long serialVersionUID 1L; protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { request.setCharacterEncoding(UTF-8); String name request.getParameter(ownerName); String phone request.getParameter(ownerPhone); String address request.getParameter(ownerAddress); if (name null || name.trim().isEmpty()) { response.setContentType(text/html;charsetUTF-8); response.getWriter().println(scriptalert(饲主姓名不能为空);history.back();/script); return; } Owner owner new Owner(); owner.setName(name.trim()); owner.setPhone(phone.trim()); owner.setAddress(address.trim()); AddOwnerDAO dao new AddOwnerDAO(); boolean ok dao.insert(owner); if (ok) { response.sendRedirect(ownerList.jsp); } else { response.sendRedirect(error.jsp?reasoninsert_failed); } } }这段代码里有几个关键点。第一行request.setCharacterEncoding(UTF-8)必须写在取参数之前否则 POST 过来的中文会以 ISO-8859-1 解码到库里就是问号。trim()去掉首尾空格防止用户误输导致数据不干净。插入失败走error.jsp而不是直接打到错误堆栈页这是毕设答辩时的一个加分项。3.2 DAO 层为什么必须写 PreparedStatementDAO 层是这个项目的重点考察对象。很多参考代码喜欢用 Statement 拼接 SQL这在答辩时是明显的扣分点。正确写法是 PreparedStatement既能防 SQL 注入又能让 MySQL 走预编译缓存。import java.sql.Connection; import java.sql.PreparedStatement; import java.sql.ResultSet; import java.sql.SQLException; public class AddOwnerDAO { public boolean insert(Owner owner) { String sql INSERT INTO owner (name, phone, address) VALUES (?, ?, ?); try (Connection conn DBUtil.getConnection(); PreparedStatement ps conn.prepareStatement(sql)) { ps.setString(1, owner.getName()); ps.setString(2, owner.getPhone()); ps.setString(3, owner.getAddress()); return ps.executeUpdate() 0; } catch (Exception e) { e.printStackTrace(); return false; } } public Owner findById(int ownerId) { String sql SELECT id, name, phone, address FROM owner WHERE id ?; try (Connection conn DBUtil.getConnection(); PreparedStatement ps conn.prepareStatement(sql)) { ps.setInt(1, ownerId); try (ResultSet rs ps.executeQuery()) { if (rs.next()) { Owner o new Owner(); o.setId(rs.getInt(id)); o.setName(rs.getString(name)); o.setPhone(rs.getString(phone)); o.setAddress(rs.getString(address)); return o; } } } catch (Exception e) { e.printStackTrace(); } return null; } }try-with-resources是 Java 7 的语法Connection、PreparedStatement、ResultSet 用完后自动关闭不会出现连接泄露。捕获异常后打印堆栈开发阶段方便排查部署阶段建议改成日志输出。注意executeUpdate()返回的是受影响行数插入一条数据返回 1大于 0 就说明成功了。3.3 EL 表达式和 JSTL 让页面少写 Java 代码资源里petsearch.jsp.bak这个文件对应的场景是搜索宠物。搜索结果的列表展示如果用% %写 Java 代码会很丑而且 JSP 规范也不推荐。常见做法是先用一个 Servlet 查好数据放进 request再转发到 JSP 用 EL 表达式和 JSTL 标签遍历。% taglib prefixc urihttp://java.sun.com/jsp/jstl/core % % page contentTypetext/html;charsetUTF-8 languagejava % html body table border1 tr th宠物ID/th th宠物名/th th品种/th th饲主/th /tr c:forEach items${petList} varpet tr td${pet.id}/td td${pet.name}/td td${pet.breed}/td td${pet.ownerName}/td /tr /c:forEach /table /body /html这里${petList}是在 Servlet 里通过request.setAttribute(petList, list)放入的集合${pet.name}实际上调的是 Pet 对象的 getName() 方法。EL 表达式把页面里的 Java 脚本块清零了这也是当前 JSP 开发的通用习惯。注意这段代码依赖 JSTL 1.2 的 jar 包需要放进WEB-INF/lib/目录否则c:forEach标签会直接报 500 错误。4. SQL 表设计和 JDBC 事务预约冲突与诊疗记录的数据一致性JSP 页面做的是表现层的工作真正决定这个系统能不能稳定跑起来的是数据库层的设计。宠物诊所系统的核心表至少有五张用户表、饲主表、宠物表、预约表、诊疗记录表。表之间通过外键关联而预约模块因为涉及时间冲突检测事务处理成了关键技术点。4.1 五张核心表的建表 SQL 和字段说明下面这份建表 SQL 是综合资源包内容补齐的字段命名保持简单直白适合毕设报告里直接引用。CREATE DATABASE IF NOT EXISTS petclinic DEFAULT CHARSET utf8mb4; USE petclinic; CREATE TABLE owner ( id INT AUTO_INCREMENT PRIMARY KEY, name VARCHAR(50) NOT NULL, phone VARCHAR(20) NOT NULL, address VARCHAR(200), created_at DATETIME DEFAULT CURRENT_TIMESTAMP ) ENGINEInnoDB; CREATE TABLE pet ( id INT AUTO_INCREMENT PRIMARY KEY, owner_id INT NOT NULL, name VARCHAR(50) NOT NULL, species VARCHAR(30), breed VARCHAR(30), age INT, vaccine_record VARCHAR(200), photo_path VARCHAR(200), CONSTRAINT fk_pet_owner FOREIGN KEY (owner_id) REFERENCES owner(id) ) ENGINEInnoDB; CREATE TABLE doctor ( id INT AUTO_INCREMENT PRIMARY KEY, name VARCHAR(50) NOT NULL, specialty VARCHAR(100), schedule VARCHAR(200) ) ENGINEInnoDB; CREATE TABLE appointment ( id INT AUTO_INCREMENT PRIMARY KEY, pet_id INT NOT NULL, doctor_id INT NOT NULL, appoint_date DATE NOT NULL, appoint_time VARCHAR(10) NOT NULL, status VARCHAR(20) DEFAULT pending, note VARCHAR(500), CONSTRAINT fk_appt_pet FOREIGN KEY (pet_id) REFERENCES pet(id), CONSTRAINT fk_appt_doctor FOREIGN KEY (doctor_id) REFERENCES doctor(id), UNIQUE KEY uk_doctor_time (doctor_id, appoint_date, appoint_time) ) ENGINEInnoDB; CREATE TABLE medical_record ( id INT AUTO_INCREMENT PRIMARY KEY, pet_id INT NOT NULL, doctor_id INT NOT NULL, diagnosis VARCHAR(500), treatment VARCHAR(500), cost DECIMAL(10,2), record_date DATETIME DEFAULT CURRENT_TIMESTAMP, CONSTRAINT fk_med_pet FOREIGN KEY (pet_id) REFERENCES pet(id) ) ENGINEInnoDB;几个容易忽略的细节。所有表强制 InnoDB 引擎这是事务和行级锁的前提MyISAM 不提供事务支持预约模块没法做冲突控制。appointment表加了联合唯一键uk_doctor_time这意味着同一个医生在同一个日期的同一个时段只能有一条预约记录这个约束是数据库层面的最后一道防线。DECIMAL(10,2)是金额字段的标准类型不要用 FLOAT否则累计费用时会有精度损失。4.2 预约事务控制先查后插把 TRY 块包住事务预约模块最容易翻车的场景是用户选了一个时间段前端提交请求后端查到该时段已经被占了返回错误信息。但两个用户同时提交、都在查询阶段发现“时段空闲”时如果不做事务隔离两个 INSERT 会同时成功产生数据冲突。联合唯一键能挡住但报错信息不友好所以正确的做法是事务 唯一索引双保险。import java.sql.Connection; import java.sql.PreparedStatement; import java.sql.SQLException; public class AppointmentService { public boolean bookAppointment(int petId, int doctorId, String date, String time) { String checkSql SELECT id FROM appointment WHERE doctor_id? AND appoint_date? AND appoint_time?; String insertSql INSERT INTO appointment(pet_id, doctor_id, appoint_date, appoint_time) VALUES(?,?,?,?); Connection conn null; try { conn DBUtil.getConnection(); conn.setAutoCommit(false); try (PreparedStatement check conn.prepareStatement(checkSql)) { check.setInt(1, doctorId); check.setString(2, date); check.setString(3, time); if (check.executeQuery().next()) { conn.rollback(); return false; } } try (PreparedStatement insert conn.prepareStatement(insertSql)) { insert.setInt(1, petId); insert.setInt(2, doctorId); insert.setString(3, date); insert.setString(4, time); insert.executeUpdate(); } conn.commit(); return true; } catch (Exception e) { e.printStackTrace(); if (conn ! null) { try { conn.rollback(); } catch (SQLException ex) { ex.printStackTrace(); } } return false; } finally { if (conn ! null) { try { conn.setAutoCommit(true); conn.close(); } catch (SQLException e) { e.printStackTrace(); } } } } }setAutoCommit(false)之后所有 SQL 都不会立刻落盘直到显式调用commit()。如果中间任何一步抛异常rollback()会把已经执行的 INSERT 撤销回去不会留半截数据。注意 finally 块里要恢复setAutoCommit(true)否则连接归还连接池时会带着错误的状态。这里有一个细节值得在答辩时主动讲先查后插的并发安全其实不完美——两个请求同时执行 check 都返回空然后都走 INSERT此时唯一索引会拦住第二个请求并抛 DuplicateKeyException。所以 catch 块里最好单独捕获 SQLIntegrityConstraintViolationException返回“该时段已被预约”而不是笼统的“系统错误”。5. 避坑指南.bak 备份文件、class 旧产物和编码乱流的三类翻车记录这个资源包我前后跑了三遍第一遍几乎每一步都在报错。把最常见的坑整理成四条记录每一类都是这个项目里切实会遇到的问题而不是泛泛而谈。5.1 拷贝 .bak 文件后编译报错备份文件把源码目录搞混了现象在 Eclipse 里刷新工程后源码目录下出现了一堆addownerServlet.java.bak文件项目报红叉编译时提示“重复的类定义”或“javac: 无效的标记”。原因打包者用.bak做备份但.bak文件放在src目录里Eclipse 会把所有文件都加入编译范畴。.java.bak结尾的文件不能被 javac 识别为源文件但在某些构建配置里又会语法错误地参与扫描。解决动手清理不要只删.class。在工程根目录执行find . -name *.bak -delete再右键工程选 Maven → Update Project如果是普通 Dynamic Web Project 就 Project → Clean。从那次以后我拿到任何带备份文件的源码包第一时间做全名匹配清理绝不让备份文件留在编译路径里。5.2 Tomcat 10 启动项目直接白屏或 500 全家桶现象把项目导入新装的 Tomcat 10启动后访问页面每一个 JSP 都报 500控制台提示java.lang.NoClassDefFoundError: javax/servlet/ServletContext。原因Tomcat 10 把 Java EE 的javax.*包迁移到了jakarta.*。这套 JSPServlet 老代码用的是javax.servlet的 API在 Tomcat 10 下根本找不到类。这是老 Java Web 项目迁移最常见的翻车点。解决把 Tomcat 版本降到 8.5.x 或者 9.0.x。如果你只能用 Tomcat 10就必须全局替换 import 语句把javax.servlet改成jakarta.servletJSP 页面里的默认隐含对象不涉及这个问题但所有% page importjavax.servlet.* %都要改。毕设项目没必要做这个迁移直接用 Tomcat 8.5 最省事。5.3 数据库中文全部变成问号连接串、页面编码、表字符集三层得统一现象页面表单提交中文姓名到数据库查询页面显示???但英文和数字正常。原因三个地方的字符集不一致。数据库表用了latin1或者 JDBC 连接串没加characterEncodingutf8或者 JSP 页面没有声明contentTypetext/html;charsetUTF-8。解决按顺序排查。第一步确认 MySQL 数据库和表的字符集执行SHOW CREATE TABLE owner\G看 DEFAULT CHARSET 是不是 utf8mb4不是就ALTER TABLE owner CONVERT TO CHARACTER SET utf8mb4。第二步检查 JDBC URL把characterEncodingutf8加进去。第三步确认每个 JSP 页面头部都有pageEncodingUTF-8属性。三条都满足后重新启动 Tomcat 再测这个问题基本一次性解决。SHOW CREATE TABLE owner\G; ALTER TABLE owner CONVERT TO CHARACTER SET utf8mb4;5.4 .class 文件是别人环境的编译产物别拿来直接解释“代码是对的”现象用反编译工具打开addpet_jsp.class看到里面的源码内容和你当前的源码对不上或者反编译出来有大量 jsp 内部类的代码片段让你误以为源码缺失。原因.class是 JSP 引擎编译后的 java 中间文件的再编译产物内容是 Tomcat 自动生成的_jspService方法实现包含大量out.write调用和手写的 JSP 源码是两回事。它只能证明“有人在某个 Tomcat 版本上编译过这个页面”不能代表当前源码的行为。解决把.class文件全部删掉以.jsp源文件为准。我的习惯是解压后立即执行find . -name *.class -delete不需要留任何编译产物。后面完全靠自己的代码跑出正确的行为才真正算掌握了这套系统。6. 从源码到 WAR 包用三条命令完成打包并在浏览器里验收前面五章讲的都是怎么把代码跑起来这一章聊最后一公里的交付——打 WAR 包。手动在 IDE 里导 WAR 的流程是右键工程 → Export → WAR File但构建过程太隐蔽出了问题不好查。我建议你把构建步骤固定成一条命令链路这样每次打包前都能稳定复现。下面是我用的方式# 用 javac 一次性编译整个工程假设源码在 src/页面在 WebContent/ mkdir -p build/classes find src -name *.java sources.txt javac -encoding UTF-8 -cp lib/*:$CATALINA_HOME/lib/* -d build/classes sources.txt # 把编译产物和 JSP 页面、web.xml 一起打成 WAR mkdir -p build/WEB-INF/classes cp -r build/classes/* build/WEB-INF/classes/ cp -r WebContent/* build/ jar -cvf petclinic.war -C build .打完包后验证分两步。第一步用jar tf petclinic.war | head -30查看 WAR 包内结构确认WEB-INF/classes下有编译好的 classWEB-INF/lib下有 JDBC 驱动和 JSTL 包。第二步把petclinic.war复制到 Tomcat 的webapps/目录启动 Tomcat然后浏览器访问http://localhost:8080/petclinic/login.jsp手动跑一遍新增饲主 → 新增宠物 → 搜索宠物 → 预约 → 录入诊疗记录这条完整业务链。jar tf petclinic.war | grep -E (class|jar)$ cp petclinic.war $CATALINA_HOME/webapps/ $CATALINA_HOME/bin/startup.sh最后一步验收要带上业务视角不要只看到页面能打开就收工。正确的做法是准备一个小表格列五条验收用例注册重复用户名能否拦截、空姓名能否被前端拉下来、宠物搜索能否按品种模糊匹配、预约同一时段两次是否被拒、诊疗费用录入后能否在记录里查回。我第一遍打包时就是只看了页面没跑业务结果到答辩演示环节预约冲突那条直接翻车页面报了一个裸的 500。从那以后我每次打 WAR 包都会强制自己走一遍这份验收清单确认所有表格里的记录都能从数据库里查回来才敢说“这个包是可部署的”。说到底这种毕设资源包的正确打开方式不是拿来看的是拿来解的——解包、清缓存、改配置、跑业务、打 WAR全流程走通了这套系统的每一行代码才算真正归你所有。希望这份笔记能帮你少踩几个我踩过的坑。本文还有配套的精品资源点击获取
锦
锦皓数字建站
深耕本土企业品牌数字化升级,专注原创端正雅致商务官网,从视觉设计到稳定运维全程保驾护航。