news 2026/10/11 10:56:00

从零搭建JavaWeb登录注册项目:Servlet+JSP+JDBC实战

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
从零搭建JavaWeb登录注册项目:Servlet+JSP+JDBC实战

简介:面向Java Web初学者与在校学生的完整入门项目,整合用户登录、注册、信息修改与删除等常见模块,覆盖从表单到后台的完整请求链路,帮助理解Servlet、JSP、DAO分层与JDBC数据库操作流程。压缩包共89个文件,包含14个Java源文件、14个编译后的class文件、8个JSP页面、8个CSS样式与8个JS脚本,以及JAR依赖库、XML配置、图片字体等资源,整体仅4.67MB,结构清晰便于对照学习。项目源码按control、bean、dao、service、util等包划分,控制层负责请求分发,业务层封装校验逻辑,数据访问层使用JDBC连接数据库,并配有Bootstrap前端页面,可直接在Eclipse中导入运行。已有6302人浏览学习,适合快速搭建本地Java Web环境并完成增删改查实践。通过各层职责划分清晰的源码,读者可以掌握用户表的增删改查实现、注册登录验证逻辑、数据库连接封装方法,以及Bootstrap前端页面的基础用法,是巩固Java Web基本功的实用参考资料。

1. 一个最简单的 JavaWeb 项目,为什么值得你亲手做一遍

很多人觉得用 Spring Boot 几分钟就能生成一个带登录注册的后端工程,为什么还要回头写 Servlet + JSP + JDBC 这种“老古董”?但恰恰是在这类最简单的 JavaWeb 项目里,你才会真正搞懂 HTTP 请求是怎么被接住的、Session 是怎么维持登录状态的、一个表单提交的数据又是怎么一路落到数据库里的。这些底层的来龙去脉,被 Spring Boot 的注解和自动配置层层包裹之后,你很难再有机会亲眼看见。

这个项目讲的就是一回事:不依赖任何重量级框架,用传统的 JavaWeb 技术栈实现登录、注册、用户信息的修改和删除。它能解决你的实际诉求——如果你正在做课程设计、准备实习面试、或者刚学完 Java 基础想找一个小而完整的练手项目,这类项目是最合适的起步点。做完之后你会得到一套自己动手搭建的 MVC 分层代码,而不是一个自动生成的脚手架。

整套东西跑起来只需要三样:一个 JDK、一个 Tomcat、一个 MySQL。下文会按照“技术选型 → 数据库与登录 → 增删改查 → 踩坑记录 → 还能怎么升级”的顺序,逐步拆给你看。每个步骤我都会给出可以直接抄的代码和参数说明,你不用再去翻那些讲得云里雾里的教程。

2. 动手之前:技术选型与项目骨架,为什么还是 Servlet + JSP 这条路

2.1 三个方案摆在一起,你为什么该选传统 Servlet 栈

现在做一个 JavaWeb 项目,你面前通常有三条路:最传统的 Servlet + JSP + JDBC;稍重一点的 SSM(Spring + Spring MVC + MyBatis);以及目前最主流的 Spring Boot。大多数教程会直接把你推向 Spring Boot,但如果你是一个需要把登录注册 CRUD 真正讲清楚的初学者,直接上 Spring Boot 反而会让排查问题变得困难。

举个例子,在 Spring Boot 里你写一个@RequestMapping("/login")注解,框架就帮你把请求分发到对应方法了。但注解背后是谁解析了 URL?谁调用了你的方法?返回值又是如何被渲染成页面的?这些过程对你来说是个黑匣子。而在传统的 Servlet 方案里,你要自己写web.xml或使用@WebServlet注解去声明一个 Servlet 的访问路径,自己从HttpServletRequest里取参数,自己forward到 JSP 页面。每一步都是显式的,跑通了,你对 JavaWeb 的请求处理链路就有了肌肉记忆。

