☰
JavaWeb第三阶段实战:转发重定向、Session与三层架构拆解
2026/10/14 3:55:29 网站建设 项目流程

我们刚结束第二个阶段的增删改查练习,手上的基础Servlet也写得能跑通了,结果看到一个标题为JavaWeb_03的练习包,整个人精神又绷紧了。凡是带编号的项目,说明一套完整的课件或训练营正在往深处走,第三阶段的玩意儿大概率要碰状态管理、请求跳转和分层设计。说实话,第二个阶段最让人头疼的不是代码本身,而是逻辑乱:业务代码全堆在Servlet里面,一个方法几百行,出问题只能一行行盯,改完了又怕碰坏别的地方。到了第三阶段,其实就是在帮你强制止血,用MVC的思路把东西拆开。这篇东西我就围绕实际做这个项目的过程,聊聊标题背后到底藏了哪些必须搞定的知识点,以及模块怎么落地才不会翻车。

1. 项目整体设计与思路拆解

看到JavaWeb_03这个编号时,我的第一反应不是去看它具体包含哪些代码,而是去推测这一阶段训练的核心目标。正常一套JavaWeb训练路径,第一阶段通常是HTML、CSS、JavaScript和Tomcat环境搭建,第二阶段开始接触Servlet基础语法和request、response的基本使用,第三阶段必然迎来第一个分水岭——如何处理多个页面之间的数据流转和状态保持。换句话说,这个阶段你就不能再把Servlet当成一个简单的“Java类+doGet方法”来看待了,而是要把它放进一个完整的请求处理链条里去理解:浏览器发出请求,服务器接收,业务层处理数据,控制层决定跳转到哪个页面,数据传递过程是否安全可靠,用户状态是否在多次请求间保持。

配图和代码示例里已经出现“服务器内部转发”和“重定向”的字样,这两个概念恰好就是第三阶段最核心的基础设施。初学者最容易踩的坑是把request.getRequestDispatcher().forward()和response.sendRedirect()混着用,觉得都是跳转页面,随手写一个就完事。但这两者在请求次数、地址栏变化、数据共享三个维度上的表现完全不同,直接决定了后续登录状态、错误提示、参数传递等功能能走通多少。

第三阶段还有一个隐含的主线是“三层架构的萌芽”。当Servlet里同时出现了数据库查询、业务判断、页面跳转这三类职责时,代码就会快速膨胀到难以维护。我在早期做练习项目X时也犯过类似的毛病,一个LoginServlet里塞了连接数据库、校验密码、处理中文乱码、跳转页面,末尾还顺带打印了一堆调试日志,写完看着能用,但加一个新功能心里就发怵。JavaWeb_03这个项目在结构上其实就是逼你开始把代码拆成控制层、业务层和持久层,哪怕只是简单的几个包,也能让后面的项目逻辑清晰不少。

所以我个人的判断是:这一阶段的核心不是学什么新语法,而是建立“请求生命周期”和“分层开发”这两种思维模型。前者决定了你的页面跳转和数据共享是否可靠,后者决定了你再接三个模块时是否还能改得动代码。后面所有实操细节,都是围绕这两条主线展开的。

1.1 三层结构的选型与原则

很多人写JavaWeb小项目时会纠结:要不要一开始就搞分层?我的建议是,只要项目里出现了两个以上的实体类要做增删改查,就值得拆包。JavaWeb_03这种练习项目,规模不算大,但至少应该区分出entity、dao、service、servlet这四类包,严格一点的话再单独建一个util包管理数据库连接和字符串处理。

拆包的原则很简单:Servlet只负责接收请求和决定跳转方向,Service层处理业务规则,Dao层只用SQL和数据打交道。比如处理登录,Servlet接收用户名和密码后,直接调用UserService的login方法,至于密码怎么校验、用户是否存在、状态是否正常,全部交给Service去处理;Service再调用UserDao去查数据库,拿到User对象后做一些业务判断;UserInfoDao只负责执行那条select语句并返回结果。谁在哪一层做了什么,一目了然。

这听着像是多余的动作,但实际调试的时候就知道好处了:如果登录失败,你先查Service里的逻辑有没有错,再查Dao返回的结果集是否正常,不用像从前那样在Servlet里从头到尾读一遍混合代码。要是数据库字段改了,也只需要动Dao层的SQL,控制层完全不受影响。用一句话总结就是:分层带来的最大收益不是代码少了,而是出问题时定位快了,改功能时胆子大了。

