从Servlet到JDBC:宠物医院管理系统JavaWeb实战解析
2026/9/15 20:33:07 网站建设 项目流程

简介:这是一套面向计算机相关专业毕业设计、课程设计与期末大作业的 JavaWeb 宠物医院管理系统完整项目。代码经导师指导并获评审 99 分,功能覆盖宠物档案、医生排班、用户登录等典型业务模块,前端以 JSP 页面呈现,后端由 Servlet 与 Java 类支撑,并配有可直接导入的 SQL 数据库脚本,可帮助初学者快速跑通前后端联调流程,适合动手实践与二次开发。压缩包共含 113 个文件,主要包括 40 个 JSP 页面、39 个 Java 源码、11 个 XML 配置、SQL 脚本及少量 CSS/JS 与图片素材,整体仅 750KB,轻量易部署;目录结构按实体类、控制层、页面文件划分,便于对照学习。目前已有 102 人学习下载。从内容预览看,PetServlet、userServlet、DoctorServlet 等关键代码一应俱全,配合数据库脚本即可本地还原完整系统,无论用于毕设答辩还是项目实战演练,都有较高参考价值。

1. 宠物医院管理系统,为什么从 Servlet 而不是 Spring Boot 起步

白天刚帮人挑完一份毕设代码,压缩包解压后只有几个.java文件和一份.sql脚本,拿起来就能跑。这是一套 javaweb 的宠物医院管理系统源码,PetServlet、userServlet、DoctorServlet 三个控制器加上数据库脚本,就是整套系统的骨架。它没有 Spring Boot 的自动配置,也没有 MyBatis 的动态 SQL,所有请求转发、JDBC 查询都摆在明面上,反而更适合用来回答“什么请求走到了哪个方法”这类答辩问题。

系统覆盖宠物档案管理、用户登录、医生信息维护、预约接诊这几条核心业务线,代码量不大,数据库脚本一次导入即可完成初始化。适合正在做 JavaWeb 课程设计、又不想靠 Maven 拉几百兆依赖的计算机相关专业学生;有几年经验的开发也可以拿它当“拆解入门项目”的样本,看老式 Servlet 工程如何组织业务。

拿到这个源码,第一件事不是点运行,而是先看懂它的请求入口和数据流。标题里说的“高分毕设、代码完整确保可以运行”,指的是编译级完整,但每次换电脑都可能遇到 JDK、MySQL、Tomcat 版本不一致的问题,下面章节按可直接复现的方式写。

2. 系统代码拆解:PetServlet、userServlet、DoctorServlet 的请求流转与数据层实现

2.1 从文件结构先读懂请求入口

一个 Servlet 3.0 规范下的 JavaWeb 工程,不一定需要 web.xml 配置所有路径。源码里 PetServlet.java、userServlet.java、DoctorServlet.java 三个控制器,文件名首字母小写后就是 URL 前缀,也就是/pet/user/doctor。这种命名方式不是巧合,它把宠物医院系统的三类角色直接映射成了路由前缀:宠物档案、平台用户、医生信息。看下面这张映射表:

Servlet映射路径核心业务
PetServlet/pet宠物档案新增、修改、查询、删除
userServlet/user用户注册、登录、注销与权限判断
DoctorServlet/doctor医生信息维护、排班与接诊状态

这张表能直接在答辩 PPT 里用。导师问“路由怎么设计的”,答案是:通过 URL 前缀 + action 参数实现资源定位,例如/pet?action=save。换成 RESTful 风格会变成POST /petDELETE /pet/{id},但课设项目用 action 参数更直观,模型层代码几乎不用动。

2.2 PetServlet 的 doGet/doPost 调度模型

看 Servlet 源码时,大部分时间不用逐行读完,重点看 doGet 和 doPost 怎么处理 action 就行。常见的实现是把逻辑集中到同一个方法,这份源码里 PetServlet 的结构基本是下面这种写法:

@WebServlet("/pet") public class PetServlet extends HttpServlet { @Override protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { String action = request.getParameter("action"); if ("list".equals(action)) { // 调用 PetDao 查询宠物列表,转发到 pet_list.jsp } else if ("add".equals(action)) { // 跳转到新增宠物表单页 pet_add.jsp } else if ("save".equals(action)) { // 获取表单参数,组装 Pet 对象,调用 PetDao.insert() } else if ("delete".equals(action)) { // 获取 petId,调用 PetDao.deleteById(petId) } // 默认 action 也按 list 处理,避免空指针 } @Override protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { // 必须放在最前面,否则中文表单参数会乱码 request.setCharacterEncoding("UTF-8"); doGet(request, response); } }

这个写法的优势是逻辑集中。doPost里设置编码后转发给doGet,GET 和 POST 共用同一套分支。需要加新功能时,只要在 action 判断里加一个else if,不需要新建类,也没有 Spring MVC 的 HandlerMapping 概念。劣处是方法会变长,当 action 超过六七个,就要考虑抽 service 层了。

从实用角度给三个参数级建议。第一,action 名不要带特殊字符,特别是&?,会直接截断参数。第二,petId这类数值参数必须在进入 DAO 前做一次Integer.parseInt异常处理,否则用户手输/pet?id=abc会直接 500。第三,重定向用response.sendRedirect("pet?action=list"),转发用request.getRequestDispatcher("pet_list.jsp").forward(request, response),两者混用会导致浏览器地址栏刷新时重复提交表单。

2.3 userServlet 与 DoctorServlet 的角色区分

userServlet 和 DoctorServlet 的区别,本质是权限模型的区别。用户端登录逻辑是:userServlet 接收用户名密码,查 user 表后把 userId 和 role 放进 session,再根据 role 跳转到用户首页或管理员页。医生端则不做登录,通常只做信息展示与排班查询,所以 DoctorServlet 一般只提供 list 和 detail 两个 action。这种拆分让改动边界很清晰:以后要加“医生登录”,就在 DoctorServlet 中加 login action,不影响用户端逻辑。

数据层沿用 JDBC,用最传统的 DBUtil 类注册驱动和获取连接。源码里不一定把 DBUtil 单独列出来,但只要是纯 Servlet + JDBC 工程,必然有类似下面的工具类:

public class DBUtil { private static final String URL = "jdbc:mysql://localhost:3306/pet_hospital" + "?useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai"; 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 SQLException { return DriverManager.getConnection(URL, USER, PASSWORD); } }

代码逻辑说明:

  • Class.forName("com.mysql.cj.jdbc.Driver")是注册 MySQL 驱动。8.x 版本驱动类名必须带cj,5.x 版本是com.mysql.jdbc.Driver,用错会直接 ClassNotFound。
  • getConnection()每次调用都新建物理连接,在课设并发量下没问题;生产环境要换成连接池,见第 5 章。
  • 密码不建议硬编码在类里,但课设源码常见做法确实如此。如果想让代码更好看,抽一个jdbc.properties并用静态代码块读取。

再往下就是 DAO 层。PetDao 查列表通常是SELECT * FROM pet WHERE del_flag=0,新增用PreparedStatement预编译,setStringsetInt按查询参数顺序赋值。这里容易踩的坑是 PreparedStatement 参数从 1 开始计数,写 0 会抛IndexOutOfBoundsException

3. 数据库脚本设计:宠物、医生、用户表结构与导入实践

3.1 核心表结构的闭环设计

高分毕设数据库脚本,讲究的是“够用且闭合”。宠物医院管理系统中,user 保存登录用户和角色,pet 表通过owner_id指向 user,doctor 表独立维护,appointment 表再把用户、宠物、医生关联起来。常见表结构如下:

表名职责关键字段
pet宠物档案id, pet_name, pet_type, owner_id, age, sex, del_flag
user登录用户id, username, password, real_name, phone, role
doctor医生信息id, doctor_name, department, title, work_time
appointment预约接诊记录id, pet_id, doctor_id, user_id, appoint_time, status

这套设计能回答两个答辩必问问题。为什么 pet 不直接存 owner_name?因为关系数据库讲究范式,owner_name 会随 user 表变化而重复存储,改一次用户名要同步多行。为什么 appointment 要单独建表?因为 pet 和 doctor 是多对多关系,拆出中间表才能保存“谁在什么时间约了哪位医生”的完整上下文。字段里保留del_flag是因为宠物档案通常需要留存,不做物理删除,只做逻辑删除。

3.2 用命令行导入数据库脚本

拿到脚本后不需要手工建表,直接导入即可。打开命令行或终端,进入脚本所在目录执行:

mysql -u root -p --default-character-set=utf8mb4 pet_hospital < pet_hospital.sql

如果还没有pet_hospital数据库,先登录 MySQL 执行:

CREATE DATABASE pet_hospital DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;

命令参数说明:

  • --default-character-set=utf8mb4控制客户端读取脚本文件时使用的编码。很多人直接双击导入遇到中文乱码,就是因为脚本文件是 UTF-8,而连接默认用了 latin1。
  • pet_hospital.sql如果包含CREATE DATABASE,目标库名可以省略;但建议保持与 DBUtil 里 URL 配置的库名一致。
  • 导入后检查表:SHOW TABLES;SHOW CREATE TABLE pet\G,不要只看图形化工具的左侧列表。

如果数据库是 MySQL 8.0,老项目驱动的认证插件可能会报Unable to load authentication plugin 'caching_sha2_password'。那是因为 8.0 默认认证插件是 caching_sha2_password,而旧驱动不认识。临时解决方案是把账号改回 mysql_native_password:

ALTER USER 'root'@'localhost' IDENTIFIED WITH mysql_native_password BY '123456'; FLUSH PRIVILEGES;

这条命令只影响 root 账号的认证方式,不动表结构和数据,适合让老 JDBC 驱动立刻跑通。

3.3 初始化测试账号

高分毕设的脚本通常会带测试数据,例如管理员 admin / admin123、医生 d01 / 123456。这些初始数据放在 user 表的 role 字段里。如果脚本没有初始化,手动插入时注意代码里是否用了 MD5 工具类:

INSERT INTO `user` (username, password, real_name, phone, role) VALUES ('admin', MD5('admin123'), '系统管理员', '13800000000', 'admin');

说明:MD5 固定加密在安全上不够强,但课设级别能跑通。答辩时可以说“密码经过 MD5 摘要后入库,后续可替换为 BCrypt 或 SHA-256 加盐”,这句话比老实存明文要高一个档次。插入完成后重启 Tomcat,重新登录,避免 session 里还留着旧的登录状态。

4. IDEA + Tomcat + MySQL 环境下的部署与配置

4.1 环境版本组合

这套源码没有 Maven 坐标,依赖 jar 都在WEB-INF/lib下,所以版本匹配比依赖管理更重要。推荐组合如下:

组件推荐版本说明
JDK1.8大部分课设源码编译目标为 Java 8,更高版本可能触发 Illegal reflective access 警告
Tomcat8.5 / 9.0支持 Servlet 3.1 / 4.0,注解扫描正常
MySQL5.7 / 8.08.0 需要改 JDBC URL 时区参数
IDEA2020.3 以上社区版即可运行 Web 项目

Tomcat 不建议用 10.x,因为它默认走 Jakarta EE,Servlet 包名从javax.servlet变成了jakarta.servlet,老代码的 import 全部失效。JDK 也建议锁 1.8,避免在jsp-api.jarservlet-api.jar上踩版本坑。

4.2 导入源码并配置 Artifact

打开 IDEA 后按下面步骤走:

  1. 选择File -> New -> Project from Existing Sources,定位到解压后的项目根目录,项目类型选 Eclipse 或普通 Java 模块均可。
  2. 右键src目录,Mark Directory as -> Sources Root;右键web目录(或WebContent),Mark Directory as -> Web Resources Directory
  3. 打开Project Structure -> Artifacts,点+ -> Web Application: Exploded,把项目模块添加进去。
  4. 确认WEB-INF/lib下有mysql-connector-java驱动包,没有就从本地 Maven 仓库拷贝一份,jar 文件放进 lib 后需要重新构建 Artifact。
  5. 配置 Tomcat:Run -> Edit Configurations -> + -> Tomcat Server -> Local,指定 Tomcat 目录,Deployment 里添加第 3 步的 Artifact,Application context 建议改成/
  6. 启动前确认 DBUtil 或 jdbc.properties 里的数据库账号密码与本地一致,启动 Tomcat,浏览器访问http://localhost:8080/pet?action=list

整个流程不联网也可以完成,因为项目不依赖 Maven 中央仓库。如果启动后页面报The requested resource [/pet] is not available,优先检查 Artifact 名称和访问路径。IDEA 默认会把 Application context 设置成模块名,比如/宠物系统_war_exploded/pet,改成/后访问/pet就正常了。

4.3 启动时的常见报错与定位

Tomcat 日志是最直接的诊断入口,catalina.out里出现关键字时按下面表格对号入座:

报错关键字可能原因解决方式
ClassNotFoundException: com.mysql.jdbc.Driver驱动 jar 没在 WEB-INF/lib拷贝 mysql-connector-java-8.0.x.jar 并重建 Artifact
Cannot create PoolableConnectionFactory数据库服务没启动检查 MySQL 服务和 3306 端口
Access denied for user 'root'@'localhost'账号密码错误核对 DBUtil 里的 USER 和 PASSWORD
Port 8080 was already in use端口被其他进程占用改 server.xml 的 Connector 端口,或结束占用进程
The superclass "javax.servlet.http.HttpServlet" was not found缺少 Servlet API项目里添加 Tomcat 依赖库
java.sql.SQLException: The server time zone valueMySQL 8.0 时区问题URL 加 serverTimezone=Asia/Shanghai

其中时区错误最常见。MySQL 8.0 默认时区是 UTC,老代码 URL 没带serverTimezone,连接建立就会抛异常。改成:

jdbc:mysql://localhost:3306/pet_hospital?useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai

重新编译后即可。这里有个技巧:如果项目里同时有多个链接位置,先用全局搜索找到jdbc:mysql://然后把所有连接串统一替换,不要只改 DBUtil 一个类。

5. 上线前检查与三个常见改造点

5.1 中文乱码的全链路处理

很多同学把项目跑起来后,发现页面显示正常,但插入宠物名字变成问号。原因不是代码写错,而是请求、数据库、响应三层里有某层编码丢失。除了 DBUtil 里加characterEncoding=UTF-8,还要在web.xml配置一个编码过滤器:

<filter> <filter-name>encodingFilter</filter-name> <filter-class>com.pet.filter.EncodingFilter</filter-class> </filter> <filter-mapping> <filter-name>encodingFilter</filter-name> <url-pattern>/*</url-pattern> </filter-mapping>

Filter 对应的 Java 类如下:

public class EncodingFilter implements Filter { public void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain) throws IOException, ServletException { req.setCharacterEncoding("UTF-8"); resp.setCharacterEncoding("UTF-8"); resp.setContentType("text/html;charset=UTF-8"); chain.doFilter(req, resp); } }

提示:只写req.setCharacterEncoding不够,响应也要设置ContentType。同时数据库连接要带useUnicode=true,三者缺一都可能出现中文乱码。

5.2 把 JDBC 连接替换成 HikariCP

如果答辩被问“系统有什么不足”,最容易被攻击的点就是每次请求都新建数据库连接。改造方式很简单,不用换框架,只要引入 HikariCP jar 和 slf4j-api.jar,然后把 DBUtil 改成连接池:

public class DBUtil { private static final HikariDataSource DATA_SOURCE; static { HikariConfig config = new HikariConfig(); config.setJdbcUrl("jdbc:mysql://localhost:3306/pet_hospital" + "?serverTimezone=Asia/Shanghai"); config.setUsername("root"); config.setPassword("123456"); config.setDriverClassName("com.mysql.cj.jdbc.Driver"); config.setMaximumPoolSize(10); config.setMinimumIdle(2); DATA_SOURCE = new HikariDataSource(config); } public static Connection getConnection() throws SQLException { return DATA_SOURCE.getConnection(); } }

这段代码替换原 DBUtil 后,所有 DAO 里的getConnection()调用不用改。MaximumPoolSizeMinimumIdle是连接池核心参数,课设环境 10 和 2 就够,不需要像生产环境那样做压测调优。改造完成后启动日志里会出现 HikariPool 的初始化记录,答辩时可以拿这个作为性能优化点。

5.3 给宠物列表加一个分页参数

没有分页的列表页在数据量超过几十条后就会很卡。加分页不需要额外插件,MySQL 的LIMIT就能解决。PetDao 里加一个方法:

public List<Pet> findPage(int page, int pageSize) throws SQLException { String sql = "SELECT * FROM pet WHERE del_flag = 0 ORDER BY id DESC LIMIT ?, ?"; try (Connection conn = DBUtil.getConnection(); PreparedStatement ps = conn.prepareStatement(sql)) { ps.setInt(1, (page - 1) * pageSize); ps.setInt(2, pageSize); // 执行查询并封装 List<Pet> } }

PetServlet 的 list 分支中读取分页参数:

int page = 1; int pageSize = 10; String pageParam = request.getParameter("page"); if (pageParam != null) { page = Integer.parseInt(pageParam); } request.setAttribute("page", page); request.setAttribute("pets", petDao.findPage(page, pageSize));

分页参数说明:LIMIT ?, ?第一个?是偏移量,第二个是每页条数。第 1 页从 0 开始,第 2 页从 10 开始,所以(page - 1) * pageSize正好对应当前页起点。接口保持/pet?action=list&page=2的格式,页面里只加“上一页”“下一页”两个链接就够了。这段逻辑复制到现有list分支里,列表页浏览体验会明显提升。

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

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询