资讯详情 JSP+Servlet+JavaBean手写Web三层架构实战指南
📅 2026/10/23 14:54:07
简介这是一份面向Java Web初学者的MVC架构实战项目资源基于JSPJavaBeanServlet技术栈完整实现用户登录注册、留言板交互及数据分页三大核心功能适用于高校课程设计、Web开发入门实训与传统JavaEE技术栈巩固学习。资源包共78个文件包含14个JSP页面负责视图展示、14个Java源码含Servlet控制器与JavaBean模型类、14个编译后class文件、10个CSS样式文件、9个XML配置文件如web.xml、数据库连接配置以及SQL建库脚本等整体压缩包仅1.86MB轻量易部署。已有1611人学习下载适配无密码本地MySQL数据库库名webapp目录结构清晰呈现标准Web应用分层src/java为逻辑层WEB-INF/web.xml定义路由db.sql提供初始化脚本css与images保障基础界面呈现。读者可直接导入NetBeans运行快速理解Model-View-Controller职责分离、请求流转机制与数据库CRUD集成实践。1. 为什么还在用 JSPJavaBeanServlet 做登录注册和留言板这不是“过时”而是教学闭环的刚需很多同学看到这个标题第一反应是“都2024年了还写JSPSpring Boot不香吗”——但现实是某高校《Java Web程序设计》课程设计明确要求禁用框架必须手写三层结构某实验室课设答辩现场导师盯着web.xml里servlet-mapping的路径配置连问三遍“为什么不能写成/user/*”还有A同学交毕设被退回两次只因LoginServlet中混写了 JDBC 连接池初始化逻辑被批“职责混乱”。这套组合不是技术怀旧而是对 Web 基础链路最严苛的压测从 HTTP 请求如何触发 Servlet到 JavaBean 如何承载表单数据并校验再到 JSP 如何安全渲染动态内容——它逼你亲手拧紧每一个螺丝。适合正在赶 JavaWeb 课设、毕设 deadline 的人也适合想真正搞懂“请求怎么进来、响应怎么出去”的开发者。资源已按教学验收标准打包含完整可运行工程Tomcat 9 兼容、数据库建表脚本、各层关键代码注释、常见报错排查清单以及一份能直接粘贴进答辩PPT的架构说明页。2. 总体架构设计MVC 三层解耦与四个核心模块的职责边界这套方案严格遵循经典 MVC 模式但不是教科书里的抽象图而是每一行代码都对应明确职责的落地结构。整个系统划分为四个物理模块login_register登录注册主流程、message_board留言板CRUD、common_utils工具类、db_config数据库连接管理。它们之间通过接口契约通信杜绝跨层调用——比如MessageBoardServlet绝不直接 newMessageDAO而是通过MessageService接口注入。这种设计在答辩时能清晰回答“为什么这里要加 Service 层”而不是含糊说“为了分层”。2.1 控制层ServletHTTP 协议的守门人与调度中心Servlet 不是万能胶它的唯一使命是解析请求、调用业务、选择视图。以LoginServlet为例它只做三件事从request.getParameter()提取用户名密码调用UserService.login(username, password)获取返回结果根据结果request.getRequestDispatcher()转发到success.jsp或login.jsp并携带错误信息。关键点在于绝不处理任何业务逻辑绝不拼接 SQL绝不操作 session 以外的状态。下面这段代码是实际项目中经过三次重构后定稿的// LoginServlet.java protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { // 1. 提取参数注意此处必须做 trim()否则空格密码会绕过校验 String username request.getParameter(username).trim(); String password request.getParameter(password).trim(); // 2. 参数非空校验这是控制层的底线职责 if (username.isEmpty() || password.isEmpty()) { request.setAttribute(error, 用户名或密码不能为空); request.getRequestDispatcher(/login.jsp).forward(request, response); return; // 必须 return避免后续逻辑执行 } // 3. 调用服务层获取结果 UserService userService new UserServiceImpl(); User user userService.login(username, password); // 4. 根据业务结果跳转注意forward 是服务器端跳转不会改变URL if (user ! null) { // 登录成功将用户信息存入 session不是 request request.getSession().setAttribute(currentUser, user); response.sendRedirect(request.getContextPath() /message_board.jsp); } else { request.setAttribute(error, 用户名或密码错误); request.getRequestDispatcher(/login.jsp).forward(request, response); } }提示response.sendRedirect()和request.getRequestDispatcher().forward()的选择是高频答辩问题。前者是客户端重定向URL 变化、两次请求后者是服务器端转发URL 不变、一次请求。登录成功后跳转到留言板必须用sendRedirect()否则刷新页面会重复提交表单——这是血泪经验。2.2 业务层Service事务边界与校验规则的执行者Service 层是真正的“大脑”它协调 DAO、封装事务、执行校验。以UserService为例它的核心方法login()必须满足原子性查询用户、比对密码、更新最后登录时间三步要么全成功要么全回滚一致性密码必须用BCryptPasswordEncoder加密比对绝不能明文比较隔离性同一用户并发登录时不能出现脏读比如查到未更新的 last_login_time。因此UserServiceImpl中的login方法必须包裹在事务中。由于我们禁用框架事务管理需手动控制// UserServiceImpl.java public User login(String username, String password) { Connection conn null; PreparedStatement pstmt null; ResultSet rs null; User user null; try { conn DBUtil.getConnection(); // 从DBUtil获取连接 conn.setAutoCommit(false); // 关闭自动提交开启事务 // 1. 查询用户注意使用 ? 占位符防SQL注入 String sql SELECT id, username, password, email, last_login_time FROM users WHERE username ?; pstmt conn.prepareStatement(sql); pstmt.setString(1, username); rs pstmt.executeQuery(); if (rs.next()) { String dbPassword rs.getString(password); // 2. 密码加密比对BCrypt 是 JavaWeb 教学标配比 MD5/SHA 更安全 if (BCrypt.checkpw(password, dbPassword)) { user new User(); user.setId(rs.getLong(id)); user.setUsername(rs.getString(username)); user.setEmail(rs.getString(email)); // 3. 更新最后登录时间同一事务内 updateLastLoginTime(conn, rs.getLong(id)); } } conn.commit(); // 手动提交事务 } catch (SQLException e) { try { if (conn ! null) conn.rollback(); // 出错回滚 } catch (SQLException rollbackEx) { rollbackEx.printStackTrace(); } e.printStackTrace(); } finally { DBUtil.closeAll(rs, pstmt, conn); // 必须关闭资源否则连接泄漏 } return user; } private void updateLastLoginTime(Connection conn, Long userId) throws SQLException { String sql UPDATE users SET last_login_time NOW() WHERE id ?; try (PreparedStatement pstmt conn.prepareStatement(sql)) { pstmt.setLong(1, userId); pstmt.executeUpdate(); } }注意DBUtil.closeAll()是自定义工具方法确保ResultSet、PreparedStatement、Connection按反向顺序关闭。很多同学翻车就在这里——conn.close()写在rs.close()前导致rs关闭失败却无异常后续查不到数据还以为逻辑错了。2.3 数据访问层DAOSQL 的翻译官与连接的守夜人DAO 层只做一件事把 Java 对象和 SQL 语句互相翻译。它不关心事务不处理校验只专注 CRUD。以MessageDAO为例其listMessages()方法必须做到分页支持教学项目常要求显示前10条时间倒序最新留言在最上面防 SQL 注入所有参数必须用?字段映射准确比如content字段可能含 HTML 标签需转义输出。// MessageDAO.java public ListMessage listMessages(int offset, int limit) { ListMessage messages new ArrayList(); Connection conn null; PreparedStatement pstmt null; ResultSet rs null; try { conn DBUtil.getConnection(); // 使用 LIMIT ? OFFSET ? 实现分页MySQL语法 String sql SELECT id, user_id, content, create_time FROM messages ORDER BY create_time DESC LIMIT ? OFFSET ?; pstmt conn.prepareStatement(sql); pstmt.setInt(1, limit); pstmt.setInt(2, offset); rs pstmt.executeQuery(); while (rs.next()) { Message msg new Message(); msg.setId(rs.getLong(id)); msg.setUserId(rs.getLong(user_id)); // 注意content 直接取值但 JSP 渲染时需用 EL 表达式转义 msg.setContent(rs.getString(content)); msg.setCreateTime(rs.getTimestamp(create_time)); messages.add(msg); } } catch (SQLException e) { e.printStackTrace(); } finally { DBUtil.closeAll(rs, pstmt, conn); } return messages; }提示LIMIT ? OFFSET ?是 MySQL 分页标准写法。如果部署到 Oracle需改为ROWNUM子查询——但教学环境默认 MySQL不必过度扩展。重点是让学生理解“分页是数据库做的不是 Java 循环截取的”。3. 关键实现细节从 JavaBean 封装到 JSP 安全渲染的全链路JavaBean 不是摆设它是 MVC 中承上启下的数据载体JSP 也不只是模板它是最后一道安全防线。这两个组件的实现质量直接决定系统是否“看着能跑、一用就崩”。3.1 JavaBean带校验逻辑的 POJO不是属性容器很多同学把 JavaBean 写成只有 getter/setter 的空壳这是重大误区。合格的 JavaBean 必须包含字段约束如username长度 3~20email符合正则状态标识isValid()方法统一返回校验结果错误收集getErrors()返回 Map记录每个字段的错误信息。以User类为例// User.java public class User { private Long id; private String username; private String password; private String email; private Date lastLoginTime; // 校验错误存储 private MapString, String errors new HashMap(); public boolean isValid() { errors.clear(); // 每次校验前清空 // 用户名校验 if (username null || username.trim().length() 3 || username.trim().length() 20) { errors.put(username, 用户名长度必须为3-20个字符); } // 邮箱校验简单正则教学够用 if (email ! null !email.matches(^[a-zA-Z0-9._%-][a-zA-Z0-9.-]\\.[a-zA-Z]{2,}$)) { errors.put(email, 邮箱格式不正确); } // 密码非空注册时校验登录时不校验密码强度 if (password ! null password.length() 6) { errors.put(password, 密码长度不能少于6位); } return errors.isEmpty(); } public MapString, String getErrors() { return Collections.unmodifiableMap(errors); } // getter/setter 省略... }注意isValid()方法不抛异常而是通过errorsMap 返回具体问题。这样在 Servlet 中可以if (!user.isValid()) { request.setAttribute(errors, user.getErrors()); }JSP 用${errors.username}直接取错体验远好于弹窗 alert。3.2 JSP 页面EL 表达式与 JSTL 的安全协作JSP 是最终面向用户的界面必须解决两个致命问题XSS 攻击用户在留言板输入scriptalert(1)/script必须原样显示而非执行表单重复提交刷新成功页导致重复发帖。解决方案是所有动态内容必须用 EL 表达式${}且对可能含 HTML 的字段强制转义表单提交必须用 POST且成功后重定向PRG 模式。message_board.jsp关键片段% page contentTypetext/html;charsetUTF-8 languagejava % % taglib prefixc urihttp://java.sun.com/jsp/jstl/core % % taglib prefixfn urihttp://java.sun.com/jsp/jstl/functions % !DOCTYPE html html head title留言板/title meta charsetUTF-8 /head body !-- 1. 显示当前用户 -- div欢迎${currentUser.username}a href${pageContext.request.contextPath}/logout退出/a/div !-- 2. 留言表单POST 提交防止刷新重复 -- form action${pageContext.request.contextPath}/AddMessageServlet methodpost textarea namecontent placeholder写下你的想法... required/textarea button typesubmit发布/button /form !-- 3. 留言列表关键${fn:escapeXml(msg.content)} 防 XSS -- div classmessages c:forEach items${messages} varmsg varStatusstatus div classmessage-item strong用户${msg.userId}/strong span${fn:escapeXml(msg.content)}/span small${msg.createTime}/small /div /c:forEach /div !-- 4. 分页导航JSTL 控制显示逻辑 -- c:if test${not empty messages} div classpagination c:forEach begin1 end${totalPages} vari a href?page${i}${i}/a /c:forEach /div /c:if /body /html提示fn:escapeXml()是 JSTL 函数库的核心安全措施它把转成lt;转成gt;彻底阻断脚本执行。很多同学漏掉这行导致“留言板变 XSS 演示台”。另外c:forEach的varStatus属性能获取循环索引方便做“仅显示前5条”等教学需求。3.3 web.xml 配置Servlet 映射与全局初始化的硬性规范web.xml是整个 Web 应用的宪法教学项目中常被忽视却是答辩必问点。它必须明确Servlet 名称与类的绑定servletURL 路径与 Servlet 的映射servlet-mapping全局上下文参数如数据库驱动、URL监听器注册如ContextLoaderListener初始化 DAO。以下是精简但完整的web.xml片段?xml version1.0 encodingUTF-8? web-app xmlnshttp://xmlns.jcp.org/xml/ns/javaee xmlns:xsihttp://www.w3.org/2001/XMLSchema-instance xsi:schemaLocationhttp://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_4_0.xsd version4.0 !-- 1. 全局上下文参数 -- context-param param-namedriver/param-name param-valuecom.mysql.cj.jdbc.Driver/param-value /context-param context-param param-nameurl/param-name param-valuejdbc:mysql://localhost:3306/javaweb_demo?useSSLfalseamp;serverTimezoneUTC/param-value /context-param context-param param-nameusername/param-name param-valueroot/param-value /context-param context-param param-namepassword/param-name param-value123456/param-value /context-param !-- 2. Servlet 定义 -- servlet servlet-nameLoginServlet/servlet-name servlet-classcom.example.servlet.LoginServlet/servlet-class /servlet servlet servlet-nameAddMessageServlet/servlet-name servlet-classcom.example.servlet.AddMessageServlet/servlet-class /servlet !-- 3. Servlet 映射路径必须以 / 开头 -- servlet-mapping servlet-nameLoginServlet/servlet-name url-pattern/login/url-pattern /servlet-mapping servlet-mapping servlet-nameAddMessageServlet/servlet-name url-pattern/add_message/url-pattern /servlet-mapping !-- 4. 监听器应用启动时初始化数据库连接池 -- listener listener-classcom.example.listener.ContextLoaderListener/listener-class /listener !-- 5. 欢迎文件 -- welcome-file-list welcome-filelogin.jsp/welcome-file /welcome-file-list /web-app注意url-pattern的值必须是/login不能是login缺/会导致 404param-value中的必须写成amp;否则 XML 解析失败welcome-file设为login.jsp确保项目启动后直接进入登录页符合教学演示逻辑。4. 避坑指南五个让答辩老师当场皱眉的典型问题及根治方案这套方案在多个高校课设中验证过以下问题是学生踩坑率最高、老师提问最密集的五个点。每一条都来自真实翻车现场附带可立即执行的修复命令和验证方法。4.1 现象登录后跳转到空白页浏览器 URL 显示http://localhost:8080/success.jsp原因LoginServlet中用了request.getRequestDispatcher(/success.jsp).forward(request, response)但success.jsp文件实际放在WebContent下的子目录如pages/success.jsp路径没写对。解决检查web.xml中welcome-file和所有getRequestDispatcher()的路径。路径必须相对于WebContent根目录。若success.jsp在WebContent/pages/下则应写request.getRequestDispatcher(/pages/success.jsp)。验证方法直接在浏览器访问http://localhost:8080/pages/success.jsp能打开即路径正确。4.2 现象留言板显示乱码中文变成??或方块原因三个环节编码不一致JSP 页面声明% page contentTypetext/html;charsetUTF-8 %缺失Tomcatserver.xml中Connector未设置URIEncodingUTF-8MySQL 连接 URL 未加characterEncodingutf8。解决确保所有 JSP 顶部有% page contentTypetext/html;charsetUTF-8 %修改 Tomcatconf/server.xml找到Connector port8080 ... /添加URIEncodingUTF-8修改web.xml中数据库 URL 为jdbc:mysql://localhost:3306/javaweb_demo?useSSLfalseserverTimezoneUTCcharacterEncodingutf8。验证重启 Tomcat在登录框输入中文用户名提交后看数据库users表中username字段是否为中文。4.3 现象注册新用户时报错java.sql.SQLException: Field xxx doesnt have a default value原因MySQL 表字段设置了NOT NULL但插入 SQL 中未提供该字段值如last_login_time未设默认值或未在 INSERT 中指定。解决修改建表语句为NOT NULL字段加DEFAULT。例如CREATE TABLE users ( id BIGINT PRIMARY KEY AUTO_INCREMENT, username VARCHAR(20) NOT NULL, password VARCHAR(100) NOT NULL, email VARCHAR(50), last_login_time DATETIME DEFAULT CURRENT_TIMESTAMP );验证执行INSERT INTO users (username, password) VALUES (test, 123);确认不报错。4.4 现象点击“退出”链接后再点浏览器后退又回到留言板session 未销毁原因LogoutServlet中只调用了session.invalidate()但未重定向到登录页导致浏览器缓存了留言板页面。解决LogoutServlet必须invalidate()后sendRedirect()protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { HttpSession session request.getSession(false); if (session ! null) { session.invalidate(); } response.sendRedirect(request.getContextPath() /login.jsp); // 关键 }验证退出后按浏览器后退键页面应显示“404”或跳转回登录页而非留言板。4.5 现象Eclipse 中启动 Tomcat 报错java.lang.ClassNotFoundException: com.mysql.cj.jdbc.Driver原因MySQL 驱动 JAR 包未放入WEB-INF/lib目录或放入了但名称含版本号如mysql-connector-java-8.0.33.jar而web.xml中driver参数写的是com.mysql.jdbc.Driver老版。解决下载mysql-connector-java-8.0.xx.jar复制到项目WebContent/WEB-INF/lib/下确认web.xml中param-value为com.mysql.cj.jdbc.Driver注意cjEclipse 中右键项目 →Properties→Deployment Assembly→ 确认lib目录已添加。验证重启 Tomcat在DBUtil.getConnection()中打日志确认能成功获取连接。5. 验收与答辩用三张表、一个流程图、两次实测搞定全部考核点答辩不是背诵代码而是证明你真正掌控了整个链路。我带过的十几个项目最终能一次性通过的都做了这三件事用表格固化验收标准、用流程图讲清数据流向、用两次实测覆盖边界场景。下面直接给你可复用的交付物。5.1 功能验收表对照打钩答辩前逐项自检这张表是答辩材料的核心附件打印出来贴在笔记本上每完成一项打钩。老师提问时直接翻开说“第3项我验证了……”比说“我做了”有力十倍。序号功能模块验收标准自检结果✓/✗备注1用户登录输入正确账号密码 → 跳转留言板错误密码 → 提示“用户名或密码错误”✓已用 Postman 测试2用户注册输入合法信息 → 插入数据库用户名重复 → 提示“用户名已存在”✓数据库 unique 约束生效3留言发布发布后立即显示在列表顶部内容含script→ 原样显示不执行✓fn:escapeXml()已验证4会话管理登录后关闭浏览器再打开 → 仍需登录退出后后退 → 显示登录页✓session.invalidate()生效5数据库连接启动 Tomcat 时控制台无ClassNotFoundExceptionDBUtil.getConnection()返回非 null✓mysql-connector-java-8.0.33.jar已部署提示备注栏写具体验证方式比如“用 Postman 测试”比“已测试”专业得多。老师可能追问“怎么用 Postman 测试的”你就答“用 POST 方法请求/loginBody 选 x-www-form-urlencoded填入 usernametestpassword123看响应状态码和 body 内容”。5.2 核心流程图一张图讲清“用户点击登录按钮后发生了什么”答辩时老师最爱问“这个请求是怎么处理的”画流程图是最直观的回答。不要用 Visio 画复杂 UML就用纯文本描述确保每一步对应一行代码用户点击登录按钮 ↓ 浏览器发送 POST 请求到 /login由 web.xml 中 servlet-mapping 定义 ↓ Tomcat 调用 LoginServlet 的 doPost() 方法 ↓ LoginServlet 调用 request.getParameter() 提取参数 ↓ LoginServlet 调用 UserServiceImpl.login() 方法 ↓ UserServiceImpl 通过 DBUtil.getConnection() 获取数据库连接 ↓ UserServiceImpl 执行 PreparedStatement 查询 users 表 ↓ 查询结果封装为 User 对象返回给 LoginServlet ↓ LoginServlet 判断 user 是否为 null决定 forward 或 sendRedirect ↓ 最终渲染 login.jsp 或 message_board.jsp注意流程图中每个箭头必须指向一个具体的类或方法名不能写“业务处理”“数据处理”等模糊词。当老师问“查询是在哪一步执行的”你能立刻指出“第7步UserServiceImpl 中的 PreparedStatement.executeQuery()”。5.3 边界场景实测两次必做测试堵死所有质疑漏洞教学项目最怕“理论上可行实际上翻车”。我要求所有学生必须完成以下两次实测并把截图和日志贴在答辩 PPT 最后一页实测一高并发登录压力测试用 Apache Bench目的验证UserService.login()的线程安全性。虽然教学项目不强调并发但老师可能问“如果100人同时登录会怎样”。命令ab -n 100 -c 10 http://localhost:8080/login?usernametestpassword123预期结果Complete requests: 100Failed requests: 0Requests per second: 50。关键证据查看 Tomcatlogs/catalina.out确认无ConcurrentModificationException或连接池耗尽日志。实测二XSS 攻击模拟测试手工注入目的证明fn:escapeXml()生效。步骤在留言板输入img srcx onerroralert(xss)提交后查看源代码Chrome 右键 → 查看网页源代码确认变成了lt;变成了gt;。预期结果页面显示纯文本img srcx onerroralert(xss)不弹窗、不加载图片。关键证据截图源代码中转义后的 HTML 片段。从那以后我每次带学生做 JavaWeb 课设都强制他们在LoginServlet的doPost方法开头加一行日志System.out.println([DEBUG] Login request received for user: username);并在答辩前用ab命令跑一遍并发测试——不是为了炫技而是让“我能跑”变成“我证明了它能跑”。希望帮到你。本文还有配套的精品资源点击获取