1.2 请求生命周期是理解一切跳转的地基

在做JavaWeb_03之前,我一直建议身边的新手先把HTTP请求的完整生命周期画一遍,哪怕只是画个草图,也比直接抄代码强很多。一个典型的Servlet请求大概长这样:浏览器发出请求,Servlet容器(比如Tomcat)解析HTTP报文并生成request对象,这个request对象携带着参数、请求头、Cookie等所有信息;然后容器创建response对象作为响应容器,调用你写的service方法;等service方法执行完毕后,容器把response里存的内容按照HTTP协议写回给浏览器。

转发和重定向之所以容易弄混,是因为它们都发生在service方法执行过程中,但位置和结果完全不同。服务器内部转发是在请求处理过程中,把当前的request和response对象直接递交给另一个资源,整个过程是一次请求,浏览器全程不知道中间发生了什么;重定向则是Servlet先返回一个302状态码和Location头给浏览器,浏览器看到之后自动发起了第二次请求,第二次请求的地址变成了Location指定的新地址,此时原来的request对象已经失效了,参数、Attribute等数据统统不在。

这个区别不是理论抠字眼,而是后续功能能不能跑通的关键。比如你做一个表单提交后回显错误提示:如果你用转发,Servlet可以把错误信息塞进request的Attribute,页面用EL表达式直接取出来;如果你用重定向,你只能通过URL拼接参数或者存在Session里再去取,麻烦程度完全不是一个量级。JavaWeb_03的练习里肯定会出现这两个概念的对比,我建议你在做练习时亲手各实现一遍,然后用地址栏的变化、是否有两次网络请求记录来验证,不要只背表格。

2. 核心细节解析与实操要点

理解了大方向之后,这一阶段真正花时间的其实是代码层面的各种小细节。很多人看教程觉得每段代码都认识,但一合起来运行就各种问题。原因在于JavaWeb里每一个看起来不起眼的细节都可能成为拦路虎:编码格式不对导致乱码、路径写错导致404、重定向时参数丢失、Session取不到值。这里我把第三阶段最常要面对的核心细节拆开来逐个讲,每个都带上实际场景和操作要点。

2.1 请求转发与重定向的落地决策

先记住一个快速判断的口诀:需要携带数据到下一个页面并且不希望用户看到中间路径的,优先用转发;需要防止表单重复提交、跳转到外部地址或者完成某个操作后刷新了新地址的,优先用重定向。举两个例子说明。

登录成功后跳转到首页,我通常用重定向。原因是如果登录使用转发跳转,浏览器地址栏还停留在登录页的URL,用户按一下F5就会再次触发登录请求,造成重复提交。重定向之后地址栏变成了首页地址,刷新也没问题。而登录失败时的错误提示,我会用转发,把错误信息封装成一个message对象放进request里,然后转发到登录页面,页面使用EL表达式把message显示出来,体验比URL拼接乱码参数好得多。

写转发和重定向时还有个路径问题容易出岔子。转发用的是项目内路径,比如request.getRequestDispatcher("/login.jsp").forward(request, response),这个路径以项目根为基准,需要留意上下文路径,但实际开发中通常配合相对路径使用;重定向时如果用了response.sendRedirect("login.jsp"),这个路径是相对于当前请求地址来解析的,稍不留神就会跑到下一级目录。遇到404时要先看看Tomcat控制台的URL,再对照代码里的路径,就能快速定位是WebContent下的JSP放错了位置还是路径写成了绝对路径加少了项目名前缀。

2.2 Session和Cookie的实际使用边界

JavaWeb_03项目里几乎必定会涉及登录状态保持。你要让系统知道“这个用户已经登录过了”,最直接的办法就是在登录成功后把用户信息存进Session,然后在后续请求中检查Session里是否有这个用户对象,没有就重定向回登录页。Session的数据存储在服务器端,使用一个唯一的JSESSIONID来标识同一个浏览器,Tomcat会在首次访问时自动创建Session并通过Cookie把这个ID下发到浏览器端。

Session的重点在于它的生命周期和失效场景。默认情况下,Tomcat中Session的闲置超时时间是30分钟,如果你希望调整,可以在web.xml里配置session-timeout,单位是分钟。这里有一个很常见的坑:如果你在Servlet里写了一个重定向,又想着把数据塞进request里带到下个页面,那注定是取不到的,因为重定向是第二次请求,原来的request已经销毁了。你必须把数据放入Session再重定向,然后在下一个页面取出Session数据后手动移除,不然数据会一直残留在Session里,污染后续请求。

