☰
JavaWeb MVC登录注册:Servlet+JSP+JDBC分层实践
2026/10/13 7:39:19 网站建设 项目流程

1. 项目概述:为什么一个“用户登录注册”功能值得用MVC三层架构重做一遍?

你有没有遇到过这样的情况:写完一个JavaWeb登录页面,把数据库连接、SQL查询、页面跳转全塞在同一个Servlet里?改个密码校验逻辑,得翻三四个文件;加个邮箱验证,发现JSP里已经混着Java代码和HTML;等产品说“注册页要加个图形验证码”,你盯着那堆嵌套的<% %>标签,手抖得不敢敲回车——这根本不是开发,是考古现场。

这个标题里的“JavaWeb之利用MVC三层架构和Servlet+Jsp+JDBC实现用户登陆注册功能”,表面看是个教学级小项目,但背后藏着JavaWeb工程化落地的第一道分水岭。它不是教你怎么写request.getParameter("username"),而是逼你回答三个现实问题:数据从哪来、逻辑在哪跑、页面怎么画。MVC不是银弹,但它强制你把这三件事拆开——Model管数据库映射和业务规则(比如“密码不能少于6位”),View只负责渲染(JSP里连if都该用JSTL而不是<% if %>),Controller(Servlet)则像交通指挥员,只做路由和参数搬运,绝不碰SQL也不写HTML。

我带过的几个刚毕业的开发者,在某高校实验室做模拟项目X时,第一版登录功能用了23行内嵌SQL的JSP,上线三天就因SQL注入被导师叫停;第二版改成纯Servlet处理,但所有校验逻辑散落在doPost方法里,加个手机号格式校验就得改8处;直到第三版严格按MVC分层,他们才第一次体会到“改需求不等于改代码量”——产品经理说“注册成功后发站内信”,我们只在Service层加了个notifyUser()调用,Controller和View一行没动。这种可维护性,才是企业级开发的硬通货。

关键词“Servlet+Jsp+JDBC”不是怀旧复古,而是精准锚定JavaWeb最轻量、最可控的技术栈组合。它不依赖Spring Boot的自动配置,让你看清每个HTTP请求如何穿过Tomcat容器、被哪个Servlet拦截、怎样通过JDBC驱动连接MySQL、最终把User对象塞进request域再转发给JSP。这种“裸机感”对理解Web本质至关重要——就像学开车先摸手动挡,不是因为手动挡高级,而是它能让你感知离合、油门、档位之间的物理反馈。而“用户登陆注册”这个场景,恰好覆盖了Web开发90%的核心能力:表单提交、会话管理、密码加密、数据库增删查改、前后端数据流转、错误提示机制。它小到能在一个下午完成,又大到能照见你整个知识结构的漏洞。

所以别把它当练手Demo。当你在IDEA里新建一个Maven项目,敲下第一个public class UserServlet extends HttpServlet时,你真正启动的是一套工业级协作规范:前端工程师改JSP不用懂DAO层SQL,测试工程师写JUnit只需Mock Service接口,运维部署时知道Controller层就是流量入口。这个标题的价值,从来不在功能本身,而在于它用最朴素的技术组件,搭建起软件工程的第一座脚手架。

2. 架构设计与分层逻辑:为什么MVC不是“把代码切成三块”那么简单?

很多人以为MVC就是把代码按文件夹分成model、view、controller,然后各写各的。我在某公司带新人做某跨平台系统时,见过最典型的反模式:一个UserDaoImpl.java里塞了17个SQL语句,其中5个是重复的SELECT * FROM user WHERE id=?;Service层方法名叫loginCheck(),但里面直接new了一个JDBC连接;JSP页面里用<%= new SimpleDateFormat("yyyy-MM-dd").format(new Date()) %>动态生成时间——这根本不是分层,是把意大利面煮成一锅粥再盛进三个碗。

真正的MVC分层,核心是职责隔离和依赖倒置。我们来看这个登录注册功能里每一层到底该干什么、不该干什么:

2.1 Model层:数据契约与业务规则的守门人

Model层绝不是简单的JavaBean。以User类为例,它必须同时承担三重身份:

  • 数据传输载体(DTO):字段与数据库user表严格对应,包括id、username、password、email、create_time等;
  • 业务规则容器(Domain Object):提供isValidPassword()校验方法,内部调用BCrypt加密库比对密文,而不是让Controller去拼接SQL;
  • 持久化契约(PO):通过JDBCUtils工具类统一管理Connection,避免每个DAO都自己写Class.forName("com.mysql.cj.jdbc.Driver")。

