JSP+Servlet+MySQL轻量Web开发实战指南
2026/9/14 13:25:08 网站建设 项目流程

简介:本资源是一套面向计算机专业本科生的毕业设计实践项目,聚焦Web应用开发核心能力训练,适用于JSP与SQL数据库技术入门到进阶的学习者。系统实现宠物诊所全流程信息化管理,涵盖用户权限控制、宠物档案维护、在线预约挂号、诊疗记录存档及费用统计等实用功能,可作为课程设计、毕设参考或Java Web开发实战范例。压缩包共445个文件,含26个JSP页面(负责动态交互与展示)、38个Java源码(含DAO、Servlet及业务逻辑类)、83个编译后Class文件(支撑运行),以及XML配置、备份文件和测试脚本等,整体2.77MB,结构完整、模块清晰,便于逐层理解MVC分层架构与JDBC数据库操作。目前已有95人学习下载,提供完整可运行源码、规范的数据库设计思路、典型错误处理示例及常见功能扩展点说明,助读者快速掌握从需求分析、编码实现到部署测试的全周期开发流程。

1. 这不是“过时技术”的毕业设计,而是JSP在轻量级Web系统中依然成立的工程实践现场

很多同学看到“JSP+SQL”就下意识划走,觉得这是十年前的技术栈,不配进2024年的项目清单。但真实情况是:在中小型宠物诊所、社区兽医站、校园实训平台这类对并发压力低、交付周期紧、运维能力有限的场景里,JSP+Tomcat+MySQL组合仍是零框架依赖、部署即用、调试直观、教学闭环完整的最优解。这个毕业设计包里没有Spring Boot自动装配、没有MyBatis动态SQL、没有Vue组件化——但它用addownerservlet.java.bakpetview_005fname_jsp.class这种带备份后缀与编译痕迹的文件,暴露出开发者在真实开发节奏中的试错路径:先写JSP页面接收表单,再补Servlet做请求分发,最后回填DAO层封装JDBC连接。它解决的不是高并发吞吐问题,而是“让前台护士能3分钟录入一只新猫的疫苗信息,并立刻在诊疗台查到”的具体业务流。适合刚学完Servlet生命周期、还没啃透MVC分层边界的计算机专业学生;也适合需要快速搭建内部管理工具的小型动物医疗团队——你不需要懂IOC容器,但必须清楚request.getParameter("petName")为什么不能直接拼进SQL字符串。


2. JSP页面与Servlet协同机制:从addowner.jspAddOwnerServlet的数据流转链路

2.1 JSP作为视图层的核心职责与边界约束

JSP在本系统中承担的是纯前端渲染角色,而非业务逻辑载体。以addowner.jsp为例,其核心结构遵循“表单驱动+EL表达式回显”模式:

<!-- addowner.jsp --> <%@ page contentType="text/html;charset=UTF-8" %> <%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %> <html> <head><title>添加主人信息</title></head> <body> <form action="AddOwnerServlet" method="post"> <label>姓名:<input type="text" name="ownerName" required></label><br> <label>电话:<input type="tel" name="phone" pattern="^1[3-9]\\d{9}$"></label><br> <label>地址:<textarea name="address"></textarea></label><br> <input type="submit" value="提交"> </form> <!-- 错误提示区:使用JSTL判断是否有错误消息 --> <c:if test="${not empty errorMsg}"> <div style="color:red">${errorMsg}</div> </c:if> </body> </html>

注意:该页面未包含任何Java脚本片段(<% %>),所有动态内容通过EL表达式${}和JSTL标签库控制。这种写法强制分离了展示逻辑与业务逻辑,避免出现<% out.print(rs.getString("name")); %>这类破坏可维护性的嵌入式代码。pattern属性对手机号做前端校验,但仅作体验优化,后端必须重复验证——因为浏览器可禁用JavaScript或篡改DOM。

2.2 Servlet作为控制器的请求解析与响应分发

