简介:这是一套面向计算机相关专业毕业设计、课程设计与期末大作业的 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 /pet和DELETE /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预编译,setString和setInt按查询参数顺序赋值。这里容易踩的坑是 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下,所以版本匹配比依赖管理更重要。推荐组合如下:
| 组件 | 推荐版本 | 说明 |
|---|---|---|
| JDK | 1.8 | 大部分课设源码编译目标为 Java 8,更高版本可能触发 Illegal reflective access 警告 |
| Tomcat | 8.5 / 9.0 | 支持 Servlet 3.1 / 4.0,注解扫描正常 |
| MySQL | 5.7 / 8.0 | 8.0 需要改 JDBC URL 时区参数 |
| IDEA | 2020.3 以上 | 社区版即可运行 Web 项目 |
Tomcat 不建议用 10.x,因为它默认走 Jakarta EE,Servlet 包名从javax.servlet变成了jakarta.servlet,老代码的 import 全部失效。JDK 也建议锁 1.8,避免在jsp-api.jar和servlet-api.jar上踩版本坑。
4.2 导入源码并配置 Artifact
打开 IDEA 后按下面步骤走:
- 选择
File -> New -> Project from Existing Sources,定位到解压后的项目根目录,项目类型选 Eclipse 或普通 Java 模块均可。 - 右键
src目录,Mark Directory as -> Sources Root;右键web目录(或WebContent),Mark Directory as -> Web Resources Directory。 - 打开
Project Structure -> Artifacts,点+ -> Web Application: Exploded,把项目模块添加进去。 - 确认
WEB-INF/lib下有mysql-connector-java驱动包,没有就从本地 Maven 仓库拷贝一份,jar 文件放进 lib 后需要重新构建 Artifact。 - 配置 Tomcat:
Run -> Edit Configurations -> + -> Tomcat Server -> Local,指定 Tomcat 目录,Deployment 里添加第 3 步的 Artifact,Application context 建议改成/。 - 启动前确认 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 value | MySQL 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()调用不用改。MaximumPoolSize和MinimumIdle是连接池核心参数,课设环境 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分支里,列表页浏览体验会明显提升。
本文还有配套的精品资源,点击获取