news 2026/10/2 19:22:51

JavaWeb核心必考点:Servlet生命周期、Session会话与JDBC实战梳理

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
JavaWeb核心必考点:Servlet生命周期、Session会话与JDBC实战梳理

JavaWeb这门课,在自学Java的人眼里通常是个分水岭。前面学JavaSE的时候,每天对着控制台黑框框System.out.println,写个贪吃蛇都恨不得用光标当蛇身,成就感来得快去得也快。等到第二阶段JavaWeb,才算是真正从“写代码”跨到“做系统”,也是第一次能把Java代码和浏览器、数据库、网络请求这些东西串起来。这篇笔记是我在跟黑马Java第二阶段课程时整理的,不说废话,把Servlet、请求响应、Session、JDBC、过滤器这些必考点和实战思路给你捋清楚,适合学完Java基础打算往后端方向走的同学,也适合正在复习JavaWeb准备面试的朋友拿来查漏补缺。

1. JavaWeb阶段到底在学什么

1.1 从控制台到浏览器的技术跨越

很多人刚进JavaWeb阶段会蒙圈,原因在于技术栈一下子变多了:Tomcat、Servlet、JSP、HTTP协议、MySQL、JDBC、Maven,每样都见过名字,但不知道它们是怎么配合工作的。

其实核心只有一条主线:浏览器发请求,服务器处理完再把结果还回去。整条链路上,每个组件都有自己的位置。Tomcat是Web容器,负责接收HTTP请求,并且把请求交给对应的Java程序处理,处理完再把响应返回给浏览器。Servlet就是那个真正写业务逻辑的Java类,对应“收到请求之后怎么办”。JSP是服务端的页面模板,早期项目用来动态渲染HTML。JDBC是Java连数据库的官方API,负责把数据从MySQL里查出来、存进去。Maven管理项目依赖,帮你把各种jar包理清楚。

这条链路想明白,后面学什么都不会乱。学Servlet的时候你会知道处理请求时需要读参数、调数据库、回写结果;学JSP的时候你明白它只是展示层的工具;学Maven的时候你也清楚它解决的还是“jar包怎么放”的问题。技术点再多,都在同一条主线上。

1.2 一条注册请求的完整旅行

举个最典型的场景,用户在前端页面填完注册信息,点了提交按钮。浏览器构造出POST请求,带着表单里的用户名和密码,发到Tomcat的8080端口。Tomcat拿到URL后,会去web.xml或者注解里找匹配的Servlet路径,找到之后调用Servlet的service方法,然后根据请求类型分发到doPost方法里。

doPost里做的事情通常是:从request对象里拿参数,做基础校验,然后调Service层的方法,Service再走Dao层通过JDBC把数据写入MySQL。写入成功之后,Servlet要么转发到一个JSP页面显示“注册成功”,要么直接重定向到登录页面。整个流程走完,浏览器地址栏变化一下,用户看到结果,一次典型JavaWeb请求就结束了。

这个过程看似简单,但每个环节展开讲都有不少坑。比如Tomcat找不到Servlet会报404,JDBC连不上库会报Communications link failure,JSP页面取值取不到会在页面上显示null。我在下面的章节里把这些点拆开细讲。

2. Servlet生命周期与请求响应核心

2.1 Servlet本质与实现步骤

Servlet在JavaWeb里是绝对不能绕开的东西。说起来它就是个Java类,只不过它实现了一套接口,能被Tomcat这样的容器管理起来。你写的普通Java类在main方法里自己new、自己调,Servlet的生命周期完全交给容器控制。

对比一下,普通程序处理一次请求,你得自己启动Socket监听、解析HTTP报文、拼响应头,工作量非常吓人。Servlet把这些都封装好了,你只需要关注业务逻辑本身,类似你点外卖只需要等餐,不用自己去田间地头种菜。

创建一个最基础的Servlet只需要三步:写一个类继承HttpServlet,重写doGet和doPost方法,加上@WebServlet注解配上访问路径。

