简介:面向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=changeitpackage 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
本文还有配套的精品资源,点击获取