Cookie的使用场景和Session互补。Session适合存用户身份这类敏感信息,因为数据在服务端不那么容易被篡改;Cookie则适合存“记住用户名”“阅读偏好”这类不敏感的数据,因为Cookie存储在浏览器端,用户可以随意查看甚至修改。如果你要把“记住登录状态”做成七天免登录,一般做法是把用户ID加密后写入Cookie,并设定Cookie的MaxAge为七天,浏览器会在每次请求时自动携带这个Cookie。但注意,千万不要把明文密码放Cookie里,这是个老生常谈但依然有人犯的错误。

2.3 数据库操作的分层:从JDBC到Dao封装

第三阶段多半会引入基于JDBC的数据库操作。哪怕后续你换了MyBatis或者JPA,JDBC本身仍然是理解持久层的最佳起点。一个标准的JDBC操作流程包含五步:加载驱动、获取连接、创建Statement或PreparedStatement、执行SQL并处理结果集、释放资源。每步都有坑。

PreparedStatement相比Statement有两个核心优势:防止SQL注入和预编译。比如用户登录时输入用户名和密码,如果你使用字符串拼接生成SQL,比如"SELECT * FROM t_user WHERE username='" + name + "' AND password='" + pwd + "'",当用户输入一个带有单引号的恶意字符串时,整个SQL语义就被破坏了。这种注入问题用PreparedStatement基本上能杜绝,因为参数值被当作参数传入而不参与SQL语句的编译,即便里面带有单引号也只会被当作字面量处理。

资源释放同样不能马虎。ResultSet、Statement、Connection这三个对象都要在finally块或者Java 7的try-with-resources里关闭。每次操作都新建连接并关闭,对练习项目来说没有性能问题,但要养成一个习惯:连接关闭的顺序要和打开顺序相反。实际操作中我见过不少人只关了Connection,结果Statement和ResultSet长期不释放,数据库连接数很快就被耗光了,表现为系统跑着跑着突然卡死,报错信息里出现连接超时。如果你在一个循环里执行多条SQL,这种症状会特别明显。

3. 实操过程与核心环节实现

框架和原理讲得再多,最后还是要落到代码上。这一部分我会把一个典型的JavaWeb_03项目——从数据库表设计到页面展示的完整登录+用户列表模块——的落地方案展开细讲。所有的配置、代码片段和步骤顺序,都按照我实际操作的顺序来写,确保你可以照着一步步做出来。

3.1 从零搭建一个可运行的项目骨架

先别急着写代码,把目录结构建好。假设项目名是某WebDemo,完整的包结构一般是这样的:

  • src目录下按包分:com.demo.entity存放实体类,com.demo.dao存放数据库访问对象,com.demo.service存放业务逻辑,com.demo.servlet存放控制器,com.demo.util存放数据库连接工具类。
  • WebContent目录下:存放JSP页面和静态资源,通常包括login.jsp、register.jsp、list.jsp、header.jsp等,如果需要,可以建一个WEB-INF目录存放受保护的JSP页面。WEB-INF下的页面不能通过URL直接访问,只能通过Servlet内部转发的方式打开,这个特性很适合放置后台管理的页面,能防止用户绕过登录直接访问页面文件。

新建一个动态Web项目时,固定要设置一下Tomcat的运行环境。然后把数据库驱动包(通常是MySQL的Connector/J驱动)放到WEB-INF/lib目录下。这一步忘掉的话,运行时会出现ClassNotFoundException,报错指向com.mysql.cj.jdbc.Driver,一眼就能看出是驱动没加载到。

项目跑通之后,再开始按三层结构写代码,顺序建议是:先写实体类,再写工具类,然后写Dao接口和实现,再写Service,最后写Servlet和JSP。如果你从Servlet开始写,往往会发现自己跳过了必要的准备工作,代码漏洞百出,这不叫开发,叫补丁工程。

3.2 数据库建表和连接工具类

先设计一张最简单的用户表。以MySQL为例,建表SQL如下:

CREATE TABLE t_user ( id INT PRIMARY KEY AUTO_INCREMENT, username VARCHAR(50) NOT NULL UNIQUE, password VARCHAR(64) NOT NULL, nickname VARCHAR(50), create_time DATETIME DEFAULT CURRENT_TIMESTAMP );

