Java Web开发实战:基于Servlet/JSP构建人员信息管理系统 1. 从零到一为什么需要一个人员信息管理系统在任何一个超过十个人的团队或组织里你迟早会面临一个头疼的问题人员信息怎么管一开始可能就是一个Excel表格大家往里填姓名、电话、部门。随着人越来越多表格版本开始混乱A同事改了电话没同步B同事离职了信息还在表里领导要个统计报表你得吭哧吭哧筛选半天。更别提权限了难道把包含所有人身份证号的表格发到全公司大群里这种基于文件共享的管理方式在团队规模小、变动不频繁时还能凑合一旦业务发展起来立刻就成了效率的瓶颈和信息安全的隐患。这就是为什么我们需要一个人员信息管理系统。它本质上是一个将人员数据的增、删、改、查CRUD操作从本地文件搬到网络浏览器中的Web应用。它的核心价值在于集中化、规范化、流程化和安全化。所有数据存在一个统一的数据库里版本唯一通过网页表单进行数据录入和修改确保了格式规范可以设计审批流程比如入职信息录入后需要上级确认更重要的是可以通过登录账号和角色来精确控制谁能看、谁能改哪些数据。我最近用Idea 2024配合Tomcat完整地走了一遍从环境搭建、数据库设计、后端开发、前端页面到最终部署上线的全流程并把所有代码开源了。这个项目麻雀虽小五脏俱全涵盖了单体Web项目最核心的技术栈Java Web、Servlet、JSP、JDBC、MySQL、HTML/CSS/JavaScript以及Tomcat服务器。无论你是想学习Java Web开发的全貌还是需要一个基础框架进行二次开发这个项目都能提供一个清晰的、可运行的参考。接下来我就把这个项目的构建思路、关键技术选型、具体实现步骤以及我踩过的那些“坑”毫无保留地分享给你。2. 技术选型与环境搭建为什么是这套组合拳在启动一个项目前明确技术栈是第一步。我的选择是Java Servlet/JSP MySQL Tomcat开发工具是IntelliJ IDEA 2024。这套组合可能不是最“潮”的现在更流行Spring Boot MyBatis/Spring Data JPA Vue/React但它绝对是学习Java Web最经典、最透彻的路径。理解了这套底层机制日后学习任何高级框架都会事半功倍因为你知道它们到底在帮你简化什么。2.1 核心组件解析与选型理由后端语言Java理由很直接生态成熟、稳定、跨平台。企业级应用的后端Java依然是中流砥柱。我们使用纯Java编写业务逻辑和数据访问代码不依赖任何重型框架以便看清本质。Web层Servlet JSP这是Java EE现Jakarta EE处理Web请求的标准。Servlet是运行在服务器端的Java程序用于接收和响应HTTP请求JSPJavaServer Pages则可以方便地在HTML中嵌入Java代码用于动态生成页面。选择它们是为了掌握Web请求从浏览器到服务器再返回的完整生命周期。数据库MySQL关系型数据库的代表免费、开源、应用广泛。人员信息这类结构化数据用关系型数据库来管理是天然契合的。我们将使用标准的SQL语句和JDBC进行交互。服务器Apache Tomcat一个轻量级的、开源的Java Web应用服务器专门用于运行Servlet和JSP。它配置简单是学习和开发阶段的不二之选。开发工具IntelliJ IDEA 2024 UltimateIDEA在Java开发领域的体验是顶级的。2024版本对Web开发的支持更加智能特别是创建和配置Web项目、管理依赖、以及集成Tomcat进行热部署都非常流畅。社区版功能也足够但Ultimate版对Web和数据库的支持更完善。2.2 IDEA 2024中创建Web项目的详细步骤这里是最容易出错的起点。很多教程基于旧版本IDEA界面和选项已经变化。以下是2024版本中的正确姿势新建项目打开IDEA点击“New Project”。在左侧列表中选择“Java Enterprise”。这是关键不要选成普通的“Java”项目。配置项目模板在右侧你会看到“Project Template”部分。确保勾选了“Web Application”。这会自动为你创建标准的Web目录结构WEB-INF, web.xml等。下方的“Application Server”需要你提前配置好Tomcat如果还没配置点击“New...”然后找到你本地Tomcat的安装目录。选择技术在“Additional Libraries and Frameworks”部分我们暂时不添加任何框架如Spring。确保“Java EE”版本选择了合适的如Jakarta EE 9或Java EE 8这决定了Servlet等API的版本。点击“Create”。项目结构验证创建完成后查看项目目录。你应该能看到一个src目录放Java代码一个web目录放Web资源。web目录下应有WEB-INF文件夹内含web.xml和index.jsp文件。这个结构是Tomcat能识别的标准结构。注意如果你在创建时漏选了“Web Application”项目将是一个普通的Java项目没有Web目录结构。补救办法是在项目上右键 - Add Framework Support - 勾选“Java EE”下的“Web Application”。但不如一开始就选对来得干净。2.3 本地Tomcat服务器的配置与集成项目创建好后需要让IDEA知道如何运行它。添加运行配置点击IDEA右上角运行按钮旁边的下拉菜单选择“Edit Configurations...”。新增Tomcat配置点击“”号选择“Tomcat Server” - “Local”。如果列表里没有说明你之前没在IDEA中配置过Tomcat需要点击“Configure...”来添加本地Tomcat的安装路径。部署项目在配置界面切换到“Deployment”标签页。点击“”号选择“Artifact”。你会看到一个以你的项目名命名的war包或 exploded展开目录。强烈推荐选择带“exploded”后缀的选项这代表是展开的目录支持热部署修改JSP或静态资源后无需重启Tomcat即可生效。配置上下文路径在“Application context”栏你可以设置访问路径比如改成/personnel那么应用启动后的访问地址就是http://localhost:8080/personnel。默认为/即根路径。启动测试点击“OK”保存配置然后点击绿色三角运行按钮。IDEA会自动启动Tomcat并部署你的项目。控制台无报错并最终看到类似“Server startup in [xxxx] milliseconds”的日志后打开浏览器访问http://localhost:8080/你的上下文路径应该能看到默认的index.jsp页面内容。至此你的开发环境就绪一个纯净的Java Web项目骨架已经搭建完成。接下来我们要开始赋予它灵魂——数据库和业务逻辑。3. 数据库设计与JDBC连接如何规划你的数据“仓库”任何管理系统的核心都是数据。在设计数据库时我的原则是满足当前需求预留扩展空间遵循设计范式但不过度设计。对于人员信息管理系统我们从一个最核心的employee表开始。3.1 数据表结构设计我们先设计一个包含基础信息的员工表。SQL脚本如下CREATE DATABASE IF NOT EXISTS personnel_db DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; USE personnel_db; CREATE TABLE employee ( id INT PRIMARY KEY AUTO_INCREMENT COMMENT 员工ID主键, emp_id VARCHAR(20) NOT NULL UNIQUE COMMENT 员工工号, name VARCHAR(50) NOT NULL COMMENT 姓名, gender TINYINT DEFAULT NULL COMMENT 性别0-女1-男, age INT DEFAULT NULL COMMENT 年龄, department VARCHAR(100) DEFAULT NULL COMMENT 部门, position VARCHAR(100) DEFAULT NULL COMMENT 职位, phone VARCHAR(20) DEFAULT NULL COMMENT 电话, email VARCHAR(100) DEFAULT NULL COMMENT 邮箱, hire_date DATE DEFAULT NULL COMMENT 入职日期, create_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP COMMENT 记录创建时间, update_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT 记录更新时间 ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT员工信息表;设计思路解析id代理主键。使用自增整数与业务无关性能好且能保证唯一性。emp_id工号虽然业务唯一但可能变更不宜作主键。emp_id业务唯一标识。添加UNIQUE约束确保工号不重复。字段类型VARCHAR长度根据实际情况估算utf8mb4字符集支持所有Unicode字符包括Emoji。gender用TINYINT而非VARCHAR(1)便于程序处理0/1。日期时间hire_date用DATE类型。create_time和update_time是审计字段非常有用可以追踪数据变化。ON UPDATE CURRENT_TIMESTAMP能让update_time在记录修改时自动更新。注释养成写COMMENT的习惯这对后期维护至关重要。3.2 使用JDBC建立稳健的数据库连接在Java中连接MySQL我们使用最原始的JDBC。为了代码清晰和可维护我们采用DAOData Access Object模式将数据库操作封装起来。首先需要在项目中引入MySQL驱动。如果你使用Maven在pom.xml中添加依赖。如果是普通项目则将mysql-connector-java-8.x.x.jar文件放入WEB-INF/lib目录下。接着创建一个数据库连接工具类DBUtil.javapackage com.yourcompany.personnel.util; import java.sql.Connection; import java.sql.DriverManager; import java.sql.SQLException; public class DBUtil { private static final String URL jdbc:mysql://localhost:3306/personnel_db?useUnicodetruecharacterEncodingutf8useSSLfalseserverTimezoneAsia/Shanghai; private static final String USER your_username; private static final String PASSWORD your_password; static { try { // 加载驱动MySQL 8 需要显式加载 Class.forName(com.mysql.cj.jdbc.Driver); } catch (ClassNotFoundException e) { e.printStackTrace(); throw new RuntimeException(Failed to load MySQL JDBC driver., e); } } public static Connection getConnection() throws SQLException { return DriverManager.getConnection(URL, USER, PASSWORD); } // 关闭连接的方法重载用于关闭Connection, Statement, ResultSet public static void close(AutoCloseable... closeables) { for (AutoCloseable closeable : closeables) { if (closeable ! null) { try { closeable.close(); } catch (Exception e) { e.printStackTrace(); // 生产环境应使用日志框架 } } } } }关键点与避坑指南连接字符串参数useSSLfalse在本地开发环境通常关闭SSL。serverTimezoneAsia/Shanghai是必须的否则可能遇到“服务器时区”错误。这是MySQL 8.x驱动的一个常见坑。驱动类名MySQL 5.x用的是com.mysql.jdbc.Driver而MySQL 8.x是com.mysql.cj.jdbc.Driver。用错会导致ClassNotFoundException。资源关闭务必在finally块或使用try-with-resources语句关闭Connection、Statement、ResultSet否则会导致数据库连接泄漏最终拖垮服务器。上面的close方法是一个简单的工具方法。密码安全实际项目中绝对不要将密码硬编码在代码里。应该使用配置文件如.properties或环境变量来管理。有了连接工具我们就可以创建EmployeeDAO类来封装对employee表的增删改查操作了。这里以新增和查询为例package com.yourcompany.personnel.dao; import com.yourcompany.personnel.model.Employee; import com.yourcompany.personnel.util.DBUtil; import java.sql.*; import java.util.ArrayList; import java.util.List; public class EmployeeDAO { public boolean addEmployee(Employee emp) { String sql INSERT INTO employee(emp_id, name, gender, age, department, position, phone, email, hire_date) VALUES(?,?,?,?,?,?,?,?,?); try (Connection conn DBUtil.getConnection(); PreparedStatement pstmt conn.prepareStatement(sql)) { pstmt.setString(1, emp.getEmpId()); pstmt.setString(2, emp.getName()); pstmt.setObject(3, emp.getGender()); // 使用setObject处理可能为null的Integer pstmt.setObject(4, emp.getAge()); pstmt.setString(5, emp.getDepartment()); pstmt.setString(6, emp.getPosition()); pstmt.setString(7, emp.getPhone()); pstmt.setString(8, emp.getEmail()); pstmt.setDate(9, emp.getHireDate() ! null ? new java.sql.Date(emp.getHireDate().getTime()) : null); return pstmt.executeUpdate() 0; } catch (SQLException e) { e.printStackTrace(); return false; } } public ListEmployee getAllEmployees() { ListEmployee list new ArrayList(); String sql SELECT * FROM employee ORDER BY create_time DESC; try (Connection conn DBUtil.getConnection(); Statement stmt conn.createStatement(); ResultSet rs stmt.executeQuery(sql)) { while (rs.next()) { Employee emp new Employee(); emp.setId(rs.getInt(id)); emp.setEmpId(rs.getString(emp_id)); emp.setName(rs.getString(name)); emp.setGender(rs.getInt(gender)); emp.setAge(rs.getInt(age)); emp.setDepartment(rs.getString(department)); emp.setPosition(rs.getString(position)); emp.setPhone(rs.getString(phone)); emp.setEmail(rs.getString(email)); emp.setHireDate(rs.getDate(hire_date)); emp.setCreateTime(rs.getTimestamp(create_time)); list.add(emp); } } catch (SQLException e) { e.printStackTrace(); } return list; } // 其他方法updateEmployee, deleteEmployee, getEmployeeById 等 }DAO层经验谈使用PreparedStatement所有涉及用户输入的SQL操作增、删、改、带条件的查必须使用PreparedStatement并通过setXxx方法传参。这是防止SQL注入攻击的底线。处理NULL值对于可能为null的字段如年龄、性别使用setObject或先判断再setNull避免类型转换异常。Java与SQL类型转换Java的java.util.Date需要转换为java.sql.Date、Time或Timestamp才能存入数据库。从ResultSet取出时根据字段类型使用对应的getDate、getTimestamp等方法。模型类Employee需要创建一个对应的JavaBeanPOJO包含所有字段及其getter/setter方法。这是数据在Java层流转的载体。数据库和持久层是系统的基石这部分代码的健壮性直接决定了整个应用的稳定性。务必重视异常处理和资源管理。4. Servlet控制器与JSP视图如何打通前后端的任督二脉在经典的MVC模式中Servlet扮演控制器Controller的角色负责接收请求、调用业务逻辑DAO、准备数据并转发给视图JSP进行渲染。JSP则负责展示数据。这是理解Web请求响应的关键一环。4.1 设计核心ServletEmployeeServlet我们通常用一个EmployeeServlet来处理所有与员工相关的请求。为了区分不同的操作新增、查询、删除等我们可以在请求中携带一个action参数。首先在web.xml中配置Servlet如果使用Servlet 3.0注解方式更简单但显式配置有助于理解机制?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_4_0.xsd version4.0 servlet servlet-nameEmployeeServlet/servlet-name servlet-classcom.yourcompany.personnel.controller.EmployeeServlet/servlet-class /servlet servlet-mapping servlet-nameEmployeeServlet/servlet-name url-pattern/employee/url-pattern /servlet-mapping welcome-file-list welcome-fileindex.jsp/welcome-file /welcome-file-list /web-app然后创建EmployeeServlet.javapackage com.yourcompany.personnel.controller; import com.yourcompany.personnel.dao.EmployeeDAO; import com.yourcompany.personnel.model.Employee; import javax.servlet.ServletException; import javax.servlet.annotation.WebServlet; import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException; import java.text.ParseException; import java.text.SimpleDateFormat; // 也可以使用注解 WebServlet(/employee) 替代 web.xml 配置 public class EmployeeServlet extends HttpServlet { private EmployeeDAO employeeDAO new EmployeeDAO(); Override protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { doPost(request, response); // 通常我们将GET请求也转到POST处理简化逻辑 } Override protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { request.setCharacterEncoding(UTF-8); // 解决POST请求中文乱码 response.setContentType(text/html;charsetUTF-8); String action request.getParameter(action); if (add.equals(action)) { addEmployee(request, response); } else if (delete.equals(action)) { deleteEmployee(request, response); } else if (toUpdate.equals(action)) { toUpdatePage(request, response); } else if (update.equals(action)) { updateEmployee(request, response); } else { // 默认动作为列出所有员工 listEmployees(request, response); } } // 列出所有员工 private void listEmployees(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { request.setAttribute(employeeList, employeeDAO.getAllEmployees()); request.getRequestDispatcher(/employeeList.jsp).forward(request, response); } // 添加员工 private void addEmployee(HttpServletRequest request, HttpServletResponse response) throws IOException { Employee emp new Employee(); emp.setEmpId(request.getParameter(empId)); emp.setName(request.getParameter(name)); // 注意前端传来的gender是字符串需要转换。空字符串处理是关键。 String genderStr request.getParameter(gender); emp.setGender(genderStr ! null !genderStr.isEmpty() ? Integer.parseInt(genderStr) : null); // 同理处理age等可能为空的字段... String ageStr request.getParameter(age); emp.setAge(ageStr ! null !ageStr.isEmpty() ? Integer.parseInt(ageStr) : null); emp.setDepartment(request.getParameter(department)); emp.setPosition(request.getParameter(position)); emp.setPhone(request.getParameter(phone)); emp.setEmail(request.getParameter(email)); // 日期处理SimpleDateFormat不是线程安全的但Servlet中每次请求新建实例可以接受。生产环境建议用Java 8的DateTimeFormatter。 String hireDateStr request.getParameter(hireDate); if (hireDateStr ! null !hireDateStr.isEmpty()) { try { SimpleDateFormat sdf new SimpleDateFormat(yyyy-MM-dd); emp.setHireDate(sdf.parse(hireDateStr)); } catch (ParseException e) { e.printStackTrace(); // 日期格式错误处理 } } boolean success employeeDAO.addEmployee(emp); // 添加完成后重定向到列表页防止表单重复提交 response.sendRedirect(request.getContextPath() /employee?actionlist); } // 其他方法deleteEmployee, toUpdatePage, updateEmployee 实现类似... }Servlet开发核心要点请求分发通过action参数来区分不同业务操作这是小型项目常用的简单策略。中文乱码必须处理。request.setCharacterEncoding(UTF-8)解决POST请求体乱码response.setContentType(text/html;charsetUTF-8)设置响应编码。GET请求的乱码需要在Tomcat的server.xml中配置URIEncodingUTF-8或者在代码中对参数手动编解码。数据封装将request.getParameter获取的字符串参数转换为合适的类型并封装到Employee对象中再传递给DAO层。这个过程称为“数据绑定”。页面跳转request.getRequestDispatcher(xxx.jsp).forward(request, response)是服务器端转发地址栏不变请求域request中的数据可以带到JSP。response.sendRedirect(url)是客户端重定向地址栏变化会发起新的请求之前的request数据会丢失。添加、删除、更新等改变数据的操作后应使用重定向PRG模式Post-Redirect-Get以避免用户刷新页面导致表单重复提交。4.2 使用JSP和JSTL构建动态页面JSP页面负责展示。我们将员工列表页面employeeList.jsp放在/WEB-INF/目录下这样可以防止用户直接通过URL访问JSP增强安全性必须通过Servlet转发。为了在JSP中优雅地处理Java代码和循环我们引入JSTLJSP Standard Tag Library。它提供了一套标签可以替代% ... %脚本片段使页面更清晰。首先需要将JSTL的jar包如javax.servlet.jsp.jstl-api和javax.servlet.jsp.jstl放入WEB-INF/lib。employeeList.jsp的核心部分% page contentTypetext/html;charsetUTF-8 languagejava % % taglib prefixc urihttp://java.sun.com/jsp/jstl/core % !DOCTYPE html html head title员工信息列表/title style /* 简单的CSS样式 */ table { border-collapse: collapse; width: 100%; } th, td { border: 1px solid #ddd; padding: 8px; text-align: left; } th { background-color: #f2f2f2; } .actions a { margin-right: 10px; } /style /head body h2员工信息管理/h2 a hrefemployee?actiontoAdd新增员工/a table thead tr th工号/thth姓名/thth性别/thth年龄/thth部门/thth职位/thth电话/thth邮箱/thth入职日期/thth操作/th /tr /thead tbody c:choose c:when test${not empty employeeList} c:forEach varemp items${employeeList} tr td${emp.empId}/td td${emp.name}/td td c:choose c:when test${emp.gender 1}男/c:when c:when test${emp.gender 0}女/c:when c:otherwise未知/c:otherwise /c:choose /td td${emp.age}/td td${emp.department}/td td${emp.position}/td td${emp.phone}/td td${emp.email}/td td${emp.hireDate}/td td classactions a hrefemployee?actiontoUpdateid${emp.id}编辑/a a hrefjavascript:if(confirm(确定删除吗)) location.hrefemployee?actiondeleteid${emp.id}删除/a /td /tr /c:forEach /c:when c:otherwise trtd colspan10 styletext-align:center;暂无员工数据/td/tr /c:otherwise /c:choose /tbody /table /body /htmlJSP与JSTL实战技巧EL表达式${emp.name}是ELExpression Language表达式用于从pageContext,request,session,application四个作用域中获取属性。它比% emp.getName() %更简洁且能自动处理null值显示为空字符串而不是抛出异常。JSTL核心标签c:forEach用于循环c:choose/c:when/c:otherwise用于条件判断。它们彻底取代了Java脚本让页面专注于展示。数据格式化对于日期、数字的显示可以使用JSTL的格式化标签库fmt例如fmt:formatDate value${emp.hireDate} patternyyyy-MM-dd/。路径问题在JSP中写链接或表单action时建议使用${pageContext.request.contextPath}来获取应用上下文路径例如href${pageContext.request.contextPath}/employee?actiontoAdd。这样即使应用部署的上下文路径变了链接也不会失效。至此一个具备基本CRUD功能的人员信息管理系统就完成了。用户可以通过浏览器访问查看列表、新增、编辑、删除员工信息。但这只是一个开始一个健壮的系统还需要考虑更多。5. 功能增强与实战优化让系统更健壮、更友好基础功能跑通后我们需要从用户体验、代码质量和系统健壮性角度进行优化。这部分往往是区分“玩具项目”和“可用的项目”的关键。5.1 输入验证与异常处理守住数据质量的防线用户输入是不可信的。前端可以做初步验证如HTML5的required、pattern属性但后端必须做最终、最严格的校验。后端验证示例在Servlet的addEmployee方法中private boolean validateEmployee(Employee emp) { if (emp.getEmpId() null || emp.getEmpId().trim().isEmpty()) { request.setAttribute(errorMsg, 工号不能为空); return false; } // 工号格式字母数字组合长度6-10位 if (!emp.getEmpId().matches(^[A-Za-z0-9]{6,10}$)) { request.setAttribute(errorMsg, 工号格式错误应为6-10位字母数字); return false; } if (emp.getName() null || emp.getName().trim().isEmpty()) { request.setAttribute(errorMsg, 姓名不能为空); return false; } // 邮箱格式验证简单版 if (emp.getEmail() ! null !emp.getEmail().isEmpty()) { String emailRegex ^[A-Za-z0-9_.-](.)$; if (!emp.getEmail().matches(emailRegex)) { request.setAttribute(errorMsg, 邮箱格式不正确); return false; } } return true; }在addEmployee方法中先调用validateEmployee如果失败则将错误信息存入request并转发回添加页面显示错误。全局异常处理在web.xml中配置错误页面可以捕获特定状态码或异常类型跳转到友好页面。error-page error-code500/error-code location/error/500.jsp/location /error-page error-page exception-typejava.lang.Exception/exception-type location/error/exception.jsp/location /error-page5.2 分页查询应对海量数据的必备技能当员工数据成千上万时一次性加载所有数据到页面是不可行的。我们需要实现分页。思路数据库分页使用MySQL的LIMIT子句。SELECT * FROM employee ORDER BY id DESC LIMIT ?, ?。第一个?是起始索引(pageNo-1) * pageSize第二个?是每页条数pageSize。前端传递参数列表页面需要传递pageNo当前页码和pageSize每页大小可固定到Servlet。Servlet处理接收参数计算起始索引调用DAO的分页查询方法。DAO实现新增一个方法ListEmployee getEmployeesByPage(int start, int pageSize)执行带LIMIT的SQL。计算总页数需要另一个DAO方法int getTotalCount()获取总记录数然后totalPages (totalCount pageSize - 1) / pageSize。JSP显示分页控件根据总页数和当前页生成“上一页”、“下一页”、页码数字的链接。这是一个非常经典的功能实现它能让你的项目立刻提升一个档次。5.3 使用连接池提升性能我们之前的DBUtil每次操作都新建一个数据库连接用完后关闭。在高并发场景下频繁创建和销毁连接开销巨大。数据库连接池如HikariCP, Druid是解决这个问题的标准方案。它预先创建一批连接放在池子里应用需要时从池中取用用完后归还而不是真正关闭。以阿里巴巴的Druid为例集成非常简单引入Druid的jar包。在WEB-INF下创建druid.properties配置文件配置连接参数。在项目启动时如实现ServletContextListener初始化Druid数据源。修改DBUtil.getConnection()方法从Druid数据源获取连接。连接池的配置参数如初始连接数、最大连接数、超时时间需要根据实际压力进行调整这是后端性能调优的基础知识。5.4 日志记录不可或缺的“黑匣子”千万不要再用System.out.println()或e.printStackTrace()了。使用日志框架如Log4j 2或SLF4JLogback来记录程序运行信息、错误和调试信息。好处分级输出可以控制不同环境开发/生产输出不同级别的日志DEBUG, INFO, WARN, ERROR。输出到文件可以按日期、大小滚动生成日志文件便于长期保存和查阅。结构化信息可以方便地记录线程、类、方法等信息。在DAO层、Servlet的关键步骤中加入日志记录当线上出现问题时这些日志是定位问题的第一手资料。6. 项目打包与Tomcat部署从开发环境到生产环境开发调试完成后我们需要将项目打包并部署到独立的Tomcat服务器上模拟生产环境。6.1 使用IDEA打包WAR文件在IDEA中点击菜单栏的“Build” - “Build Artifacts...”。选择你的项目artifact通常就是项目名点击“Build”。IDEA会在out/artifacts/或项目根目录下生成一个.war文件例如personnel_management.war。WARWeb Application Archive文件是一种标准的Java Web应用打包格式它包含了编译后的class文件、JSP、HTML、CSS、JS以及WEB-INF/lib下的所有jar包。6.2 部署WAR包到独立Tomcat停止Tomcat确保你本地的独立Tomcat服务器已经停止运行。清理旧部署进入Tomcat的webapps目录删除之前可能存在的你的项目文件夹如personnel和同名的.war文件。放置WAR包将上一步生成的.war文件复制到Tomcat的webapps目录下。启动Tomcat运行Tomcat的bin/startup.batWindows或bin/startup.shLinux/Mac。自动解压Tomcat启动时会自动将webapps目录下的.war文件解压成一个同名的文件夹并加载其中的应用。访问应用打开浏览器访问http://localhost:8080/你的war包名不含.war后缀。例如如果你的war包叫personnel_management.war那么访问路径就是http://localhost:8080/personnel_management。6.3 部署过程中的常见问题与解决端口冲突如果8080端口被占用Tomcat会启动失败。可以修改Tomcatconf/server.xml文件中的Connector port8080 ...改为其他端口如8081。中文乱码如果部署后页面或数据库出现乱码检查以下几个点JSP页面的pageEncoding和contentType是否都是UTF-8。Servlet中是否设置了request.setCharacterEncoding(UTF-8)和response.setCharacterEncoding(UTF-8)。数据库、表、连接字符串的字符集是否都是utf8mb4。Tomcat的conf/server.xml中Connector标签是否添加了URIEncodingUTF-8属性针对GET请求参数乱码。找不到类/ClassNotFoundException通常是因为WAR包中缺少必要的jar包。确保所有依赖MySQL驱动、JSTL jar包等都放在了WEB-INF/lib目录下并且被打包进了WAR文件。应用上下文路径部署后访问的路径是/war包名。如果你希望用根路径/访问有两种方法一是将WAR包重命名为ROOT.war二是在server.xml的Host标签内添加Context path docBase你的war包绝对路径 /。完成部署并成功访问意味着你的第一个完整的Java Web项目已经可以独立运行了。这个过程让你亲身体验了从编码到上线的完整链路这是理论学习无法替代的。整个项目从环境搭建到部署上线的完整代码我已经整理好并开源。代码中包含了上述所有核心功能、分页查询的实现、Druid连接池的集成以及基本的日志配置。你可以直接下载、导入IDEA运行也可以以此为骨架添加更复杂的功能如用户登录认证、权限管理、文件上传、图表统计等。希望这个详尽的复盘能帮你打通Java Web开发的任督二脉。