简介面向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. 从一张登录表单看 JSPJavaBeanServlet 的最小闭环别急着堆框架有人把 JSPJavaBeanServlet 当成课程作业里的老古董可登录注册留言板恰恰是检验会话、参数校验、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 按字符数限制status1 表示正常展示 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是持久化对象只放表字段PageResultT是分页结果只放页码、总数和列表页面需要的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不属于留言表但作为只读展示字段可以接受。PageResultT负责算总页数别让 JSP 里做除法package bean; import java.util.List; public class PageResultT { private final int pageNo; private final int pageSize; private final long total; private final ListT list; public PageResult(int pageNo, int pageSize, long total, ListT 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 ListT 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.urljdbc:yourdb://host:port/dbname db.userapp db.passwordchangeitpackage 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固定为 1created_at由应用生成。不要用字符串拼接用户名注册接口是 SQL 注入的高发点。唯一索引冲突会抛SQLIntegrityConstraintViolationExceptionServlet 捕获后转成“用户名已存在”不要把这个异常栈直接打到页面。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 顶部要有pageEncodingUTF-8Servlet 输出 JSON 或 HTML 时再设Content-Type。这三处漏一处中文就可能变成问号。web.xml里配欢迎页和错误页不放业务映射也行web-app xmlnshttps://jakarta.ee/xml/ns/jakartaee version5.0 welcome-file-list welcome-filelogin/welcome-file /welcome-file-list error-page error-code404/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本文还有配套的精品资源点击获取
阅读完成 · 觉得有帮助?