这张表包含了基本用户信息的字段。注意password字段,不要设计成明文存储,即使是练习项目也应该至少使用MD5或SHA-256做一次散列。MD5已经不够安全,但它在这个阶段的教学意义在于让你理解单向散列的思路。后面有时间可以升级成带盐值的SHA-256,甚至是BCrypt。

数据库连接工具类是整个持久层的地基。我用Java 7之后的try-with-resources写法,能有效避免资源泄漏。核心代码如下:

public class DBUtil { private static final String URL = "jdbc:mysql://localhost:3306/webdemo?useSSL=false&characterEncoding=utf8"; private static final String USER = "root"; private static final String PASSWORD = "123456"; 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, Statement stmt, ResultSet rs) { if (rs != null) { try { rs.close(); } catch (SQLException e) { e.printStackTrace(); } } if (stmt != null) { try { stmt.close(); } catch (SQLException e) { e.printStackTrace(); } } if (conn != null) { try { conn.close(); } catch (SQLException e) { e.printStackTrace(); } } } }

要注意的点有三个。第一,URL里必须显式加上characterEncoding=utf8,否则你往数据库里写入中文时很可能出现问号或者乱码。第二,驱动类com.mysql.cj.jdbc.Driver属于新版驱动包的写法,旧版是com.mysql.jdbc.Driver,用错驱动类名同样跑不起来。第三,Class.forName这个静态块只会在类第一次被加载时执行一次,也就是说驱动只需要注册一次,不要每调用一次连接就注册一次。

3.3 登录流程的完整代码走读

登录流程是JavaWeb_03项目里比较完整的闭环。从页面提交到数据库校验,再到跳转和状态存储,每一步都可以拆开讲解。我先把标准的控制层代码放出来:

@WebServlet("/login") public class LoginServlet extends HttpServlet { private UserService userService = new UserService(); protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { // 设置编码,防止中文乱码 request.setCharacterEncoding("utf-8"); String username = request.getParameter("username"); String password = request.getParameter("password"); // 非空校验 if (username == null || username.trim().isEmpty() || password == null || password.trim().isEmpty()) { request.setAttribute("msg", "用户名和密码不能为空"); request.getRequestDispatcher("login.jsp").forward(request, response); return; } // 把明文密码加密后再比对 String md5Password = MD5Util.md5(password); User user = userService.login(username, md5Password); if (user != null) { // 登录成功:把用户信息存入Session HttpSession session = request.getSession(); session.setAttribute("loginUser", user); response.sendRedirect("list"); } else { // 登录失败:转发回登录页,并携带错误提示 request.setAttribute("msg", "用户名或密码错误"); request.getRequestDispatcher("login.jsp").forward(request, response); } } }

这一段代码里其实藏着很多容易被忽略的细节。比如request.setCharacterEncoding("utf-8")必须在第一次读取参数之前调用,否则post请求的中文参数会乱码。如果你用的是Tomcat 8以上版本,GET请求的中文乱码问题大多已经解决(Tomcat 8之后默认使用UTF-8),但POST请求依然需要这句设置来确保参数正确解码。如果你哪天发现中文全部变成了问号,优先检查这一行的位置是否正确。

为什么登录成功用重定向而不是转发?正是因为重定向会产生第二次请求,浏览器地址栏变成list,刷新时不会重新提交登录表单。而登录失败用转发,因为错误提示msg塞在了request范围里,转发到login.jsp还能取出来。写的时候还要注意:重定向的路径"list"是相对路径,此时当前地址是"/项目名/login",所以它会解析成"/项目名/list",正好指向用户列表Servlet。如果你在重定向时写了"/list",就会变成从服务器根路径去找list,报404。

Service层和Dao层逻辑比较简单,但有一个很深的原则问题:Service才是业务判断的地方。完整的Service如下:

public class UserService { private UserDao userDao = new UserDao(); public User login(String username, String password) { return userDao.findByUsernameAndPassword(username, password); } }

这里虽然看起来只是把Dao的结果透传了一下,但它在结构上还有扩展空间。比如后面你要加“连续失败五次锁定账户”的逻辑,改的地方就应该是Service而不是Servlet。同时也让Servlet变得很干净:一个方法里只有参数处理、调用Service、页面跳转三段逻辑,任何一段出了问题,只需要改动对应类即可。

Dao层的代码需要处理SQL和结果集映射:

public class UserDao { public User findByUsernameAndPassword(String username, String password) { String sql = "SELECT id, username, nickname FROM t_user WHERE username=? AND password=?"; try (Connection conn = DBUtil.getConnection(); PreparedStatement ps = conn.prepareStatement(sql)) { ps.setString(1, username); ps.setString(2, password); try (ResultSet rs = ps.executeQuery()) { if (rs.next()) { User user = new User(); user.setId(rs.getInt("id")); user.setUsername(rs.getString("username")); user.setNickname(rs.getString("nickname")); return user; } } } catch (SQLException e) { e.printStackTrace(); } return null; } }

强烈建议查询时不要select *,而是只select需要的字段,然后手工映射成一个User对象。select *虽然写着省事,但一旦表结构发生变化,代码里rs.getString("xxx")的字段映射就会和数据库字段对不上,轻则返回null,重则直接抛异常。在练习阶段多写几行映射代码,养成手工处理结果集的习惯,后面学ORM框架时会更容易理解它帮你做了什么。

3.4 用户列表展示与权限控制

登录做完了,通常还要做一个登录后才能访问的用户列表页面,不然Session的用处展现不出来。列表展示本身不复杂:查出一堆用户数据,循环展示在表格里。重点是做权限控制——如果用户没有登录,直接访问list页面时必须被拦截或重定向回登录页。

在每个需要登录才能访问的Servlet中,只需在doGet或doPost开头加一段检查:

HttpSession session = request.getSession(false); if (session == null || session.getAttribute("loginUser") == null) { response.sendRedirect("login.jsp"); return; }

这段检查的核心就是:session.getSession(false)不创建新的Session,如果当前没有Session或者Session里没有用户,就重返登录页。如果你用request.getSession()不带参数,Tomcat会为你新建一个Session,导致原本没登录的用户也拿到一个空Session,检查就失效了。这是一个极其隐蔽的坑,很多人一时看不出问题,因为在开发时浏览器可能已经存在一个JSESSIONID,带着旧Session访问,检查就通过了;但如果换一个全新浏览器直接访问列表页,就可能出现绕过登录的漏洞。

如果你愿意更进一步,可以学习一下Filter(过滤器)。JavaWeb_03项目里加入一个LoginFilter是很好的加分项:

@WebFilter("/admin/*") public class LoginFilter implements Filter { public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { HttpServletRequest req = (HttpServletRequest) request; HttpServletResponse resp = (HttpServletResponse) response; HttpSession session = req.getSession(false); if (session != null && session.getAttribute("loginUser") != null) { chain.doFilter(request, response); } else { resp.sendRedirect(req.getContextPath() + "/login.jsp"); } } }

这样每个需要保护的资源只要放在/admin目录下,Filter会自动进行登录态检查,不用在每个Servlet里都重复写一次。这段代码也解释了为什么要用重定向而不是转发——因为Filter执行阶段还在请求刚进来时,还没进入Servlet,重定向直接让浏览器跳转到登录页,而转发则是让登录页的代码在当前请求里被当作回应,两者面向的场景不同。但有一点要小心:req.getContextPath()必须加上,否则部署在非根路径下时,重定向的URL会找不到项目,直接404。

3.5 JSP中的EL表达式与JSTL配合

数据查出来之后最终要展示在页面上。JSP页面中避免使用Java脚本片段(也就是<% %>),一个比较容易踩坑的写法是直接在JSP里写for循环,整个页面乱糟糟的。推荐的做法是使用JSTL标签和EL表达式。

在WebContent/WEB-INF/lib目录下放两个JSTL依赖包(jstl.jar和standard.jar,或者使用新版的两个JAR),然后在JSP页面顶部引用:

<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>

展示用户列表的表格可以这样写:

<table border="1" cellspacing="0" cellpadding="5"> <tr> <th>ID</th> <th>用户名</th> <th>昵称</th> </tr> <c:forEach items="${userList}" var="u"> <tr> <td>${u.id}</td> <td>${u.username}</td> <td>${u.nickname}</td> </tr> </c:forEach> </table>

${userList}这个表达式对应Servlet里setAttribute存进去的List ,它的命名规则是:setAttribute("userList", list),EL表达式里写userList。这里有一个新手常犯的错误是:在Servlet里setAttribute存的是"userList",在JSP里却写出了${users},结果页面上什么都没有,后台也不报错,因为EL表达式找不到数据时会静默返回空字符串。

如果列表为空,加一个友好提示:

<c:if test="${empty userList}"> <tr><td colspan="3">暂无用户数据</td></tr> </c:if>

注意HTML的table里colspan是合并列,这个在页面布局里很常用。此时JSP里不应该再出现任何<% %>的Java代码,页面结构会很干净,别人接手你的项目时也能快速找到数据在哪里渲染,可读性提升了何止一个档次。

4. 常见问题与排查技巧实录

每做完一个JavaWeb项目,我基本都会攒出一堆问题来。JavaWeb_03这个阶段因为涉及了网络请求、Servlet生命周期、数据库连接、页面渲染四个层次,出现问题时的排查链条明显变长了。这里把我实际遇到过的问题按现象分门别类整理出来,每个都附上定位思路和解决方法,遇到同样的坑可以少走很多弯路。

4.1 中文乱码问题:从页面到数据库的全链路排查

中文乱码是JavaWeb项目里出现频率较高的老问题。它可能发生在四个环节:浏览器到Servlet的请求参数乱码、Servlet到JSP回显乱码、Servlet到数据库写入乱码、数据库到页面读取乱码。针对不同环节,解决方式很不相同。

