ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

JSP+SQL Server宠物诊所系统毕业设计实战指南

JSP+SQL Server宠物诊所系统毕业设计实战指南 简介本资源是一套面向计算机专业本科生的毕业设计实践项目聚焦JSPSQL技术栈的Web应用开发为宠物诊所提供信息化管理解决方案。系统完整实现用户管理、宠物档案、在线预约、诊疗记录等核心功能覆盖需求分析、MVC架构设计、数据库建模、前后端交互及Tomcat部署全流程适合Web开发初学者巩固Java Web基础并完成课程设计或毕设选题。压缩包共445个文件含26个JSP页面负责动态视图展示、38个Java源文件含Servlet、DAO、Bean等业务逻辑层代码、83个编译后Class文件可直接运行以及XML配置、备份文件和测试用例等整体大小2.77MB结构清晰、模块分离明确。已有95人学习下载资源附带完整项目报告与可运行源码包含多版本Java文件如java~1~至java~28~和JSP备份如addowner.jsp.bak、petsearch.jsp.bak便于对比学习代码演进与调试排错过程。1. 为什么用 JSP SQL 做宠物诊所系统仍是计算机专业毕业设计的务实选择很多同学看到“JSP”第一反应是“过时了”转头就去卷 Spring Boot 或 Vue。但现实是在高校教学体系里JSP 仍是 Web 开发入门最扎实的落点——它不隐藏 Servlet 生命周期、不抽象 JDBC 连接细节、不自动注入 DAO 层所有请求流转、页面跳转、数据库交互都明明白白写在.jsp和.java文件里。一个完整的宠物诊所系统要管理宠物档案品种、年龄、免疫记录、医生排班、就诊预约、药品库存、收费明细这些业务逻辑用 JSP 的% %脚本块JavaBeanJDBC 直接嵌套反而比绕一大圈配 MyBatis XML 映射更易追溯数据流向。尤其对毕业设计评审老师而言能清晰看到request.getParameter(petName) → PreparedStatement.setString(1, ...) → ResultSet.next()这条链路比跑通一个黑盒框架更有说服力。本项目不是追求技术前沿而是用最小技术栈覆盖完整 CRUD 场景用户登录校验走 session 管理预约列表用 JSTLc:forEach渲染药品库存预警靠 SQLWHERE stock 5直接查出所有源代码可逐行调试、参数可单步追踪、SQL 可直接粘贴到 SQL Server Management Studio 执行验证。2. 搭建 JSP SQL Server 开发环境从 JDK 到 Tomcat 再到数据库连接池2.1 JDK 8 与 Tomcat 9 的版本匹配要点JSP 规范依赖 Servlet 容器实现而 Tomcat 版本必须与 JDK 兼容。Tomcat 9 要求 JDK 8 或更高版本但不能使用 JDK 17——因为 JSP 编译器Jasper在 JDK 11 后移除了javax.xml.bind包而旧版 JSP 标签库如 JSTL 1.2仍引用该包会导致NoClassDefFoundError: javax/xml/bind/DatatypeConverter。实测稳定组合为JDK 8u361 Apache Tomcat 9.0.85。安装后需在CATALINA_HOME/conf/server.xml中确认端口未被占用Connector port8080 protocolHTTP/1.1 connectionTimeout20000 redirectPort8443 /提示若启动报错SEVERE: Failed to initialize connector [Connector[HTTP/1.1-8080]]先检查 8080 端口是否被 Skype 或其他服务占用可用netstat -ano | findstr :8080查看 PID 并在任务管理器中结束进程。2.2 SQL Server 2019 驱动配置与连接池初始化宠物诊所系统需频繁查询宠物档案、医生排班表、就诊记录直接new Connection()会耗尽数据库连接。必须使用连接池。推荐commons-dbcp2非 HikariCP因后者需 Java 11。在WEB-INF/lib/下放入sqljdbc42.jarMicrosoft 官方 SQL Server JDBC Driver for SQL Server 2019支持 TLS 1.2commons-dbcp2-2.9.0.jarcommons-pool2-2.11.1.jarWEB-INF/web.xml中声明资源引用resource-ref descriptionSQL Server DataSource/description res-ref-namejdbc/PetClinicDB/res-ref-name res-typejavax.sql.DataSource/res-type res-authContainer/res-auth /resource-refCATALINA_HOME/conf/context.xml中配置连接池关键参数已加注释Resource namejdbc/PetClinicDB authContainer typejavax.sql.DataSource factoryorg.apache.commons.dbcp2.BasicDataSourceFactory driverClassNamecom.microsoft.sqlserver.jdbc.SQLServerDriver urljdbc:sqlserver://localhost:1433;databaseNamePetClinic;encrypttrue;trustServerCertificatetrue; usernamesa passwordYourStrongPass123 maxTotal20 !-- 最大连接数避免数据库拒绝 -- maxIdle10 !-- 空闲连接上限 -- minIdle5 !-- 最小空闲连接防冷启动延迟 -- initialSize5 !-- 初始化时创建的连接数 -- validationQuerySELECT 1 !-- 连接有效性检测SQL -- testOnBorrowtrue !-- 从池中借出前检测 -- timeBetweenEvictionRunsMillis30000 !-- 每30秒清理无效连接 -- /注意SQL Server 2019 默认启用加密连接encrypttrue若本地测试环境未配置证书必须添加trustServerCertificatetrue否则抛出SSL Exception。生产环境应替换为真实证书。2.3 在 JSP 中获取连接池并执行基础查询连接池配置生效后在任意 JSP 页面顶部引入 JSTL 并获取 DataSource% page importjavax.naming.*,javax.sql.*,java.sql.* % % taglib prefixc urihttp://java.sun.com/jsp/jstl/core % % Context ctx new InitialContext(); DataSource ds (DataSource) ctx.lookup(java:comp/env/jdbc/PetClinicDB); Connection conn ds.getConnection(); // 查询所有待就诊宠物按预约时间排序 String sql SELECT p.id, p.name, p.breed, a.appoint_time, d.name AS doctor_name FROM Pet p JOIN Appointment a ON p.id a.pet_id JOIN Doctor d ON a.doctor_id d.id WHERE a.status pending ORDER BY a.appoint_time; PreparedStatement ps conn.prepareStatement(sql); ResultSet rs ps.executeQuery(); request.setAttribute(appointments, rs); rs.close(); ps.close(); conn.close(); // 必须关闭否则连接池耗尽 %2.3.1 为什么必须手动 close()连接池不是自动回收吗BasicDataSource的连接对象是代理ProxyConnection调用close()实际是将连接归还池中而非物理关闭。若忘记close()该连接会一直被标记为“占用”直到超时默认 30 秒才由 evictor 回收期间新请求会阻塞在ds.getConnection()。实测漏关 3 个连接第 4 个请求就会卡在getConnection()超过 30 秒最终抛出SQLException: Could not retrieve connection from pool。2.3.2 如何验证连接池是否生效部署项目后访问http://localhost:8080/yourapp/test.jsp内容如下% page importjavax.naming.*,javax.sql.* % % Context ctx new InitialContext(); DataSource ds (DataSource) ctx.lookup(java:comp/env/jdbc/PetClinicDB); out.print(DataSource class: ds.getClass().getName() br); out.print(Pool active connections: ((org.apache.commons.dbcp2.BasicDataSource)ds).getNumActive() br); out.print(Pool idle connections: ((org.apache.commons.dbcp2.BasicDataSource)ds).getNumIdle()); %正常输出应类似DataSource class: org.apache.commons.dbcp2.BasicDataSource Pool active connections: 0 Pool idle connections: 5说明初始 5 个连接已创建且空闲。3. 宠物诊所核心业务模块的 JSP 实现从预约登记到药品库存预警3.1 宠物档案录入页pet_add.jsp的关键字段校验宠物信息需保证品种、年龄、主人联系方式有效。JSP 表单提交前用 JavaScript 做前端校验后端再用 Java 验证防绕过form actionPetServlet?actionadd methodpost onsubmitreturn validateForm() input typetext namename placeholder宠物姓名 required select namebreed required option value请选择品种/option option valueGolden Retriever金毛寻回犬/option option valuePersian波斯猫/option option valuePoodle贵宾犬/option /select input typenumber nameage min0 max25 placeholder年龄岁 required input typetel nameowner_phone pattern^1[3-9]\d{9}$ placeholder主人手机号 required input typesubmit value提交 /form script function validateForm() { const phone document.querySelector(input[nameowner_phone]).value; if (!/^1[3-9]\d{9}$/.test(phone)) { alert(手机号格式错误请输入11位中国大陆手机号); return false; } return true; } /script3.1.1 后端 Servlet 如何处理重复提交用户快速点击两次“提交”按钮可能插入两条相同宠物记录。在PetServlet.java的add方法中用INSERT ... SELECT WHERE NOT EXISTS避免String sql INSERT INTO Pet (name, breed, age, owner_phone, create_time) SELECT ?, ?, ?, ?, GETDATE() WHERE NOT EXISTS (SELECT 1 FROM Pet WHERE owner_phone ? AND name ?); PreparedStatement ps conn.prepareStatement(sql); ps.setString(1, name); ps.setString(2, breed); ps.setInt(3, age); ps.setString(4, ownerPhone); ps.setString(5, ownerPhone); // WHERE 条件中的 owner_phone ps.setString(6, name); // WHERE 条件中的 name int rows ps.executeUpdate(); if (rows 0) { request.setAttribute(error, 该手机号下已存在同名宠物请勿重复录入); request.getRequestDispatcher(pet_add.jsp).forward(request, response); }3.2 就诊预约列表页appointment_list.jsp的分页与状态筛选预约数据量大时需分页。JSP 中不硬编码分页逻辑而是由 Servlet 计算偏移量传入!-- URL 示例/AppointmentServlet?actionlistpage2size10statuspending -- c:forEach items${appointments} vara tr td${a.id}/td td${a.petName}${a.breed}/td tdfmt:formatDate value${a.appointTime} patternyyyy-MM-dd HH:mm//td td${a.doctorName}/td td c:choose c:when test${a.status pending}待就诊/c:when c:when test${a.status completed}已完成/c:when c:otherwise已取消/c:otherwise /c:choose /td td a hrefAppointmentServlet?actionupdateid${a.id}statuscompleted完成/a | a hrefAppointmentServlet?actiondeleteid${a.id}取消/a /td /tr /c:forEach !-- 分页导航 -- div classpagination c:if test${currentPage 1} a href?page${currentPage-1}size${pageSize}status${param.status}上一页/a /c:if span第 ${currentPage} 页共 ${totalPages} 页/span c:if test${currentPage totalPages} a href?page${currentPage1}size${pageSize}status${param.status}下一页/a /c:if /div3.2.1 SQL Server 分页查询的高效写法兼容 2012避免OFFSET FETCH在大数据量时性能陡降改用ROW_NUMBER()窗口函数SELECT id, pet_name, appoint_time, doctor_name, status FROM ( SELECT a.id, p.name AS pet_name, a.appoint_time, d.name AS doctor_name, a.status, ROW_NUMBER() OVER (ORDER BY a.appoint_time DESC) AS row_num FROM Appointment a JOIN Pet p ON a.pet_id p.id JOIN Doctor d ON a.doctor_id d.id WHERE a.status ? ) AS numbered WHERE row_num BETWEEN ? AND ?; -- 参数1status如 pending -- 参数2(currentPage-1)*pageSize 1 起始行号 -- 参数3currentPage*pageSize 结束行号3.3 药品库存预警功能SQL 查询驱动前端高亮当药品库存低于阈值时列表页自动标红。在medicine_list.jsp中c:forEach items${medicines} varm tr class${m.stock 5 ? low-stock : } td${m.name}/td td${m.specification}/td td${m.stock}/td td c:choose c:when test${m.stock 5} span stylecolor:red;font-weight:bold;库存不足/span /c:when c:when test${m.stock 10} span stylecolor:orange;库存紧张/span /c:otherwise span stylecolor:green;充足/span /c:choose /td /tr /c:forEach对应 CSS.low-stock { background-color: #fff8f8 !important; }3.3.1 如何让预警实时生效避免缓存导致误判浏览器可能缓存 JSP 输出。在medicine_list.jsp顶部强制禁用缓存% response.setHeader(Cache-Control, no-cache, no-store, must-revalidate); response.setHeader(Pragma, no-cache); response.setDateHeader(Expires, 0); %同时SQL 查询需确保读取最新数据避免脏读。在MedicineDAO.java中设置事务隔离级别conn.setTransactionIsolation(Connection.TRANSACTION_READ_COMMITTED); // 此级别保证读取已提交数据防止幻读影响库存判断4. 关键安全加固与常见问题排查SQL 注入、JSP 编译路径、Session 失效4.1 彻底防御 SQL 注入为什么 PreparedStatement 不是万能解药PreparedStatement能防大部分注入但动态表名、列名、ORDER BY 子句无法参数化。例如按医生姓名排序的查询// ❌ 危险拼接列名 String orderBy request.getParameter(sort); String sql SELECT * FROM Appointment ORDER BY orderBy DESC; // ✅ 安全方案白名单校验 String[] allowedSorts {appoint_time, pet_name, doctor_name}; boolean isValidSort Arrays.asList(allowedSorts).contains(orderBy); if (!isValidSort) { orderBy appoint_time; // 默认安全列 } String sql SELECT * FROM Appointment ORDER BY orderBy DESC;4.1.1 JSP 页面中如何防止 XSS 攻击用户输入的宠物名字、主人留言可能含script标签。JSTL 的c:out默认转义 HTML!-- 安全自动转义 -- tdc:out value${a.petName}//td !-- 危险直接输出 -- td${a.petName}/td若需显示富文本如医生简介含br则用fn:escapeXml()显式过滤% taglib prefixfn urihttp://java.sun.com/jsp/jstl/functions % td${fn:escapeXml(a.doctorBio)}/td4.2 JSP 编译后的 Java 文件位置与调试技巧Tomcat 将.jsp编译为.java再编译成.class路径为work/Catalina/localhost/yourapp/org/apache/jsp/。例如pet_list.jsp编译后生成pet_list_jsp.javapet_list_jsp.class若修改 JSP 后页面无变化不是缓存问题而是 class 文件未更新。解决方法删除work/Catalina/localhost/yourapp/整个目录重启 Tomcat或热部署时触发重新编译提示在CATALINA_HOME/conf/web.xml中设置init-paramparam-namedevelopment/param-nameparam-valuetrue/param-value/init-param可开启开发模式——JSP 修改后自动重编译无需重启。4.3 Session 失效导致用户反复登录的根因与修复宠物诊所系统用session.setAttribute(user, user)存储登录态但常出现操作半小时后突然跳回登录页。原因有三Tomcat 默认 session 超时 30 分钟在web.xml中延长session-config session-timeout120/session-timeout !-- 单位分钟 -- /session-config浏览器关闭导致 session cookie 失效若需保持登录改用持久化 CookieCookie loginCookie new Cookie(userToken, token); loginCookie.setMaxAge(60 * 60 * 24 * 7); // 7天 loginCookie.setPath(/); response.addCookie(loginCookie);集群环境下 session 不共享单机部署无需处理若未来扩展改用 Redis 存储 session需tomcat-redis-session-manager插件。5. 毕业设计答辩高频问题预演与源码结构优化技巧5.1 答辩必问为什么选 JSP 而不是 Spring Boot如何回应技术选型质疑评审老师常问“现在都用 Spring Boot你为什么用 JSP” 回答需体现工程权衡意识而非技术情怀“我对比了两种方案Spring Boot 虽现代化但需额外学习 IOC、AOP、RESTful 设计而本项目核心诉求是清晰展示‘用户请求→服务器处理→数据库交互→页面渲染’全流程。JSP 的request.getParameter()和out.print()能直观对应 HTTP 请求的 key-value 结构SQL 查询直接写在 DAO 层所有数据流向在 200 行代码内可追踪。这更符合毕业设计‘理解基础原理’的教学目标。当然我也在 README.md 中注明若需扩展为微服务可将各模块拆分为 Spring Boot Starter复用现有 SQL 和业务逻辑。”5.1.1 源码结构如何让老师一眼看出专业性扁平目录所有 .jsp 放一起会被认为缺乏组织。按 MVC 分层重构src/ ├── dao/ // 数据访问层PetDAO.java, AppointmentDAO.java ├── model/ // 实体类Pet.java, Doctor.java ├── servlet/ // 控制器PetServlet.java, LoginServlet.java └── util/ // 工具类DBUtil.java, StringUtil.java WebContent/ ├── WEB-INF/ │ ├── web.xml // 配置 Servlet 映射 │ └── lib/ // 第三方 jar ├── css/ // 样式文件 ├── js/ // 脚本文件 └── jsp/ // 视图层按功能分文件夹 ├── admin/ // 管理员页面doctor_list.jsp ├── user/ // 用户页面pet_add.jsp, appointment_list.jsp └── common/ // 公共组件header.jsp, footer.jsp5.2 三个让代码“看起来更高级”的实用技巧5.2.1 用 Filter 统一处理中文乱码避免每个 Servlet 都写request.setCharacterEncoding(UTF-8)。新建EncodingFilter.javapublic class EncodingFilter implements Filter { public void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain) throws IOException, ServletException { HttpServletRequest request (HttpServletRequest) req; HttpServletResponse response (HttpServletResponse) resp; request.setCharacterEncoding(UTF-8); response.setCharacterEncoding(UTF-8); response.setContentType(text/html;charsetUTF-8); chain.doFilter(request, response); } }web.xml中注册filter filter-nameEncodingFilter/filter-name filter-classutil.EncodingFilter/filter-class /filter filter-mapping filter-nameEncodingFilter/filter-name url-pattern/*/url-pattern /filter-mapping5.2.2 用自定义 TagLib 简化重复 HTML将分页导航封装为pet:pagination/标签。WEB-INF/pagination.tld?xml version1.0 encodingUTF-8? taglib xmlnshttp://java.sun.com/xml/ns/javaee version2.1 tlib-version1.0/tlib-version short-namepet/short-name uri/pet-tags/uri tag namepagination/name tag-classtags.PaginationTag/tag-class body-contentempty/body-content attribute namecurrentPage/name requiredtrue/required rtexprvaluetrue/rtexprvalue /attribute /tag /taglibJSP 中使用% taglib prefixpet uri/pet-tags % pet:pagination currentPage${currentPage}/5.2.3 在 SQL Server 中用 computed column 自动计算字段宠物年龄不应存为固定值会过期而应由出生日期计算。建表时用计算列CREATE TABLE Pet ( id INT IDENTITY(1,1) PRIMARY KEY, name NVARCHAR(50), birth_date DATE, age AS DATEDIFF(YEAR, birth_date, GETDATE()) PERSISTED -- 持久化列可建索引 );这样 Java 层无需Calendar.getInstance()计算SQL 查询直接SELECT age即得当前年龄且PERSISTED保证索引可用。注意GETDATE()是 SQL Server 函数MySQL 对应CURDATE()Oracle 对应SYSDATE。跨数据库迁移时需调整。最后检查PetClinic数据库中是否存在名为Pet的表其age列是否为AS DATEDIFF...类型——这是体现数据库设计功底的关键细节。本文还有配套的精品资源点击获取
返回列表