@WebServlet("/register") public class RegisterServlet extends HttpServlet { @Override protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { resp.setContentType("text/html;charset=UTF-8"); resp.getWriter().write("这里是注册页的GET请求处理"); } @Override protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { req.setCharacterEncoding("UTF-8"); String username = req.getParameter("username"); String password = req.getParameter("password"); System.out.println("收到注册请求:" + username + " / " + password); resp.getWriter().write("注册成功"); } }

在Tomcat 7之前的版本,你需要手动在web.xml里配置Servlet的映射关系;现在用注解省事很多。阿里巴巴的Java开发手册里建议用全路径命名Servlet,避免重名导致路由冲突,实际项目里这个习惯值得养成。

2.2 request和response到底能干什么

HttpServletRequest封装了请求的所有信息,包括请求头、参数、请求体。最常用的几个方法:getParameter拿表单或URL上的参数,setCharacterEncoding设置请求体编码,getRequestDispatcher做请求转发,getSession获取会话对象。

除了这些基础操作,request还有一个容易被忽略的点:它本身自带一个Map类型的作用域对象,叫request域。在Servlet里往request里setAttribute,在转发到的JSP里getAttribute,数据就能带过去。这个机制比地址栏传参安全,因为数据不会暴露在URL上。

HttpServletResponse则负责把结果写回浏览器。setContentType告诉浏览器返回的是什么类型的数据;getWriter返回一个PrintWriter流,用它write出来的内容就是响应体。这里有个高频踩坑点:写中文之前必须设置编码,否则浏览器会乱码。哪怕在Tomcat 8以上版本get请求的编码处理好了,response输出的中文依然需要手动指定UTF-8。

关于请求转发和重定向的区别,面试高频,我在这里说清楚。转发是服务器内部行为,地址栏不变,request域数据可以带过去,整个过程一次请求;重定向是服务器告诉浏览器“你去访问另一个地址”,地址栏会变,需要浏览器再发一次请求,request域失效。写登录成功后跳首页这种场景,推荐用重定向,规避表单重复提交的问题。

2.3 生命周期与线程安全问题

Servlet生命周期只有三条方法:init、service、destroy。第一次请求到达时,Tomcat创建Servlet实例并调用init;后续请求直接复用同一个实例;容器关闭时调用destroy做清理工作。这个模式意味着Servlet是单实例多线程的,并发场景下同一个对象会被多个线程同时访问,写成员变量就会有线程安全问题。

举个例子,如果你在Servlet里定义一个成员变量int count = 0,每次请求里count++再输出,高并发时你会看到结果有误差。因为多线程同时读写一个共享变量,操作不是原子的。解决思路很简单:有状态的信息放到局部变量里,或者放到request/session域里,让每个线程各拿各的副本做处理。

这里有一条我个人的实操经验:写Servlet时,除了静态的Service对象这种典型的只读依赖,尽量别在Servlet类里放可变的成员变量。以前我很喜欢把数据库连接对象当成员属性,后来在高并发下遇到过几次连接错乱的问题,排查了很久才定位到是实例变量导致的共享污染。捕获异常后把traceId打到日志里,再结合变量赋值位置,立刻就能看清楚问题来源。

3. 会话跟踪:Cookie与Session的底层协作

3.1 无状态HTTP下的登录状态难题

HTTP协议本身是无状态的,一个请求和一个请求之间互相不认识。但业务场景要求“登录之后的所有操作都能识别用户身份”,这就需要额外机制。你可以把它类比成去健身房:第一次去办卡,前台给你一张会员卡,之后每次进门刷一下卡,工作人员就知道你是谁。

JavaWeb里这张会员卡对应的就是Cookie和Session的组合。Cookie存在浏览器端,是一小段文本数据,服务器可以通过Set-Cookie响应头种到浏览器里,浏览器之后的请求会自动带上它。Session存在服务器端,是一块保存在内存里的对象空间,每个Session对象有一个唯一的ID作为标识。

