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请求;JSP(JavaServer 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 '记录更新时间' ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 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。为了代码清晰和可维护,我们采用DAO(Data Access Object)模式,将数据库操作封装起来。
首先,需要在项目中引入MySQL驱动。如果你使用Maven,在pom.xml中添加依赖。如果是普通项目,则将mysql-connector-java-8.x.x.jar文件放入WEB-INF/lib目录下。
接着,创建一个数据库连接工具类DBUtil.java:
package 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?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=Asia/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(); // 生产环境应使用日志框架 } } } } }关键点与避坑指南:
- 连接字符串参数:
useSSL=false在本地开发环境通常关闭SSL。serverTimezone=Asia/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 List<Employee> getAllEmployees() { List<Employee> 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):需要创建一个对应的JavaBean(POJO),包含所有字段及其getter/setter方法。这是数据在Java层流转的载体。
数据库和持久层是系统的基石,这部分代码的健壮性直接决定了整个应用的稳定性。务必重视异常处理和资源管理。
4. Servlet控制器与JSP视图:如何打通前后端的任督二脉?
在经典的MVC模式中,Servlet扮演控制器(Controller)的角色,负责接收请求、调用业务逻辑(DAO)、准备数据,并转发给视图(JSP)进行渲染。JSP则负责展示数据。这是理解Web请求响应的关键一环。
4.1 设计核心Servlet:EmployeeServlet
我们通常用一个EmployeeServlet来处理所有与员工相关的请求。为了区分不同的操作(新增、查询、删除等),我们可以在请求中携带一个action参数。
首先,在web.xml中配置Servlet(如果使用Servlet 3.0+注解方式更简单,但显式配置有助于理解机制):
<?xml version="1.0" encoding="UTF-8"?> <web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_4_0.xsd" version="4.0"> <servlet> <servlet-name>EmployeeServlet</servlet-name> <servlet-class>com.yourcompany.personnel.controller.EmployeeServlet</servlet-class> </servlet> <servlet-mapping> <servlet-name>EmployeeServlet</servlet-name> <url-pattern>/employee</url-pattern> </servlet-mapping> <welcome-file-list> <welcome-file>index.jsp</welcome-file> </welcome-file-list> </web-app>然后,创建EmployeeServlet.java:
package 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;charset=UTF-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?action=list"); } // 其他方法:deleteEmployee, toUpdatePage, updateEmployee 实现类似... }Servlet开发核心要点:
- 请求分发:通过
action参数来区分不同业务操作,这是小型项目常用的简单策略。 - 中文乱码:必须处理。
request.setCharacterEncoding("UTF-8")解决POST请求体乱码;response.setContentType("text/html;charset=UTF-8")设置响应编码。GET请求的乱码需要在Tomcat的server.xml中配置URIEncoding="UTF-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代码和循环,我们引入JSTL(JSP Standard Tag Library)。它提供了一套标签,可以替代<% ... %>脚本片段,使页面更清晰。
首先,需要将JSTL的jar包(如javax.servlet.jsp.jstl-api和javax.servlet.jsp.jstl)放入WEB-INF/lib。
employeeList.jsp的核心部分:
<%@ page contentType="text/html;charset=UTF-8" language="java" %> <%@ taglib prefix="c" uri="http://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 href="employee?action=toAdd">新增员工</a> <table> <thead> <tr> <th>工号</th><th>姓名</th><th>性别</th><th>年龄</th><th>部门</th><th>职位</th><th>电话</th><th>邮箱</th><th>入职日期</th><th>操作</th> </tr> </thead> <tbody> <c:choose> <c:when test="${not empty employeeList}"> <c:forEach var="emp" 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 class="actions"> <a href="employee?action=toUpdate&id=${emp.id}">编辑</a> <a href="javascript:if(confirm('确定删除吗?')) location.href='employee?action=delete&id=${emp.id}'">删除</a> </td> </tr> </c:forEach> </c:when> <c:otherwise> <tr><td colspan="10" style="text-align:center;">暂无员工数据</td></tr> </c:otherwise> </c:choose> </tbody> </table> </body> </html>JSP与JSTL实战技巧:
- EL表达式:
${emp.name}是EL(Expression 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}" pattern="yyyy-MM-dd"/>。 - 路径问题:在JSP中写链接或表单action时,建议使用
${pageContext.request.contextPath}来获取应用上下文路径,例如href="${pageContext.request.contextPath}/employee?action=toAdd"。这样即使应用部署的上下文路径变了,链接也不会失效。
至此,一个具备基本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-code>500</error-code> <location>/error/500.jsp</location> </error-page> <error-page> <exception-type>java.lang.Exception</exception-type> <location>/error/exception.jsp</location> </error-page>5.2 分页查询:应对海量数据的必备技能
当员工数据成千上万时,一次性加载所有数据到页面是不可行的。我们需要实现分页。
思路:
- 数据库分页:使用MySQL的
LIMIT子句。SELECT * FROM employee ORDER BY id DESC LIMIT ?, ?。第一个?是起始索引((pageNo-1) * pageSize),第二个?是每页条数(pageSize)。 - 前端传递参数:列表页面需要传递
pageNo(当前页码)和pageSize(每页大小,可固定)到Servlet。 - Servlet处理:接收参数,计算起始索引,调用DAO的分页查询方法。
- DAO实现:新增一个方法
List<Employee> 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或SLF4J+Logback)来记录程序运行信息、错误和调试信息。
好处:
- 分级输出:可以控制不同环境(开发/生产)输出不同级别的日志(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)。
WAR(Web 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.bat(Windows)或bin/startup.sh(Linux/Mac)。 - 自动解压:Tomcat启动时,会自动将
webapps目录下的.war文件解压成一个同名的文件夹,并加载其中的应用。 - 访问应用:打开浏览器,访问
http://localhost:8080/你的war包名(不含.war后缀)。例如,如果你的war包叫personnel_management.war,那么访问路径就是http://localhost:8080/personnel_management。
6.3 部署过程中的常见问题与解决
- 端口冲突:如果8080端口被占用,Tomcat会启动失败。可以修改Tomcat
conf/server.xml文件中的<Connector port="8080" ...>,改为其他端口,如8081。 - 中文乱码:如果部署后页面或数据库出现乱码,检查以下几个点:
- JSP页面的
pageEncoding和contentType是否都是UTF-8。 - Servlet中是否设置了
request.setCharacterEncoding("UTF-8")和response.setCharacterEncoding("UTF-8")。 - 数据库、表、连接字符串的字符集是否都是
utf8mb4。 - Tomcat的
conf/server.xml中,<Connector>标签是否添加了URIEncoding="UTF-8"属性(针对GET请求参数乱码)。
- JSP页面的
- 找不到类/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开发的任督二脉。