还有一个很现实的原因:很多高校的课程设计和实验环境明确要求使用 JSP + Servlet + JDBC 完成,不允许使用框架。即便没有这个限制,先做一遍传统的 JavaWeb 项目,再回头学 Spring Boot,你对DispatcherServlet、ModelAndView这些概念的理解会轻松很多。所以在这个项目里,我们选型的原则是:Servlet 负责接收请求,Service 层负责业务逻辑,DAO 层负责和数据库打交道,JSP 负责页面展示。不引入任何框架,不引入 Maven 以外的额外依赖。

2.2 目录结构:一个让新手少走弯路的包划分方式

很多人的第一个 JavaWeb 项目翻车,不是因为代码写不出来,而是项目结构一团乱:Servlet 里直接写 JDBC 代码,JSP 里嵌了一大段 Java 脚本片段,最后改一个需求要动七八个文件。与其等到那时候后悔,不如在一开始就把分层结构定好。我常用的包名划分方式如下:

src ├── main │ ├── java │ │ └── com.example.demo │ │ ├── dao // 数据库访问层,只写 SQL 和 ResultSet 处理 │ │ ├── entity // 实体类,对应数据库表结构 │ │ ├── service // 业务逻辑层,如密码校验、用户状态判断 │ │ ├── servlet // 控制层,接收请求、调用服务、转发页面 │ │ └── util // 工具类,如 JDBC 连接、MD5 加密 │ ├── resources │ │ └── db.properties // 数据库连接配置文件 │ └── webapp │ ├── WEB-INF │ │ └── web.xml // Servlet 映射配置(3.0+ 可用注解替代) │ ├── css // 静态资源 │ ├── js │ └── jsp // 存放登录、注册、用户列表等页面

这里的核心原则是:JSP 里不写 Java 业务代码,Servlet 里不写 SQL,DAO 层不处理业务判断。每一层只做自己职责范围内的事,出问题的时候顺着“页面 → 控制层 → 业务层 → 数据层”一条链查下去,很快就能定位。

依赖方面,如果你的项目使用 Maven 管理,只需要在pom.xml里声明javax.servlet-api(或jakarta.servlet-api,取决于你的 Tomcat 版本)和 MySQL 驱动即可。不推荐引入任何 ORM 框架,目的就是让你先亲手体会一次PreparedStatement和ResultSet的用法,这也是面试中经常被问到的点。

3. 从数据库到登录注册:把一条用户数据的完整旅程打通

3.1 用户表设计:不要只建三个字段,这些细节决定后续好不好改

登录注册功能的第一步不是写代码,而是设计表结构。很多人图省事,直接CREATE TABLE user (id INT, username VARCHAR(20), password VARCHAR(20)),等做到修改密码、忘记密码、用户状态禁用功能时才发现字段不够用,又回头改表,连带改实体类、改 DAO、改 JSP,非常折腾。

我一般会在一开始就把基础字段设计得相对完整,但不刻意堆砌。下面这份建表脚本足够支撑登录、注册、修改、删除这四个核心功能,同时也给后续扩展留了余地:

CREATE DATABASE IF NOT EXISTS javaweb_demo DEFAULT CHARACTER SET utf8mb4; USE javaweb_demo; CREATE TABLE t_user ( id INT PRIMARY KEY AUTO_INCREMENT COMMENT '用户ID', username VARCHAR(50) NOT NULL UNIQUE COMMENT '用户名,唯一索引', password CHAR(32) NOT NULL COMMENT '密码,MD5加密后的32位字符串', nickname VARCHAR(50) DEFAULT NULL COMMENT '昵称,可为空', email VARCHAR(100) DEFAULT NULL COMMENT '邮箱', status TINYINT DEFAULT 1 COMMENT '状态:1正常,0禁用', create_time DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间' ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户表';

有几个设计上的考虑需要说明。密码字段使用CHAR(32)而不是VARCHAR(20),是因为我们不打算明文存储密码,而是统一用 MD5 加密成 32 位十六进制字符串。username加唯一索引,是防止注册时出现重名用户,从数据库层面做了兜底校验。status字段做一个软删除和账号禁用的预留,这样后面做“删除用户”功能时,可以选择物理删除,也可以选择把status置为 0,灵活度更高。create_time和update_time加上默认值,省去应用层手动维护时间的麻烦。

3.2 手写 JDBC 连接工具类:把连接池的兼容性问题提前解决

传统 JavaWeb 项目里最常见的 JDBC 写法是在每个 DAO 方法里DriverManager.getConnection(),用完之后关闭。这种写法在小项目里没有问题,但有一个隐患:每次请求都会创建新的数据库连接,在高并发场景下性能不好。引入连接池是更专业的做法,但初学者直接上手连接池又容易遇到版本兼容问题。

这里我提供一个偏稳妥的做法:使用 C3P0 连接池配合配置文件管理数据库连接。版本选择上,c3p0 0.9.5.5 和 mysql-connector-java 5.1.49 的组合是我验证过比较稳定的,不会出现高版本 MySQL 驱动在旧连接池上认证方式不兼容的问题。

package com.example.util; import com.mchange.v2.c3p0.ComboPooledDataSource; import java.sql.Connection; import java.sql.SQLException; public class JdbcUtil { // c3p0-config.xml 中配置了数据库连接信息 private static ComboPooledDataSource dataSource = new ComboPooledDataSource(); /** * 从连接池获取一个数据库连接 */ public static Connection getConnection() throws SQLException { return dataSource.getConnection(); } /** * 关闭资源:注意关闭顺序,ResultSet -> Statement -> Connection */ public static void close(AutoCloseable... resources) { for (AutoCloseable resource : resources) { if (resource != null) { try { resource.close(); } catch (Exception e) { e.printStackTrace(); } } } } }

代码逻辑很直观:ComboPooledDataSource在类加载时初始化,从c3p0-config.xml读取数据库地址、用户名和密码。getConnection()每次从连接池借出一个连接,close()方法接收不定长参数,按ResultSet → Statement → Connection的逆序关闭。

参数说明:连接池的核心配置在c3p0-config.xml里,重点关注这几个参数——initialPoolSize表示启动时创建的连接数,一般设 5 左右;maxPoolSize是连接池上限,太大容易耗尽数据库资源;checkoutTimeout是获取连接的超时时间,单位毫秒,连接池满了之后等待多久会抛出异常。

<c3p0-config> <default-config> <property name="jdbcUrl">jdbc:mysql://localhost:3306/javaweb_demo?useSSL=false&amp;characterEncoding=utf8</property> <property name="user">root</property> <property name="password">123456</property> <property name="driverClass">com.mysql.jdbc.Driver</property> <property name="initialPoolSize">5</property> <property name="maxPoolSize">20</property> <property name="checkoutTimeout">3000</property> </default-config> </c3p0-config>

配置中的useSSL=false参数值得留意,MySQL 8.0 版本默认开启 SSL 认证,如果不显式关闭,控制台会刷出一大堆 warning 日志,虽然不影响运行,但非常干扰观察错误信息。characterEncoding=utf8必须加,否则插入中文数据会变成乱码,这是新手踩得最多的坑之一。

3.3 注册功能实现:JDBC 防注入写法与用户名重复校验

注册功能的核心逻辑是:接收表单参数 → 校验用户名是否已存在 → 密码加密 → 插入数据库。这里有两个不能妥协的细节——使用PreparedStatement而不是拼接 SQL,以及密码不能明文入库。

package com.example.dao; import com.example.entity.User; import com.example.util.JdbcUtil; import java.sql.Connection; import java.sql.PreparedStatement; import java.sql.ResultSet; public class UserDao { /** * 根据用户名查询用户,用于注册时的重复校验和登录时的用户查询 */ public User findByUsername(String username) { String sql = "SELECT id, username, password, nickname, email, status FROM t_user WHERE username = ?"; try (Connection conn = JdbcUtil.getConnection(); PreparedStatement ps = conn.prepareStatement(sql)) { ps.setString(1, username); try (ResultSet rs = ps.executeQuery()) { if (rs.next()) { User user = new User(); user.setId(rs.getInt("id")); user.setUsername(rs.getString("username")); user.setPassword(rs.getString("password")); user.setNickname(rs.getString("nickname")); user.setEmail(rs.getString("email")); user.setStatus(rs.getInt("status")); return user; } } } catch (Exception e) { e.printStackTrace(); } return null; } /** * 插入新用户,返回受影响行数 */ public int insertUser(User user) { String sql = "INSERT INTO t_user (username, password, nickname, email) VALUES (?, ?, ?, ?)"; try (Connection conn = JdbcUtil.getConnection(); PreparedStatement ps = conn.prepareStatement(sql)) { ps.setString(1, user.getUsername()); ps.setString(2, user.getPassword()); ps.setString(3, user.getNickname()); ps.setString(4, user.getEmail()); return ps.executeUpdate(); } catch (Exception e) { e.printStackTrace(); return 0; } } }

代码中用到的try-with-resources语法是 Java 7 引入的,可以自动关闭连接和语句对象,不需要手动调用close(),有效避免忘记关连接导致的内存泄漏问题。PreparedStatement的?占位符配合setString()赋值,是防止 SQL 注入的标准写法——绝对不能写成"SELECT ... WHERE username = '" + username + "'",否则用户名传入' OR '1'='1这类值时会直接绕过校验。

注册的 Service 层逻辑中,先调用findByUsername判断用户是否已存在,如果已存在则返回一个错误提示,不存在则对密码做 MD5 加密后再调用insertUser。MD5 加密可以放在一个独立的工具类里,避免在 Servlet 中直接写加密逻辑。需要说明的是,MD5 并非最安全的加密算法,但在这个入门级的项目里,它的价值在于让你理解哈希的概念——存储的是密文,即使数据库泄露,原始密码也不会直接暴露。

4. 登录会话与增删改查:把用户管理的 CRUD 完整落地

4.1 登录功能与 Session 会话管理:拦截器该拦哪些路径

登录功能的核心不在于验证用户名密码,而在于登录成功之后如何让系统认识这个用户。Servlet 规范提供的方案是HttpSession:用户登录成功后,把用户信息放进 Session,之后的请求里,只要 Session 还在,就认为用户已经登录。

package com.example.servlet; import com.example.entity.User; import com.example.service.UserService; 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 javax.servlet.http.HttpSession; import java.io.IOException; @WebServlet("/login") public class LoginServlet extends HttpServlet { private UserService userService = new UserService(); @Override protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { request.setCharacterEncoding("UTF-8"); String username = request.getParameter("username"); String password = request.getParameter("password"); User user = userService.login(username, password); if (user != null) { HttpSession session = request.getSession(); session.setAttribute("loginUser", user); // 重定向到用户列表页,避免表单重复提交 response.sendRedirect(request.getContextPath() + "/user/list"); } else { request.setAttribute("errorMsg", "用户名或密码错误"); request.getRequestDispatcher("/jsp/login.jsp").forward(request, response); } } }

这段代码里包含三个值得细看的点。第一,doPost方法开头设置request.setCharacterEncoding("UTF-8"),必须写在取参数之前,否则 POST 请求中的中文参数会出现乱码。第二,登录成功使用sendRedirect而不是forward,这是防止 F5 刷新导致重复提交的关键做法,因为重定向是新的请求,浏览器不会重复执行上一次的 POST 提交。第三,Session 中存的键名loginUser要统一,后面所有需要登录才能访问的页面都从 Session 中取这个键判断。

有了登录状态,就还需要一个控制访问权限的过滤器。常见的做法是创建一个LoginFilter,在doFilter里判断请求路径是否涉及登录相关接口,如果不是并且 Session 中没有loginUser,就跳回登录页。过滤器的拦截路径需要仔细设计:/login和/register必须放行,静态资源如 CSS、图片也需要放行,否则登录页会因为没有样式而非常难看。

@WebFilter("/*") public class LoginFilter implements Filter { @Override public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { HttpServletRequest req = (HttpServletRequest) request; HttpServletResponse resp = (HttpServletResponse) response; String path = req.getRequestURI().substring(req.getContextPath().length()); // 无需登录即可访问的路径 if (path.startsWith("/login") || path.startsWith("/register") || path.startsWith("/css") || path.startsWith("/js")) { chain.doFilter(request, response); return; } HttpSession session = req.getSession(false); if (session != null && session.getAttribute("loginUser") != null) { chain.doFilter(request, response); } else { // 未登录,重定向到登录页 resp.sendRedirect(req.getContextPath() + "/jsp/login.jsp"); } } }

值得注意的是req.getSession(false)和req.getSession()的区别:带false参数时,如果当前请求没有关联的 Session,会返回null而不是新建一个。在过滤器里用false是合理的,因为如果一个未登录用户只是访问首页,没必要为他浪费服务器内存维护一个空 Session。

4.2 用户列表展示与分页查询:不要SELECT *一次全查出来

用户列表是增删改查里最容易被轻视的部分。很多入门教程直接SELECT * FROM t_user然后遍历展示全部数据,这在数据量只有几条时毫无问题,一旦用户表到了几千条甚至几万条,页面渲染就会变得卡顿。分页查询是任何管理系统都绕不开的需求,最标准的实现方式是LIMIT加偏移量。

public List<User> findPage(int pageNum, int pageSize) { String sql = "SELECT id, username, nickname, email, status, create_time FROM t_user ORDER BY id DESC LIMIT ?, ?"; List<User> list = new ArrayList<>(); try (Connection conn = JdbcUtil.getConnection(); PreparedStatement ps = conn.prepareStatement(sql)) { ps.setInt(1, (pageNum - 1) * pageSize); ps.setInt(2, pageSize); try (ResultSet rs = ps.executeQuery()) { while (rs.next()) { User user = new User(); user.setId(rs.getInt("id")); user.setUsername(rs.getString("username")); user.setNickname(rs.getString("nickname")); user.setEmail(rs.getString("email")); user.setStatus(rs.getInt("status")); user.setCreateTime(rs.getTimestamp("create_time")); list.add(user); } } } catch (Exception e) { e.printStackTrace(); } return list; }

这里有一个参数细节:LIMIT的第一个参数如果直接传pageNum,那么第一页会跳过第一条数据。正确的偏移量计算方式是(pageNum - 1) * pageSize,因为第一页的偏移量应该是 0,而不是 1。我在实际指导中见过不止一次这个错误,现象是第一页少一条数据,后续每一页都会重复上一条。除了查列表,还需要写一个count()方法统计总记录数,用总记录数除以每页条数就能得到总页码数,这一步必不可少,因为 JSP 底部的页码导航需要这个值。

4.3 修改与删除操作的请求路径设计:URL 里传参的两种安全姿势

修改和删除在请求方式上有一个常见的争论:用 GET 还是 POST?从功能实现的角度,两种方式都能完成操作,但从安全角度,删除操作用 GET 会有一个风险——如果页面里有图片或者其他资源请求不小心访问到了删除 URL,就会触发一次删除操作。比较稳妥的做法是:修改用 POST,删除用 POST,或者至少把删除操作放到单独的 Servlet 里,不要直接暴露在普通链接的href中。

删除功能的实现逻辑相对简单,根据 URL 传入的 id 删除对应记录。但在实际项目中,我更推荐使用软删除,也就是把status字段从 1 改为 0,而不是执行DELETE FROM语句。软删除的好处是数据不会真正消失,误操作时还有后悔药吃,后续做数据统计时也能保留历史记录。

@WebServlet("/user/delete") public class UserDeleteServlet extends HttpServlet { private UserService userService = new UserService(); @Override protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { // 兼容表单提交和 AJAX 请求,取参数方式相同 String idStr = request.getParameter("id"); if (idStr == null || !idStr.matches("\\d+")) { response.sendError(HttpServletResponse.SC_BAD_REQUEST, "参数不合法"); return; } int id = Integer.parseInt(idStr); boolean success = userService.deleteById(id); if (success) { // 重定向回列表页,刷新数据 response.sendRedirect(request.getContextPath() + "/user/list?pageNum=1"); } else { request.setAttribute("errorMsg", "删除失败"); request.getRequestDispatcher("/jsp/error.jsp").forward(request, response); } } }

参数校验的细节写在注释里:idStr.matches("\\d+")是一个正则表达式,要求字符串只包含数字,这从根本上排除了传字母、特殊符号导致的类型转换异常。当用户通过浏览器地址栏手动访问删除接口并传入恶意参数时,这个校验是第一道防线。修改操作也是类似的流程,差别在于查询出原有数据回显到编辑页面,用户提交后执行UPDATE语句,SQL 中同样使用PreparedStatement占位符,不再赘述。

5. 避坑指南:登录注册 CRUD 项目中最常见的 6 个翻车现场

5.1 数据库连接失败:时区问题、驱动问题和中文乱码

现象:项目启动后访问任意一个需要查询数据库的接口,控制台报Cannot create PoolableConnectionFactory或Communications link failure,有时还伴随The server time zone value '�й���׼ʱ��' is unrecognized这种乱码信息。

原因:这个报错涉及三个叠加的问题,但最常见的是 MySQL 8.0 的驱动需要显式声明时区。旧版 5.x 驱动不校验时区,而 8.0 驱动默认会校验连接时区,如果连接串里没有serverTimezone=Asia/Shanghai,就会直接拒绝连接。

解决:在jdbcUrl中追加时区参数:jdbc:mysql://localhost:3306/javaweb_demo?useSSL=false&serverTimezone=Asia/Shanghai&characterEncoding=utf8。同时确认驱动版本和 MySQL 服务端版本匹配,MySQL 8.0 服务端建议使用com.mysql.cj.jdbc.Driver这个新驱动类名。

5.2 表单提交后页面 URL 不变,刷新时弹出“重新提交表单”提示

现象:登录成功后浏览器地址栏还是/login,按 F5 刷新页面,浏览器弹出确认框询问是否重新提交表单,点确定后竟然重复执行了登录操作。

原因:这是 Servlet 中使用了forward转发到目标页面的典型副作用。forward是服务器内部跳转,浏览器并不知道页面已经换了,地址栏不会更新,刷新时浏览器认为“上次请求是表单提交”,于是再次提交。

解决:登录成功后应使用response.sendRedirect()重定向到列表页,而不是request.getRequestDispatcher().forward()。sendRedirect会向浏览器返回 302 响应和一个新的地址,浏览器自动发起 GET 请求访问新地址。数据提交类的业务操作(增加、修改、删除)成功之后都应该遵循这个 PRG(Post-Redirect-Get)模式。

5.3 获取 POST 参数中文乱码,改代码也没用

现象:注册时在表单里输入“张三”,存入数据库后变成“å¼ ä¸�”或“???”,登录页的提示信息也乱码。

原因:request.setCharacterEncoding("UTF-8")没有放在取参数之前,或者 set 的编码与页面表单声明的编码不一致。还有一种隐蔽情况是 Tomcat 8 之前的版本,对 GET 请求的参数编码处理方式不同,需要额外配置URIEncoding。

解决:在 Servlet 的doPost开头第一行写request.setCharacterEncoding("UTF-8");JSP 页面顶部声明pageEncoding="UTF-8";在 Tomcat 的server.xml中给 Connector 增加URIEncoding="UTF-8"属性(Tomcat 8+ 默认已是 UTF-8,无需修改)。数据库侧的characterEncoding=utf8同样不可省略,三处缺一不可。

5.4 Session 失效判断错误:每次请求都跳回登录页

现象:登录成功后访问列表页正常,但点击任意一个操作按钮就跳到登录页,回到登录页后明明输对了用户名密码却又提示错误。

原因:最常见的原因是req.getSession()在过滤器和 Servlet 里被混用。过滤器中使用req.getSession(true)会强制创建一个新 Session,而这个新 Session 中不可能有之前登录时存入的用户信息,于是判定为未登录。另一个可能原因是session.setAttribute存进去的是一个刚查出来的新对象,而后续判断时取不到值,比如两个不同的键名。

解决:全项目统一 Session 键名常量,例如在一个公共类里定义public static final String LOGIN_USER = "loginUser"。过滤器判断时使用req.getSession(false),取不到 Session 或取不到用户信息都跳转登录页。如果需要排查,在过滤器中加一行日志输出session.getId()和session.getAttribute的值,观察每次请求是不是同一个 Session。

5.5 页面点击删除,列表中的多条数据被同时删掉

现象:用户列表每条记录都有一个删除按钮,点击某一条的删除,刷新后发现这个用户还在,反而是其他记录被删掉了。

原因:这是一个经典的 HTML 嵌套标签错误——删除按钮的onclick事件或<form>标签没有正确包裹,导致点击时事件冒泡到了相邻的行,或者所有删除按钮共用一个<form>,第一个按钮的submit事件把整个列表的隐藏域id都提交了。

解决:每一行单独使用一个<form>,且表单必须完整包裹当前行的隐藏id字段。推荐做法是删除按钮用onclick="if(confirm('确认删除?')) location.href='${pageContext.request.contextPath}/user/delete?id=${user.id}'",让每个按钮直接跳转到独立的删除地址,避免表单嵌套问题。

5.6 修改操作提交后,数据库中的create_time被清零

现象:编辑一个用户资料,只改了昵称,保存后发现这条记录的创建时间变成了当前时间或 NULL。

原因:修改操作默认执行UPDATE t_user SET username=?, nickname=?, email=?这类语句,如果没有在 SQL 中带上create_time字段,而实体类User中该字段值为null,执行 setter 时就会覆盖原值。或者使用了ON UPDATE CURRENT_TIMESTAMP,任何字段更新都会触发时间重写。

解决:修改操作的 SQL 语句要明确列出所有需要更新的字段,保持字段列表和页面表单一致;不需要更新的字段不要出现在SET子句中。另外建表时如果不需要更新时间字段,就不要加ON UPDATE CURRENT_TIMESTAMP,或者接受它的语义是“自动记录最后更新时间”。

6. 再进一步:把登录注册 CRUD 项目升级成拿得出手的作品

做完上面这套基础版本,你已经有了一个完整的、能跑通的 JavaWeb 项目。但说实话,能登录、能增删改查只是起点——如果你想拿它去面试、去答辩,还需要加几个性价比高的增强功能。从一个面试官的角度,这套登录注册 CRUD 代码能体现的能力分水岭,通常在以下三个方向上。

第一个方向是登录安全性的增强。比如给密码加盐再哈希,替代目前简单的 MD5;给登录接口增加验证码校验,防止暴力破解;用HttpSession记录登录失败次数,连续失败 5 次锁定账号 15 分钟。这些功能在代码层面改动不大,但能体现你对安全的理解。

第二个方向是用户体验的增强。注册页面把表单校验从前端搬到后端,并返回准确的中文提示;用户列表增加关键字搜索,在 DAO 中动态拼接WHERE username LIKE ?条件;编辑页面使用 AJAX 校验用户名是否已存在。AJAX 和 JSON 是现代 Web 项目的标配,在 Servlet 里返回 JSON 只需要引入一个jackson-databind依赖,序列化对象为 JSON 字符串后response.getWriter().write(json)即可。

第三个方向是工程化体验的增强。把 JSP 中的Java 脚本片段(<% %>)彻底移除,改用 JSTL 标签和 EL 表达式;引入Filter统一处理请求编码;把数据库连接信息放到 JNDI 数据源中,脱离代码配置。最后一个最有意思:你可以在项目里加一个简单的@WebListener实现的ApplicationListener,统计在线人数,展示在一个 JSP 页面上,这是一个非常出彩的小设计。

至于那个“修改密码”的功能,表面上也是 CRUD 的一种,它让你重新审视“修改”这个操作:密码字段需要单独表单,旧密码要先校验,新密码要二次确认,如果当前 Session 中的用户与操作者不一致还要校验权限。这比我前面写的“修改用户资料”更考验边界处理能力,很适合作为自我测验的练习。完成这个功能的时候,你会发现自己对 Servlet + JSP + JDBC 的控制力已经和刚开始完全不同了。

最后说一个我自己的习惯。做这类入门项目时,我总是在每完成一个功能后手动测试一遍完整链路:注册一个带中文的用户、退出登录、重新登录、修改用户信息、删除用户、查看列表数据与数据库中的记录是否一致。这些操作看似琐碎,但往往是定位隐藏 bug 最快的方式——等你亲眼见过一次“数据库里少了一条记录”和“多了一个乱码用户名”,以后再遇到类似问题就不会慌了。希望这套方案能帮你在 JavaWeb 的起步阶段少走些弯路。

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

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/11 10:55:43

【Linux】Shell 命令以及运行原理+权限管理

本文面向 Linux 初学者&#xff0c;系统讲解 Shell 命令执行原理与 Linux 权限体系两大核心主题。全文目录如下&#xff1a;Shell 基础、命令分类、运行原理、PATH 环境变量、权限管理、chmod/chown/chgrp 修改权限、umask 文件掩码、粘滞位。建议按顺序阅读&#xff0c;并结合…

作者头像 李华
网站建设 2026/10/11 10:55:43

Python卷积神经网络实现人脸表情识别:从数据集到实时摄像头

简介&#xff1a;这份资源面向计算机相关专业正在做毕业设计的学生&#xff0c;以及需要项目实战练习的学习者&#xff0c;提供了一套基于卷积神经网络的人脸表情识别系统完整方案。内容涵盖源代码、数据集、论文与训练好的模型&#xff0c;项目经导师指导并通过评审&#xff0…

作者头像 李华
网站建设 2026/10/11 10:55:40

Windows 变慢排查实战:识别软件后台小动作,清理启动项与右键菜单

地址 https://freedw.com/archives/7987 电脑装的软件一多&#xff0c;常见的不适感主要有三种&#xff1a;开机等待时间变长、右键菜单展开变慢、空闲时磁盘和 CPU 占用偏高。这三种现象往往对应不同的藏身位置&#xff0c;按位置逐项排查&#xff0c;比反复卸载重装更有效。…

作者头像 李华
网站建设 2026/10/11 10:54:59

山海鲸可视化 VS ChartCube:让复杂信息变得更加直观

随着企业数字化建设持续深入&#xff0c;可视化工具正在服务于越来越多的业务场景。园区、工厂、楼宇和城市管理需要通过三维空间呈现现场状态&#xff0c;经营分析、项目汇报和数据传播则需要借助清晰、美观的图表表达核心信息。山海鲸可视化与 ChartCube 都能够帮助用户提升信…

作者头像 李华
网站建设 2026/10/11 10:54:23

从能跑到无可挑剔:代码质量提升的六个维度与自检清单

1. 一个词引发的执念&#xff1a;为什么我要死磕“impeccable”第一次在代码评审里被人甩了一句“this is not impeccable”&#xff0c;我盯着屏幕愣了半分钟。不是生气&#xff0c;是突然意识到——我们天天把“能用”“跑通”“没报错”当终点&#xff0c;但真正拉开工程师差…

作者头像 李华
网站建设 2026/10/11 10:54:06

DeepSeek本地部署实战:基于Ollama的模型运行与WebUI集成指南

简介&#xff1a;这是一份围绕DeepSeek-R1本地部署的实战型技术文档&#xff0c;面向机器学习与AI应用开发者&#xff0c;适合已掌握基础命令行与容器概念的工程师、研究人员在Windows、macOS或Linux环境下快速搭建推理环境。资源包共1个文件&#xff0c;以docx格式呈现&#x…

作者头像 李华