ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

JSP+JavaBean+Servlet手搓登录注册与留言板教学闭环

JSP+JavaBean+Servlet手搓登录注册与留言板教学闭环

简介:这是一份面向Java Web初学者的MVC架构实战项目资源,基于JSP+JavaBean+Servlet技术栈,完整实现用户登录注册、留言板管理及分页展示功能,适用于高校Web开发课程实验、毕业设计基础模块或自学练手。资源包共78个文件,包含14个JSP页面(负责视图渲染)、14个Java源码(含Servlet控制器与JavaBean模型)、14个编译后class文件、10个CSS样式文件、9个XML配置文件(如web.xml、数据库连接配置)以及1个SQL建库脚本,整体压缩包仅1.86MB,轻量易部署。已有1611人学习下载,适配无密码本地MySQL环境(数据库名webapp),结构清晰体现经典三层分层思想:Model层封装DAO操作,View层专注展示逻辑,Controller层协调请求流转。读者可直接导入NetBeans运行,快速掌握传统JavaWeb开发流程、目录组织规范及前后端交互细节。

1. 这不是复古怀旧,而是 JavaWeb 教学闭环里最扎实的一块砖:用 JSP + JavaBean + Servlet 手搓登录注册+留言板,不碰框架、不绕弯子,专治“学了 Servlet 不知道串哪”“写了 JSP 不懂怎么传值”“JavaBean 写成 POJO 就停笔”的典型卡点

很多刚学完 HTTP 协议、Servlet 生命周期、JSP 九大内置对象的开发者,一到写完整功能就卡在“不知道数据从哪来、到哪去、中间谁该干啥”。这个资源不是炫技,它是一套被某高校实验课反复验证过、某公司新员工岗前训练用过的最小可运行闭环:用户填表单 → 提交到 Servlet → Servlet 调用 JavaBean 封装/校验/存库 → 转发回 JSP 展示结果或错误 → 留言列表实时刷新。它不依赖 Spring MVC 的自动绑定,不靠 Thymeleaf 的模板语法糖,所有请求流转、作用域传递、状态保持都暴露在代码里。适合两类人:一是想真正吃透 JavaWeb 底层协作逻辑的初学者;二是需要快速搭建教学 Demo、验证某个 Servlet 生命周期细节(比如doPost中request.setAttribute()在转发后是否还有效)的带教者。它不是生产级系统,但它是你理解 Filter、Listener、Session 管理之前,必须亲手拧紧的那几颗螺丝。


2. 为什么是 JSP + JavaBean + Servlet 这个组合?不是 Struts、不是 Spring Boot,更不是纯前端调 API

2.1 选型逻辑:三件套各自不可替代的“职责锚点”