关键设计点在于:Model层不持有任何数据库连接,也不依赖Servlet API。我见过太多初学者在User类里写request.getSession().setAttribute("user", this),这直接污染了领域模型。正确的做法是——User只管“我是谁、我有什么属性、我能做什么校验”,至于“谁把我存进session”,那是Controller的事。

提示:User类的password字段必须声明为private String password,并提供setPassword(String rawPassword)方法,在内部调用BCrypt.hashpw(rawPassword, BCrypt.gensalt())。这样即使前端传入明文密码,Model层也自动完成加密,上层无需关心加密细节。

2.2 View层:纯粹的模板引擎,拒绝任何逻辑入侵

JSP在这里的角色,是HTML的增强版模板,不是编程语言。它的边界非常清晰:只做三件事——展示数据、组织结构、响应用户交互事件。所有业务判断必须剥离:

  • ❌ 错误示范:<% if(user.getRole().equals("admin")) { %><a href="/admin">后台</a><% } %>
  • ✅ 正确做法:Controller将isAdmin布尔值放入request域,JSP用JSTL<c:if test="${isAdmin}">控制显示。

更关键的是资源加载策略。登录页的CSS和JS不能写死在JSP里,而应通过<link rel="stylesheet" href="${pageContext.request.contextPath}/css/login.css">动态解析路径。我踩过的坑是:本地调试时路径正常,但部署到Tomcat子目录(如/myapp/)时所有静态资源404——因为没加contextPath前缀,浏览器直接请求/css/login.css而非/myapp/css/login.css。

注意:JSP中禁止出现任何Java代码块(<% %>),必须用JSTL标签库替代。比如循环用户列表用<c:forEach items="${users}" var="u">,空值判断用<c:if test="${not empty errorMsg}">。这不仅是规范,更是安全防线——JSTL会自动转义EL表达式输出,防止XSS攻击。

2.3 Controller层:无状态的请求调度中心

Servlet作为Controller,本质是HTTP协议的翻译官。它的唯一使命是:接收原始请求、转换为领域对象、调用Service、决定响应方式。这里有两个致命误区必须避开:

  • 误区一:在Servlet里写SQL。曾有个开发者在LoginServlet的doPost方法里直接写String sql = "SELECT * FROM user WHERE username=? AND password=?";——这彻底废掉了DAO层,导致密码加密逻辑散落各处;
  • 误区二:在Servlet里操作session细节。比如HttpSession session = request.getSession(); session.setAttribute("user", user);看似合理,但违反了“Controller不管理状态”的原则。正确做法是Service返回User对象,Controller再决定是否存入session。

实际编码中,LoginServlet的doPost方法应该像这样精简:

protected void doPost(HttpServletRequest request, HttpServletResponse response) { // 1. 参数封装(非空校验在此层) String username = request.getParameter("username"); String password = request.getParameter("password"); if (StringUtils.isBlank(username) || StringUtils.isBlank(password)) { request.setAttribute("errorMsg", "用户名或密码不能为空"); request.getRequestDispatcher("/login.jsp").forward(request, response); return; } // 2. 调用Service(只传必要参数,不传request/response) User user = userService.login(username, password); // 3. 响应决策(成功跳转主页,失败返回登录页) if (user != null) { request.getSession().setAttribute("currentUser", user); response.sendRedirect(request.getContextPath() + "/home.jsp"); } else { request.setAttribute("errorMsg", "用户名或密码错误"); request.getRequestDispatcher("/login.jsp").forward(request, response); } }

看到没?没有SQL,没有JDBC连接,甚至没有try-catch——异常由Service层抛出,Controller只处理业务结果。这种设计让单元测试变得极其简单:Mock掉userService,就能100%覆盖所有分支逻辑。

3. 核心模块实现详解:从数据库建表到JSP渲染的完整链路

现在我们把MVC骨架填充血肉。这个过程不是机械复制代码,而是理解每个环节的“为什么”。比如为什么用户表要加UNIQUE KEY username?为什么密码字段长度设为60?为什么注册成功后要用response.sendRedirect()而不是forward()?这些细节背后全是生产环境踩过的坑。

3.1 数据库设计:不只是建表,更是安全边界的划定

MySQL建表语句看着简单,但每个字段类型和约束都暗藏玄机:

CREATE TABLE `user` ( `id` bigint NOT NULL AUTO_INCREMENT, `username` varchar(50) NOT NULL COMMENT '用户名,唯一', `password` varchar(60) NOT NULL COMMENT 'BCrypt加密后的密码,60字符足够', `email` varchar(100) DEFAULT NULL COMMENT '邮箱,允许为空', `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', `status` tinyint NOT NULL DEFAULT '1' COMMENT '状态:1-启用,0-禁用', PRIMARY KEY (`id`), UNIQUE KEY `uk_username` (`username`) COMMENT '强制用户名唯一' ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户基础信息表';

重点解析三个设计决策:

  • password字段长度为60:这不是拍脑袋定的。BCrypt加密后字符串格式为$2a$10$xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx,其中$2a$是算法标识,10是强度因子,后面60位是Base64编码的哈希值。如果设成50,加密结果会被截断,导致永远无法登录;
  • email字段允许NULL:注册时邮箱非必填项,但若设为NOT NULL,用户不填邮箱就会插入失败。更合理的做法是设DEFAULT NULL,后续通过业务逻辑校验是否需要邮箱验证;
  • status字段用tinyint而非enum:虽然MySQL支持ENUM,但Java中处理枚举类型容易引发序列化问题。用整型状态码更稳定,且便于未来扩展(比如增加-1表示“待审核”)。

实操心得:建表后立刻执行INSERT INTO user(username,password) VALUES('test', '$2a$10$N9qo8uLOickgx2ZMRZoMyeIjZAgcfl7p92ldGxad68LJZdL17lhWy')插入一条测试数据。这个密码明文是"123456",用BCrypt在线工具可验证。这样启动项目就能直接登录,避免卡在“数据库空”导致前端报500错误。

3.2 JDBC工具类:如何让数据库连接既安全又高效?

JDBCUtils不是简单封装DriverManager.getConnection()。它要解决三个核心问题:连接复用、资源释放、线程安全。我们采用DBCP连接池(Apache Commons DBCP2),而非每次请求都新建连接:

public class JDBCUtils { private static BasicDataSource dataSource; static { dataSource = new BasicDataSource(); dataSource.setUrl("jdbc:mysql://localhost:3306/myweb?useSSL=false&serverTimezone=UTC"); dataSource.setUsername("root"); dataSource.setPassword("123456"); dataSource.setInitialSize(5); // 初始连接数 dataSource.setMaxTotal(20); // 最大连接数 dataSource.setMaxWaitMillis(3000); // 获取连接超时3秒 } public static Connection getConnection() throws SQLException { return dataSource.getConnection(); } public static void close(Connection conn, PreparedStatement ps, ResultSet rs) { if (rs != null) try { rs.close(); } catch (SQLException e) { /* 忽略 */ } if (ps != null) try { ps.close(); } catch (SQLException e) { /* 忽略 */ } if (conn != null) try { conn.close(); } catch (SQLException e) { /* 忽略 */ } } }

为什么不用DriverManager?因为每次getConnection()都会建立TCP连接,耗时约50-200ms。而连接池预先创建好5个连接,请求时直接分配,耗时降到1ms以内。更重要的是,close()方法在这里不是关闭物理连接,而是归还到连接池——这才是高性能的关键。

注意事项:dataSource.setInitialSize(5)不能设为0。实测发现,首次请求时连接池为空,会触发同步初始化,导致首屏加载慢2秒以上。设为5能保证服务启动后立即有连接可用。

3.3 DAO层实现:用PreparedStatement堵死SQL注入的门

UserDao接口定义了数据访问契约,其实现类UserDaoImpl必须遵循“参数化查询”铁律:

public class UserDaoImpl implements UserDao { @Override public User findByUsername(String username) { String sql = "SELECT id, username, password, email, create_time, status FROM user WHERE username = ?"; try (Connection conn = JDBCUtils.getConnection(); PreparedStatement ps = conn.prepareStatement(sql)) { ps.setString(1, username); // 用setString()而非字符串拼接 try (ResultSet rs = ps.executeQuery()) { if (rs.next()) { User user = new User(); user.setId(rs.getLong("id")); user.setUsername(rs.getString("username")); user.setPassword(rs.getString("password")); user.setEmail(rs.getString("email")); user.setCreateTime(rs.getTimestamp("create_time")); user.setStatus(rs.getByte("status")); return user; } } } catch (SQLException e) { throw new RuntimeException("根据用户名查询用户失败", e); } return null; } }

关键点在于ps.setString(1, username)——它把用户输入的username作为参数值传递,数据库驱动会自动处理引号转义。对比字符串拼接:"SELECT * FROM user WHERE username = '" + username + "'",当username是admin' --时,SQL变成SELECT * FROM user WHERE username = 'admin' --',注释符--让后面密码校验失效,直接登录成功。

实操技巧:在DAO方法中捕获SQLException后,不要直接抛出给上层。要包装成自定义异常(如DataAccessException),并记录详细日志。我在线上环境见过因MySQL连接超时导致的SQLException,如果Controller直接打印堆栈,用户看到的是500错误页,而日志里只有Communications link failure——根本看不出是数据库问题还是网络问题。

3.4 Service层:业务逻辑的熔断器与编排中心

UserServiceImpl是真正的业务中枢。它不关心数据怎么存,只关心“登录”这件事该怎么定义:

public class UserServiceImpl implements UserService { private UserDao userDao = new UserDaoImpl(); @Override public User login(String username, String password) { // 1. 查询用户(可能为空) User user = userDao.findByUsername(username); if (user == null) { return null; // 用户不存在 } // 2. 状态校验(禁用账户不能登录) if (user.getStatus() == 0) { return null; // 账户已禁用 } // 3. 密码比对(使用BCrypt.matches) if (BCrypt.checkpw(password, user.getPassword())) { // 登录成功,清除敏感字段 user.setPassword(null); // 不把密码传给前端 return user; } return null; } }

这里体现了Service层的两大价值:

  • 业务规则聚合:登录成功需同时满足“用户存在”、“状态启用”、“密码正确”三个条件,这些规则分散在DAO和Model中,Service负责编排;
  • 安全过滤:user.setPassword(null)是关键防护。如果Controller把完整User对象存入session,JSP里用${user.password}可能意外输出密文,造成信息泄露。

常见问题:为什么Service层要new UserDaoImpl而不是注入?因为本项目未引入Spring,采用最简依赖管理。但要注意——如果DAO需要事务控制(如注册时要同时插入user和profile表),就必须用ThreadLocal绑定Connection,否则每个DAO获取的Connection不同,无法回滚。本项目登录注册功能无需事务,故简化处理。

3.5 JSP页面:用JSTL和EL构建防错前端

login.jsp不是写HTML,而是设计用户旅程。它必须处理至少五种状态:

  • 正常加载(空表单)
  • 提交后校验失败(用户名为空)
  • 登录失败(用户名密码错误)
  • 注册成功跳转(URL参数携带success=1)
  • CSRF防护(隐藏token字段)

完整login.jsp结构如下:

<%@ page contentType="text/html;charset=UTF-8" language="java" %> <%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %> <html> <head> <title>用户登录</title> <link rel="stylesheet" href="${pageContext.request.contextPath}/css/login.css"> </head> <body> <div class="login-container"> <h2>用户登录</h2> <!-- 错误提示 --> <c:if test="${not empty errorMsg}"> <div class="error-msg">${errorMsg}</div> </c:if> <!-- 成功提示(注册后跳转) --> <c:if test="${param.success == '1'}"> <div class="success-msg">注册成功!请登录</div> </c:if> <form action="${pageContext.request.contextPath}/login" method="post"> <div class="input-group"> <label for="username">用户名:</label> <input type="text" id="username" name="username" value="${param.username}" required> </div> <div class="input-group"> <label for="password">密码:</label> <input type="password" id="password" name="password" required> </div> <div class="button-group"> <button type="submit">登录</button> <a href="${pageContext.request.contextPath}/register.jsp">立即注册</a> </div> </form> </div> </body> </html>

关键细节:

  • ${param.username}实现表单回显:用户输错密码后刷新页面,用户名还在输入框里,不用重新输入;
  • required属性是HTML5原生校验,减轻后端压力;
  • 所有路径都用${pageContext.request.contextPath}动态解析,确保部署到任意上下文路径都有效。

实操心得:JSP中${errorMsg}的值来自Controller的request.setAttribute("errorMsg", "xxx")。如果忘记这步,EL表达式会输出空字符串而非报错,导致调试困难。建议在开发阶段开启JSP调试模式:在web.xml中添加<servlet>配置,设置<init-param>为development=true,这样EL错误会直接显示在页面上。

4. 关键技术点深度解析:Servlet生命周期、会话管理与密码安全实践

前三章讲“怎么做”,这一章必须深挖“为什么这么做”。很多开发者能跑通登录功能,但一问request.getSession(true)和request.getSession(false)区别就卡壳;一说“密码要加密”,就直接用MD5——这些知识盲区,正是线上事故的温床。

4.1 Servlet生命周期:从加载到销毁的三次握手

Servlet不是每次请求都新建实例,而是由Servlet容器(Tomcat)管理其生命周期。理解这个过程,才能写出线程安全的代码:

  • 加载与实例化:Web应用启动时,Tomcat读取web.xml或扫描@WebServlet注解,加载LoginServlet类并创建唯一实例(注意:是单例!);
  • 初始化(init):调用init(ServletConfig config)方法,此时可加载配置文件、初始化连接池。这是线程安全的,只执行一次;
  • 服务(service):每次HTTP请求到达,容器调用service(HttpServletRequest, HttpServletResponse),再分发到doGet或doPost。这是多线程并发的,doPost方法内不能有共享变量;
  • 销毁(destroy):应用卸载时调用,释放资源(如关闭连接池)。

这就解释了为什么LoginServlet里不能定义private String username;——多个用户同时登录时,这个字段会被相互覆盖。我见过最惨的案例:一个开发者在Servlet里用static变量缓存用户列表,结果A用户登录后看到B用户的订单数据。

验证方法:在LoginServlet的init()方法里加System.out.println("Servlet初始化:" + this),在doPost里加System.out.println("请求处理:" + this)。启动项目后多次刷新登录页,观察控制台输出——你会发现init只打印一次,而doPost每次请求都打印,且this的hashCode始终相同。

4.2 HttpSession机制:Cookie、URL重写与会话超时的三角关系

用户登录后,服务器如何记住“你是谁”?靠HttpSession。但Session ID的传递方式有三种,必须根据场景选择:

  • Cookie(默认):服务器返回Set-Cookie: JSESSIONID=ABC123,浏览器后续请求自动带上。问题:用户禁用Cookie时失效;
  • URL重写:在链接后追加;jsessionid=ABC123,如<a href="home.jsp;jsessionid=${pageContext.session.id}">。问题:所有URL都要手动重写,且不安全(Session ID暴露在URL中);
  • 表单隐藏字段:<input type="hidden" name="jsessionid" value="${pageContext.session.id}">。问题:仅对POST有效,GET请求仍需URL重写。

生产环境最佳实践是优先Cookie,降级URL重写。Tomcat默认开启URL重写,但需在web.xml中配置:

<session-config> <cookie-config> <http-only>true</http-only> <!-- 防止JS读取Session ID --> <secure>true</secure> <!-- 仅HTTPS传输 --> </cookie-config> <tracking-mode>COOKIE</tracking-mode> </session-config>

安全警告:<http-only>true</http-only>必须开启!否则XSS攻击者可通过document.cookie窃取JSESSIONID,冒充用户登录。某次渗透测试中,我们发现某系统未开启此选项,用<script>alert(document.cookie)</script>就拿到了管理员Session。

4.3 密码安全:为什么MD5、SHA-1都是定时炸弹?

“密码加密”是登录功能最易被忽视的安全雷区。我整理了常见错误及正确方案:

加密方式是否安全原因替代方案
明文存储❌ 绝对禁止数据库泄露即密码泄露必须加密
MD5/SHA-1❌ 已淘汰彩虹表可秒破,GPU每秒计算数十亿次改用BCrypt
AES对称加密❌ 错误方向加密可逆,密钥泄露即全盘崩溃密码不可逆,只可验证
BCrypt✅ 推荐自带盐值、可调强度、抗暴力破解使用BCrypt.hashpw()

BCrypt的强度因子(strength)是关键参数。BCrypt.gensalt(10)中10表示2^10次哈希迭代,耗时约200ms。设为4则太快(20ms),易被暴力破解;设为14则太慢(3秒),影响用户体验。实测10是最佳平衡点。

注册时密码处理流程:

// UserServiceImpl.register() String salt = BCrypt.gensalt(10); String hashedPassword = BCrypt.hashpw(rawPassword, salt); user.setPassword(hashedPassword); // 存入数据库的是$2a$10$...格式字符串

登录时验证流程:

// UserServiceImpl.login() if (BCrypt.checkpw(inputPassword, storedHashedPassword)) { // 密码正确 }

实操技巧:BCrypt.checkpw()方法会自动解析storedHashedPassword中的盐值,无需单独存储盐。这是BCrypt的设计精妙之处——盐值、哈希值、算法标识全部编码在同一字符串中,极大简化了开发。

5. 常见问题排查与避坑指南:从500错误到中文乱码的实战解决方案

再完美的设计,上线也会遇到各种诡异问题。我把过去三年在多个项目中积累的登录注册功能典型故障,按发生频率排序,给出可直接复用的排查路径。

5.1 HTTP 500错误:定位Java异常的黄金三步法

500错误是后端抛出未捕获异常的信号。不要盲目重启服务器,按顺序检查:

第一步:看浏览器Network面板的Response

  • 如果返回纯白页,说明异常发生在JSP渲染阶段(如EL表达式错误);
  • 如果返回Tomcat默认错误页,说明异常在Servlet或Service层。

第二步:查Tomcat日志(logs/catalina.out)

  • 搜索Caused by:关键字,定位根异常。常见类型:
    • ClassNotFoundException: com.mysql.cj.jdbc.Driver→ MySQL驱动jar包未放入WEB-INF/lib;
    • SQLException: Access denied for user→ 数据库用户名密码错误;
    • NullPointerException→ 某个对象为null,比如userDao未初始化。

第三步:在关键位置加日志在LoginServlet的doPost开头加:

System.out.println("=== LoginServlet接收到请求 ==="); System.out.println("username: " + username); System.out.println("password length: " + (password != null ? password.length() : "null"));

如果日志没打印,说明请求根本没到达Servlet——检查web.xml的<servlet-mapping>路径是否匹配。

独家技巧:在web.xml中配置全局错误页,把500错误重定向到友好提示页:

<error-page> <error-code>500</error-code> <location>/error/500.jsp</location> </error-page>

这样用户看到的不是Tomcat黄页,而是“系统繁忙,请稍后再试”。

5.2 中文乱码:GET与POST的双重陷阱

中文乱码是JavaWeb新手最高频问题,根源在于字符集不一致。解决方案分两层:

POST请求乱码(表单提交):

  • 在LoginServlet的doPost方法第一行加:
    request.setCharacterEncoding("UTF-8"); // 必须在getParameter()之前
  • 同时确保JSP页面声明<%@ page contentType="text/html;charset=UTF-8" %>。

GET请求乱码(URL参数,如?username=张三):

  • Tomcat默认用ISO-8859-1解码URL,需在conf/server.xml中修改Connector:
    <Connector port="8080" protocol="HTTP/1.1" connectionTimeout="20000" redirectPort="8443" URIEncoding="UTF-8" /> <!-- 关键配置 -->

验证方法:在doPost中打印new String(username.getBytes("ISO-8859-1"), "UTF-8"),如果能正确显示中文,说明是GET乱码;如果仍是乱码,检查JSP页面编码声明。

5.3 JSP无法解析EL表达式:JSTL版本与TLD文件的隐秘战争

现象:${user.username}直接显示在页面上,而不是输出值。原因不是EL被禁用,而是JSTL标签库未正确加载。

解决方案:

  • 确保WEB-INF/lib中有jstl-1.2.jar(不是jstl-api.jar);
  • JSP顶部必须声明taglib:
    <%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
  • 检查web.xml的Servlet版本是否≥2.4(JSP2.0才支持EL):
    <web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_4_0.xsd" version="4.0">

实操心得:如果用Maven,直接在pom.xml中添加:

<dependency> <groupId>javax.servlet</groupId> <artifactId>jstl</artifactId> <version>1.2</version> </dependency>

Maven会自动下载jstl-1.2.jar和标准TLD文件,比手动拷贝更可靠。

5.4 数据库连接失败:从驱动到防火墙的全链路诊断

当JDBCUtils.getConnection()抛出异常,按此顺序排查:

检查项命令/操作预期结果故障表现
MySQL服务是否运行systemctl status mysqld(Linux)或任务管理器(Windows)active (running)Connection refused
数据库是否存在mysql -u root -p -e "SHOW DATABASES;"列出myweb数据库Unknown database 'myweb'
用户权限是否正确mysql -u root -p -e "SELECT host,user FROM mysql.user;"root用户host为%或localhostAccess denied for user
端口是否被占用netstat -an | findstr :3306LISTENINGConnection timed out
防火墙是否放行sudo ufw status(Ubuntu)3306端口状态为ALLOW本地能连,远程连不上

终极方案:在LoginServlet中写一个测试方法:

public void testDBConnection() { try (Connection conn = JDBCUtils.getConnection()) { System.out.println("数据库连接成功!当前时间:" + conn.createStatement().executeQuery("SELECT NOW()").next()); } catch (Exception e) { e.printStackTrace(); } }

在init()方法中调用,服务启动时就能看到连接结果。

6. 项目扩展与工程化演进:从单体Servlet到可维护系统的升级路径

完成基础登录注册只是起点。我在某实验室指导模拟项目X时,团队用两周时间把这个MVC骨架扩展为具备生产可用性的系统。以下是经过验证的演进路线,每一步都解决一个真实痛点。

6.1 引入Filter实现统一权限控制

当系统增加“管理员后台”功能后,每个Servlet都要写if (user == null || !user.isAdmin()) { response.sendError(403); return; }。这时Filter就是救星:

@WebFilter("/admin/*") public class AdminFilter implements Filter { @Override public void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain) { HttpServletRequest request = (HttpServletRequest) req; HttpServletResponse response = (HttpServletResponse) resp; User user = (User) request.getSession().getAttribute("currentUser"); if (user == null || user.getRole() != Role.ADMIN) { response.sendRedirect(request.getContextPath() + "/login.jsp?error=access_denied"); return; } chain.doFilter(req, resp); } }

@WebFilter("/admin/*")表示拦截所有/admin/开头的请求。这样Controller层彻底解放,专注业务逻辑。

注意事项:Filter链顺序很重要。如果同时有编码Filter和权限Filter,必须让编码Filter先执行,否则request.getParameter()会乱码。

6.2 用Properties文件管理配置

把数据库URL、用户名、密码硬编码在JDBCUtils里是灾难。创建src/main/resources/db.properties:

jdbc.url=jdbc:mysql://localhost:3306/myweb?useSSL=false&serverTimezone=UTC jdbc.username=root jdbc.password=123456

在JDBCUtils中用ResourceBundle加载:

static { ResourceBundle bundle = ResourceBundle.getBundle("db"); dataSource.setUrl(bundle.getString("jdbc.url")); dataSource.setUsername(bundle.getString("jdbc.username")); dataSource.setPassword(bundle.getString("jdbc.password")); }

这样更换数据库只需改properties文件,无需重新编译Java代码。

6.3 添加验证码功能:从简单数字到图形验证码

基础版用JSP生成随机数即可:

<% String code = String.valueOf((int)(Math.random() * 9000 + 1000)); session.setAttribute("verifyCode", code); %> <input type="text" name="verifyCode" placeholder="验证码"> <span style="color:red"><%=code%></span>

但生产环境必须用图形验证码。推荐使用Kaptcha库,它能生成带干扰线、旋转字符的图片:

<dependency> <groupId>com.github.penggle</groupId> <artifactId>kaptcha</artifactId> <version>2.3.2</version> </dependency>

配置web.xml:

<servlet> <servlet-name>Kaptcha</servlet-name> <servlet-class>com.google.code.kaptcha.servlet.KaptchaServlet</servlet-class> </servlet> <servlet-mapping> <servlet-name>Kaptcha</servlet-name> <url-pattern>/kaptcha.jpg</url-pattern> </servlet-mapping>

JSP中引用:<img src="${pageContext.request.contextPath}/kaptcha.jpg" onclick="this.src='${pageContext.request.contextPath}/kaptcha.jpg?'+Math.random()">

实操心得:验证码校验必须在Service层完成,Controller只负责传递参数。这样测试时可以Mock验证码服务,避免每次测试都要识别图片。

6.4 日志系统接入:从System.out

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询