ARTICLE DETAIL

建站实战干货

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

JSP+JavaBean+Servlet实战:登录注册留言板,彻底搞懂MVC

2026/9/9 13:48:38 拓冰建站 浏览量
JSP+JavaBean+Servlet实战:登录注册留言板,彻底搞懂MVC 简介一份基于JSPJavaBeanServlet的JavaWeb入门级MVC项目面向刚学习JavaWeb分层开发的学生或开发者解决登录注册、留言板与分页展示等典型需求。资源共78个文件涵盖14个Java类、14个JSP页面、14个class文件以及SQL脚本、XML配置、CSS样式、JAR依赖等其中model层通过JavaBean封装数据库读写view层用JSP负责信息展示controller层以Servlet完成请求转发与业务交互整体仅1.86MB结构清晰便于阅读。已有1610人学习下载。通过该资源可拿到可直接运行的NetBeans工程源码包含数据库脚本、分页实现、会话管理、表单校验等适合对照MVC流程理解各层职责也可作为课程设计或毕业设计的基础模板进行二次开发。 刚把 Java 基础学完很多人的第一反应是直接冲 Spring Boot。但我的看法不一样JSPJavaBeanServlet 这三个老伙计组合起来写一个登录注册加留言板反而是把 Web 开发的底子打得最扎实的一条路。为什么这么说因为这几个技术正好覆盖了一个 Web 应用最核心的完整链路浏览器发请求、Servlet 接收和处理、JavaBean 承载数据和业务、JSP 渲染页面、JDBC 操作数据库。把这套链路走通一遍你以后再去看 Spring MVC、MyBatis会发现那些框架无非是在这个骨架上做了一层又一层的封装。这篇文章就用一个完整的登录注册加留言板项目把这条链路从头到尾拆开揉碎讲清楚。先说清楚这个项目最终能做什么用户可以在注册页填写用户名和密码完成注册用注册的账号登录登录成功后进入留言板页面看到所有用户的留言也能提交自己的留言留言会持久化到数据库。项目里还会涉及 Session 会话管理、表单数据校验、中文乱码处理、SQL 注入防御这些实际开发里躲不开的问题。如果你正在做课程设计或者想找一个能完整跑通的 MVC 练手项目这篇文章应该能给你省下不少时间。1. 为什么我坚持用 JSPJavaBeanServlet 练这个项目很多初学者会觉得 JSP 已经过时了现在谁还直接写 JSP这话对了一半。JSP 在大型商业项目里确实用得少了但作为教学项目它有一个无法替代的价值它能让你肉眼看到请求、数据、页面三者之间是怎么流动的。1.1 这个组合其实是 MVC 最朴素的样子你可以把这三个角色对应到 MVC 模式里JSP承担视图层 View 的角色负责展示数据、收集用户输入Servlet承担控制器 Controller 的角色负责接收请求、调用业务、跳转页面JavaBean承担模型 Model 的角色负责封装数据表对应的实体同时承载部分业务逻辑。用这个项目做例子用户在注册页面填完信息点提交表单数据被送到注册的 ServletServlet 把数据封装成 User 这个 JavaBean 对象然后调用 DAO 方法把用户信息写入数据库最后跳转到登录页面提示注册成功。整个过程一目了然没有框架替你隐藏任何细节。等你理解了这层关系再去看 Spring MVC 里面Controller、RequestMapping这些注解本质上是把 Servlet 的活换了一种更简洁的方式来做而已。1.2 这个项目适合谁来写我建议三类人认真做一遍刚学完 JSP/Servlet 正在找综合练习的在校学生准备面试但发现框架项目说不出底层原理的转行新人以及想带学生做课设、需要一套干净可跑的参考代码的老师和助教。这个项目的代码量不大大概十几个 Java 文件加四五个 JSP 页面但麻雀虽小五脏俱全数据库、会话、异常处理、中文编码全都能练到。2. 搭骨架项目目录结构、数据库设计、依赖清单工程开工最重要的不是写代码而是先把骨架搭对。很多人写到一半乱得不行问题就出在目录结构和依赖上。2.1 项目目录结构我习惯用 Eclipse 或者 IDEA 的 Dynamic Web Project 结构下面这个目录是经过多个项目验证、最清晰的布置方式src ├── com.demo.bean // 实体类User.java、Message.java ├── com.demo.dao // 数据库访问层UserDao.java、MessageDao.java ├── com.demo.servlet // 控制器RegisterServlet、LoginServlet、LogoutServlet、AddMessageServlet └── com.demo.util // 工具类DBUtil.java数据库连接 WebContent ├── index.jsp // 首页/登录页 ├── register.jsp // 注册页 ├── list.jsp // 留言板页面 ├── failure.jsp // 操作失败提示页 └── WEB-INF ├── web.xml └── lib // mysql-connector-java.jar、jstl.jar这里要提醒一句JSP 页面尽量不要放到 WEB-INF 目录里因为放在那里只能通过 Servlet 转发访问不能直接通过 URL 请求。本项目为了演示直接访问跳转所以把 JSP 放在 WebContent 根目录下。如果你后面要做到更严格的权限控制再考虑把部分页面挪到 WEB-INF 也不迟。2.2 数据库设计这个项目只需要两张表逻辑非常清晰表名字段说明userid自增主键、username唯一、password、created_at用户表messageid自增主键、user_id外键关联 user.id、content、create_time留言表建表 SQL 如下CREATE DATABASE IF NOT EXISTS demo_web DEFAULT CHARSET utf8mb4; USE demo_web; CREATE TABLE user ( id INT PRIMARY KEY AUTO_INCREMENT, username VARCHAR(50) NOT NULL UNIQUE, password VARCHAR(64) NOT NULL, created_at DATETIME DEFAULT CURRENT_TIMESTAMP ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; CREATE TABLE message ( id INT PRIMARY KEY AUTO_INCREMENT, user_id INT NOT NULL, content TEXT NOT NULL, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, CONSTRAINT fk_message_user FOREIGN KEY (user_id) REFERENCES user(id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;两个设计细节值得注意。第一密码字段我留了 64 位是为了给后面做 MD5/SHA-256 加固定位。第二user_id 外键保证了留言一定对应一个真实存在的用户不会出现孤儿数据。2.3 数据库连接工具类不要每写一个 Dao 方法就重复一次DriverManager.getConnection()封装一个工具类后面所有方法都能复用package com.demo.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/demo_web?useSSLfalseserverTimezoneAsia/ShanghaicharacterEncodingutf8; private static final String USER root; private static final String PASSWORD 123456; static { try { Class.forName(com.mysql.cj.jdbc.Driver); } catch (ClassNotFoundException e) { e.printStackTrace(); } } public static Connection getConnection() throws SQLException { return DriverManager.getConnection(URL, USER, PASSWORD); } }注意 URL 里我带了characterEncodingutf8和serverTimezoneAsia/Shanghai这两个参数分别解决中文乱码和 MySQL 8.x 时区报错的问题。如果你用的是 MySQL 5.7驱动类名要写com.mysql.jdbc.Driver这个坑后面第五节细说。3. 登录注册模块Servlet 接收请求、JavaBean 封装数据、Session 贯穿全程这是整个项目的核心模块也是最容易写乱的部分。我的经验是把一条用户请求的完整生命周期想清楚再动手代码自然有条理。3.1 注册流程表单到 Servlet 再到数据库注册页面register.jsp的表单不必花哨重点要保证 name 属性与 Servlet 端拿参数的名字一致form actionregister methodpost input typetext nameusername placeholder用户名 required input typepassword namepassword placeholder密码 required button typesubmit注册/button /form对应的 RegisterServlet 是整个流程的枢纽package com.demo.servlet; import java.io.IOException; 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 com.demo.bean.User; import com.demo.dao.UserDao; WebServlet(/register) public class RegisterServlet extends HttpServlet { private static final long serialVersionUID 1L; private UserDao userDao new UserDao(); protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { request.setCharacterEncoding(utf-8); String username request.getParameter(username); String password request.getParameter(password); if (username null || username.trim().isEmpty() || password null || password.isEmpty()) { request.setAttribute(msg, 用户名和密码不能为空); request.getRequestDispatcher(register.jsp).forward(request, response); return; } User user new User(); user.setUsername(username.trim()); user.setPassword(password); boolean exists userDao.findByUsername(username.trim()) ! null; if (exists) { request.setAttribute(msg, 用户名已存在换一个试试); request.getRequestDispatcher(register.jsp).forward(request, response); return; } boolean ok userDao.addUser(user); if (ok) { response.sendRedirect(login.jsp?regsuccess); } else { request.setAttribute(msg, 注册失败请稍后重试); request.getRequestDispatcher(register.jsp).forward(request, response); } } }有几个关键点说一下。第一行request.setCharacterEncoding(utf-8)必须放在读取参数之前否则 POST 提交的中文用户名全会乱码。注册成功后我用的是sendRedirect而不是forward这是一个很重要的区别重定向是浏览器重新发起一次新请求地址栏会变成 login.jsp用户刷新页面不会重复提交注册表单转发则是服务端内部跳转地址栏不变刷新会重复提交。用户名重复的检查放在 UserDao 里这样后面登录模块也能复用同一个方法。3.2 JavaBean 实体类是怎么写的User 这个 JavaBean 非常简单但要注意一个规范必须提供无参构造、私有属性、公有 getter/setter。Servlet 里new User()的时候调的就是无参构造框架时代很多序列化工具也依赖这个约定所以从现在就要养成习惯。package com.demo.bean; public class User { private int id; private String username; private String password; public User() {} public int getId() { return id; } public void setId(int id) { this.id id; } public String getUsername() { return username; } public void setUsername(String username) { this.username username; } public String getPassword() { return password; } public void setPassword(String password) { this.password password; } }3.3 登录流程与 Session 会话登录比注册多了一件重要的事就是 Session 的管理。用户登录成功后把用户信息放进 Session这样后续访问留言板时才能识别你是谁。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 loginUser userDao.findByUsernameAndPassword(username, password); if (loginUser ! null) { HttpSession session request.getSession(); session.setAttribute(loginUser, loginUser); session.setMaxInactiveInterval(30 * 60); response.sendRedirect(list); } else { request.setAttribute(msg, 用户名或密码错误); request.getRequestDispatcher(login.jsp).forward(request, response); } }登录成功后跳转的不是list.jsp而是list这是一个 Servlet 的 URL 映射它去查询所有留言再转发给 JSP。为什么要绕这一圈因为留言列表的数据不可能写死在 JSP 里必须由后端查库之后喂给页面这正是 MVC 的核心思想。session.setMaxInactiveInterval(30 * 60)表示会话 30 分钟不操作就失效。这个值可以根据需求调整但不要设得太长否则用户离开电脑后别人可以继续操作他的账号。4. 留言板核心从表单提交到 JDBC 持久化数据是怎么流起来的留言板是这个项目的第二个重点它的技术含量主要在 DAO 层和数据安全上。4.1 留言的添加流程AddMessageServlet 接收 JSP 表单传过来的留言内容随手补上当前登录人的 user_id然后插入数据库WebServlet(/addMessage) public class AddMessageServlet extends HttpServlet { private MessageDao messageDao new MessageDao(); protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { request.setCharacterEncoding(utf-8); HttpSession session request.getSession(); User loginUser (User) session.getAttribute(loginUser); if (loginUser null) { response.sendRedirect(login.jsp); return; } String content request.getParameter(content); if (content null || content.trim().isEmpty()) { request.setAttribute(msg, 留言内容不能为空); request.getRequestDispatcher(list).forward(request, response); return; } Message message new Message(); message.setUserId(loginUser.getId()); message.setContent(content.trim()); boolean ok messageDao.addMessage(message); response.sendRedirect(list); } }这里有一个容易被忽视的安全点绝对不能信任前端传过来的 user_id。如果表单里隐藏域传 userId 到后端别人伪造一个请求就能冒充任何人留言。正确做法就是代码里这样从 Session 里取当前登录人的 ID。4.2 查询留言并用 JSP 展示MessageDao 里查询留言列表时往往需要把用户名的信息也带出来这样才能在页面上显示谁留的言。所以 SQL 要用 JOIN 而不是只查 message 表public ListMessage findAllWithUsername() { ListMessage list new ArrayList(); String sql SELECT m.id, m.user_id, m.content, m.create_time, u.username FROM message m JOIN user u ON m.user_id u.id ORDER BY m.id DESC; try (Connection conn DBUtil.getConnection(); PreparedStatement ps conn.prepareStatement(sql); ResultSet rs ps.executeQuery()) { while (rs.next()) { Message msg new Message(); msg.setId(rs.getInt(id)); msg.setUserId(rs.getInt(user_id)); msg.setContent(rs.getString(content)); msg.setCreateTime(rs.getTimestamp(create_time)); msg.setUsername(rs.getString(username)); list.add(msg); } } catch (SQLException e) { e.printStackTrace(); } return list; }注意我在 Message 这个 JavaBean 里额外加了一个username字段它不属于 message 表但为了方便页面展示而存在。JavaBean 不是数据库表的死板映射它可以根据业务需要扩展。ListServlet 查询完把结果放到 request 里再转发WebServlet(/list) public class ListServlet extends HttpServlet { private MessageDao messageDao new MessageDao(); protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { HttpSession session request.getSession(); User loginUser (User) session.getAttribute(loginUser); if (loginUser null) { response.sendRedirect(login.jsp); return; } ListMessage messages messageDao.findAllWithUsername(); request.setAttribute(messages, messages); request.getRequestDispatcher(list.jsp).forward(request, response); } protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { doGet(request, response); } }这里 doPost 直接转发给 doGet 是一个实用技巧因为列表页可能从表单跳转过来也可能从链接点过来保证两种请求都能正确处理。4.3 用 PreparedStatement 而不是 Statement这是整个项目里最重要的安全隐患点。看下面这段反面教材// 千万不要这样写 String sql SELECT * FROM user WHERE username username AND password password ; Statement st conn.createStatement(); ResultSet rs st.executeQuery(sql);如果用户名输入 OR 11SQL 就变成了WHERE username OR 11 AND password攻击者不需要密码就能登录这就是最经典的 SQL 注入。所有 SQL 都必须用 PreparedStatement 的?占位符传参就像上面 findAllWithUsername 写的那样数据库驱动会自动转义特殊字符。我见过不少课设代码犯了这个问题自己本地怎么测都没事但真要上线不出半小时就会被扫描工具攻破。这个习惯从现在开始养成后面用 MyBatis 时你也会感谢现在的自己。5. 乱码、路径与 Session 失效新手最容易翻车的三个位置这一节的内容是我自己踩坑踩出来的每个问题都值得单独讲清楚。5.1 中文乱码问题的完整解决思路乱码是 JSP 项目里出现频率最高的问题但它的解决思路其实很固定。数据库连接 URL 带characterEncodingutf8对应 MySQL 的 utf8/utf8mb4所有 Servlet 处理 POST 请求前执行request.setCharacterEncoding(utf-8)页面顶部写上% page contentTypetext/html;charsetUTF-8 languagejava %如果用了 Tomcat 8 以上版本GET 请求的中文乱码可以用server.xml里配置 URIEncoding 解决或者干脆约定 GET 请求不传中文。Get 参数中文乱码的底层原因是 URL 编码默认是 ISO-8859-1Tomcat 8 默认改成了 UTF-8所以用新版本 Tomcat 基本不用操心。5.2 路径问题相对路径为什么总出错新手写跳转最常见的问题是用了相对路径比如在list.jsp里写hreflogin.jsp看起来没问题但如果当前浏览器地址是/demo/list点这个链接会跳到/demo/login.jsp没问题可如果你把 JSP 放在 WEB-INF 下通过其他路径访问相对路径就会指向错误的位置。我的建议是项目中统一使用绝对路径在 JSP 页面顶部引入% String path request.getContextPath(); String basePath request.getScheme() :// request.getServerName() : request.getServerPort() path /; % base href%basePath%这样页面里所有href和src都以项目根路径开头无论请求从哪来都不会找不到资源。模板引擎时代这个需求被c:url标签和 Thymeleaf 的{}表达式优雅解决了但原理是相通的。5.3 Session 失效后的页面跳转当用户长时间不操作、Session 过期后他手里的页面不会自动消失但访问受保护资源时会因为取不到loginUser而空指针。我的处理方式是全局统一判断在每个需要登录才能访问的 Servlet 里开头就检查 Session 是否为空、loginUser是否存在。上面 ListServlet 就是这么做的。更优雅的方案是写一个 Filter 统一拦截但在本项目中手动判断更直观适合学习。同时注意 JSP 页面直接访问时也可能取到 null 的 Session 属性展示用户信息时最好加一个非空判断c:if test${not empty sessionScope.loginUser} 欢迎你${sessionScope.loginUser.username} /c:if为了让${}表达式在 JSP 里生效记得到 WEB-INF/lib 里放 jstl.jar 和 standard.jar或者用 Tomcat 自带的 EL 解析。不过既然 JSP 等级默认支持 EL只要不用成纯脚本%直接用${}就行前提是 web.xml 里isELIgnored不设为 true。6. 从能用到高分日志、分页、权限这三个升级最值得做如果你把这个项目作为课程设计交上去做到目前这一步已经能及格了。但想拿高分或者想在面试时有一个我主动思考过优化的谈资下面这三个升级性价比最高。6.1 日志打印取代 System.out.println调试时用System.out.println很方便但项目里全是这类输出会显得很业余。至少用java.util.logging或者直接引入 Log4j 2把关键节点用户注册成功、登录成功、留言新增、数据库异常记录下来。这样出问题的时候你能从日志里还原完整的操作轨迹而不是盯着控制台黑窗口猜。6.2 留言分页现在留言一多list.jsp 会越拉越长。加一个分页逻辑其实不复杂查询总数SELECT COUNT(*) FROM message根据当前页码计算出 LIMIT 的 offset 和 size页面底部渲染上一页/下一页链接URL 携带page2这样的参数ListServlet 里通过Integer.parseInt(request.getParameter(page))接收即可。这是面试里出现频率很高的考点核心就一条 SQLSELECT ... LIMIT ?, ?高度配合 PreparedStatement 占位符。6.3 删除留言的权限控制目前我还没有给大家写删除留言的功能。如果想加关键点就是不仅登录用户能删还要判断这条留言是否属于他本人。删除的 SQL 可以这样写DELETE FROM message WHERE id? AND user_id?先把 user_id 从 Session 里取出来作为第二个条件这样就算别人猜到了留言 ID也没法删除不属于自己的留言。这个细节在数据库层面做了一层权限控制比单纯在 JSP 里判断要可靠得多。6.4 密码加密存储第五点升级是安全方向的重中之重把密码明文存数据库是大忌。最简单的做法是注册时用MessageDigest做一次 SHA-256 哈希再入库登录时把用户输入的密码做同样的哈希后比对public static String sha256(String input) throws NoSuchAlgorithmException { MessageDigest md MessageDigest.getInstance(SHA-256); byte[] hash md.digest(input.getBytes(StandardCharsets.UTF_8)); StringBuilder sb new StringBuilder(); for (byte b : hash) { sb.append(String.format(%02x, b)); } return sb.toString(); }正式项目通常还会加盐salt处理但这套思路已经能挡住大部分简单风险了。把密码字段从 64 位改成 128 位也完全够用就看你选 SHA-256 还是更长的算法。我在做这个项目的过程中最大的一个体会是写代码本身不是最难的部分最花时间的是把每一条数据从表单到数据库再回到页面的路径理顺并且守住每一个可能出错的边界情况。空值判断、Session 判断、编码统一、权限校验这些看起来繁琐的细节恰恰是 Web 开发里衡量一个程序员是否靠谱的分水岭。最后再分享一个调试上的小技巧如果某个功能一模一样照着写却不生效别急着怀疑代码逻辑先在浏览器按 F12 打开 Network 标签页看请求是否真的发出去了、状态码是 404 还是 500、表单参数是否完整。我之前见过一个学员改了三个小时都没解决的 bug最后发现是表单按钮没有写 typesubmit点击之后只是触发了 JS 校验失败请求压根就没发到 Servlet 那里。Web 开发这条路上九成的灵异现象都出在请求根本没到达你预期的地方。本文还有配套的精品资源点击获取