两者怎么凑一起?服务器创建Session之后,把Session的ID写进一个名为JSESSIONID的Cookie里返回给浏览器。浏览器请求时带上这个Cookie,服务器根据JSESSIONID找到对应的Session对象,就知道请求来自谁了。查看浏览器开发者工具时你会看到这个细节:Application面板的Cookies列表里,总会躺着一个JSESSIONID。

3.2 Session使用与失效场景

获取当前会话对象很容易,request.getSession()即可。第一次调用会创建新的Session,后面再调用会返回之前创建的那个。往Session里存user对象,后续的Servlet就能取出来判断用户是否登录。

// 登录成功,保存用户到Session HttpSession session = request.getSession(); session.setAttribute("loginUser", user); // 其他请求里校验登录状态 HttpSession session = request.getSession(false); if (session == null || session.getAttribute("loginUser") == null) { resp.sendRedirect("login.jsp"); return; }

注意第二段代码里的getSession(false),这个重载方法表示“如果当前没有Session就返回null,不要创建新的”。很多新手在这里习惯写getSession(),结果就是每次请求都会新建Session对象,服务端内存里的无用Session越攒越多,日志里全是新建Session的记录。

Session有几个常见的失效场景:默认30分钟无操作,服务器自动销毁;调用session.invalidate()主动销毁;关掉浏览器之后,虽然服务端Session还有,但浏览器端的JSESSIONID没了,下一次打开就是一个全新会话。这里有个容易忽略的坑,在同一浏览器多个标签页之间,Session是共享的,因为Cookie属于同一个域名。但如果你开了无痕窗口,所有状态都会“丢失”,排查Session问题时可以先用无痕窗口排除浏览器缓存干扰。

3.3 多台服务器时Session该怎么办

单机部署时Session问题不大,所有请求都落在同一个Tomcat里,内存里的Session直接可用。但实际项目一旦上了多台Tomcat,比如Nginx做了负载均衡,问题就来了:同一个用户两次请求可能落在不同的服务器上。第一次请求在A机器上创建了Session,第二次落在B机器,B不认识这个Session ID,用户就被强制下线了。

常见处理方案有三类。第一类Session复制,让集群里所有机器同步Session数据,简单但性能浪费大,机器多了同步开销很吓人。第二类粘滞会话,Nginx按用户IP哈希分发,让同一个IP固定打在一台机器上,实现简单但缺点也明显,某台机器挂了那部分用户就全掉线。第三类是主流方向,把登录状态做成无状态Token,服务端不存Session,用户信息放在加密签名的Token里,校验逻辑只在服务端做解密和验签。

学到这里很多同学会恍然大悟:为什么后面学SpringBoot时大家都用JWT,因为本质上JWT就是第三种方案。JavaWeb阶段学Session不是为了让你未来一定用它,而是帮你理解状态管理的演进逻辑,面试被问到分布式会话时也能答得上来。

4. JSP、EL表达式与MVC分层思想

4.1 JSP的本质与执行过程

JSP的全称是JavaServer Pages,早期项目里承担动态页面的职责。表面看起来是一个HTML文件里混着Java代码,实际上JSP文件在第一次被访问时,Tomcat会把它翻译成一个Java类,这个类继承Servlet。所以JSP的本质还是Servlet,只是让你能以写HTML的方式输出响应。

可以打开Tomcat的work目录看一下,Catalina目录下面存放着JSP翻译后的.java和.class文件,那个Java类里你会发现熟悉的方法:_jspService方法,里边把你写在JSP里的HTML通过out.write输出,把Java代码原封不动搬进去执行。所以你在JSP里写的每个表达式、每段Scriptlet,运行起来的效果和Servlet里写resp.getWriter().write(). 完全一样。

