ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

JSP+JavaBean+Servlet实战:从登录注册到留言板搞懂JavaWeb核心链路

JSP+JavaBean+Servlet实战:从登录注册到留言板搞懂JavaWeb核心链路 简介基于JSPJavaBeanServlet的JavaWeb实战项目以简单MVC模式完整实现登录注册、留言板、分页查询三项核心功能适合JavaWeb初学者、高校学生或需要课程设计参考的开发者。项目将model层的数据读写、view层的页面展示、controller层的请求分发清晰分离数据库名为webapp且无密码附带db.sql脚本便于快速还原数据环境。压缩包共78个文件、1.86MB主要包含14个JSP页面、14个Java源文件、14个class编译文件、10个CSS样式、9个XML配置以及jar包、properties配置和NetBeans工程文件等目录结构清晰可直接导入IDE运行。登录注册、留言板与分页功能贯穿用户认证、内容发布与列表查询流程已有1611人学习下载能帮助理解MVC分层协作方式是JavaWeb基础交互与持久化开发的实用范本。其中JSP页面负责表单与列表展示Java源码提供业务逻辑SQL脚本用于初始化webapp库目录结构有助于初学者按图索骥。整个项目结构紧凑、无冗余依赖适合作为JavaWeb课程设计的参考蓝本。1. 从登录注册到留言板这个JSPJavaBeanServlet项目为什么值得亲手写一遍很多做JavaWeb的同行第一个拿得出手的完整项目往往不是Spring Boot全家桶而是用JSPServlet拼出来的登录注册加留言板。这个组合听起来老但它把表单提交、请求转发与重定向、Session状态管理、JDBC数据库操作这几条主线串成了闭环是理解服务端Web请求生命周期最直接的路径。这个项目解决的核心问题是怎么让用户注册、登录、发留言并让这些数据真正落库、再查出来渲染到页面上。适合正在做课设的学生、转行Java开发的新人以及想补服务端基础、再回头啃Spring MVC的从业者。别觉得它简单把这条链路彻底吃透后面看任何框架都能一眼认出它的影子。2. JSP、JavaBean、Servlet的分工看清三者边界才能少写垃圾代码2.1 三者不是平级关系控制器、模型、视图各司其职很多初学者会把Java代码直接塞进JSP页面里比如在JSP里写% Connection conn DriverManager.getConnection(...) %虽然能跑通但页面一旦复杂就彻底失控。这个项目采用的是Model 2架构本质上就是MVC在JavaWeb里的落地形态Servlet充当控制器负责接收请求、调用业务逻辑、决定跳转到哪个页面JavaBean充当模型承载业务数据和数据处理逻辑JSP只负责渲染从请求域中取数据并输出HTML。组件角色在这个项目中的具体产物Servlet控制器LoginServlet、RegisterServlet、MessageServletJavaBean模型User、Message实体以及封装JDBC操作的DAO类JSP视图login.jsp、register.jsp、messageBoard.jsp这样拆分之后改页面样式不用碰Java代码改业务逻辑不用翻HTML维护成本明显下降。我把数据库访问单独抽成一个JdbcUtils工具类再把每个表的增删改查封装成DAOServlet里几乎不写SQL只做参数接收、逻辑判断和页面跳转整个项目的思路会清晰很多。2.2 一次登录请求的完整链路从点击按钮到页面渲染先走一遍流程再上手写代码会顺手得多。假设用户在login.jsp输入用户名和密码后点击提交请求路径大概是这样的浏览器表单提交 POST /login - web.xml中Servlet映射匹配到LoginServlet - LoginServlet调用doPost()从request中取参数 - 调用UserDao查询数据库校验用户名密码 - 校验通过执行request.getSession().setAttribute(user, user) - 重定向到messageBoard.jsp或转发到留言板Servlet - 页面从Session或请求域中取出用户数据并渲染这里有一个关键差别登录成功后到底该用response.sendRedirect()还是request.getRequestDispatcher().forward()我一般会分两种情况处理。登录成功用重定向因为要让浏览器重新发起一次GET请求刷新地址栏URL避免表单重复提交登录失败用转发把错误提示放进request域留在登录页展示。留言列表页则走Servlet查询后转发到JSP这样地址栏保持/messageList用户刷新时重复的是查询请求而不是提交请求行为更合理。3. 数据库设计与登录注册落地建表、加密、Session三条主线3.1 建表SQL与JDBC连接封装先稳住数据层用户表和留言表是基础字段尽量精简。我常用的建表语句如下CREATE DATABASE IF NOT EXISTS webdemo DEFAULT CHARACTER SET utf8mb4; USE webdemo; CREATE TABLE t_user ( id INT PRIMARY KEY AUTO_INCREMENT, username VARCHAR(50) NOT NULL UNIQUE, password VARCHAR(64) NOT NULL, nickname VARCHAR(50) DEFAULT , create_time DATETIME DEFAULT CURRENT_TIMESTAMP ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; CREATE TABLE t_message ( id INT PRIMARY KEY AUTO_INCREMENT, user_id INT NOT NULL, content VARCHAR(500) NOT NULL, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, CONSTRAINT fk_msg_user FOREIGN KEY (user_id) REFERENCES t_user(id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;两个字段值得说明一下。password我设成VARCHAR(64)对应MD5加密后32位字符串的两倍长度给后续升级加密算法留余量content限制500字是为了防止留言板被一次性塞入超大文本拖垮页面渲染。外键约束在单机小项目里可以用但一旦分库分表就会成为性能瓶颈这个项目规模用外键没问题。JDBC连接我封装成一个工具类核心代码如下public class JdbcUtils { private static final String URL jdbc:mysql://localhost:3306/webdemo?useUnicodetruecharacterEncodingutf8useSSLfalseserverTimezoneAsia/Shanghai; private static final String USER root; private static final String PASSWORD yourpassword; static { try { Class.forName(com.mysql.cj.jdbc.Driver); } catch (ClassNotFoundException e) { e.printStackTrace(); } } public static Connection getConnection() throws SQLException { return DriverManager.getConnection(URL, USER, PASSWORD); } public static void close(Connection conn, PreparedStatement ps, ResultSet rs) { if (rs ! null) { try { rs.close(); } catch (SQLException e) { e.printStackTrace(); } } if (ps ! null) { try { ps.close(); } catch (SQLException e) { e.printStackTrace(); } } if (conn ! null) { try { conn.close(); } catch (SQLException e) { e.printStackTrace(); } } } }连接串里的几个参数是踩坑换来的characterEncodingutf8保证中文读写不乱码useSSLfalse避免本地连接时报SSL警告serverTimezoneAsia/Shanghai解决MySQL 8.x版本时区偏差导致的日期报错。需要特别提醒的是Class.forName这一步在MySQL 8.x驱动里通常可以省略但保留它能让项目天然兼容旧版本驱动属于低成本高保障的做法。3.2 注册逻辑重复校验和密码加密一个都不能少注册接口的完整逻辑是接收表单参数、检查用户名是否已存在、密码加密后落库、跳转到登录页。这里要强调的是注册时绝不能把明文密码直接存进数据库否则一旦库表泄露用户在其他平台的密码也会被撞库尝试。我用的方案是MD5加盐WebServlet(/register) public class RegisterServlet extends HttpServlet { protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { request.setCharacterEncoding(UTF-8); String username request.getParameter(username); String password request.getParameter(password); String nickname request.getParameter(nickname); if (username null || username.trim().isEmpty() || password null || password.trim().isEmpty()) { request.setAttribute(error, 用户名和密码不能为空); request.getRequestDispatcher(/register.jsp).forward(request, response); return; } UserDao userDao new UserDao(); if (userDao.findByUsername(username) ! null) { request.setAttribute(error, 用户名已被占用); request.getRequestDispatcher(/register.jsp).forward(request, response); return; } String salt UUID.randomUUID().toString().substring(0, 8); String encryptedPassword Md5Util.md5(password salt); User user new User(username, encryptedPassword, salt, nickname); boolean success userDao.insert(user); if (success) { response.sendRedirect(request.getContextPath() /login.jsp); } else { request.setAttribute(error, 注册失败请稍后重试); request.getRequestDispatcher(/register.jsp).forward(request, response); } } }这段代码里有三个细节值得展开。第一request.setCharacterEncoding(UTF-8)必须写在getParameter之前否则POST提交的中文用户名会乱码第二用户名重复校验采取先查再插的方式但高并发场景下需要配合数据库唯一索引兜底否则两个请求同时查到用户名不存在、同时插入就会出现重复数据所以我在建表时加了UNIQUE约束第三加盐的做法是把随机盐拼在密码后面再做MD5盐值本身也存进用户表验证登录时用同一个盐拼上输入的密码再算一次MD5比对。3.3 登录逻辑Session写入与登录状态校验登录比注册多了一个核心动作把用户状态写入Session。我这里直接给登录Servlet的关键代码WebServlet(/login) public class LoginServlet extends HttpServlet { protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { request.setCharacterEncoding(UTF-8); String username request.getParameter(username); String password request.getParameter(password); UserDao userDao new UserDao(); User user userDao.findByUsername(username); if (user null) { request.setAttribute(error, 用户不存在); request.getRequestDispatcher(/login.jsp).forward(request, response); return; } String encrypted Md5Util.md5(password user.getSalt()); if (!encrypted.equals(user.getPassword())) { request.setAttribute(error, 密码错误); request.getRequestDispatcher(/login.jsp).forward(request, response); return; } HttpSession session request.getSession(); session.setAttribute(currentUser, user); session.setMaxInactiveInterval(30 * 60); response.sendRedirect(request.getContextPath() /messageList); } }session.setMaxInactiveInterval(30 * 60)的意思是Session在30分钟内无操作会自动失效单位是秒。我习惯把用户对象整个放进Session而不是只放用户名因为留言板页面要展示昵称和头像这类扩展信息到时候直接从Session里取对象属性即可不用再查一次库。登录成功后跳转到/messageList这是一个单独的服务端资源需要再写一个Servlet来查询留言列表并转发到JSP而不是直接重定向到一个静态的messageBoard.jsp文件否则页面上的留言数据没人提供。4. 留言板核心功能发布、列表展示与分页查询4.1 留言表与发布逻辑把当前登录用户关联上留言留言表的设计里有个关键外键user_id它保证每条留言都能追溯到发布者。发布留言的Servlet代码如下WebServlet(/message/add) public class MessageAddServlet extends HttpServlet { protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { request.setCharacterEncoding(UTF-8); HttpSession session request.getSession(false); if (session null || session.getAttribute(currentUser) null) { response.sendRedirect(request.getContextPath() /login.jsp); return; } User currentUser (User) session.getAttribute(currentUser); String content request.getParameter(content); if (content null || content.trim().isEmpty()) { request.setAttribute(error, 留言内容不能为空); request.getRequestDispatcher(/messageBoard.jsp).forward(request, response); return; } if (content.trim().length() 500) { request.setAttribute(error, 留言内容不能超过500字); request.getRequestDispatcher(/messageBoard.jsp).forward(request, response); return; } MessageDao messageDao new MessageDao(); boolean success messageDao.insert(new Message(currentUser.getId(), content.trim())); if (success) { response.sendRedirect(request.getContextPath() /messageList); } else { request.setAttribute(error, 发布失败请重试); request.getRequestDispatcher(/messageBoard.jsp).forward(request, response); } } }注意这里我用了request.getSession(false)这个API的含义是如果当前请求没有关联Session就返回null而不是像getSession()那样自动创建一个新Session。这个区别很关键——如果用户未登录getSession()会先创建一个空的Session再返回导致下面的判空逻辑永远不成立登录校验直接被绕过。发布成功后重定向到/messageList让浏览器重新发起GET请求获取最新列表避免按F5刷新时提示确认重新提交表单。4.2 列表展示与分页参数用LIMIT offset, size控制压力留言板一旦数据量上来一次性把所有留言查出来渲染到页面上数据库和浏览器的压力都会直线上升。这里我用最简单的分页方案每页10条通过URL参数传递当前页码。WebServlet(/messageList) public class MessageListServlet extends HttpServlet { private static final int PAGE_SIZE 10; protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { HttpSession session request.getSession(false); if (session null || session.getAttribute(currentUser) null) { response.sendRedirect(request.getContextPath() /login.jsp); return; } int page 1; String pageStr request.getParameter(page); if (pageStr ! null !pageStr.isEmpty()) { try { page Integer.parseInt(pageStr); } catch (NumberFormatException e) { page 1; } } if (page 1) page 1; MessageDao messageDao new MessageDao(); int total messageDao.count(); int totalPages (total PAGE_SIZE - 1) / PAGE_SIZE; if (page totalPages totalPages 0) page totalPages; ListMessage messageList messageDao.findByPage(page, PAGE_SIZE); ListMapString, Object viewData new ArrayList(); UserDao userDao new UserDao(); for (Message msg : messageList) { MapString, Object item new HashMap(); item.put(message, msg); item.put(nickname, userDao.findById(msg.getUserId()).getNickname()); viewData.add(item); } request.setAttribute(messageList, viewData); request.setAttribute(currentPage, page); request.setAttribute(totalPages, totalPages); request.getRequestDispatcher(/messageBoard.jsp).forward(request, response); } }分页参数里的totalPages (total PAGE_SIZE - 1) / PAGE_SIZE是向上取整的标准写法比如总共有25条数据每页10条计算结果是(25 9) / 10 3页。还有一个容易被忽略的细节当用户手动把地址栏的page参数改成负数或超界值时要强制归一化到合法范围否则SQL里的LIMIT -1, 10直接报错。列表查询里我顺手做了个关联查询根据user_id查出昵称拼装成视图对象这样JSP页面不用写复杂嵌套逻辑直接遍历就能渲染。5. 常见问题排查五个必踩的坑及解决记录5.1 中文乱码页面、请求、数据库三层都要一致现象注册时输入中文用户名注册成功后列表页显示为乱码或问号。原因前三层有一层编码不一致就会出问题JSP页面未设置pageEncoding、Servlet读取参数前未调用setCharacterEncoding、数据库表默认字符集不是utf8mb4。解决JSP文件头部固定写% page languagejava contentTypetext/html; charsetUTF-8 pageEncodingUTF-8%Servlet的doPost开头第一行调用request.setCharacterEncoding(UTF-8)建库时显式指定DEFAULT CHARSETutf8mb4并在JDBC连接串中加characterEncodingutf8。这三处少一处排查起来就像在迷宫里找门。5.2 表单重复提交按F5刷新后反复插入留言现象用户发布一条留言后按F5刷新浏览器弹出确认重新提交表单的提示点确定后同一条留言被重复插入。原因表单提交是POST请求刷新时浏览器默认重放上一次的POST请求Servlet再次执行插入。解决发布成功后不要转发到JSP而是用sendRedirect重定向到/messageList让浏览器改为发起GET请求。原则是所有写入操作成功后一律重定向列表展示一律走GET转发。这个习惯从这个小项目里养成了后面做任何Web项目都受益。5.3 登录状态串号或丢失Session与请求域混用现象用户A登录后在页面上偶尔能看到用户B的未读消息或个人信息。原因把用户状态存进了request域而不是Session域或者在JSP中使用不当的作用域查找导致数据穿透。解决登录用户信息统一放Session留言板当前用户身份从session.getAttribute(currentUser)取。还要注意JSP里${user}默认会依次从page、request、session、application四个作用域查找如果不同页面存了同名属性极容易出现取错数据的情况。养成习惯拿到用户数据后先在代码里用String nickname ((User)session.getAttribute(currentUser)).getNickname();显式取出而不是直接在JSP里到处写EL表达式。5.4 连接未关闭导致MySQL连接数耗尽现象项目跑一段时间后页面加载越来越慢最终报Too many connections错误重启后才恢复。原因每次请求都新建Connection但异常分支里只关闭了PreparedStatement和ResultSetConnection没有进finally块关闭连接长期泄漏。解决所有JDBC操作统一走JdbcUtils工具类在finally块中调用JdbcUtils.close(conn, ps, rs)并确保finally中先判断是否为null再关闭。注意close顺序先关ResultSet再关PreparedStatement最后关Connection反了会出现依赖未释放的警告。5.5 Servlet注解与web.xml映射双写导致路由404现象Servlet类上写了WebServlet(/login)又在web.xml中配置了servlet-mapping指向同一路径结果访问时偶尔404。原因Tomcat加载时会解析注解和web.xml二者重复映射同一URL容器行为依赖具体版本可能出现覆盖或冲突。解决二选一。小项目建议全用WebServlet注解省去web.xml配置如果必须用web.xml管理映射就把类上的注解去掉。判断规则很简单路由统一原则一份资源只在一个地方注册。6. 验证方法与进阶方向让这条链路变成可复用的基本功项目写完不算完验证做没做对才是关键。我会在浏览器按F12打开开发者工具切到Network面板然后完整走一遍注册、登录、发留言三个操作。每一步都看三样东西请求URL和方法是否如预期POST还是GET、响应状态码登录成功应是302列表页应是200、请求头里的Cookie是否带上了JSESSIONID。如果登录成功后再次访问留言板时JSESSIONID丢失多半是Session配置或重定向路径写错导致的。接口层面用Postman或curl再验证一遍更稳妥。比如模拟未登录状态下直接访问/messageList预期应被重定向到login.jsp用工具直接POST一个不存在的用户名登录预期返回错误提示且不产生异常堆栈。这类边界场景在浏览器里操作很麻烦但用工具测试能快速暴露Servlet里空指针和路径判断问题。这个项目稳定运行之后我有三个值得做的进阶方向。第一把JDBC换成Druid或HikariCP连接池替换掉DriverManager手动建连的模式这是从单体脚本走向工程化的第一步。第二密码加密从MD5加盐升级为BCryptBCrypt自带盐且每次哈希结果不同破解成本高一个量级。第三用Filter统一做登录状态校验把所有需要认证的URL前缀统一拦截而不是在每个Servlet里都写一遍Session判空逻辑。这三个方向做完这个项目就可以作为简历上的JavaWeb入门作品了。我自己在这个项目上翻过最大的跟头是当年为了省事把业务查询直接写进JSP的Scriptlet里结果页面越改越长后来实在忍不了才拆成ServletDAO。从那以后我每次接到哪怕再小的Web功能也强制自己先画一遍请求流转图分清楚谁负责接收、谁负责处理、谁负责渲染再动手写代码。这个习惯帮我在后续接手老项目和框架源码时省了大量时间希望这篇笔记也能帮到你。本文还有配套的精品资源点击获取
返回列表