news 2026/11/6 3:53:19

JSP+JavaBean+Servlet 登录注册留言板最小闭环实战

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
JSP+JavaBean+Servlet 登录注册留言板最小闭环实战

简介:面向JavaWeb入门者的MVC三层架构练手项目,用JSP、JavaBean与Servlet实现登录注册、留言板和分页浏览,适合课程设计与毕业设计选型参考,也适合刚接触Servlet、想弄清一次请求如何在视图与模型之间流转的新手。整包共78个文件、约1.86MB,其中14个java源文件与14个class分别对应各层实现,14个jsp承担视图展示,10个css与图片素材负责页面样式,9个xml、3个properties等配置文件支撑工程构建与部署,另附1个sql脚本,可直接还原数据库结构。目前已有1613人学习下载。代码按JavaBean读写数据库、JSP展示信息、Servlet联系两者的方式组织,登录注册校验、留言写入与展示、分页参数传递都有完整链路,配合sql脚本可在本地快速导入运行,便于对照理解JDBC操作、会话管理与分层解耦,也方便在此基础上替换前端样式或扩展新功能。

1. 从一张登录表单看 JSP+JavaBean+Servlet 的最小闭环:别急着堆框架

有人把 JSP+JavaBean+Servlet 当成课程作业里的老古董,可登录注册留言板恰恰是检验会话、参数校验、SQL 参数化和视图转义的最小闭环。你拿到一个空白 Web 项目,要在不引入大框架的前提下跑通注册、登录、发留言、看列表,还要避开重复用户名、会话丢失和 XSS,这套组合仍然是最短的路径。它适合刚接触服务端的学生、需要维护老系统的工程师,以及想用一个小项目把 MVC 分层讲清楚的人。下面按数据模型、Servlet 链路、留言视图、排查和验证顺序拆开,每一步都给可抄的代码和参数边界。

2. 数据模型先行:用户表、留言表和 JavaBean 的字段边界怎么定

登录注册留言板翻车,十有八九不是 Servlet 写错,而是表结构和 Bean 的职责一开始就混了。用户表里塞昵称、留言表里塞用户名、Bean 里顺手写数据库连接,后面每加一个页面都要返工。先把三件事定死:哪些字段进库、哪些对象只在层间传递、哪些字段只用于视图。

2.1 三张表与字段约束:用户、留言、状态位

最小可用模型只需要两张表:用户表和留言表。用户表保存登录凭据和展示名,留言表保存内容、作者外键和发布时间。密码不存明文,只存带盐的哈希串;留言不做物理删除,用状态位标记,避免后续加审核或回收站时改表。用户名必须唯一,这个约束放在数据库唯一索引上,不要只靠 Servlet 里先查再插,两个请求同时注册时先查再插会漏。

-- 用户表:id 按所用数据库的自增列或序列生成 create table t_user ( id bigint primary key, username varchar(32) not null, password_hash varchar(255) not null, nickname varchar(32) not null, status int not null default 1, created_at timestamp not null, constraint uk_user_username unique (username) ); -- 留言表:content 按字符数限制,status=1 表示正常展示 create table t_message ( id bigint primary key, user_id bigint not null, content varchar(500) not null, status int not null default 1, created_at timestamp not null, constraint fk_message_user foreign key (user_id) references t_user(id) ); create index idx_message_status_id on t_message(status, id);

字段长度不是拍脑袋:用户名 4 到 20 位、昵称 1 到 20 字、留言 1 到 500 字,和后面的 Servlet 校验保持一致。password_hash给 255 位,是因为 PBKDF2 的存储格式里带迭代次数、盐和哈希值,Base64 后比裸哈希长。status用整数而不是布尔,后面加“待审核”“已隐藏”不用改类型。created_at由应用写入,不依赖数据库默认值,跨库迁移时少一个隐性差异。

留言列表要按时间倒序分页,索引建在(status, id)上,查询条件先过滤状态再按 id 倒序,比只建created_at更稳。外键是否启用看部署环境,有些团队在应用层保证一致性,但表结构里写清楚关系,后来人排查时不用猜。

2.2 JavaBean 的四个边界:POJO、DTO、VO、DAO 返回值

JavaBean 最容易写成“万能袋”:既当表单接收对象,又当数据库实体,还当页面展示对象。登录注册留言板规模不大,但也建议守住边界。User和Message是持久化对象,只放表字段;PageResult<T>是分页结果,只放页码、总数和列表;页面需要的contentHtml这种预转义字段,要么在 Servlet 里转成展示对象,要么在 Bean 里加只读视图字段并明确注释“仅用于输出”。不要让 Bean 依赖HttpServletRequest,否则单元测试只能启动容器。