  • 请求参数乱码:POST请求要在获取参数前设置request.setCharacterEncoding("utf-8");GET请求在Tomcat 8以上通常已默认UTF-8,但如果你还在用老版本Tomcat,可以修改conf/server.xml里的Connector加上URIEncoding="UTF-8"。
  • 响应乱码:设置response.setContentType("text/html;charset=utf-8"),放在获取PrintWriter之前执行。
  • JSP页面乱码:保证JSP页面顶部的page指令charset是UTF-8,同时确保文件本身的保存编码是UTF-8。很多人只改了page指令,但文件是用GBK编码保存的,一样乱。
  • 数据库乱码:连接URL上加useUnicode=true&characterEncoding=utf8,同时建表时用的字符集也要是utf8mb4,字段级别的字符集更要注意。如果这些都加了还是乱,检查一下数据库的连接是否真的走了这个URL,有时项目里存在多个数据库连接配置,改的是一个,用的却是另一个。

排查中文乱码最笨也最有效的方法是:一步一步打日志,先用System.out.println在Servlet入口打印接收到的参数值,如果这一步已经是乱码,问题出在请求解析阶段;如果Servlet里正常但数据库里乱码,问题出在写入阶段。逐段隔离,很快就能定位到是哪一层出了问题。

4.2 404和500错误:路径问题和空指针的两个典型

404通常代表资源找不到。如果你访问了一个Servlet报404,先检查两件事:web.xml或@WebServlet注解里的映射路径是否和浏览器地址一致;项目是否已发布到Tomcat。你写完Servlet后需要重启Tomcat才能生效,如果没重启,Tomcat用的还是旧class文件,新写的Servlet自然404。

如果404指向的是JSP页面,路径问题更大。常见的一种情况是:在Servlet里转发或重定向到"login.jsp",但当前请求的路径层级比预期深一层,所以实际找的是子目录下的login.jsp,找不到才404。解决办法是把路径改成"/login.jsp",斜杠开头代表从项目根目录查找;使用重定向时还要考虑是否需要加上项目上下文路径req.getContextPath(),否则部署在带项目名的路径下,重定向出去的地址会缺失项目名前缀,同样404。

500错误则多和空指针异常有关。比如从request.getParameter("id")取出的字符串,你直接转成了Integer.parseInt(id),但用户没有传递id参数,parseInt抛NumberFormatException。还有一种情况是Session里取出的loginUser是null,你直接调用user.getNickname(),空指针就出现了。遇到这类500错误时,最快的定位法是看Tomcat控制台最底部的Caused by信息,那行会告诉你具体的异常类型和出错的代码行号,不用猜。

4.3 Session失效和重复提交的场景应对

Session默认的失效时间是30分钟,你对这个时间要有概念。如果做一个后台管理系统,管理员操作到一半去吃饭,回来发现页面跳回了登录页,那就是Session过期了。实际开发中,你可以通过web.xml的session-config来调整这个时间,但更重要的是页面要有“Session过期后自动跳转登录页”的处理逻辑,而不是让用户面对一个奇怪的报错页面。

表单重复提交是另一个高频问题。比如用户注册时,第一次点击提交按钮后因为网络慢又点了第二次,或者注册成功后按F5刷新,都会造成同一条记录被插入两次。这个在第三阶段可能会遇到,因为还没有引入比较复杂的防重复提交机制。

简单的处理方式是:用户注册或提交表单成功后,立刻重定向到一个新的URL,让浏览器的当前地址不再停留在表单提交的地址上。这就是Post/Redirect/Get模式(PRG),也是上面登录成功用重定向的又一个理由。

如果你想防得更细一点,可以在表单页面生成一个随机token存到Session里,表单提交时一并带上这个token,Servlet检查提交的token和Session中的token是否一致,验证通过后立即把Session里的token清除或更换。这样第二次请求带着旧token来就校验不过。这个方案虽然代码多一点,但理解之后对后面的防CSRF攻击也有很大帮助。

5. 工具选型与环境配置说明

JavaWeb_03项目的开发环境和工具选型,会直接影响你调试的效率。很多人在这一步没太在意,用的是别人给的模板配置,结果一运行就出各种奇怪的错误。这里我把自己的工具选择和配置方法说清楚,并解释为什么这么选。

5.1 工具链与运行环境组合

开发这个项目时,我用的是JDK 8、Tomcat 8.5、MySQL 5.7,开发工具用Eclipse IDE for Enterprise Java and Web Developers或者IntelliJ IDEA。JDK 8是老搭档,虽然现在新版本的JDK更新了很多,但绝大多数JavaWeb教学项目仍然以JDK 8为基础,网上的资料也多,遇到问题容易搜到答案。Tomcat 8.5兼容性好,对Servlet 3.1规范支持完整,可以用注解配置Servlet而不用写web.xml。MySQL 5.7是目前教学里最稳妥的选择,8.0版本的驱动包和默认加密规则有点区别,新手容易踩到认证协议相关的坑。

如果你用的是IDEA,创建项目时要注意选择Web Application模板,并手动配置Tomcat Server。IDEA默认打包方式是war exploded模式,方便热部署和调试,但对新手来说,有时候改完代码忘记重新部署,Tomcat里跑的仍然是旧版本。我一般会设置成Build项目时自动更新资源,这样修改JSP文件后刷新一下浏览器就能看到效果。但是修改Java代码必须重启Tomcat,除非你装了JRebel这类热加载插件,否则不要误以为修改Java代码后刷新页面就会生效。

5.2 web.xml中的常用配置项

虽然Servlet 3.0之后可以用注解代替web.xml,但你仍然需要知道web.xml里有哪些配置会影响项目运行。

Servlet版本声明一般长这个样子:

<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">

如果web.xml里的版本和Tomcat支持的版本不匹配,Tomcat启动时会报错或警告,提示你版本号不对。所以当你的项目里既有注解又有web.xml时,要让web.xml的version和你使用的主要规范保持一致,不要无脑复制别人项目的配置。

另一个常用的配置是起始页面:

<welcome-file-list> <welcome-file>login.jsp</welcome-file> </welcome-file-list>

如果不配置,访问项目根路径时Tomcat会按默认的index.html、index.jsp顺序找。配置成login.jsp后,访问项目根地址就会直接打开登录页。在实际项目里,一般访问根路径会跳到首页,再由首页判断是否登录、是否跳转到登录页,这个策略可以根据项目逻辑自己定义。

5.3 编码与字符集的统一配置

编码配置一定要在项目早期就处理好,不然后面加的数据全是乱码,排查成本远高于现在改一行配置。建议做三处设置:

  • 开发工具IDE的项目文件编码设置为UTF-8。IDEA在File -> Settings -> Editor -> File Encodings里把Global Encoding、Project Encoding和Properties Files的编码都改成UTF-8;Eclipse在Window -> Preferences -> General -> Workspace里改Text file encoding,同时在项目的Properties里也设置Resource编码。
  • JSP页面统一加page指令:<%@ page contentType="text/html;charset=UTF-8" language="java" %>,同时在HTML的meta标签里加<meta charset="UTF-8">。
  • 数据库连接URL加characterEncoding=utf8,数据库表的字符集统一为utf8mb4。之所以用utf8mb4而不是utf8,是因为MySQL的utf8实际上只能存三个字节的字符,遇到生僻字或emoji就会报错,utf8mb4才是真正的完整UTF-8编码。

这三处统一搞定后,中文乱码问题基本能避免。最怕的是IDE用GBK、JSP用UTF-8、数据库用latin1,每层都不同,那排查起来简直是一场灾难。

6. 实操心得:这个阶段做完,我彻底想明白了什么

JavaWeb_03这个项目做完后,最直观的感受是以前写Servlet时模模糊糊的东西,现在终于有一条清晰的主线了。这里把我在实际开发过程中形成的一些体会和判断标准分享出来,有些可能不是教程里会强调的东西,但恰恰是最能拉开差距的细节。

6.1 代码结构清爽带来的实际收益

以前我用一个Servlet从头写到尾,添加一个功能要复制粘贴好几段重复代码,改一个字段要小心翼翼。做了这个项目之后,我把每一段职责都分到了对应层。最直观的变化是,当我需要改业务逻辑时,不用再翻Servlet页面去找代码;当我需要改SQL时,不用担心影响页面显示。代码文件的体积也从几百行降到了几十行,读代码的时间大幅减少。

这种收益在小项目里可能不太明显,但你只要试想一下:用户在页面上点了注册,这个请求要经过JSP、Servlet、Service、Dao、数据库,任何一层出了问题,都有一个明确的搜索范围。代码结构其实是一种沟通成本,结构好的人,同事接手他的代码时不会骂人。

6.2 从报错信息中快速定位问题的能力

经过这一阶段的训练,我养成的一个习惯是:看到报错先分类,再逐层排查,不要一上来就改代码。