既然JSP本质是Servlet,那它也有生命周期,也有线程安全问题,但实际开发中JSP有几个致命痛点:Java代码和HTML混在一起非常难维护,前端工程师和后端工程师没法并行工作,页面稍微复杂一点代码就乱成一团,浏览器报错信息也不直观。这也是JSP现在被Vue、React这些前后端分离方案取代的原因。但JSP的原理思维仍然值得了解,服务端渲染的思路在模板引擎、动态邮件模板里依然随处可见。

4.2 EL表达式与JSTL标签

为了让JSP页面少写Java代码,引入了EL表达式和JSTL标签库。EL表达式的写法是${user.username},用来从request、session等域对象里取值。它最大的优势是:域里存的值取不到的时输出空字符串而不是null,不会把丑陋的null打到页面上。

EL有一套自己的取值顺序,默认从小范围到大范围查找:page域优先,然后request、session,最后application。这个查找顺序知道就好,实际编码时为了避免歧义,尽量在表达式里明确域对象,比如${sessionScope.user.username}比${user.username}更精准。

JSTL里几个核心标签使用频率非常高。c:forEach遍历集合,c:if做条件判断。遍历一个用户列表的写法:

<table> <c:forEach items="${userList}" var="u" varStatus="status"> <tr> <td>${status.count}</td> <td>${u.username}</td> <td>${u.email}</td> </tr> </c:forEach> </table>

这里有个经常被忽略的小细节:varStatus的count属性从1开始计数,index属性从0开始。在表格里显示行号,用count最合适。我在练习时写过一个小项目,表格行号从0开始,调试了很久才发现是属性用错了,这种小坑写多了就记住了。

4.3 MVC分层到底帮我们解决了什么

JavaWeb阶段最核心的设计思想之一就是MVC分层,同时这也是后面学任何框架的底层逻辑。M是Model模型层,主要处理数据和业务逻辑,对应项目里的Service和Dao;V是View视图层,负责展示数据,对应JSP页面;C是Controller控制层,接收请求、调度模型、决定返回哪个视图,对应Servlet。

分层最直观的好处是各司其职、方便改代码。在实际操作里,你可以先写Servlet处理请求和转发跳转,再写Dao操作数据库,最后写Service把Dao包装成业务方法。改列表页样式时只动JSP不用管Java代码,修改SQL只动Dao不用碰前端页面。有一个地方需要注意:有人把业务逻辑写在Servlet里,Servlet几百行之后变得很难维护,后来把业务挪到Service、把SQL挪到Dao,代码可读性立刻高了很多。

5. 数据库交互基石:JDBC编程与事务处理

5.1 JDBC六步曲与PreparedStatement选择

JDBC是Java操作数据库最原始的API,后面学的MyBatis、Hibernate都是对它的封装。JDBC的编程套路固定为六步:注册驱动、获取连接、创建Statement、执行SQL、处理结果集、释放资源。这六步无论写在哪个类里、换成什么数据库,套路都是一样。

使用DriverManager获取连接,早期的写法是Class.forName("com.mysql.jdbc.Driver"),MySQL 5之后驱动类改成了com.mysql.cj.jdbc.Driver。老教材里的写法在新版本里会直接抛异常,所以看到网上旧代码报ClassNotFoundException不用慌,先看驱动类名对不对。

执行SQL时,重点强调一件事:不要用Statement拼接SQL,一定要用PreparedStatement。原因也不能只知道SQL注入四个字,原理大概是这样的:Statement是直接把用户输入的字符串拼接进SQL语句。如果用户在用户名框里输入一个' or '1'='1,拼接出来的WHERE条件是永真式,他就能绕过密码校验,这就是最典型的SQL注入。PreparedStatement用占位符?预编译SQL结构,用户输入的内容只被当作参数传递,不会改变SQL语义。