package bean; import java.io.Serializable; import java.sql.Timestamp; public class User implements Serializable { private Long id; private String username; private String passwordHash; private String nickname; private Integer status; private Timestamp createdAt; public Long getId() { return id; } public void setId(Long id) { this.id = id; } public String getUsername() { return username; } public void setUsername(String username) { this.username = username; } public String getPasswordHash() { return passwordHash; } public void setPasswordHash(String passwordHash) { this.passwordHash = passwordHash; } public String getNickname() { return nickname; } public void setNickname(String nickname) { this.nickname = nickname; } public Integer getStatus() { return status; } public void setStatus(Integer status) { this.status = status; } public Timestamp getCreatedAt() { return createdAt; } public void setCreatedAt(Timestamp createdAt) { this.createdAt = createdAt; } }

这个类只做数据承载,不写login()、save()这类方法。登录逻辑放 Servlet 或服务类,保存逻辑放 DAO。passwordHash在登录成功后要清空再放进会话,避免整个哈希串被后续页面或日志带出去。Message同理,字段包括id、userId、content、status、createdAt,列表查询时额外带一个nickname,这个字段来自 join,不属于留言表,但作为只读展示字段可以接受。

PageResult<T>负责算总页数,别让 JSP 里做除法:

package bean; import java.util.List; public class PageResult<T> { private final int pageNo; private final int pageSize; private final long total; private final List<T> list; public PageResult(int pageNo, int pageSize, long total, List<T> list) { this.pageNo = pageNo; this.pageSize = pageSize; this.total = total; this.list = list; } public int getPageNo() { return pageNo; } public int getPageSize() { return pageSize; } public long getTotal() { return total; } public List<T> getList() { return list; } public int getTotalPages() { return total == 0 ? 1 : (int) ((total + pageSize - 1) / pageSize); } }

getTotalPages()在总数为 0 时返回 1,避免页面显示“第 1 / 0 页”。页码越界不在这里抛异常,由 DAO 返回空列表,Servlet 决定是否重定向到第一页。这个细节不写,后面分页链接点快了就会看到空白页。

2.3 用 JDBC 工具类收口连接与事务

数据库连接不要散落在每个 Servlet 里。写一个DBUtil从配置文件读驱动、URL、用户名和密码,DAO 只接收Connection或自己拿连接。示例用DriverManager是为了让空白项目能跑,生产环境换成容器数据源或连接池,接口不变。

# db.properties:放在类路径下,四个键按实际环境替换 db.driver=你的数据库驱动类 db.url=jdbc:yourdb://host:port/dbname db.user=app db.password=changeit
package util; import java.io.InputStream; import java.sql.Connection; import java.sql.DriverManager; import java.sql.SQLException; import java.util.Properties; public final class DBUtil { private static final Properties P = new Properties(); static { try (InputStream in = DBUtil.class.getClassLoader().getResourceAsStream("db.properties")) { if (in == null) throw new IllegalStateException("db.properties 未找到"); P.load(in); Class.forName(P.getProperty("db.driver")); } catch (Exception e) { throw new ExceptionInInitializerError(e); } } public static Connection getConnection() throws SQLException { return DriverManager.getConnection( P.getProperty("db.url"), P.getProperty("db.user"), P.getProperty("db.password")); } private DBUtil() {} }

db.url里的yourdb是占位,按所用数据库的连接串替换;驱动类同理。配置加载失败直接抛ExceptionInInitializerError,让应用启动时就暴露,而不是第一次登录才报空指针。DAO 里所有Connection、PreparedStatement、ResultSet都用 try-with-resources,异常路径也能关。

注册插入可以这样写,连接由 Servlet 传入,事务边界留在上层:

public void insert(Connection c, User user) throws SQLException { String sql = "insert into t_user(username, password_hash, nickname, status, created_at) " + "values(?, ?, ?, ?, ?)"; try (PreparedStatement ps = c.prepareStatement(sql)) { ps.setString(1, user.getUsername()); ps.setString(2, user.getPasswordHash()); ps.setString(3, user.getNickname()); ps.setInt(4, 1); ps.setTimestamp(5, new Timestamp(System.currentTimeMillis())); ps.executeUpdate(); } }

参数按顺序对应五个占位符,status固定为 1,created_at由应用生成。不要用字符串拼接用户名,注册接口是 SQL 注入的高发点。唯一索引冲突会抛SQLIntegrityConstraintViolationException,Servlet 捕获后转成“用户名已存在”,不要把这个异常栈直接打到页面。

3. Servlet 接管登录注册:参数校验、密码哈希和会话落地