  • 如果报错是ClassNotFoundException,优先检查WEB-INF/lib下的jar包是否齐全。
  • 如果报错是NumberFormatException,优先检查request.getParameter取到的值是不是空字符串或非数字内容。
  • 如果报错是NullPointerException,优先检查从Session或request取出的对象是否为空。
  • 如果报错是SQLException里的Data too long或者Incorrect string value,往编码方向和字段长度方向排查。
  • 如果是Tomcat启动失败且报错提到端口占用,用命令行查看哪个进程占用了8080,然后杀掉或者换端口。

这些习惯不是看书看出来的,而是踩坑踩出来的。第一阶段你可能每个错误都要花一个小时去搜,但练到第三个阶段,你会对常见的错误模式形成条件反射,速度和信心都会上来。

6.3 对后续学习路径的直接影响

做完JavaWeb_03,你的知识储备其实已经覆盖了JavaWeb开发里的核心骨架:请求处理、跳转、状态管理、持久化、页面渲染。再往后学,Spring MVC其实就是在这个骨架上做了一层封装:DispatcherServlet替代你手写的Servlet,ModelAndView替代request和Session的一部分工作,MyBatis替代手写JDBC和Dao代码。有了这一阶段的底层经验,后面学框架时会更加轻松,因为你清楚框架帮你做了什么,也清楚框架不够用时怎么回退到原生写法。

说个我的个人建议:如果时间允许,把JavaWeb_03这个项目里的所有代码用自己的话重新默写一遍,不要看着原代码抄。默写一遍你会发现很多你以为记住了的细节其实根本没记住,比如response.sendRedirect和getRequestDispatcher的路径写法差异、PreparedStatement的下标是从1开始而不是0、Session里取出来的对象需要强转。这些细节才是真正折磨人的地方。

7. 总结与心里的几句实在话

做完了JavaWeb_03,我最大的感受是:JavaWeb这套东西,如果不亲手写一遍,永远不知道自己会踩多少坑。转发和重定向的区别讲一百遍也不如自己在浏览器里看一次地址栏的变化和网络请求记录来得深刻;Session和Cookie的区别背十遍理论,也不如亲手实现一次“记住用户名”和“七天免登录”功能来得直观。

这个阶段项目可能看起来不大,但它带来的思维转变是非常明显的。你开始拥有“分层”的意识,开始明白一个请求从浏览器到服务器经历了什么,开始在报错信息面前学会冷静分析,而不是抓瞎乱试。这些能力在后续任何技术栈中都能复用。

最后分享一个小技巧:做登录功能时,先别急着写代码,先在纸上画出登录成功和失败两条流程线,标出每一步的数据位置和跳转方式。等你把这两条线画明白了,代码只是把纸上的逻辑翻译成Java而已。很多人写代码写不出来的原因,不是语法不熟,而是逻辑没想清楚。JavaWeb_03这个项目,其实就是在帮你训练这种把事情想清楚再动手的习惯,而这一点的价值,远远超出了这个项目本身。

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

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

立即咨询