AddOwnerServlet(对应源码中addownerservlet.java.bak)是典型的HTTP请求处理器,其doPost方法完成三件事:参数提取、业务处理、跳转控制:

// AddOwnerServlet.java protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { // 1. 提取表单参数(注意:中文需设置字符编码) request.setCharacterEncoding("UTF-8"); String ownerName = request.getParameter("ownerName").trim(); String phone = request.getParameter("phone"); String address = request.getParameter("address"); // 2. 参数校验(业务规则检查) if (ownerName == null || ownerName.length() == 0) { request.setAttribute("errorMsg", "主人姓名不能为空"); request.getRequestDispatcher("addowner.jsp").forward(request, response); return; } if (!phone.matches("^1[3-9]\\d{9}$")) { request.setAttribute("errorMsg", "手机号格式错误"); request.getRequestDispatcher("addowner.jsp").forward(request, response); return; } // 3. 调用DAO层执行插入(此处省略异常处理细节) OwnerDAO ownerDAO = new OwnerDAO(); boolean success = ownerDAO.insertOwner(ownerName, phone, address); // 4. 根据结果跳转不同页面 if (success) { response.sendRedirect("ownerList.jsp"); // 重定向避免重复提交 } else { request.setAttribute("errorMsg", "保存失败,请重试"); request.getRequestDispatcher("addowner.jsp").forward(request, response); } }

关键参数说明

  • request.setCharacterEncoding("UTF-8"):解决中文乱码问题,必须在getParameter()调用前设置;
  • response.sendRedirect()vsrequest.getRequestDispatcher().forward():前者产生新请求(URL变更,适合成功后跳转),后者保持原请求对象(URL不变,适合错误回显);
  • ownerDAO.insertOwner(...):封装了JDBC连接、预编译SQL、事务提交等细节,体现MVC中Model层职责。

2.3 JSP编译机制与.class文件定位验证

项目中存在addpet_jsp.classpetview_005fname_jsp.class等编译产物,这是Tomcat运行时将JSP转换为Servlet并编译的结果。验证其生成路径可确认环境配置是否正确:

# 在Tomcat安装目录下查找JSP编译类文件 find $CATALINA_HOME -name "*addpet_jsp.class" 2>/dev/null # 典型路径:$CATALINA_HOME/work/Catalina/localhost/[app-name]/org/apache/jsp/

若找不到对应.class文件,常见原因有:

  • Tomcat未启动或应用未部署成功;
  • JSP文件名含非法字符(如空格、中文),导致编译失败;
  • web.xml中未配置JSP Servlet映射(Tomcat 8+默认启用,但旧版需显式声明)。

提示.bak后缀文件(如addownerservlet.java.bak)表明开发者曾用IDE自动备份功能,实际运行应使用无后缀的.java文件。部署前需确保WEB-INF/classes/目录下存在编译后的.class文件,且包路径与源码声明一致。


3. SQL数据库设计与JDBC操作:从owner表结构到防SQL注入的参数化查询

3.1 宠物诊所核心数据表结构设计逻辑

系统数据库至少包含三张主表,其字段设计直指业务痛点:

表名字段类型约束业务含义
owneridINT PK AI主键自增主人唯一标识
nameVARCHAR(50)NOT NULL姓名(前台录入高频字段)
phoneVARCHAR(20)UNIQUE手机号(用于预约通知)
addressTEXT地址(支持长文本描述)
petidINT PK AI宠物唯一标识
nameVARCHAR(30)NOT NULL宠物名(如“咪咪”“旺财”)
speciesVARCHAR(20)ENUM('cat','dog','bird')物种(枚举值约束输入)
age_monthTINYINTCHECK(age_month>=0 AND age_month<=300)年龄(月为单位,避免小数精度问题)
owner_idINT FKREFERENCES owner(id) ON DELETE CASCADE外键关联主人(删除主人时自动清理宠物)
appointmentidINT PK AI预约单号
pet_idINT FKNOT NULL关联宠物
doctor_nameVARCHAR(20)NOT NULL接诊医生(非外键,因医生变动频繁)
appt_timeDATETIMENOT NULL预约时间(含日期与小时)
statusVARCHAR(10)DEFAULT 'pending'状态(pending/confirmed/cancelled)

