JavaWeb登录注册与增删改查实战:Servlet+JSP+JDBC完整案例 简介一个基于Servlet、JSP与JDBC的Java Web入门项目完整实现了用户注册、登录、信息修改与删除等基础CRUD操作。资源面向刚接触Java Web的学生或自学者以简洁的分层代码帮助理解Web应用从页面请求到数据库处理的完整链路适合作为课程设计或课堂练习的参考源码。压缩包共89个文件约4.67MB核心包含14个Java源文件与对应class字节码、8个JSP动态页面、8个CSS和8个JS前端资源以及web.xml、项目配置文件和数据库工具类结构上按control、service、dao、util分层便于对照学习。该项目已有6300人浏览学习热度较高。下载后可直接导入Eclipse运行配合Bootstrap前端框架可快速看到效果同时能学习用户会话管理、预编译SQL防注入和分层解耦等实用技巧为后续复杂Web项目开发打下基础。 很多刚学JavaWeb的朋友都有同感照着教程把Tomcat跑起来、能弹个页面出来觉得自己已经入门了但一听到“做一个登录注册带增删改查的项目”就慌了。要么代码全堆在JSP里要么JDBC连接写得像一团乱麻要么注册完根本没法登录。这篇文章就从一个完整的案例出发带你把登录、注册、修改、删除这一整套走通。标题里写的“简单”指的是业务不复杂但不代表代码可以乱写。真正决定一个JavaWeb项目能不能跑得长久、能不能拿得出手的是分层是否清楚、数据库设计是否合理、几个连环跳转的请求路径是否理得明白。这篇文章会用一个“学生信息管理系统”作为载体把登录注册和增删改查串起来讲清楚而且适合刚学完Servlet、JSP、JDBC、MySQL基础想动手做第一个完整项目的同学直接照着敲。1. 为什么你的登录注册一查就崩先把业务骨架想清楚很多人在动手写登录注册之前脑子里的需求其实是模糊的。“查一下用户名密码对不对对了就跳页面”只是表象真正一个完整的登录注册模块背后牵扯的是会话保持、密码存储、访问控制、重复注册校验、修改删除时的身份归属这几件大事。如果代码写完了才发现登录状态没存、密码是明文、删数据不用确认你的项目基本就只能在本地自嗨交上去被问两句就露馅了。我自己见过太多初学项目长这样所有的逻辑全部写在JSP页面里里面塞了几十行Java代码用% %把数据库查询结果直接往页面上怼。这种写法有两个致命问题第一页面一刷新就重复提交表单注册记录能多出几十条第二JSP里的Java代码一旦报错控制台的信息指向的是Tomcat生成的临时Java文件你连自己的代码第几行出错都找不着。所以做这个项目第一步不是开IDEA敲代码而是先画一张请求流程图。我们做登录注册本质上是几个跳转路径的组合未登录用户访问受保护资源 → 被拦截后跳转到登录页该功能可通过一个简单的全局会话检查实现暂不引入过滤器也可以。登录页提交表单 → 查询数据库比对用户名和密码 → 成功则写入会话Session并跳转到列表页失败则回到登录页并提示错误信息。注册页提交表单 → 校验用户名是否已存在 → 写入数据库 → 跳转登录页。列表页上的每一条数据 → 点击“修改”则携带数据回显到修改页 → 提交后更新数据库 → 回到列表页。点击“删除”则执行删除操作然后刷新列表。这张图理清了你的控制层该写几个Servlet、每个Servlet接收什么参数、处理完之后往哪里跳转也就一目了然了。很多人的代码烂就烂在这一步没想清楚就动手写到后面自己都不知道哪个请求该往哪去页面跳来跳去全乱套。2. 环境选型JDK、IDEA、Tomcat、MySQL怎么搭配才省心做JavaWeb项目第一步就是折腾环境。我建议你参考2023版IDEA创建JavaWeb项目的流程来搭工程因为新版IDEA的Web工程结构跟老教程差别挺大光是一个“没有web.xml”就能劝退一堆人。下面这组搭配是我自己长期使用、给初学者推荐过很多次的组合稳定不折腾组件推荐版本选型理由JDKJDK 8 或 JDK 11JDK 8够老够稳网上资料最多JDK 11对新语法支持更好二者在Servlet开发上差别不大IDEA2023.x 及以上新版创建Web项目时自带Servlet依赖选项适合跟随最新教程操作TomcatTomcat 9.x 或 10.x9.x对应javax.servlet规范、10.x对应jakarta.servlet规范选错会一路报包名错误MySQLMySQL 5.7 或 8.08.0的驱动类名和连接URL与5.7不同教程混着看容易踩坑数据库驱动mysql-connector-java 8.0.x驱动JAR必须手动打进Tomcat的lib目录或WEB-INF/lib这里最大的坑是Tomcat版本和Servlet包名不一致。Tomcat 9及之前用的是javax.servlet.*Tomcat 10及以上换成了jakarta.servlet.*。如果你跟着一个老教程写代码代码里全是javax.servlet但你装的Tomcat是10.1.x编译直接报“程序包javax.servlet不存在”。这个报错会让很多新手误以为是IDEA配置问题其实纯粹是规范命名空间变了。解决方案是要么用Tomcat 9.x跟着老资料走要么全部用新规范改写import别混着来。还有一个容易被忽略但很重要的细节是数据库驱动的放置位置。你把mysql驱动JAR放在WEB-INF/lib目录下那它只对当前应用生效你把它扔进Tomcat的lib目录那它会全局生效。个人建议初学阶段直接放Tomcat的lib目录省得每次新建项目都要重新拷一遍JAR。等以后涉及多应用独立部署了再往项目里放。数据库连接信息的配置不要写在Java代码里硬编码。在src下建一个db.properties文件里面放数据库驱动类名、连接URL、用户名、密码然后用一个工具类去读取。这样以后换数据库环境、改密码只需要改配置文件重启不用动代码重新编译。3. 数据库设计student表和user表怎么建才扛得住改需求这个项目的业务是学生信息管理涉及登录注册所以至少需要两张表一张用户表管理员的账号密码一张学生信息表增删改查的数据载体。这两张表怎么建直接决定了后续代码好不好写、容不容易出问题。用户表我见很多新手建得特别随意用户名不长点心、密码直接一个VARCHAR(50)就完事。这里我建议一开始就养成良好的习惯CREATE TABLE t_user ( id INT NOT NULL AUTO_INCREMENT COMMENT 主键, username VARCHAR(50) NOT NULL COMMENT 登录账号, password VARCHAR(100) NOT NULL COMMENT 密码建议存哈希值, PRIMARY KEY (id), UNIQUE KEY uk_username (username) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;两个细节解释一下。第一username字段必须加唯一约束。为什么因为注册的时候新用户要跟已存在用户比对如果你在数据库层面没有唯一约束那么并发情况下两个相同用户名的请求同时进来Java代码里的校验是挡不住第二个的最终数据库里就会出现两条一样的用户名。代码层面做了“避免重名”校验之后数据库层面的唯一索引是兜底防线。同理注册页的AJAX实时校验也可以加但归根结底还是要靠数据库约束兜底这样才稳。第二密码不能明文存。我知道初学者图省事注册的时候password字段直接存用户输入的字符串登录的时候比对username和password两列是否相等。这样写代码是简单的但这是巨大的安全隐患。正确的做法至少是用MD5或SHA-256把密码算成哈希值再入库登录时再把用户输入的密码算成同样算法的哈希值去比对。更稳妥的做法是加盐Salt再哈希每个用户生成一个随机盐值把“盐值密码”拼接后算哈希这样即使是同样的密码两个用户存进去的哈希值也完全不一样大大增加破解成本。我们再看学生信息表增删改查的业务载体CREATE TABLE t_student ( id INT NOT NULL AUTO_INCREMENT, stu_no VARCHAR(20) NOT NULL COMMENT 学号, stu_name VARCHAR(50) NOT NULL COMMENT 姓名, gender CHAR(2) DEFAULT 男, age INT DEFAULT NULL, major VARCHAR(50) DEFAULT NULL COMMENT 专业, PRIMARY KEY (id), UNIQUE KEY uk_stu_no (stu_no) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;这里有一个比较隐藏的坑学号stu_no要加唯一约束。因为从业务角度来说同一个学校同一个学制下学号是唯一的。如果不加唯一约束列表页上就可能出现两条一模一样的学号后面修改删除的时候用id作为主键去定位虽然没问题但用户在列表页看到的“重复学号”会很奇怪也容易让人质疑数据规范性。至于表之间的关联关系这个项目里t_user和t_student没有外键关系是两套独立数据。如果你将来的需求变成“每个用户只能管理自己创建的学生数据”那就需要在t_student表上加一个user_id字段做外键关联那时再做文章的修改删除就要带上WHERE id? AND user_id?条件这也是很多项目权限控制的雏形思路。4. 登录注册模块会话管理与密码处理的那些坑登录注册是每个JavaWeb项目的门面也是审核人第一个会看功能的模块。这一节我把代码逻辑拆开讲清楚每一步为什么这样写以及初学最容易掉进去的坑。4.1 注册模块校验顺序决定你的数据干不干净注册的完整链路是用户填表提交 → 后端校验参数合法性 → 校验用户名是否已存在 → 密码加密 → 插入数据库 → 跳转登录页。很多人的注册Servlet写出来问题不断核心原因是校验顺序写错了。一个常见的错误是先去数据库查“用户名是否存在”如果数据库查询抛异常页面就会直接500还有一个错误是没校验密码是否为空就查重结果是空字符串密码也入库了。正确做法是先把前端传来的所有参数在Servlet里做一遍基本校验用户名非空、密码非空、两次密码一致再查数据库最后再插入。密码加密这一步我推荐用MessageDigest写一个MD5或SHA-256工具类几十行代码就够用。不过这里有个新手特别容易犯的错直接用new String(digest.digest(bytes))把字节数组转字符串这样转出来的字符串可能包含不可见字符再存进数据库和后续比对都会莫名其妙出问题。正确做法是把字节数组转成十六进制字符串比如每一个字节用Integer.toHexString补零拼接。我贴一个可以拿来即用的工具类核心方法public static String md5(String source) { try { MessageDigest md MessageDigest.getInstance(MD5); byte[] bytes md.digest(source.getBytes(StandardCharsets.UTF_8)); StringBuilder sb new StringBuilder(); for (byte b : bytes) { String hex Integer.toHexString(b 0xff); if (hex.length() 1) { sb.append(0); } sb.append(hex); } return sb.toString(); } catch (NoSuchAlgorithmException e) { throw new RuntimeException(e); } }注意那个b 0xff千万别省。Java里byte是有符号的直接toHexString转出来的结果是负数的补码形式导致你算的哈希值跟在线工具算的永远对不上。这个细节卡了我当年整整一下午卡到后来才知道是符号问题。4.2 登录模块Session不存东西那登录跟没登有什么区别登录模块最关键的一行代码是request.getSession().setAttribute(user, user)。Session的本质是你在服务器端给每个访客发了一张带编号的房卡浏览器每次请求都会带上来服务器一看编号就知道你是谁。用户登录成功后把这个用户对象塞进Session后续访问其他页面的请求中只需要从Session里取这个对象取到就说明已登录取不到就说明没登录直接踢回登录页。很多新手的登录Servlet写得像消费了一次性的电影票比对完用户名密码之后就结束了没有在Session里留下任何状态。结果登录成功后手输一个/list地址发现根本没有做登录拦截一样能直接访问列表页。这就是“登录”和“登录状态保持”被割裂了。一个最简单的登录保护方式就是在每个需要登录才能访问的Servlet里加几行代码HttpSession session request.getSession(false); if (session null || session.getAttribute(user) null) { response.sendRedirect(login.jsp); return; }getSession(false)加了个参数false含义是“如果当前请求没有关联的Session不要创建一个新的直接返回null”。为什么要这样因为如果不加这个参数request.getSession()无论有没有登录都会强制创建一个新的Session那判断就永远不成立了。这个参数是整个登录拦截逻辑里最容易出错的小细节却决定了你页面到底能不能挡住未登录用户。登录失败的提示也很讲究。新手喜欢用response.sendRedirect(login.jsp?error1)在URL后面拼参数然后在登录页用request.getParameter(error)判断。这种做法虽然能用但会让URL上带一个难看的参数而且刷新页面后错误信息还在。更好的做法是用request.setAttribute(errorMsg, 用户名或密码错误)然后通过请求转发request.getRequestDispatcher(login.jsp).forward(request, response)回到登录页。这样登录页通过${errorMsg}EL表达式就能直接取到错误信息URL不变刷新也不会重复提示。不过要注意转发和重定向这对概念也容易把人绕晕转发是服务器内部跳转且地址栏不变重定向是浏览器重新发一次请求且地址栏变化。登录成功务必用重定向sendRedirect否则列表页刷新会重复提交登录表单造成重复登录的错觉和大量无意义的Session。4.3 注册成功后跳转请求转发还是重定向注册成功之后要跳去登录页最稳妥的是response.sendRedirect(login.jsp)。用重定向的好处是地址栏会变成login.jsp用户再按F5刷新只是刷新登录页不会重复提交注册表单。如果你用转发地址栏停留在注册的Servlet地址用户按一次F5浏览器会弹出一个“确认重新提交表单”的提示点了确定就又注册了一条记录。这个问题在演示项目时特别容易翻车所以我专门把它拎出来说。5. 学生信息的增删改查从DAO到Servlet再到JSP的完整链路登录注册搞定后增删改查就是复制粘贴式的小活了。但代码组织结构不同后续维护的难度截然不同。这一节我带你把学生信息管理的完整链路走一遍重点讲每个模块的请求流转。5.1 分层架构为什么必须分成Servlet、Service、DAO三层有些人的项目是这样写的JDBC的代码直接写在Servlet里面查询结果用while(rs.next())循环手工拼成ListMap然后在JSP里用% for(...) { %循环输出。这样写的优点只有一个少写几个类。缺点是一旦需求有一丁点变化比如给列表加个搜索条件、给删除加个批量操作你就得在几百行的Servlet里找上半天。我建议哪怕只是练手项目也要分成三层Servlet层控制层接收请求、解析参数、调用Service、决定跳转页面。Service层业务层负责业务逻辑校验、事务控制这里暂时不涉及复杂事务但保留这一层方便以后扩展。DAO层数据访问层用JDBC操作数据库只负责增删改查SQL的执行和结果集的封装。对应到代码上就是三个包controller、service、dao外加一个entity实体类包和一个util工具类包。实体类Student和User就是数据库表字段对应的Java对象属性名尽量和数据库字段名一一对应这样ResultSet的结果封装起来会非常顺手。DAO层的一个核心代码段是StudentDao里的findAll方法。它的流程是获取数据库连接 → 写SQL → 设置占位符参数 → 执行查询 → 用ResultSet遍历结果 → 逐行封装成Student对象 → 装进ListStudent返回。这里有个经验是连接和释放资源一定要写在finally块里或者用JDK 7之后引入的try-with-resources语法。很多初学项目跑着跑着就报“Too many connections”就是因为查询方法里开着连接不关一个查询漏一个连接池迟早被耗光。用try-with-resources写法最省心public ListStudent findAll() { ListStudent list new ArrayList(); String sql SELECT id, stu_no, stu_name, gender, age, major FROM t_student; try (Connection conn DBUtil.getConnection(); PreparedStatement ps conn.prepareStatement(sql); ResultSet rs ps.executeQuery()) { while (rs.next()) { Student s new Student(); s.setId(rs.getInt(id)); s.setStuNo(rs.getString(stu_no)); s.setStuName(rs.getString(stu_name)); s.setGender(rs.getString(gender)); s.setAge(rs.getInt(age)); s.setMajor(rs.getString(major)); list.add(s); } } catch (SQLException e) { e.printStackTrace(); // 生产环境请用logger记录这里为简示范 } return list; }注意这里SQL用PreparedStatement的占位符?而不是字符串拼接。为什么不只是防止SQL注入更是因为参数化的SQL执行计划可以被数据库复用性能更好。字符串拼接出来的SQL每一条都是新语句数据库每次都要重新解析。更重要的是字符串拼接如果用户的学号输入里带一个 OR 11之类的内容拼接进SQL语句就成了一个恒真条件分分钟把整张表的数据查出去。这就是SQL注入的典型场景用PreparedStatement占位符是从根上防住的。5.2 列表页跳转带不带id参数决定了修改删除的表单回显列表页最核心的代码是JSP里的表格循环输出。这里要注意千万不要用脚本表达式% %去输出数据而是用JSTL标签库和EL表达式c:forEach items${studentList} varstu tr td${stu.id}/td td${stu.stuNo}/td td${stu.stuName}/td td${stu.gender}/td td${stu.age}/td td${stu.major}/td td a hrefstudent?actiontoEditid${stu.id}修改/a a hrefstudent?actiondeleteid${stu.id} onclickreturn confirm(确定删除该学生吗)删除/a /td /tr /c:forEach修改和删除的操作都要带上这条记录的id这是整个增删改查流程里最重要的一个参数。很多新手想在修改页里靠学号定位记录这是不靠谱的。数据库主键id才是唯一稳定的定位符学号、姓名都是业务数据业务上允许变化的字段都不适合拿来做定位条件。修改页面的回显逻辑是这样的student?actiontoEditid5这个请求到达Servlet后Servlet根据id调用DAO层的findById(5)查询出这条学生记录再通过request.setAttribute(student, stu)把它传到页面修改页的input value${student.stuName}就能把现有数据显示在输入框里。用户改完提交表单表单里必须带上一个隐藏域input typehidden nameid value${student.id}这样更新的SQL才知道要改哪一行。这个隐藏域很容易被新手漏掉漏掉的后果就是更新的SQL永远只有UPDATE t_student SET ... WHERE id?但?取不到值更新就直接报错。5.3 删除操作的两个细节confirm确认和事务边界删除功能的核心SQL只有一行DELETE FROM t_student WHERE id?。但有两个细节会影响体验和数据安全。第一个细节是前端确认。直接点击一个删除超链接页面会瞬间跳走、数据瞬间消失没有后悔药。给删除链接加一个onclickreturn confirm(确定删除该学生吗)用户点取消时浏览器返回false链接的跳转就不会发生这是成本最低的防误删手段。别小看这一步演示项目里当着别人的面删错数据的尴尬情况我见过太多了。第二个细节是把删除和后续跳转的放在两个步骤里。删除操作执行完之后要么sendRedirect回列表页要么请求转发到列表Servlet重新查一遍数据再显示。我建议删除后重定向到列表页的地址比如response.sendRedirect(student?actionlist)这样列表页拿到的永远是最新的数据。如果你用转发到某一个固定的JSP页面那数据还是删除前缓存下来的老数据页面显示就跟数据库不一致了。5.4 修改操作的完整流程别把回显和更新混在一件事里很多新手在写修改功能时会冒出这样的困惑“我点击修改链接跳到一个页面里面显示的是这条记录的现有内容然后我改完点提交怎么又变成新增了”问题出在把“回显”和“更新”的路径搞混了。修改操作在Servlet层应该拆成两条独立的请求路径student?actiontoEditid5根据id查出来塞到request里转发到edit.jsp页面这是回显。student?actionupdate接收id、stuNo、stuName等表单参数调用DAO层的更新方法执行UPDATE t_student SET ... WHERE id?然后重定向回列表页这是更新。两个请求由同一个Servlet的不同if分支处理但职责完全不同。把回显和更新切成两个分支后页面跳转路径就非常清晰了。遇到“改完变新增”的同学基本都是在更新分支里忘了拿id参数或者把DAO层的更新方法写成了insert逻辑。6. 项目部署与常见报错404、乱码、驱动加载失败的排查顺序最后一个大坑是部署和联调。本地IDEA里跑得好好的一发布到Windows Server的Tomcat上就各种问题。新手的内心是崩溃的其实很多都是固定的几种问题按顺序排查十分钟内就能解决。6.1 运行时环境差异本地跑通的项目发布到Tomcat上最常遇到的第一类问题是数据库连接失败。这不是代码问题而是你的JDBC连接URL写死了localhost远程部署后数据库可能不在本机需要改成对应IP。还有本地连MySQL用的可能是root无密码大忌而服务器上设置了密码db.properties没同步更新。所以发布前第一步就是检查db.properties里的每一个配置项。第二类常见问题是Tomcat版本差异。本地用的Tomcat 9可以跑的项目服务器上装的是Tomcat 10同样会报jakarta.servlet包不存在之类的错。这时候要么服务器降级装Tomcat 9要么把代码里的javax.servlet全部替换成jakarta.servlet。我建议项目阶段就统一版本别一个机器跑9一个机器跑10来回折腾心态容易崩。第三类是文件路径问题。本地Windows上路径分隔符是\服务器Linux或Windows Server上可能有差异。代码里尽量不要写绝对路径图片上传、文件导出这些如果要用到磁盘路径用相对于项目的相对路径或者服务器配置的虚拟目录。6.2 404和500的定位顺序页面一片空白或者直接404先确定三件事访问的URL对不对项目名Servlet路径拼写是否一致初学者最容易在Servlet的WebServlet(/student)注解路径和JSP里的student?actionlist之间写错一个字符。Servlet有没有被Tomcat加载IDEA里运行如果不报Artifact ... was not deployed的错误一般说明部署成功。如果改了Servlet代码没有热部署生效重启一下Tomcat再试。页面文件放的位置对不对JSP文件只能放在webapp或web目录下面不能放在WEB-INF目录之外的其他地方更不能放在src目录里以为能直接访问到。500错误的话把Tomcat控制台或者IDEA的控制台拉到最上面找第一行Caused by:的位置。大多数新手只看中间的执行日志看着看着就看迷了。正确的定位方式是从下往上从异常栈底找第一个你自己写的类的行号这里才是错误真正产生的地方。常见的如ClassNotFoundException: com.mysql.jdbc.Driver说明驱动JAR没放对位置Access denied for user说明账号密码不对Unknown database说明数据库名不对。6.3 中文乱码问题三处统一才能根治中文乱码是JavaWeb里最折磨人的问题。我总结的规律是只要“页面编码、请求编码、数据库编码”三处不统一就一定会乱。分别检查以下三处JSP页面顶部必须有% page contentTypetext/html;charsetUTF-8 languagejava %确保页面以UTF-8输出。Servlet接收请求参数之前必须设置request.setCharacterEncoding(UTF-8)而且必须放在request.getParameter()之前才有效。POST请求靠它GET请求还需要在Tomcat的server.xml里为Connector配置URIEncodingUTF-8。数据库建表时字符集用utf8mb4连接URL加上characterEncodingutf8参数例如jdbc:mysql://localhost:3306/student_manager?useSSLfalseserverTimezoneAsia/ShanghaicharacterEncodingutf8。这三处只要是统一的UTF-8理论上不会乱码。如果还有乱码检查一下是不是本地Windows控制台打印乱码那只是控制台显示的问题跟实际存储的数据无关不要在上面浪费太多时间。6.4 老教程里最常见的三个“死坑”总结最后把我在教学生过程中反复遇到的老三样问题汇总一下如果你也遇到了一模一样的报错直接对号入座症状根本原因解决方法The server time zone value Öйú±ê׼ʱ¼ä is unrecognizedMySQL 8.0连接要求指定时区连接URL加上serverTimezoneAsia/ShanghaiAccess denied for user rootlocalhost数据库账号或密码不对检查db.properties里配置的账号密码注意MySQL 8默认认证插件是caching_sha2_password驱动版本也要配套严重: The web application [] registered the JDBC driver应用卸载时没有反注册驱动监听器或Servlet的destroy方法里调用DriverManager.deregisterDriver初学可忽略但会让你在发布的Tomcat日志里看到红色警告影响观感这三个问题都属于网上资料很多、但是标题都不一致的类型而且报错信息看着很吓人实际上都是一行代码能解决的事。遇到报错先深呼吸按这个表去检查九成情况在五分钟内能搞定。整个项目最核心的收获不是“我会写登录注册增删改查了”而是你终于把“浏览器发起请求 → Tomcat接收 → Servlet处理 → Service处理逻辑 → DAO操作MySQL → 结果逐层返回 → JSP渲染”这条JavaWeb请求链路彻底打通了。我自己的亲身体会是做完这个项目后再去看框架比如Spring Boot、MyBatis很多概念会变得非常亲切因为框架就是在这个原生的Servlet项目上做了一层又一层封装而已。上手框架前先扎扎实实把原生Servlet做一遍这个买卖明面上是亏多写了很多重复代码实际上非常划算。下一步你可以试着给这个项目加一个模糊查询、加一个分页、再加一个批量删除这几个功能做完你的JavaWeb基本功就真的扎实了。本文还有配套的精品资源点击获取