String sql = "SELECT * FROM user WHERE username = ? AND password = ?"; try (Connection conn = DruidUtils.getConnection(); PreparedStatement ps = conn.prepareStatement(sql)) { ps.setString(1, username); ps.setString(2, password); try (ResultSet rs = ps.executeQuery()) { if (rs.next()) { // 查询成功,走登录成功逻辑 } } }

注意我用了try-with-resources语法,Java 7之后支持,资源可以自动关闭。在旧代码里经常看到finally块里逐个close,代码冗长还容易漏掉,特别是异常路径上,经常会因为某个资源没关导致连接泄漏。换成try-with-resources之后,Connection、Statement、ResultSet实现AutoCloseable接口的都能自动回收。从Druid连接池里拿连接时,close实际上是把连接归还连接池而不是真正关闭,所以资源释放这段逻辑尤其重要。

5.2 事务带来的是一致的不是独立的

先想明白一个问题:没有事务的时候会发生什么?转账场景中,A账户扣掉500元,B账户加500元。如果扣款成功、加款失败,钱就凭空消失了。数据一致性被破坏,这在金融业务里是不可接受的。事务就是解决这个问题的机制,保证一组SQL操作要么全部成功,要么全部失败。

事务有ACID四个特性,但重点记忆原子性就可以展开很多:原子性意味着不可分割,整个事务是一个整体;一致性意味着操作前后数据总量不变;隔离性意味着事务之间各干各的;持久性意味着一旦提交,改动永久生效。

JDBC里事务默认是自动提交的,每条SQL执行完自动commit。要手动控制事务,先调用conn.setAutoCommit(false)关闭自动提交,业务操作完成之后调用commit提交,出错时在catch块里调用rollback回滚。

Connection conn = null; try { conn = DruidUtils.getConnection(); conn.setAutoCommit(false); String sqlA = "UPDATE account SET money = money - 500 WHERE name = '张三'"; String sqlB = "UPDATE account SET money = money + 500 WHERE name = '李四'"; // 分别用PreparedStatement执行 conn.commit(); } catch (Exception e) { if (conn != null) { conn.rollback(); } throw new RuntimeException("转账失败", e); } finally { if (conn != null) { conn.setAutoCommit(true); conn.close(); } }

有个小细节:回滚之后连接还会归还给连接池,如果你在finally里直接close而没把AutoCommit恢复成true,下一次从池子里拿到这个连接的人,会非常困惑为什么自己的SQL一直不生效。把setAutoCommit(true)放在finally里恢复很重要,这是很多人踩过但没注意的坑。

5.3 连接池与分页两个常见实操点

连接池的基本思路是,在应用启动时预先创建一批数据库连接放进容器,需要的时候从池子里取,用完再还回去。频繁创建和销毁数据库连接的成本很高,建立一次TCP连接加认证握手的时间不短,高并发场景下开销更明显。Druid是阿里开源的连接池,监控能力很强,JavaWeb阶段用它练习也很常见。

<dependency> <groupId>com.alibaba</groupId> <artifactId>druid</artifactId> <version>1.2.20</version> </dependency>

Druid的基本配置包括driverClassName、url、username、password、initialSize、maxActive几个关键项。其中maxActive控制最大活跃连接数,设置太小高并发时连接耗尽报超时,设置太大会白白占用数据库资源,建议根据压测结果慢慢往上调。实操里最常见的是配置了maxActive=50,但压测并发一上去就报Connection is not available。排除掉连接泄漏因素后,往往就是连接池配置的初始和最值关系没协调好。

分页也是一个绕不开的需求。MySQL的分页用LIMIT关键字,语法是LIMIT offset, size。第一页取10条就是LIMIT 0, 10,第二页是LIMIT 10, 10,规律是起始偏移量等于页码减一乘以每页条数。前端传来的页码btnPage,每页pageSize条,SQL拼接的时候需要手动计算偏移量:

SELECT * FROM user ORDER BY id LIMIT #{offset}, #{size}

分页时还要做总页数计算,先查总记录数count,再用总记录数和pageSize计算pageCount。小的边界场景比较多:没有数据时页面显示什么,最后一页不够pageSize时显示几条。我在测试里发现,如果pageSize不合法或者传0,SQL会出现负数偏移量直接报语法错误,所以在入口处做参数校验非常有必要——前端传的不是永远可信,这是JavaWeb阶段最该早期建立的意识。

6. 过滤器、监听器与综合案例实操

6.1 Filter:统一处理代码的复用利器

过滤器是JavaWeb里很有用处的组件,位置在请求到达Servlet之前执行。一次请求的完整过滤链是:客户端请求先经过Filter,Filter里的逻辑执行完,如果调用chain.doFilter继续放行,请求才进入Servlet。Filter的好处是可以抽离横切逻辑,把重复代码收敛到一个地方。

最常见的Filter用法是统一编码:

@WebFilter("/*") public class EncodingFilter implements Filter { @Override public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { HttpServletRequest req = (HttpServletRequest) request; req.setCharacterEncoding("UTF-8"); response.setContentType("text/html;charset=UTF-8"); chain.doFilter(request, response); } }

路径"/*"表示所有请求都先走这个过滤器。过滤器还有另外一个经典场景:登录验证过滤器。在Filter里判断当前Session里有没有用户信息,没有就重定向到登录页面,有就放行。写这个逻辑的时候要小心一个坑:登录页面本身的请求也被过滤器拦住了,如果不放行,就会陷入“访问登录页被拦截去登录”的死循环,永远到不了登录页。正确的做法是在过滤器里对login路径做特判,放行白名单路径。

6.2 监听器用的不多但面试常考

相对于Filter的日常高频,Listener的存在感会低一些。但它依然值得掌握,因为面试喜欢从这些基础组件上拉开差距。

ServletContextListener可以监听应用启动和销毁事件,通常用来做配置初始化,比如在服务器启动时加载全局配置文件。HttpSessionListener可以监听Session的创建和销毁,一个典型应用是统计在线人数——在线人数就是存活的Session数量,Session创建的时候计数加1,销毁的时候减1。

@WebListener public class OnlineCountListener implements HttpSessionListener { @Override public void sessionCreated(HttpSessionEvent se) { ServletContext app = se.getSession().getServletContext(); Integer count = (Integer) app.getAttribute("onlineCount"); if (count == null) { count = 0; } app.setAttribute("onlineCount", count + 1); } @Override public void sessionDestroyed(HttpSessionEvent se) { ServletContext app = se.getSession().getServletContext(); Integer count = (Integer) app.getAttribute("onlineCount"); if (count != null) { app.setAttribute("onlineCount", count - 1); } } }

在线人数的统计逻辑不复杂,但实现时要注意线程安全:ServletContext是全局共享对象,多用户同时在会话里时对这个变量递增递减,存在并发问题。正式项目中一般用AtomicInteger或者加锁解决,但JavaWeb阶段能写出整体逻辑就够了。

6.3 综合案例:Maven搭建一个员工管理系统

零散的Servlet、Filter、JSP知识点如果只学不练,遗忘速度非常快。分享一个我自己练手的完整项目结构,不复杂但能覆盖大部分JavaWeb知识点:基于Maven搭建的员工管理系统。

先在pom.xml里导入mysql-connector-java、druid、jstl、servlet-api这些依赖。需要说明的是servlet-api和jsp-api这种由容器提供的依赖,scope要设置为provided,意思是编译时需要、运行时不打包进去,因为Tomcat自己已经带了,重复打包反而可能导致版本冲突。

项目结构按MVC分层:

src/main/java ├── com.example.filter // 编码过滤器、登录验证过滤器 ├── com.example.web // Controller层,EmployeeServlet、LoginServlet ├── com.example.service // 业务逻辑层,EmployeeService、LoginService ├── com.example.dao // 数据访问层,EmployeeDao ├── com.example.entity // 实体类,Employee └── com.example.util // 数据库工具类,DruidUtils src/main/resources └── druid.properties // 数据库连接配置 src/main/webapp ├── login.jsp ├── employeeList.jsp └── WEB-INF/web.xml // 可选的部署描述符

功能点覆盖这样安排:登录功能使用Session验证+过滤器拦截;员工列表分页使用LIMIT查询;新增/删除员工涉及JDBC的增删改;所有页面必须登录才能访问。把Maven作为构建工具之后,依赖的声明和项目构建都会规范化,这也是第二阶段学习里很关键的收获。

下面用一个表格对比一下我看完课程之后能做什么、对照自己掌握程度:

模块知识点实操目标
Servlet生命周期、请求响应、转发重定向独立实现一个登录Servlet
Session/Cookie会话跟踪、登录校验实现自动登录和退出登录
JSP/EL/JSTL页面渲染、遍历列表用JSP展示员工表格并分页
JDBCPreparedStatement、事务自己封装一个DruidUtils并写CRUD
Filter/Listener统一编码、在线统计给项目加统一编码和访问日志
Maven依赖管理、生命周期用一条命令打包war并部署到Tomcat

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

7.1 JavaWeb新手的崩溃现场

JavaWeb阶段最常见的报错,基本都集中在以下几个场景里,很多错误信息看起来吓人,实际原因就那么几个。

现象常见原因解决建议
启动Tomcat报8080端口被占用另一个Tomcat或javaw.exe占了端口cmd里netstat -ano查到PID,任务管理器结束对应进程
访问Servlet报404访问路径与@WebServlet不一致,或项目未部署检查注解路径、项目是否成功构建到webapps
页面中文乱码请求或响应编码未统一设置为UTF-8request/response统一setCharacterEncoding("UTF-8")
报ClassNotFoundException: com.mysql.cj.jdbc.DriverMySQL驱动依赖未引入或版本不对检查pom.xml里是否引入mysql-connector-java
报Communications link failure数据库服务没启动、URL写错、用户名密码错先确认MySQL服务开启,再用客户端工具测连接
页面取不到Session中的值请求可能落在了不同的Tomcat或Session被动创建检查getSession(false)写法和服务器部署拓扑

7.2 让我印象深刻的几个bug及排查过程

第一个是环境变量配置的坑。很多人的电脑上Java装了很多个版本,Eclipse或IDEA里配的是JDK 17,命令行里java -version显示的却是JDK 8。这种环境错乱经常导致项目编译版本和运行版本不一致,出现UnsupportedClassVersionError。排查方式是确认JAVA_HOME、Path、IDE里的JVM配置三个地方保持一致。

第二个是Tomcat的缓存问题。改了代码之后重新部署,页面还是旧的效果,很可能是IDEA的构建没有同步到Tomcat的部署目录。建议搞懂IDEA里Artifacts的配置原理,或者直接使用Maven的clean package重新打包,能有效避免这种奇怪问题。

第三个是日志输出的中文乱码。控制台乱码有时候跟Tomcat的日志编码有关,在Logs目录下配置UTF-8或修改catalina.bat里的JAVA_OPTS加-Dfile.encoding=UTF-8通常能解决。这个坑跟数据库乱码不是一回事,排查的时候先分清楚是控制台还是页面编码问题。

7.3 学习过程的整理心得

关于JavaWeb阶段学习,我自己的体会是代码量要比看视频更重要。跟着课程看完一遍,和自己从头写一遍感受完全不同。看视频时觉得每个知识点都简单,真正动手写的时候,到处都是没注意的细节。

学习方法上有一点很关键:按阶段自查。JavaWeb基础部分用Maven建一个干净的Servlet项目,把生命周期七个方法的触发时机打印出来;熟悉后写一个完整的CRUD,不用框架只靠Servlet+JDBC做,锻炼自己能独立调试;最后再看那些专门讲框架的课,试着自己封装一个简易MVC,用类似Spring的思路组织Controller。

建立知识图谱式的笔记也很有效。我不会把每节课的笔记都抄下来,而是按照“HTTP请求到Servlet处理到JDBC操作到JSP展示”这条主线,将遇到的所有问题挂在对应节点上。之后无论是复习还是应付面试,看一遍自己的路径就能快速回到全貌。比如看到request作用域就会想到转发和重定向的区别,看到Filter就会想到登录校验和编码统一。

多给自己设置一些“变态需求”来练习也会很有帮助。比如“登录时把密码错误次数加进去,超过三次锁定五分钟”,或者“员工列表支持按部门筛选再分页”。这些需求单拎出来都不难,但组合起来就非常贴近真实项目,远比照着源码敲一遍更有收获。你会发现Session、Cookie、Filter、JDBC这些零散的知识点,在真实业务场景里其实都是互相配合的。

最后聊一点JavaWeb在整个Java学习路线里的位置。第二阶段说白了就是打地基,Servlet请求处理的模型理解透了,后面学SpringMVC时你会觉得它的设计很顺理成章;JDBC写多了,再看MyBatis你会明白它在帮减少样板代码;JSP的写法演进到前后端分离也自然水到渠成。很多框架层面的“约定优于配置”,底层逻辑都来自JavaWeb阶段那些“麻烦”的设计。把这层地基打好,后面往上盖楼就不会歪。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/2 19:22:33

基于Logistic回归的网站用户行为预测建模实践与落地指南

简介&#xff1a;这份资源为机器学习在网站用户行为预测中的应用研究文献&#xff0c;面向数据分析、互联网运营、旅游网站运营以及算法学习者。内容围绕logistic回归算法展开&#xff0c;详细讲解用户行为数据集的预处理、按固定比例分类及统计分布验证&#xff0c;并给出旅游…

作者头像 李华
网站建设 2026/10/2 19:20:00

基于微信小程序与Spring Boot的电商平台完整源码与部署调试

这个项目是我业余时间开发的一套基于微信小程序的电商购物平台&#xff0c;源码、部署文档、调试记录都整理在了一个仓库里。和市面上那些只有静态页面、点两下就断了的课程 demo 不一样&#xff0c;这版把登录授权、商品列表、购物车、下单、微信支付、订单管理完整串了起来&a…

作者头像 李华
网站建设 2026/10/2 19:18:55

二叉树递归深搜:剪枝与验证BST的两种核心设计

递归、深搜、回溯、二叉树——这几个词放在一起&#xff0c;刷题的人基本都能脑补出一整套套路&#xff1a;先画递归树&#xff0c;再想出口&#xff0c;再决定把当前节点放到哪一步处理。我自己在带新人和写博客时发现&#xff0c;很多人卡在“递归函数到底要不要返回值、返回…

作者头像 李华
网站建设 2026/10/2 19:18:09

Unity3D交互式数字博物馆开发实战:交互、模型与部署全解析

简介&#xff1a;一份面向数字博物馆、虚拟展馆及Unity3D开发方向的完整设计与实现论文文档&#xff0c;针对传统数字博物馆偏重数字展示、缺乏人与藏品交互的问题&#xff0c;提出以人为中心的交互式数字博物馆理念&#xff0c;强调提高藏品与人之间的互动&#xff0c;增强学习…

作者头像 李华
网站建设 2026/10/2 19:18:02

从摄像头到Unity3D:基于Mediapipe的实时动捕链路实现

简介&#xff1a;一套围绕OpenCV、Python、Mediapipe与Unity3D构建的计算机视觉动作捕捉实践资料&#xff0c;面向希望掌握人体姿态估计、多关节运动跟踪以及三维模型驱动的开发者或学习者。压缩包内共10个文件&#xff0c;整体约15.47MB&#xff0c;主要包含Python编写的摄像头…

作者头像 李华
网站建设 2026/10/2 19:17:16

2026年3月高中化学教辅推荐:高一到高三选书实用指南

每年一到三月份&#xff0c;后台私信里出现频率最高的关键词&#xff0c;差不多就是“2026年3月高中化学教辅材书籍推荐”这一长串字。说实话&#xff0c;每年这个时间点聊化学教辅&#xff0c;已经快变成我的固定节目了。为什么偏偏是三月份&#xff1f;因为三月对高一、高二、…

作者头像 李华