JSP 只负责显示和收集输入,登录注册的判断全部放 Servlet。表单提交用 POST,成功之后用重定向跳到列表页,失败才转发回表单页并带回错误信息。这个顺序能避免刷新重复提交,也能让浏览器地址栏停在正确的位置。

3.1 路由与页面流转:/login、/register、/logout 怎么排

建议把视图统一放WEB-INF/views下,浏览器不能直接访问,只能由 Servlet 转发。目录结构可以是这样:

src/ bean/User.java bean/Message.java bean/PageResult.java dao/UserDao.java dao/MessageDao.java servlet/RegisterServlet.java servlet/LoginServlet.java servlet/LogoutServlet.java servlet/MessageListServlet.java servlet/PostMessageServlet.java filter/EncodingFilter.java filter/LoginFilter.java util/DBUtil.java util/PasswordUtil.java web/ WEB-INF/ views/login.jsp views/register.jsp views/messages.jsp web.xml

路由用注解映射,登录页GET /login转发,提交POST /login处理;注册同理;退出走GET /logout,内部调用session.invalidate()后重定向到登录页。编码过滤器拦/*,在进入 Servlet 前设置请求和响应字符集。

@WebFilter("/*") public class EncodingFilter implements Filter { @Override public void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain) throws IOException, ServletException { req.setCharacterEncoding("UTF-8"); resp.setCharacterEncoding("UTF-8"); chain.doFilter(req, resp); } }

setCharacterEncoding对 POST 请求体有效,GET 查询串的编码由容器处理,所以涉及中文的参数尽量走 POST。响应只设字符集还不够,JSP 顶部要有pageEncoding="UTF-8",Servlet 输出 JSON 或 HTML 时再设Content-Type。这三处漏一处,中文就可能变成问号。

web.xml里配欢迎页和错误页,不放业务映射也行:

<web-app xmlns="https://jakarta.ee/xml/ns/jakartaee" version="5.0"> <welcome-file-list> <welcome-file>login</welcome-file> </welcome-file-list> <error-page> <error-code>404</error-code> <location>/WEB-INF/views/404.jsp</location> </error-page> </web-app>

命名空间按容器实际支持的版本调整,别把版本号当成业务代码抄。欢迎页写login而不是login.jsp,是因为视图在WEB-INF下,直接写 JSP 路径访问不到。

3.2 注册 Servlet:参数校验、重复用户名、密码哈希

注册的校验顺序建议是:字段格式 → 两次密码一致 → 唯一索引冲突。格式校验包括用户名 4 到 20 位字母数字下划线、密码 8 到 64 位、昵称 1 到 20

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

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

SAP公司间项目STO(Q类型)配置与闭环操作指南

简介&#xff1a;本资源是一份面向SAP系统操作员与项目经理的公司间项目&#xff08;Q&#xff09;STO全流程实践指南&#xff0c;聚焦跨公司采购场景下的配置、执行与排错&#xff0c;解决CO科目分配错误、收货失败、开票异常等高频实操痛点。文档以真实业务案例为蓝本&#x…

作者头像 李华
网站建设 2026/11/6 3:51:37

文件快捷方式的浏览器改写内网路径写法

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/11/6 3:50:48

【AI写提示词教程】会玩AI的人,才在时间上赢了@米斗设计

嗨&#xff0c;我是米斗&#xff0c;AI商业提效实战教练。我的打法是把AI、设计和内容结合起来&#xff0c;专门帮中小创作者和品牌方把个人IP做起来&#xff0c;把产品卖出去。 https://gitcode.com/OpenDocCN1/data/blob/master/imgs/zhuanqian_c9b4af90f0b3738a3d881b37c3e…

作者头像 李华
网站建设 2026/11/6 3:50:35

(81 赞)AI 出海领域创业方向探讨与风险分析

最近有个创业者提问&#xff0c;挺典型的。他说自己是“一人公司”&#xff0c;之前在别的赛道已经做出过成绩&#xff0c;学习能力、执行力和跨行业能力都不错&#xff0c;干了十多年&#xff0c;认知也算在线。如今精力有限&#xff0c;却想切进 AI 出海&#xff0c;找一个天…

作者头像 李华
网站建设 2026/11/6 3:49:09

FastApi(5)声明请求示例数据额外数据类型

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/11/6 3:48:09

概念模型怎么搭出来:从变量关系到可画可讲的模型图

开题会被要求"画一个概念模型图"&#xff0c;卡住的往往不是软件&#xff0c;而是逻辑没成形&#xff1a;变量有哪些、谁指向谁、这条线凭什么连。概念模型不是装饰图&#xff0c;它是研究问题的一次公开翻译。下面按认变量、定方向、画图、讲图四个动作走。先认清变…

作者头像 李华