在现代 JavaWeb 开发中,JSP、JavaBean、Servlet 各自承担着清晰且不可被上层框架完全抹平的职责边界。这不是历史包袱,而是分层设计的具象化体现:

  • Servlet 是协议守门人:它直接对接 HTTP 请求/响应,是唯一能原生读取request.getParameter()、设置response.setStatus()、控制重定向(sendRedirect)与请求转发(RequestDispatcher.forward)的组件。任何框架最终都要把它包装成@RequestMapping或@PostMapping,但底层仍是 Servlet 容器调用service()方法。本项目中,LoginServlet和RegisterServlet不做业务逻辑,只做三件事:收参、调 Bean、决定跳转——这是对“Servlet 职责单一性”的严格践行。

  • JavaBean 是数据契约体:它不是简单的 POJO。本项目中的User.java和Message.java必须满足 JavaBean 规范:私有属性、公共 getter/setter、无参构造。为什么?因为 JSP 的<jsp:useBean>标签和<jsp:setProperty property="*">依赖反射按名称匹配属性,若缺少 setter 或命名不规范(如setUserName写成setusername),JSP 页面会静默失败,连报错都不抛。它强制你写出符合 JavaBeans Introspection 规则的类,这是理解后续框架中“属性绑定”机制的起点。

  • JSP 是视图编排器,不是模板引擎:它不负责计算、不处理分支逻辑(<% if (...) { %>是反模式,本项目禁用脚本片段),只做两件事:通过 EL 表达式${user.name}渲染 JavaBean 属性;用 JSTL<c:if><c:forEach>做纯粹的展示控制。它的本质是 Servlet 容器编译后的 Java 类(.java文件在work/Catalina/...下可查),这让你直面“页面即 Servlet”的事实——没有魔法,只有编译时生成的out.print(...)。

提示:本项目刻意避开web.xml全量配置,采用@WebServlet注解,但保留web.xml中的<jsp-config>配置段(如el-ignored=false),这是为了显式声明 EL 表达式启用状态,避免新手因默认配置差异导致${}不解析。

2.2 目录结构即架构:5 个核心文件讲清数据流向

项目采用标准 Maven Webapp 结构,关键文件精简到 5 个,每个都承担明确角色:

文件路径类型核心职责关键技术点
src/main/java/com/example/LoginServlet.javaServlet接收 POST 登录请求,调用UserService.checkLogin(),根据返回结果request.getRequestDispatcher()转发或response.sendRedirect()重定向request.getSession().setAttribute("currentUser", user)实现登录态保持
src/main/java/com/example/User.javaJavaBean封装用户名、密码、邮箱等字段,提供validate()方法校验密码长度、邮箱格式validate()返回Map<String, String>存储各字段错误信息,供 JSP 显示
src/main/webapp/login.jspJSP展示登录表单,用<form action="login" method="post">提交,用${errorMap.password}显示校验错误<c:if test="${not empty errorMap}">判断是否有错误并高亮输入框
src/main/webapp/messageBoard.jspJSP显示留言列表,用<c:forEach items="${messageList}" var="msg">遍历,每条留言含作者、内容、时间时间戳用fmt:formatDate value="${msg.time}" pattern="yyyy-MM-dd HH:mm:ss"格式化
src/main/resources/db.properties配置文件存放数据库连接参数(driver、url、username、password),由DBUtil工具类加载使用ClassLoader.getSystemResourceAsStream()加载,确保打包后仍可读

这个结构拒绝“大而全”,删掉所有非必要文件(如index.html、error.jsp),迫使你关注主链路:表单提交 → Servlet 处理 → Bean 校验 → 数据库操作 → 视图渲染。每一步的输入输出都肉眼可见。

2.3 数据库设计:3 张表撑起全部功能,字段定义直指业务痛点

本项目使用 HSQLDB(内存数据库,启动即建库,无需安装),表结构极简但覆盖核心场景:

-- 用户表:密码明文存储仅用于教学演示,实际必须加盐哈希 CREATE TABLE users ( id INTEGER IDENTITY PRIMARY KEY, username VARCHAR(50) NOT NULL UNIQUE, password VARCHAR(100) NOT NULL, email VARCHAR(100), created_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP ); -- 留言表:关联用户ID,实现“谁发的留言” CREATE TABLE messages ( id INTEGER IDENTITY PRIMARY KEY, user_id INTEGER NOT NULL, content VARCHAR(500) NOT NULL, created_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (user_id) REFERENCES users(id) ); -- 会话表:模拟简单登录态管理(实际应使用 HttpSession) CREATE TABLE sessions ( session_id VARCHAR(100) PRIMARY KEY, user_id INTEGER NOT NULL, created_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP, expires_time TIMESTAMP );

关键设计意图:

  • users.username设为UNIQUE:强制注册时校验用户名重复,UserService.register()中INSERT INTO users ...失败时捕获SQLException并返回具体错误。
  • messages.user_id外键约束:确保每条留言必属某用户,MessageService.addMessage()先查users表确认用户存在,再插入messages,体现数据一致性校验。
  • sessions表虽未在代码中直接使用(登录态走HttpSession),但其结构为后续扩展“记住我”、“多端登录踢出”埋下伏笔——只需在LoginServlet中增加INSERT INTO sessions操作即可。

3. 从零跑通:5 步命令行实操,不依赖 IDE 图形界面

3.1 环境准备:JDK 8 + Tomcat 9 是黄金组合,版本错配是第一大坑

本项目基于 Servlet 3.1 规范(Tomcat 8.5+ 支持),但刻意选择JDK 8 + Tomcat 9.0.x组合,原因有三:

  • JDK 8 的javac编译器对泛型擦除、Lambda 表达式支持稳定,避免 JDK 11+ 的模块化(--add-modules)干扰;
  • Tomcat 9 默认启用EL 3.0,支持${user?.name}安全导航操作符,比 Tomcat 7 的 EL 2.2 更健壮;
  • Maven 插件tomcat7-maven-plugin对 Tomcat 9 兼容性差,必须改用tomcat9-maven-plugin。

验证环境命令(Linux/macOS):

# 检查 JDK 版本(必须 1.8.x) java -version javac -version # 检查 Maven(需 3.6.3+) mvn -v # 检查 Tomcat(解压后 bin/catalina.sh 可执行) $TOMCAT_HOME/bin/version.sh

注意:若java -version输出11.0.x,需切换 JDK:export JAVA_HOME=$(/usr/libexec/java_home -v 1.8)(macOS)或export JAVA_HOME=/usr/lib/jvm/java-8-openjdk-amd64(Ubuntu)。版本不匹配会导致javax.servlet.http.HttpServlet类找不到、@WebServlet注解失效等静默失败。

3.2 项目构建:Maven 打包时的三个关键插件配置

pom.xml中必须包含以下三个插件,缺一不可:

<build> <plugins> <!-- 1. 编译插件:强制使用 JDK 8,避免 Maven 默认用高版本 --> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-compiler-plugin</artifactId> <version>3.8.1</version> <configuration> <source>1.8</source> <target>1.8</target> </configuration> </plugin> <!-- 2. 打包插件:生成 WAR 包,排除测试类和源码 --> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-war-plugin</artifactId> <version>3.2.3</version> <configuration> <failOnMissingWebXml>false</failOnMissingWebXml> <warSourceExcludes>**/*.java,**/test/**</warSourceExcludes> </configuration> </plugin> <!-- 3. Tomcat 插件:内嵌 Tomcat,一键启动(开发用) --> <plugin> <groupId>org.apache.tomcat.maven</groupId> <artifactId>tomcat9-maven-plugin</artifactId> <version>1.0</version> <configuration> <port>8080</port> <path>/demo</path> <uriEncoding>UTF-8</uriEncoding> </configuration> </plugin> </plugins> </build>

关键参数说明:

  • <failOnMissingWebXml>false</failOnMissingWebXml>:允许无web.xml,启用注解驱动(@WebServlet);
  • <warSourceExcludes>:确保打包时不把.java源文件打进 WAR,避免线上暴露源码;
  • <uriEncoding>UTF-8</uriEncoding>:解决中文表单提交乱码(GET 请求需额外在server.xml中加URIEncoding="UTF-8",本项目用 POST,故此处足够)。

3.3 启动与验证:一条命令跑起来,三步验证链路通

进入项目根目录,执行:

mvn clean compile tomcat9:run

等待日志出现INFO: Server startup in [xxx] milliseconds即启动成功。此时访问http://localhost:8080/demo/login.jsp。

三步验证法(必须手动执行,不能跳过):

  1. 表单提交验证:在login.jsp输入admin/123456,点击登录。观察浏览器地址栏是否变为http://localhost:8080/demo/messageBoard.jsp(重定向)或保持login.jsp但显示“登录成功”(转发)。这验证了LoginServlet的跳转逻辑。
  2. EL 表达式验证:在messageBoard.jsp中,检查<h2>欢迎,${currentUser.username}!</h2>是否正确显示用户名。若显示${currentUser.username}字面量,说明 EL 被禁用,需检查web.xml中<jsp-config><el-ignored>false</el-ignored></jsp-config>。
  3. 数据库验证:在register.jsp注册新用户后,直接访问http://localhost:8080/demo/hsql(HSQLDB 内置控制台),执行SELECT * FROM users;查看新记录是否插入。这确认UserService.register()的 JDBC 操作生效。

提示:若mvn tomcat9:run报错Address already in use: bind,说明 8080 端口被占用。修改pom.xml中<port>为8081,或执行lsof -i :8080(macOS)/netstat -ano | findstr :8080(Windows)查杀进程。


4. 避坑指南:5 个血泪经验总结,专治“页面空白”“404”“参数为空”“登录态丢失”

4.1 现象:访问login.jsp显示 404,但mvn tomcat9:run日志无报错

原因:login.jsp文件未放在src/main/webapp/目录下,而是误放至src/main/resources/或src/main/java/。Maven WAR 插件只打包webapp目录下的资源,其他路径文件不会出现在最终 WAR 包的根路径。
解决:严格遵循标准目录结构,login.jsp必须位于src/main/webapp/login.jsp。检查target/demo.war解压后是否存在login.jsp。

4.2 现象:表单提交后,LoginServlet.doPost()中request.getParameter("username")返回null

原因:HTML 表单的name属性与 Servlet 获取参数的 key 不一致。常见错误是表单写<input type="text" id="username">(只有id没有name),或name="userName"(驼峰)而 Servlet 写request.getParameter("username")(下划线)。
解决:检查login.jsp中<form>内所有<input>标签,确保name属性值与 Servlet 中getParameter()的字符串完全一致(区分大小写),例如<input type="text" name="username">。

4.3 现象:登录成功后跳转到messageBoard.jsp,但${currentUser.username}显示为空或报错

原因:LoginServlet中使用了response.sendRedirect("messageBoard.jsp")(重定向),导致request.setAttribute("currentUser", user)失效。重定向会发起新请求,原request对象生命周期结束。
解决:登录成功时必须用RequestDispatcher.forward()转发,而非sendRedirect()。正确代码:

// ✅ 正确:转发,request 作用域内属性可见 request.setAttribute("currentUser", user); request.getRequestDispatcher("/messageBoard.jsp").forward(request, response); // ❌ 错误:重定向,request 属性丢失 response.sendRedirect("messageBoard.jsp");

4.4 现象:注册时输入中文用户名,数据库存入乱码(如李四),但login.jsp显示正常

原因:HSQLDB 连接 URL 未指定字符集。db.properties中jdbc.url=jdbc:hsqldb:mem:demo缺少;hsqldb.default_table_type=cached;charset=UTF-8参数。
解决:修改db.properties:

jdbc.url=jdbc:hsqldb:mem:demo;hsqldb.default_table_type=cached;charset=UTF-8 jdbc.driver=org.hsqldb.jdbc.JDBCDriver

同时在DBUtil.getConnection()中,DriverManager.getConnection(url, props)前添加System.setProperty("file.encoding", "UTF-8");。

4.5 现象:关闭浏览器再打开messageBoard.jsp,仍显示“欢迎,admin!”,登录态未清除

原因:HttpSession默认超时时间为 30 分钟,且未在退出时主动session.invalidate()。关闭浏览器只是销毁了 Cookie,服务端 Session 仍在。
解决:在LogoutServlet(需自行添加)中执行:

HttpSession session = request.getSession(false); if (session != null) { session.invalidate(); // 彻底销毁 Session } response.sendRedirect("login.jsp");

并在web.xml中配置全局超时(可选):

<session-config> <session-timeout>1</session-timeout> <!-- 单位:分钟,设为 1 便于测试 --> </session-config>

5. 进阶技巧:用一个 Filter 实现全站登录拦截,三行代码让未登录用户无法访问留言板

5.1 为什么不用web.xml配置 Filter?手写@WebFilter更直观

本项目不使用web.xml声明 Filter,而是用注解方式,让拦截逻辑与 Servlet 一样“看得见、摸得着”:

@WebFilter(urlPatterns = {"/messageBoard.jsp", "/addMessage"}) public class LoginCheckFilter implements Filter { @Override public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { HttpServletRequest req = (HttpServletRequest) request; HttpServletResponse resp = (HttpServletResponse) response; // 1. 获取当前 Session,不创建新 Session HttpSession session = req.getSession(false); // 2. 检查 Session 中是否存在 currentUser if (session == null || session.getAttribute("currentUser") == null) { // 3. 未登录,重定向到登录页,并保存原请求路径(便于登录后跳回) String originalUrl = req.getRequestURL().toString(); resp.sendRedirect(req.getContextPath() + "/login.jsp?redirect=" + URLEncoder.encode(originalUrl, "UTF-8")); return; // ✅ 关键:return 阻止后续 chain.doFilter() } // 已登录,放行 chain.doFilter(request, response); } }

三行核心逻辑解析:

  • req.getSession(false):false参数表示“不创建新 Session”,避免未登录用户访问/messageBoard.jsp时意外生成空 Session;
  • session.getAttribute("currentUser") == null:检查登录态,currentUser是LoginServlet中session.setAttribute("currentUser", user)设置的 key;
  • resp.sendRedirect(...):重定向到登录页,并用redirect参数携带原始 URL,登录成功后可跳回(需在LoginServlet中读取request.getParameter("redirect"))。

5.2 登录成功后自动跳回原页面:LoginServlet的增强写法

在LoginServlet.doPost()验证成功后,加入跳转逻辑:

String redirectUrl = request.getParameter("redirect"); if (redirectUrl != null && !redirectUrl.isEmpty()) { // 清洗 redirectUrl,防止开放重定向漏洞(只允许跳转到本应用内) if (redirectUrl.startsWith(request.getContextPath())) { response.sendRedirect(redirectUrl); return; } } // 默认跳转到留言板 response.sendRedirect(request.getContextPath() + "/messageBoard.jsp");

提示:redirectUrl.startsWith(request.getContextPath())是基础防护,生产环境需用白名单校验(如Set<String> allowedPaths = Set.of("/messageBoard.jsp", "/profile.jsp");)。

5.3 留言防刷:用 Session 计数器限制每小时最多发 5 条

在AddMessageServlet中加入频率控制,无需 Redis,纯内存实现:

// 获取用户ID(从Session中) User user = (User) request.getSession().getAttribute("currentUser"); if (user == null) { response.sendError(HttpServletResponse.SC_FORBIDDEN); return; } // 构造计数器key:user:id:hour String counterKey = "user:" + user.getId() + ":" + LocalDateTime.now().getHour(); // 从Session中获取计数器(Integer类型) HttpSession session = request.getSession(); Integer count = (Integer) session.getAttribute(counterKey); if (count == null) { count = 0; } if (count >= 5) { request.setAttribute("error", "您已达到今日留言上限,请稍后再试"); request.getRequestDispatcher("/messageBoard.jsp").forward(request, response); return; } // 更新计数器 session.setAttribute(counterKey, count + 1); // 执行正常留言逻辑...

关键点:

  • 计数器 key 包含小时数(LocalDateTime.now().getHour()),保证每小时重置;
  • session.setAttribute()是线程安全的,同一用户 Session 内操作无需额外同步;
  • 若需跨节点集群部署,此方案失效,需升级为 Redis 计数器——这正是你理解“Session 共享”痛点的入口。

从那以后我每次带新人做 JavaWeb 实战,都会先让他们删掉spring-boot-starter-web,用这套 JSP+JavaBean+Servlet 手搓一遍登录和留言板。不是守旧,是逼他们看清request怎么从 TCP 包里解析出来、session怎么靠 Cookie 维持、forward和redirect的字节码差异在哪。当messageBoard.jsp上终于刷出自己刚提交的留言,那种“原来如此”的顿悟感,是任何框架文档都给不了的。希望帮到你。

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

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进