设计依据age_month用整型而非DATE类型存储出生日期,是因为诊所更关注“当前年龄”而非“出生日”,且月龄计算比日期差更稳定;doctor_name不建外键,是考虑到医生离职/更名时无需同步修改历史预约记录,符合医疗数据审计要求。

3.2 JDBC连接池配置与参数化查询实现

OwnerDAO.java.bak中JDBC操作必须规避字符串拼接SQL,否则将面临SQL注入风险(如输入' OR '1'='1绕过登录)。正确做法是使用PreparedStatement

// OwnerDAO.java public class OwnerDAO { private static final String INSERT_SQL = "INSERT INTO owner (name, phone, address) VALUES (?, ?, ?)"; public boolean insertOwner(String name, String phone, String address) { Connection conn = null; PreparedStatement pstmt = null; try { conn = DBUtil.getConnection(); // 从连接池获取连接 pstmt = conn.prepareStatement(INSERT_SQL); // 使用setXxx方法绑定参数(索引从1开始) pstmt.setString(1, name); // 对应第一个? pstmt.setString(2, phone); // 对应第二个? pstmt.setString(3, address); // 对应第三个? int rowsAffected = pstmt.executeUpdate(); return rowsAffected > 0; } catch (SQLException e) { e.printStackTrace(); return false; } finally { DBUtil.close(pstmt, conn); // 必须关闭资源 } } }

参数化查询原理?占位符由JDBC驱动解析为二进制协议参数,数据库引擎将其视为纯数据而非SQL语法的一部分,从根本上阻断注入路径。对比危险写法:

// ❌ 危险!用户输入可破坏SQL结构 String sql = "INSERT INTO owner VALUES ('" + name + "', '" + phone + "', '" + address + "')";

3.3 数据库连接池配置(Tomcat context.xml)

为避免每次请求新建连接,需在META-INF/context.xml中配置JNDI数据源:

<!-- META-INF/context.xml --> <?xml version="1.0" encoding="UTF-8"?> <Context> <Resource name="jdbc/PetClinicDB" auth="Container" type="javax.sql.DataSource" maxTotal="20" maxIdle="10" minIdle="5" initialSize="5" factory="org.apache.tomcat.jdbc.pool.DataSourceFactory" driverClassName="com.mysql.cj.jdbc.Driver" url="jdbc:mysql://localhost:3306/pet_clinic?useSSL=false&amp;serverTimezone=Asia/Shanghai&amp;characterEncoding=utf8" username="root" password="123456"/> </Context>

DBUtil.getConnection()实现如下:

// DBUtil.java public class DBUtil { private static DataSource dataSource; static { try { Context initCtx = new InitialContext(); Context envCtx = (Context) initCtx.lookup("java:comp/env"); dataSource = (DataSource) envCtx.lookup("jdbc/PetClinicDB"); } catch (Exception e) { throw new RuntimeException("JNDI DataSource lookup failed", e); } } public static Connection getConnection() throws SQLException { return dataSource.getConnection(); } public static void close(AutoCloseable... resources) { for (AutoCloseable r : resources) { if (r != null) { try { r.close(); } catch (Exception e) { // 记录日志,不抛出异常避免掩盖主流程错误 } } } } }

关键配置项说明

  • maxTotal="20":连接池最大连接数,需根据服务器内存与并发量调整;
  • urlserverTimezone=Asia/Shanghai防止MySQL时区错误;
  • characterEncoding=utf8确保中文存取不乱码;
  • useSSL=false在本地开发环境可关闭SSL(生产环境应启用)。

4. MVC分层落地与常见陷阱排查:从petsearch.jspPetSearchServlet的请求链路断裂诊断

4.1 视图层petsearch.jsp的EL表达式失效场景分析

petsearch.jsp.bak中若出现${petList}无法渲染列表,常见原因有三:

现象可能原因验证命令解决方案
页面空白,无报错petList未被request.setAttribute()设置查看Servlet中request.setAttribute("petList", list)是否执行在Servlet末尾添加System.out.println("petList size: " + list.size());
显示[Lcom.example.Pet;@xxxxxpetList是数组而非ArrayList,JSTL<c:forEach>不支持数组迭代System.out.println(petList.getClass().getName());将数组转为new ArrayList<>(Arrays.asList(petArray))
中文显示为??JSP页面未声明contentTypepageEncoding检查<%@ page contentType="text/html;charset=UTF-8" %>是否存在补全page指令,且确保.jsp文件本身以UTF-8编码保存

典型petsearch.jsp片段:

<!-- petsearch.jsp --> <%@ page contentType="text/html;charset=UTF-8" %> <%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %> <html> <body> <h2>宠物搜索结果</h2> <c:choose> <c:when test="${empty petList}"> <p>未找到匹配的宠物</p> </c:when> <c:otherwise> <table border="1"> <tr><th>宠物名</th><th>物种</th><th>主人</th></tr> <c:forEach items="${petList}" var="pet"> <tr> <td>${pet.name}</td> <td>${pet.species}</td> <td>${pet.ownerName}</td> <!-- 注意:ownerName需在Pet实体中定义getter --> </tr> </c:forEach> </table> </c:otherwise> </c:choose> </body> </html>

注意pet.ownerName要求Pet类存在getOwnerName()方法,若数据来自JOIN查询,需在DAO中手动封装字段,而非依赖ORM自动映射。

4.2 Servlet请求映射404错误的逐层排查

当访问http://localhost:8080/petclinic/PetSearchServlet返回404,按以下顺序检查:

  1. URL路径是否匹配web.xmlservlet-mapping

    <!-- web.xml --> <servlet> <servlet-name>PetSearchServlet</servlet-name> <servlet-class>com.petclinic.servlet.PetSearchServlet</servlet-class> </servlet> <servlet-mapping> <servlet-name>PetSearchServlet</servlet-name> <url-pattern>/PetSearchServlet</url-pattern> </servlet-mapping>
    • 检查<servlet-class>是否与实际包路径一致(如com.petclinic.servlet);
    • 确认<url-pattern>无多余斜杠(如//PetSearchServlet会导致匹配失败)。
  2. Class文件是否在正确位置

    # 应存在于: WEB-INF/classes/com/petclinic/servlet/PetSearchServlet.class # 若路径为 com.petclinic.PetSearchServlet.class,则包声明错误
  3. Tomcat日志中是否有ClassNotFoundException
    查看$CATALINA_HOME/logs/catalina.out,若出现:

    java.lang.ClassNotFoundException: com.petclinic.servlet.PetSearchServlet

    说明类路径缺失,需检查编译输出目录是否正确指向WEB-INF/classes

4.3 数据库连接超时与事务回滚配置

addpet_jsp.class执行失败时,若数据库未回滚已插入的主人记录,说明事务未正确控制。在PetDAO.insertPet()中需显式管理事务:

public boolean insertPet(String name, String species, int ownerId) { Connection conn = null; PreparedStatement pstmt = null; try { conn = DBUtil.getConnection(); conn.setAutoCommit(false); // 关闭自动提交 // 插入宠物 String sql = "INSERT INTO pet (name, species, owner_id) VALUES (?, ?, ?)"; pstmt = conn.prepareStatement(sql); pstmt.setString(1, name); pstmt.setString(2, species); pstmt.setInt(3, ownerId); pstmt.executeUpdate(); // 模拟可能失败的操作(如更新主人统计数) // updateOwnerPetCount(ownerId); conn.commit(); // 显式提交 return true; } catch (SQLException e) { if (conn != null) { try { conn.rollback(); // 发生异常时回滚 } catch (SQLException rollbackEx) { rollbackEx.printStackTrace(); } } e.printStackTrace(); return false; } finally { DBUtil.close(pstmt, conn); } }

事务边界原则:一个业务操作(如“添加宠物”)应包含所有相关DB操作,要么全部成功,要么全部失败。conn.setAutoCommit(false)是开启事务的起点,commit()rollback()是终点。


5. 毕业设计实战技巧:基于现有源码快速扩展“宠物疫苗记录”功能模块

5.1 新增数据库表与JDBC实体映射

为支持疫苗管理,需新增vaccine表并创建对应JavaBean:

-- 新增疫苗表 CREATE TABLE vaccine ( id INT PRIMARY KEY AUTO_INCREMENT, pet_id INT NOT NULL, vaccine_name VARCHAR(50) NOT NULL, batch_no VARCHAR(30), inoculation_date DATE NOT NULL, next_due_date DATE, FOREIGN KEY (pet_id) REFERENCES pet(id) ON DELETE CASCADE );

Vaccine.java实体类(需与数据库字段严格对应):

public class Vaccine { private int id; private int petId; private String vaccineName; private String batchNo; private Date inoculationDate; // java.util.Date private Date nextDueDate; // getter/setter方法(IDE可自动生成) public int getId() { return id; } public void setId(int id) { this.id = id; } // ... 其他字段的getter/setter }

关键点inoculationDatenextDueDate使用java.util.Date,JDBC会自动转换为java.sql.Date;若需时间精度,改用Timestamp类型。

5.2 复用现有Servlet结构快速构建疫苗管理接口

参考AddOwnerServlet,新建AddVaccineServlet,复用相同模式:

// AddVaccineServlet.java protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { request.setCharacterEncoding("UTF-8"); int petId = Integer.parseInt(request.getParameter("petId")); String vaccineName = request.getParameter("vaccineName"); String batchNo = request.getParameter("batchNo"); String inoculationDate = request.getParameter("inoculationDate"); // 格式:yyyy-MM-dd VaccineDAO vaccineDAO = new VaccineDAO(); boolean success = vaccineDAO.insertVaccine(petId, vaccineName, batchNo, inoculationDate); if (success) { response.sendRedirect("petDetail.jsp?petId=" + petId); } else { request.setAttribute("errorMsg", "疫苗记录添加失败"); request.getRequestDispatcher("addvaccine.jsp").forward(request, response); } }

对应addvaccine.jsp表单中需传递petId(通过URL参数或隐藏域):

<!-- addvaccine.jsp --> <input type="hidden" name="petId" value="${param.petId}"> <label>疫苗名称:<input type="text" name="vaccineName" required></label><br> <label>批号:<input type="text" name="batchNo"></label><br> <label>接种日期:<input type="date" name="inoculationDate" required></label><br>

5.3 在petview_005fname_jsp.class中集成疫苗记录展示

修改petview_005fname.jsp(对应编译后的.class文件源码),在宠物详情页下方追加疫苗记录表格:

<!-- petview_005fname.jsp --> <h3>疫苗记录</h3> <c:if test="${empty vaccineList}"> <p>暂无疫苗记录</p> </c:if> <c:forEach items="${vaccineList}" var="vaccine"> <div> <strong>${vaccine.vaccineName}</strong> (批号:${vaccine.batchNo},接种日期:${vaccine.inoculationDate}) <c:if test="${not empty vaccine.nextDueDate}"> ,下次到期:${vaccine.nextDueDate} </c:if> </div> </c:forEach>

技巧petview_005fname_jsp.class中的005fname暗示该页面可能按宠物名排序(如第5个名字),但实际功能与文件名无关。重点是确保vaccineList在Servlet中通过request.setAttribute("vaccineList", list)传入,且PetViewServlet需调用VaccineDAO.findByPetId(petId)查询数据。

通过以上三步,可在2小时内为现有系统增加完整疫苗管理功能,无需重构MVC分层,直接复用JSP+Servlet+JDBC技术栈。这种渐进式扩展能力,正是JSP在教学与轻量级项目中不可替代的价值所在——它不追求架构炫技,而专注解决下一个具体业务需求。

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

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

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

立即咨询