ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

JavaWeb用户管理系统实战:从Servlet到JSP的MVC架构全解析

2026/8/14 4:22:19 拓冰建站 浏览量
JavaWeb用户管理系统实战:从Servlet到JSP的MVC架构全解析 1. 项目概述与核心价值最近在后台和社群里不少刚学完Java基础、准备上手Web开发的朋友都在问同一个问题有没有一个能串起所有核心知识点的实战项目既能巩固Servlet、JSP、JDBC又能提前接触点MVC分层的思想最好还能看到真实的业务逻辑流转。说实话我当年自学的时候也卡在这个阶段理论学了一堆一打开IDE就不知道从哪下手。今天要拆解的这个“JavaWeb用户管理系统”就是专门为这个阶段设计的“毕业设计级”练手项目。它麻雀虽小五脏俱全几乎涵盖了JavaWeb初级阶段你需要掌握的所有技术栈。这个系统的核心目标非常明确对用户信息进行增删改查CRUD。别小看这四个操作一个完整的Web应用无论前台多么花哨后台都离不开对数据的基本管理。通过实现它你能亲手把Servlet如何接收请求、JSP如何展示页面、JavaBean如何封装数据、JDBC如何操作数据库这一整套流程跑通。更重要的是我会带着你一行行读源码不仅告诉你怎么写更重点讲清楚为什么这么写——比如为什么要把数据库连接信息单独放到配置文件里为什么查询用户列表后要先把结果集转成List再传给JSP这些设计背后的考量才是从“会敲代码”到“懂开发”的关键跨越。项目采用经典的JSP Servlet JavaBean模式数据库选用最普遍的MySQL。这套组合是理解后续任何高级框架如Spring MVC的基石。我会假设你已经有Java基础和简单的SQL知识但即使你刚入门跟着详细的步骤和源码讲解也能一步步搭起来。接下来我们就从最核心的设计思路开始把这个项目彻底拆开揉碎讲明白。2. 系统整体设计与架构拆解2.1 技术选型与架构模式解析为什么选择JSPServletJavaBean而不是直接用Spring Boot对于初学者而言过早接触高度封装的框架就像还没学会走路就先坐上了汽车虽然快但会错过对底层HTTP协议、请求-响应生命周期、数据库连接池等核心机制的理解。JSPServletJavaBean模式通常被称为Model 1向Model 2的过渡形态是理解MVCModel-View-Controller思想的绝佳起点。在这个项目中我们清晰地划分了三层视图层View由JSP页面担任。负责数据的展示和用户交互表单的呈现。例如userList.jsp用来展示所有用户的表格addUser.jsp是添加用户的表单页。JSP中应尽量避免写入大量的Java代码Scriptlet我们通过JSTL标签和EL表达式来简化展示逻辑。控制层Controller由Servlet担任。它是系统的“交通枢纽”所有来自浏览器的请求如/addUser,/deleteUser都由对应的Servlet接收。Servlet负责解析请求参数、调用业务逻辑、并将处理结果数据或跳转指令传递给视图层。例如AddUserServlet会获取表单提交的用户名、年龄等信息然后调用服务层的方法进行插入操作。模型层Model由JavaBean和数据库操作类DAO共同构成。JavaBean如User.java是一个纯粹的实体类它的属性id, name, age等与数据库表中的字段一一对应并提供了标准的getter/setter方法。DAOData Access Object数据访问对象如UserDao.java封装了所有对数据库的操作细节增删改查为Servlet提供干净的接口。这种分层的好处是职责分离。JSP只关心怎么显示Servlet只关心流程调度JavaBean和DAO只关心数据和业务逻辑。当需要修改页面样式时你几乎不用动Java代码当数据库从MySQL换成Oracle时你也只需要修改DAO层的实现。这种可维护性和可扩展性是软件工程的核心追求。2.2 数据库设计与核心表结构任何管理系统设计好数据库是第一步。我们设计一张user表结构力求简单但典型CREATE TABLE user ( id int(11) NOT NULL AUTO_INCREMENT COMMENT 用户ID主键, username varchar(50) NOT NULL COMMENT 用户名, password varchar(50) NOT NULL COMMENT 密码实际项目中应加密存储, email varchar(100) DEFAULT NULL COMMENT 电子邮箱, gender varchar(10) DEFAULT NULL COMMENT 性别, birthday date DEFAULT NULL COMMENT 出生日期, create_time datetime DEFAULT CURRENT_TIMESTAMP COMMENT 记录创建时间, PRIMARY KEY (id), UNIQUE KEY uk_username (username) COMMENT 用户名唯一 ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT用户信息表;设计思路与细节考量主键id使用自增INT类型这是最简单高效的主键策略。AUTO_INCREMENT让数据库自动生成唯一ID避免程序层处理并发带来的重复问题。字段类型与长度username和password用了varchar(50)这是一个折中的长度。对于邮箱varchar(100)更保险。gender用varchar(10)是为了兼容“男”、“女”、“未知”等中文存储。唯一索引uk_username在username上建立唯一约束这是业务逻辑的强要求——系统不允许存在两个同名的用户。数据库层面的约束是最可靠、性能最好的防重复手段。注释养成给表和字段写COMMENT的习惯。一个月后回头看或者交接给同事时这些注释价值千金。存储引擎与字符集InnoDB支持事务和外键虽然本项目未用到是MySQL的默认和推荐引擎。utf8mb4字符集能完整支持存储Emoji表情避免未来出现乱码问题。注意在实际生产环境中password字段绝不应该明文存储。这里为了简化直接存了明文。正确的做法是使用BCrypt、PBKDF2等不可逆的哈希算法加盐Salt后存储。你可以在DAO层的插入和验证逻辑中加入加密步骤这是一个非常重要的安全实践扩展点。3. 核心模块源码逐行精讲3.1 模型层ModelUser实体与UserDao数据访问对象1. User.java (JavaBean/实体类)这个类的对象就是系统中“用户”这个概念的载体。它严格遵循JavaBean规范。package com.yourdomain.usermgr.model; import java.util.Date; public class User { private Integer id; private String username; private String password; private String email; private String gender; private Date birthday; private Date createTime; // 无参构造器是JavaBean的规范要求很多框架如反射、EL表达式依赖它来创建对象。 public User() { } // 带参构造器方便测试和快速创建对象。 public User(String username, String password, String email) { this.username username; this.password password; this.email email; } // 以下是所有属性的getter和setter方法。 // IDE可以一键生成但你要理解每个方法的作用。 public Integer getId() { return id; } public void setId(Integer id) { this.id id; } public String getUsername() { return username; } public void setUsername(String username) { this.username username; } // ... 其他getter/setter省略 // 重写toString()方法方便调试时打印对象信息。 Override public String toString() { return User{ id id , username username \ , email email \ }; } }关键点Date类型对应数据库的datetime/date。createTime的命名采用了驼峰式而数据库字段是create_time这需要在JDBC操作时进行转换我们稍后在DAO层会看到如何处理。2. UserDao.java (数据访问对象)DAO层是连接Java对象和数据库表的桥梁。它封装了所有SQL语句对外提供面向对象的接口。package com.yourdomain.usermgr.dao; import com.yourdomain.usermgr.model.User; import com.yourdomain.usermgr.util.DBUtil; // 假设有一个封装好的数据库工具类 import java.sql.*; import java.util.ArrayList; import java.util.List; public class UserDao { // 1. 新增用户 public boolean addUser(User user) { Connection conn null; PreparedStatement pstmt null; String sql INSERT INTO user(username, password, email, gender, birthday) VALUES(?,?,?,?,?); try { conn DBUtil.getConnection(); // 从工具类获取连接 pstmt conn.prepareStatement(sql); // 设置参数注意序号从1开始 pstmt.setString(1, user.getUsername()); pstmt.setString(2, user.getPassword()); pstmt.setString(3, user.getEmail()); pstmt.setString(4, user.getGender()); // 处理Date类型将java.util.Date转换为java.sql.Date if (user.getBirthday() ! null) { pstmt.setDate(5, new java.sql.Date(user.getBirthday().getTime())); } else { pstmt.setNull(5, Types.DATE); } int affectedRows pstmt.executeUpdate(); // 执行插入返回影响的行数 return affectedRows 0; } catch (SQLException e) { e.printStackTrace(); // 实际项目应使用日志框架如Log4j return false; } finally { DBUtil.close(pstmt, conn); // 在finally块中确保资源被释放 } } // 2. 根据ID删除用户 public boolean deleteUserById(int id) { // 实现类似SQL: DELETE FROM user WHERE id? } // 3. 更新用户信息 public boolean updateUser(User user) { // SQL: UPDATE user SET username?, password?, email?, gender?, birthday? WHERE id? // 注意更新时要设置所有字段或者动态构造SQL更复杂。 } // 4. 根据ID查询单个用户 public User getUserById(int id) { Connection conn null; PreparedStatement pstmt null; ResultSet rs null; User user null; String sql SELECT * FROM user WHERE id?; try { conn DBUtil.getConnection(); pstmt conn.prepareStatement(sql); pstmt.setInt(1, id); rs pstmt.executeQuery(); if (rs.next()) { // 因为id是主键最多只有一条记录所以用if user resultSetToUser(rs); // 将ResultSet的一行转换为User对象 } } catch (SQLException e) { e.printStackTrace(); } finally { DBUtil.close(rs, pstmt, conn); } return user; // 没查到就返回null } // 5. 查询所有用户核心中的核心 public ListUser getAllUsers() { ListUser userList new ArrayList(); Connection conn null; PreparedStatement pstmt null; ResultSet rs null; String sql SELECT * FROM user ORDER BY id DESC; // 按ID倒序让新加的显示在前面 try { conn DBUtil.getConnection(); pstmt conn.prepareStatement(sql); rs pstmt.executeQuery(); while (rs.next()) { // 遍历结果集 User user resultSetToUser(rs); userList.add(user); } } catch (SQLException e) { e.printStackTrace(); } finally { DBUtil.close(rs, pstmt, conn); } return userList; // 即使为空也返回一个空的List而不是null避免调用方空指针。 } // 6. 将ResultSet转换为User对象的辅助方法重要 private User resultSetToUser(ResultSet rs) throws SQLException { User user new User(); user.setId(rs.getInt(id)); user.setUsername(rs.getString(username)); user.setPassword(rs.getString(password)); // 注意这里取出的密码是明文 user.setEmail(rs.getString(email)); user.setGender(rs.getString(gender)); // 处理数据库中的date/datetime类型 Date birthday rs.getDate(birthday); if (birthday ! null) { user.setBirthday(new Date(birthday.getTime())); // java.sql.Date - java.util.Date } Timestamp createTime rs.getTimestamp(create_time); // 数据库字段是下划线 if (createTime ! null) { user.setCreateTime(new Date(createTime.getTime())); } return user; } }DAO层编码核心经验永远使用PreparedStatement不要用Statement拼接SQL字符串PreparedStatement能有效防止SQL注入攻击并且数据库会对预编译的SQL有更好的执行效率。setString,setInt等方法会自动处理特殊字符。资源释放务必放在finally块Connection,Statement,ResultSet都是宝贵的数据库资源必须确保在任何情况下正常执行或发生异常都能被关闭。否则会导致连接泄漏最终拖垮数据库。查询结果集到对象的转换resultSetToUser这个方法体现了ORM对象关系映射的雏形。手动映射虽然繁琐但让你深刻理解数据和对象的关系。这也是MyBatis等框架帮你做的事情。返回空集合而非nullgetAllUsers()方法返回一个空的ArrayList而不是null。这是《Effective Java》中提倡的最佳实践可以避免调用方进行繁琐的null检查减少NullPointerException。3.2 控制层ControllerServlet的请求分发与处理Servlet是MVC中的C它像路由器一样工作。我们以处理用户列表请求的Servlet为例。package com.yourdomain.usermgr.servlet; import com.yourdomain.usermgr.dao.UserDao; import com.yourdomain.usermgr.model.User; 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.util.List; WebServlet(/user/list) // 使用注解配置Servlet的访问路径替代web.xml配置 public class UserListServlet extends HttpServlet { private UserDao userDao new UserDao(); // 持有DAO实例 Override protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { // 1. 处理业务逻辑调用DAO获取所有用户数据 ListUser userList userDao.getAllUsers(); // 2. 将数据存入请求域request scope传递给JSP request.setAttribute(userList, userList); // 3. 请求转发到展示页面 request.getRequestDispatcher(/userList.jsp).forward(request, response); } // 通常列表页是GET请求所以我们只重写doGet方法。 // 如果是处理表单提交如添加用户则需要重写doPost方法。 }Servlet编程关键点解析注解WebServlet这是Servlet 3.0之后的标准配置方式比在web.xml中配置servlet和servlet-mapping更简洁直观。/user/list是这个Servlet的URL映射。请求方法doGet/doPost根据HTTP请求方法GET用于获取数据POST用于提交数据来重写对应的方法。这是RESTful风格的雏形。请求域setAttributeServlet和JSP之间传递数据的主要方式。将userList对象以键值对的形式存放在HttpServletRequest对象中转发到的JSP页面可以通过EL表达式${userList}直接获取。请求转发forward服务器端跳转地址栏URL不变仍是/user/list但内容由userList.jsp生成。与之相对的是重定向sendRedirect它是客户端跳转地址栏会变且request域中的数据会丢失。记住一个原则需要传递数据给下一个资源时用转发其他情况如提交后跳回列表页防止重复提交常用重定向。3.3 视图层ViewJSP页面与JSTL/EL表达式JSP页面负责渲染。一个良好的JSP应该尽可能少地出现Java代码块% ... %。我们使用JSTLJSP Standard Tag Library和ELExpression Language来让页面更清晰。userList.jsp (用户列表页)% page contentTypetext/html;charsetUTF-8 languagejava % % taglib prefixc urihttp://java.sun.com/jsp/jstl/core % !DOCTYPE html html head title用户管理系统 - 用户列表/title link relstylesheet hrefhttps://cdn.jsdelivr.net/npm/bootstrap4.6.0/dist/css/bootstrap.min.css style .container { margin-top: 20px; } .table th, .table td { vertical-align: middle; } /style /head body div classcontainer h2 classmb-4用户列表 a hrefaddUser.jsp classbtn btn-success btn-sm float-right添加新用户/a /h2 c:if test${not empty message} div classalert alert-info${message}/div /c:if table classtable table-bordered table-hover thead classthead-light tr thID/th th用户名/th th邮箱/th th性别/th th出生日期/th th注册时间/th th操作/th /tr /thead tbody %-- 使用JSTL的c:forEach遍历Servlet传过来的userList --% c:forEach items${userList} varuser varStatusstatus tr td${user.id}/td td${user.username}/td td${user.email}/td td c:choose c:when test${user.gender M}男/c:when c:when test${user.gender F}女/c:when c:otherwise未知/c:otherwise /c:choose /td td${user.birthday}/td %-- EL表达式会自动调用Date的toString() --% td${user.createTime}/td td a hrefeditUser.jsp?id${user.id} classbtn btn-warning btn-sm编辑/a a hrefjavascript:if(confirm(确定删除用户【${user.username}】吗)) location.hrefdelete?id${user.id} classbtn btn-danger btn-sm删除/a /td /tr /c:forEach %-- 如果列表为空 --% c:if test${empty userList} tr td colspan7 classtext-center text-muted暂无用户数据/td /tr /c:if /tbody /table /div script srchttps://code.jquery.com/jquery-3.6.0.min.js/script script srchttps://cdn.jsdelivr.net/npm/bootstrap4.6.0/dist/js/bootstrap.bundle.min.js/script /body /htmlJSP页面核心技术点% taglib %指令引入JSTL核心标签库。prefixc表示在页面中使用c:作为前缀来调用这些标签。EL表达式${}这是页面获取数据的灵魂。${userList}会自动从pageContext,request,session,application四个作用域中查找名为userList的属性。它比传统的% request.getAttribute(userList) %简洁安全得多避免了Scriptlet和空指针。JSTL标签c:if条件判断。c:forEach循环遍历集合var定义循环变量items指定要遍历的集合。c:choose/c:when/c:otherwise多分支选择相当于Java的switch-case。前端框架Bootstrap引入了Bootstrap的CSS和JS用极少的代码就能做出美观、响应式的表格和按钮。这对于后端开发者快速搭建可用的管理界面非常友好。删除操作的确认使用javascript:confirm()在真正发起删除请求前给用户一次确认机会这是一个良好的用户体验设计。4. 项目环境搭建与详细配置实操4.1 开发环境与工具链准备工欲善其事必先利其器。一个顺手的开发环境能极大提升效率。JDK确保安装JDK 8或以上版本。在命令行输入java -version和javac -version验证。IDEIntelliJ IDEA Ultimate社区版对JavaWeb支持有限或Eclipse for Enterprise Java Developers。IDEA在智能提示、代码分析和部署方面体验更佳。我以IDEA为例。应用服务器Apache Tomcat 9.x。这是最流行的Servlet容器轻量、稳定、文档齐全。从官网下载ZIP包解压即可。数据库MySQL 5.7或8.0。同时需要一个图形化管理工具如MySQL Workbench、Navicat或DBeaver用于执行建表SQL和查看数据。4.2 在IDEA中创建并配置Web项目这是从零开始的关键一步很多新手在这里卡住。新建项目打开IDEA选择File - New - Project。选择项目类型左侧选择Java Enterprise右侧确保Web Application被勾选并选择正确的Java EE version如Java EE 7或8。点击Next。配置Tomcat在下一步点击Application Server旁边的New...选择你解压的Tomcat目录。IDEA会自动识别。完成创建输入项目名如UserManagementSystem选择项目位置点击Finish。项目创建后你会看到标准的目录结构UserManagementSystem ├── src │ └── (你的Java包和类放在这里如 com.yourdomain.usermgr) ├── web │ ├── WEB-INF │ │ ├── web.xml (部署描述符Servlet 3.0后可选) │ │ └── lib (存放项目依赖的Jar包如数据库驱动) │ └── index.jsp (欢迎页面) └── External Libraries (IDEA管理的库)添加依赖Jar包这是最容易出错的地方。你需要手动下载以下Jar包并放入web/WEB-INF/lib/目录下mysql-connector-java-8.0.xx.jarMySQL数据库驱动。jstl-1.2.jar和taglibs-standard-impl-1.2.5.jarJSTL标签库的实现。可选druid-1.2.8.jar如果你打算使用阿里巴巴的Druid连接池。关键操作在IDEA中选中这些Jar包右键选择Add as Library...将它们添加到项目的类路径中。否则代码编译会报错“找不到类”。4.3 数据库连接工具类DBUtil实现直接在每个DAO方法里写连接代码会导致大量重复且资源管理混乱。我们需要一个工具类来集中管理数据库连接。package com.yourdomain.usermgr.util; import java.io.IOException; import java.io.InputStream; import java.sql.*; import java.util.Properties; public class DBUtil { private static String url; private static String user; private static String password; private static String driver; // 静态代码块在类加载时执行一次用于读取配置文件并注册驱动 static { try { // 1. 读取配置文件 db.properties Properties props new Properties(); // 使用类加载器读取资源文件确保无论在IDE还是服务器上都能找到 InputStream in DBUtil.class.getClassLoader().getResourceAsStream(db.properties); props.load(in); // 2. 获取配置信息 url props.getProperty(jdbc.url); user props.getProperty(jdbc.username); password props.getProperty(jdbc.password); driver props.getProperty(jdbc.driver); // 3. 注册驱动 (对于MySQL Connector/J 6.0这步可以省略但写上更稳妥) Class.forName(driver); } catch (IOException | ClassNotFoundException e) { e.printStackTrace(); throw new ExceptionInInitializerError(数据库配置初始化失败); } } // 获取数据库连接 public static Connection getConnection() throws SQLException { return DriverManager.getConnection(url, user, password); } // 重载的关闭方法用于关闭Connection和Statement public static void close(Statement stmt, Connection conn) { close(null, stmt, conn); } // 通用的资源关闭方法注意关闭顺序ResultSet - Statement - Connection public static void close(ResultSet rs, Statement stmt, Connection conn) { if (rs ! null) { try { rs.close(); } catch (SQLException e) { e.printStackTrace(); } } if (stmt ! null) { try { stmt.close(); } catch (SQLException e) { e.printStackTrace(); } } if (conn ! null) { try { conn.close(); } catch (SQLException e) { e.printStackTrace(); } } } }对应的配置文件src/db.properties# 数据库连接配置 jdbc.drivercom.mysql.cj.jdbc.Driver jdbc.urljdbc:mysql://localhost:3306/user_db?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/ShanghaiuseSSLfalse jdbc.usernameroot jdbc.passwordyour_password工具类设计精讲为什么用静态代码块数据库连接参数只需要在程序启动时加载一次放在静态代码块中最合适。为什么用Properties文件将配置与代码分离。当数据库地址、密码变更时无需重新编译Java代码只需修改配置文件。这在部署到不同环境开发、测试、生产时至关重要。Class.forName(driver)的作用早期JDBC规范要求显式加载驱动类。对于MySQL Connector/J 6.0以上版本由于SPI机制这行代码可以省略。但保留它是个好习惯能明确指明所使用的驱动且兼容旧版本。关闭资源的顺序必须按照ResultSet - Statement - Connection的逆序关闭。Connection是最底层的资源如果先关闭它由它产生的Statement和ResultSet可能无法正常关闭。异常处理关闭资源时发生的SQLException通常不重要比如连接早已超时断开所以我们只打印日志不向上抛出避免掩盖主逻辑的异常。5. 功能扩展与高级特性实现一个基础CRUD做完后可以尝试加入以下特性让项目更接近实用。5.1 用户登录与会话管理Session管理系统必须有权限控制。我们增加一个登录功能。创建登录Servlet (LoginServlet)WebServlet(/login) public class LoginServlet extends HttpServlet { private UserDao userDao new UserDao(); Override protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { String username req.getParameter(username); String password req.getParameter(password); // 这里应该验证密码实际是验证密码哈希值 User user userDao.findUserByUsernameAndPassword(username, password); if (user ! null) { // 登录成功将用户信息存入Session HttpSession session req.getSession(); session.setAttribute(loginUser, user); // 跳转到主页或用户列表页 resp.sendRedirect(req.getContextPath() /user/list); } else { // 登录失败返回错误信息 req.setAttribute(errorMsg, 用户名或密码错误); req.getRequestDispatcher(/login.jsp).forward(req, resp); } } }创建登录页面 (login.jsp)一个简单的表单提交到/login。添加过滤器 (AuthFilter)保护所有需要登录才能访问的页面如/user/*。WebFilter(/*) // 过滤所有请求 public class AuthFilter implements Filter { Override public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { HttpServletRequest req (HttpServletRequest) request; HttpServletResponse resp (HttpServletResponse) response; String uri req.getRequestURI(); // 放行登录相关、静态资源等请求 if (uri.endsWith(login.jsp) || uri.endsWith(login) || uri.contains(/css/) || uri.contains(/js/)) { chain.doFilter(request, response); return; } // 检查Session中是否有登录用户 HttpSession session req.getSession(false); // 不创建新session if (session null || session.getAttribute(loginUser) null) { // 未登录重定向到登录页 resp.sendRedirect(req.getContextPath() /login.jsp); return; } // 已登录放行 chain.doFilter(request, response); } }过滤器原理它像一道安检门在所有Servlet处理请求之前和之后执行。我们在doFilter方法中判断用户是否登录未登录则拦截并跳转。5.2 密码加密存储明文存储密码是严重的安全漏洞。使用BCrypt算法是当前的最佳实践。添加依赖下载bcrypt-xxx.jar并放入lib目录。创建密码工具类import org.mindrot.jbcrypt.BCrypt; public class PasswordUtil { // 生成加密后的密码 public static String hashPassword(String plainPassword) { return BCrypt.hashpw(plainPassword, BCrypt.gensalt()); } // 验证密码 public static boolean checkPassword(String plainPassword, String hashedPassword) { return BCrypt.checkpw(plainPassword, hashedPassword); } }修改DAO层在addUser方法中调用PasswordUtil.hashPassword(user.getPassword())得到密文再存入数据库。在登录验证的findUserByUsernameAndPassword方法中先根据用户名查出用户包含密文密码然后调用PasswordUtil.checkPassword(用户输入的明文, 数据库中的密文)进行验证。5.3 分页查询功能当用户数据成千上万时一次性查询所有数据是不可接受的。必须实现分页。修改DAO接口增加一个方法ListUser getUsersByPage(int pageNo, int pageSize)。修改SQL使用LIMIT子句。SELECT * FROM user ORDER BY id DESC LIMIT ?, ?。第一个?是偏移量(pageNo-1) * pageSize第二个?是pageSize。修改Servlet接收前端传递的page参数默认为1调用DAO的分页方法。修改JSP在页面底部生成分页导航条计算总页数并生成类似[上一页] 1 2 3 ... [下一页]的链接每个链接都指向自己并带上page参数。6. 项目部署、调试与常见问题排查6.1 将项目打包成WAR并部署到独立Tomcat在IDEA中开发调试很方便但最终项目需要部署到独立的服务器上。打包在IDEA中点击Build - Build Artifacts...选择你的项目然后Build。这会在out/artifacts/目录下生成一个YourProjectName.war文件。部署将war文件复制到独立Tomcat的webapps/目录下。启动运行Tomcat的bin/startup.bat(Windows) 或bin/startup.sh(Linux/Mac)。访问打开浏览器访问http://localhost:8080/YourProjectName/。6.2 开发与调试中的高频问题及解决方案以下是我在带新人过程中他们最容易踩的坑问题现象可能原因排查步骤与解决方案404错误页面找不到1. URL路径写错。2. 项目未成功部署。3.web.xml或WebServlet配置错误。1. 检查浏览器地址栏URL是否包含项目上下文路径Context Path。2. 查看Tomcat日志logs/catalina.out确认项目是否启动成功。3. 检查Servlet的注解路径或web.xml映射是否正确。500错误服务器内部错误1. Java代码运行时异常空指针、类找不到等。2. JSP编译错误。1.这是最重要的线索来源立即查看Tomcat日志logs/localhost.yyyy-MM-dd.log或控制台输出的完整异常堆栈信息。2. 根据堆栈信息定位到具体的Java文件和行号逐行检查。数据库连接失败1.db.properties文件未找到或路径错误。2. 数据库服务未启动。3. 连接参数IP、端口、用户名、密码错误。4. 驱动Jar包未正确添加到类路径。1. 确认db.properties文件在src目录下且编译后会在WEB-INF/classes中。2. 用命令行或图形工具测试是否能连上MySQL。3. 检查DBUtil静态代码块中的日志看配置是否加载成功。4. 确认mysql-connector-java.jar在WEB-INF/lib下且被Add as Library。JSTL标签或EL表达式不生效1. 未引入JSTL的Jar包。2. Jar包版本冲突或未添加到类路径。3. 页面未声明taglib指令。1. 确保jstl.jar和standard.jar或对应的实现包在lib目录下。2. 检查IDEA的Project Structure - Modules - Dependencies确认这些Jar包被包含。3. 检查JSP文件头部的% taglib %指令的URI是否正确。中文乱码1. 数据库、连接、页面三处字符集不统一。2. Servlet未设置请求/响应编码。终极解决方案四步曲1.数据库/表创建时指定CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci。2.连接字符串在JDBC URL中加入?characterEncodingutf8useUnicodetrue。3.Servlet在doGet/doPost方法第一行加上request.setCharacterEncoding(UTF-8);和response.setContentType(text/html;charsetUTF-8);。4.JSP页面在文件顶部加上% page pageEncodingUTF-8 contentTypetext/html; charsetUTF-8%。删除或更新后页面数据没变化1. 数据库操作成功但页面缓存了旧数据。2. Servlet操作成功后跳转方式错误应使用重定向而非转发。1. 在浏览器中按CtrlF5强制刷新。2.重要原则对于POST请求如添加、删除、更新处理成功后应该使用response.sendRedirect(list)重定向到列表页。这能防止用户刷新页面时重复提交表单Post/Redirect/Get模式。6.3 性能与安全考量进阶当这个项目跑通后你可以思考如何让它变得更好数据库连接池现在的DBUtil每次操作都新建连接性能极差。应引入Druid或HikariCP连接池在应用启动时创建一批连接用时取出用完归还。日志框架将代码中的System.out.println和e.printStackTrace()替换为Log4j2或SLF4J可以灵活控制日志级别和输出目的地。输入验证在Servlet接收参数后必须进行验证非空、长度、格式等防止无效或恶意数据进入系统。这既是健壮性要求也是安全防线如防止XSS脚本注入。异常统一处理可以编写一个全局的异常处理器实现Servlet的ErrorPage配置或使用过滤器将未捕获的异常转化为友好的错误页面而不是暴露堆栈信息给用户。前端验证在JSP中使用JavaScript进行初步验证如必填项可以提高用户体验但切记前端验证不可靠后端必须进行二次验证。这个“用户管理系统”项目就像一副完整的骨架。你现在已经学会了如何把每一块骨头技术点拼接起来。接下来要做的就是在这个骨架上增加肌肉和皮肤更多的业务功能、更好的UI、更优的架构。当你能够不参考任何教程独立从头到尾实现它并且能清晰解释每一行代码的作用时你的JavaWeb入门阶段才算真正圆满毕业了。