news 2026/10/15 11:04:04

Servlet基础实验全流程:从环境搭建到登录功能实现

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Servlet基础实验全流程:从环境搭建到登录功能实现

1. 为什么还要死磕Servlet基础

很多人刚接触Java Web开发,第一反应就是直接上Spring Boot,觉得Servlet是上古时代的东西,学了没用。我带过不少新人,几乎每隔一段时间就会听到类似的说法。但实际情况是,你如果不懂Servlet,后面遇到过滤器不生效、请求参数拿不到、中文乱码、Session丢失、转发和重定向搞混这些问题,你连排查的方向都找不到。Servlet是整个Java Web体系的底层地基,Spring MVC再封装,它最终也是跑在Servlet容器里的一个Servlet。你把这个地基摸透了,上层框架对你来说就是透明的。

这篇内容适合谁看?一是正在上Java Web课程、需要完成Servlet基础实验的同学;二是自学Java后端、想真正搞明白请求响应全流程的开发者;三是从其他语言转过来、对Java Web这套机制比较陌生的朋友。我会从一个完整的实验项目出发,把Servlet的核心知识点、实操步骤、踩坑经验全部拆开讲清楚。你跟着走一遍,能独立写出一个基于Servlet的完整Web应用,并且知道每一行代码为什么这么写。

先明确一下这个实验到底要做什么。所谓Servlet基础实验,核心目标就是:用最原始的方式,不依赖任何高级框架,手写Servlet来处理浏览器发来的HTTP请求,并返回响应内容。听起来简单,但里面涉及的知识点非常多:Servlet的生命周期、请求参数的获取、中文编码处理、请求转发与重定向的区别、Session和Cookie的使用、ServletContext的作用等等。这些知识点不是孤立的,它们串起来就是一次完整的请求处理链路。

我个人的习惯是,学任何东西都要先搞清楚它在整个体系里的位置。Servlet在Java Web里的位置,简单说就是:浏览器发HTTP请求,Web服务器(比如Tomcat)接收请求,根据URL找到对应的Servlet,调用Servlet的方法处理,Servlet把结果写回响应,服务器再把响应发回浏览器。整个链路里,Servlet就是那个真正干活的角色。你把这个链路理解了,后面学什么框架都是在这个基础上加东西。

2. 实验环境搭建与项目结构设计

2.1 工具选型与版本搭配

做Servlet实验,环境搭建是第一步,也是最容易劝退人的一步。我见过太多人卡在环境上,还没开始写代码就放弃了。这里我把选型和理由说清楚,你照着配就行。

JDK版本建议用JDK 8或者JDK 11。为什么不用更高的版本?因为Servlet实验本身不涉及新特性,JDK 8足够稳定,而且大部分教程和容器对JDK 8的兼容性最好。如果你已经装了更高版本,也能跑,但要注意编译级别设置一致。

Tomcat版本要和Servlet规范对应。Tomcat 9对应Servlet 4.0,Tomcat 10对应Servlet 5.0。这里有个大坑:Tomcat 10开始,Servlet的包名从javax.servlet变成了jakarta.servlet。如果你用Tomcat 10,但代码里import的是javax.servlet,会直接报ClassNotFoundException。所以新手我建议先用Tomcat 9,网上资料多,踩坑少。等你熟悉了再升级。

IDE用IntelliJ IDEA或者Eclipse都行。IDEA的体验更好,但对新手来说,Eclipse的Web项目结构更直观,能让你看清楚每个文件放在哪。我下面以IDEA为主讲,Eclipse的操作逻辑类似。

构建工具方面,这个实验我建议先用最原始的方式:手动建Web项目,手动导jar包。为什么不用Maven?因为用Maven会自动帮你处理依赖和目录结构,你反而看不清Servlet到底需要哪些东西。等你把原始方式走通了,再用Maven重构一遍,理解会更深。

2.2 标准Web项目目录结构

一个标准的Java Web项目目录结构是这样的,我用表格列出来,你对照着建:

目录/文件作用是否必须
src存放Java源代码必须
web/WEB-INF/web.xml部署描述文件,配置Servlet映射必须(注解方式可省略)
web/WEB-INF/lib存放第三方jar包按需
web/WEB-INF/classes编译后的class文件编译后自动生成
web/index.jsp默认首页可选
web/static存放静态资源可选

这里重点说WEB-INF目录。这个目录是受保护的,浏览器不能直接访问里面的文件。你把JSP放在WEB-INF下,就只能通过Servlet转发访问,这是一种安全设计。很多新手把JSP随便放在web根目录下,结果用户可以直接访问,绕过了你的权限控制,这是很危险的。

web.xml是部署描述文件,早期Servlet只能通过它来配置映射。Servlet 3.0之后支持注解配置,用@WebServlet就能搞定,但web.xml依然有用,比如配置欢迎页、错误页、过滤器顺序等。我建议你两种方式都试一遍,理解它们的区别。

2.3 第一个Servlet的完整搭建过程

我带你走一遍从零搭建的过程。打开IDEA,新建项目,选择Java Enterprise,勾选Web Application。如果没有这个选项,就新建普通Java项目,然后手动添加Web框架支持。

建好之后,在src下新建一个包,比如com.example.servlet。然后新建一个类,叫HelloServlet,让它继承HttpServlet。代码如下:

package com.example.servlet; import javax.servlet.ServletException; import javax.servlet.annotation.WebServlet; import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException; import java.io.PrintWriter; @WebServlet("/hello") public class HelloServlet extends HttpServlet { @Override protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { resp.setContentType("text/html;charset=UTF-8"); PrintWriter out = resp.getWriter(); out.println("<h1>Hello Servlet</h1>"); } }

这段代码有几个关键点。@WebServlet("/hello")是注解配置,表示访问/hello路径时由这个Servlet处理。doGet方法处理GET请求,如果是POST请求就要重写doPost。resp.setContentType设置响应类型和编码,这一步不做的话中文会乱码。getWriter()拿到字符输出流,往里面写内容就是返回给浏览器的内容。

配置Tomcat运行环境,把项目部署上去,启动。浏览器访问http://localhost:8080/项目名/hello,就能看到Hello Servlet。如果报404,检查URL路径和注解是否一致;如果报500,看控制台异常堆栈。

注意:IDEA里配置Tomcat时,Deployment选项卡里的Application context决定了URL前缀。如果设成/,访问路径就是http://localhost:8080/hello;如果设成/demo,就是http://localhost:8080/demo/hello。这个前缀很多人会搞混。

3. Servlet核心机制深度拆解

3.1 生命周期:Servlet什么时候被创建和销毁

Servlet的生命周期是面试高频题,也是理解Servlet运行机制的关键。整个生命周期分四个阶段:加载和实例化、初始化、请求处理、销毁。

加载和实例化由容器负责。默认情况下,Servlet是在第一次被访问时才创建实例,这叫懒加载。你也可以在web.xml里配置<load-on-startup>1</load-on-startup>,让容器启动时就创建,数字越小优先级越高。为什么要用启动时加载?因为有些Servlet初始化很耗时,如果等用户第一次访问才初始化,用户体验会很差。

初始化阶段调用init(ServletConfig config)方法。这个方法只调用一次,适合做资源加载、数据库连接池初始化等操作。你可以重写它,但记得调用super.init(config),否则getServletConfig()会返回null。

请求处理阶段调用service()方法,service()根据请求方法类型分发到doGet()、doPost()等方法。每次请求都会调用一次。这里有个重要特性:Servlet是单实例多线程的。容器只创建一个Servlet实例,多个请求并发访问同一个实例。这意味着你的Servlet里如果有成员变量,会有线程安全问题。

我举个实际踩过的坑。有一次我在Servlet里定义了一个成员变量private int count = 0;,然后在doGet里count++,想统计访问次数。结果并发测试时发现数字对不上,有时候还会少。原因就是多个线程同时读写这个变量,没有同步。正确的做法是用AtomicInteger,或者把变量放到数据库、Session里。这个坑很典型,新手一定要记住:Servlet里尽量不定义可变的成员变量。

销毁阶段调用destroy()方法,在容器关闭或应用卸载时执行,用来释放资源。同样只调用一次。

3.2 请求参数的获取与中文乱码根治

获取请求参数是Servlet最常用的功能。HttpServletRequest提供了几个方法:getParameter(String name)获取单个参数值,getParameterValues(String name)获取多个值(比如复选框),getParameterMap()获取所有参数的Map。

GET请求的参数在URL里,比如?name=张三&age=20。POST请求的参数在请求体里。这里有个经典的中文乱码问题:POST请求如果不在读取参数前设置编码,中文会乱码。解决办法是在doPost方法的第一行调用req.setCharacterEncoding("UTF-8")。注意,这行代码必须在getParameter之前调用,否则无效。

GET请求的中文乱码处理比较麻烦。Tomcat 8之后,GET请求默认用UTF-8解码,一般不会乱码。但如果你的Tomcat版本较老,或者配置改了,就需要在server.xml里配置URIEncoding="UTF-8"。我建议统一在代码里处理,不要依赖容器配置。

我整理了一个参数获取的对照表:

场景方法注意事项
单个参数getParameter返回String,不存在返回null
多个值getParameterValues返回String[],如复选框
所有参数getParameterMap返回Map,适合封装
参数名不存在getParameter返回null,不是空字符串
参数值为空getParameter返回空字符串""

实操心得:判断参数是否存在,不要用getParameter(name) != null,因为如果参数名存在但值为空,返回的是空字符串。更严谨的做法是结合getParameterMap().containsKey(name)来判断。

3.3 请求转发与重定向的本质区别

这是Servlet实验里最容易搞混的知识点,也是面试必问。我用一个生活类比来解释:请求转发像是你在公司前台问路,前台直接带你去目的地,你全程只发了一次请求;重定向像是前台告诉你"你去二楼找某某",你得自己再走一趟,发了两次请求。

技术上,请求转发用req.getRequestDispatcher("/target").forward(req, resp),重定向用resp.sendRedirect("/target")。区别体现在几个方面:

地址栏变化:转发不变,重定向变。因为转发是服务器内部行为,浏览器不知道;重定向是服务器告诉浏览器"你去新地址",浏览器会发起新请求。

请求次数:转发1次,重定向2次。这意味着重定向会丢失request域中的数据,因为第二次请求是全新的。

能否访问WEB-INF:转发可以,重定向不可以。因为转发是服务器内部跳转,能访问受保护资源;重定向是浏览器行为,访问不了WEB-INF。

效率:转发效率高,重定向效率低。但重定向能防止表单重复提交,这是它的优势场景。

我画个表格对比更清楚:

对比项请求转发重定向
方法forwardsendRedirect
地址栏不变改变
请求次数1次2次
request域数据保留丢失
访问WEB-INF可以不可以
目标范围本应用内任意URL
典型场景查询后展示登录成功后跳转

3.4 Session与Cookie的配合使用

HTTP是无状态协议,服务器默认不记得你是谁。Session和Cookie就是用来解决这个问题的。Cookie存在浏览器端,Session存在服务器端,Session的ID通过Cookie传给浏览器。

创建Session很简单:HttpSession session = req.getSession();。如果当前没有Session就创建一个,有就返回现有的。存数据用session.setAttribute("key", value),取数据用session.getAttribute("key")。

Session有个超时时间,默认30分钟。可以在web.xml里配置<session-config><session-timeout>30</session-timeout></session-config>,单位是分钟。超时后Session失效,数据丢失。

Cookie的创建稍微麻烦一点:

Cookie cookie = new Cookie("username", "zhangsan"); cookie.setMaxAge(60 * 60 * 24); // 保存一天 cookie.setPath("/"); resp.addCookie(cookie);

读取Cookie需要遍历req.getCookies()数组,因为Cookie可能有很多个。

注意:Cookie的值不能包含中文和特殊字符。如果要存中文,需要先URL编码,读取时再解码。另外,Cookie有大小限制,单个Cookie不超过4KB,一个域名下的Cookie数量也有限制。

Session和Cookie的选择原则:敏感数据放Session,因为服务器端安全;不敏感的小数据可以放Cookie,减轻服务器压力。但要注意,Session在服务器集群环境下需要额外处理共享问题,这是后话。

4. 完整实验案例:用户登录功能实现

4.1 功能拆解与流程设计

光讲理论不够,我带你做一个完整的登录功能,把前面讲的知识点全部串起来。功能需求是:用户输入用户名和密码,提交后服务器验证,成功则跳转到欢迎页并显示用户名,失败则返回登录页并提示错误。

流程设计如下:登录页(login.jsp)提交表单到LoginServlet,LoginServlet获取参数,验证用户名密码,成功则把用户信息存入Session,然后重定向到WelcomeServlet;失败则把错误信息存入request,转发回login.jsp。WelcomeServlet从Session取用户信息,如果取不到说明没登录,重定向回登录页。

这个流程用到了参数获取、中文编码、Session、转发、重定向,是一个很典型的综合练习。

4.2 登录页与表单提交

先写login.jsp:

<%@ page contentType="text/html;charset=UTF-8" language="java" %> <html> <head> <title>登录</title> </head> <body> <h2>用户登录</h2> <p style="color:red">${error}</p> <form action="login" method="post"> 用户名:<input type="text" name="username"><br> 密码:<input type="password" name="password"><br> <input type="submit" value="登录"> </form> </body> </html>

表单的action是login,对应LoginServlet的映射路径。method是post,因为登录涉及密码,不能用get把密码暴露在URL里。${error}是EL表达式,用来显示错误信息。

4.3 LoginServlet的完整实现

@WebServlet("/login") public class LoginServlet extends HttpServlet { @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"); if ("admin".equals(username) && "123456".equals(password)) { HttpSession session = req.getSession(); session.setAttribute("user", username); resp.sendRedirect("welcome"); } else { req.setAttribute("error", "用户名或密码错误"); req.getRequestDispatcher("login.jsp").forward(req, resp); } } }

这段代码有几个细节值得说。第一行设置编码,必须在获取参数前。验证逻辑这里写死了,实际项目要查数据库。成功时用重定向而不是转发,为什么?因为如果转发到welcome,地址栏还是login,用户刷新页面会重复提交表单。重定向后地址栏变成welcome,刷新不会重复提交。失败时用转发,因为要把错误信息通过request传回login.jsp,重定向会丢失request数据。

4.4 WelcomeServlet与登录校验

@WebServlet("/welcome") public class WelcomeServlet extends HttpServlet { @Override protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { resp.setContentType("text/html;charset=UTF-8"); HttpSession session = req.getSession(false); if (session == null || session.getAttribute("user") == null) { resp.sendRedirect("login.jsp"); return; } String username = (String) session.getAttribute("user"); PrintWriter out = resp.getWriter(); out.println("<h1>欢迎你," + username + "</h1>"); out.println("<a href='logout'>退出登录</a>"); } }

注意req.getSession(false),参数false表示如果没有Session就返回null,不创建新的。这个细节很重要,如果写成getSession(),每次访问都会创建Session,就失去了登录校验的意义。

4.5 退出登录与Session销毁

@WebServlet("/logout") public class LogoutServlet extends HttpServlet { @Override protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { HttpSession session = req.getSession(false); if (session != null) { session.invalidate(); } resp.sendRedirect("login.jsp"); } }

session.invalidate()会销毁整个Session,所有数据清空。退出登录后重定向回登录页。

5. 常见问题排查与避坑指南

5.1 启动与部署类问题

404错误是最常见的。排查顺序:先看URL路径对不对,注意项目名和Servlet映射路径;再看web.xml或注解配置有没有写错;最后看Tomcat的部署日志,确认项目是否成功部署。我遇到过一个情况,IDEA里改了代码但没重新部署,访问的还是旧版本,重启Tomcat就好了。

500错误通常是代码抛异常了。看控制台堆栈,定位到具体行。常见原因有:空指针、类型转换错误、SQL异常等。如果是ClassNotFoundException,检查jar包有没有导入,或者包名是不是javax和jakarta搞混了。

端口占用:Tomcat默认8080端口,如果被占用启动会失败。改端口在server.xml里改<Connector port="8080">,或者关掉占用端口的程序。

5.2 中文乱码问题全解

中文乱码分三种情况,我整理成表格:

场景原因解决方案
POST请求参数乱码请求体编码不对req.setCharacterEncoding("UTF-8")
GET请求参数乱码URL编码不对配置URIEncoding或手动解码
响应内容乱码响应头编码不对resp.setContentType("text/html;charset=UTF-8")
JSP页面乱码页面编码不对page指令设置contentType

实操心得:最省事的做法是在项目里加一个CharacterEncodingFilter,统一处理所有请求的编码。这样就不用每个Servlet都写一遍了。过滤器的写法后面会讲。

5.3 转发重定向的典型误用

我见过最多的误用是:登录成功后用转发跳转,结果用户刷新页面重复提交表单。记住一个原则:涉及数据修改的操作(增删改),成功后用重定向;只是查询展示,可以用转发。

还有一个坑:转发后,浏览器地址栏不变,用户以为还在原页面,如果此时刷新,会重复执行上一次请求。重定向就没这个问题。

另外,转发不能跨应用,只能在本应用内跳转。如果要跳到外部网站,只能用重定向。

5.4 Session失效与Cookie禁用

Session默认30分钟超时,用户如果长时间不操作,再访问时Session就没了。这时候要引导用户重新登录,而不是报错。可以在过滤器里统一判断。

如果用户浏览器禁用了Cookie,Session ID没法通过Cookie传递,Session就会失效。解决办法是URL重写,用resp.encodeURL()把Session ID拼到URL后面。不过现在禁用Cookie的情况很少了,了解即可。

6. 过滤器与监听器:Servlet的左右护法

6.1 Filter解决全局编码问题

前面提到每个Servlet都要写编码设置很麻烦,用Filter可以一次性解决。Filter可以在请求到达Servlet之前拦截,做预处理;也可以在响应返回浏览器之前拦截,做后处理。

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

@WebFilter("/*")表示拦截所有请求。chain.doFilter是放行,让请求继续往下走。如果不调用这行,请求就被拦截了,不会到达Servlet。

Filter还可以做登录校验、权限控制、日志记录等。多个Filter的执行顺序由web.xml里的配置顺序决定,注解方式无法指定顺序,这是个局限。

6.2 Listener监听应用生命周期

Listener用来监听Web应用中的事件,比如应用启动、Session创建销毁等。最常用的是ServletContextListener,在应用启动时做一些初始化工作。

@WebListener public class AppListener implements ServletContextListener { @Override public void contextInitialized(ServletContextEvent sce) { System.out.println("应用启动了"); } @Override public void contextDestroyed(ServletContextEvent sce) { System.out.println("应用关闭了"); } }

实际项目中,可以在contextInitialized里加载配置文件、初始化数据库连接池等。这样整个应用只执行一次,效率高。

7. 从Servlet到框架:理解底层才能用好上层

做完这个实验,你应该对Java Web的请求处理流程有了完整的认识。现在回头看Spring MVC,你会发现它其实就是把Servlet的这些东西封装了一层。DispatcherServlet本质上就是一个Servlet,它负责接收所有请求,然后根据配置分发到不同的Controller方法。你之前手动写的参数获取、编码设置、转发重定向,框架都帮你做了,但底层原理没变。

我个人的体会是,学框架之前一定要先把Servlet搞明白。不然你遇到框架的奇怪问题,根本不知道从哪查。比如Spring MVC里中文乱码,你知道是CharacterEncodingFilter的问题;比如重定向和转发的选择,你知道背后的原理。这些都不是框架教你的,是Servlet基础给你的。

这个实验做完之后,建议你再做两件事:一是用Maven重构一遍项目,理解依赖管理和标准目录结构;二是尝试用JSP+Servlet+JDBC做一个完整的增删改查小系统,把数据库操作也串进来。做完这两个,你的Java Web基础就相当扎实了。

最后分享一个我调试Servlet的小技巧:在doGet或doPost的第一行打印请求方法和URL,System.out.println(req.getMethod() + " " + req.getRequestURI());。这样每次请求你都能在控制台看到,排查问题时非常直观,尤其是分不清请求到底走到哪个Servlet的时候。

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

Python爬虫实战:图书电商数据抓取与监控系统构建

1. 项目背景与核心价值 Books to Scrape是一个专门用于爬虫练习的图书类电商网站&#xff0c;包含了数千本虚拟图书的完整信息。这个项目看似简单&#xff0c;但完整实现后可以掌握爬虫工程师80%的日常工作技能点。我去年用类似的方案为某出版集团搭建了竞品监控系统&#xff0…

作者头像 李华
网站建设 2026/10/15 11:03:55

Elasticsearch 7.7.0 Linux安装深度指南:系统调优与生产就绪配置

1. 项目概述&#xff1a;为什么7.7.0版本的Elasticsearch安装在Linux上仍值得深挖ELK日志系统里的Elasticsearch&#xff0c;不是装上就能用的黑盒子。尤其当你面对的是7.7.0这个承前启后的关键小版本——它既没像7.0那样激进地移除type&#xff0c;又还没到8.x的强制安全认证和…

作者头像 李华
网站建设 2026/10/15 11:03:48

WorkBuddy:基于MCP协议的跨工具工作流自动化中枢

1. WorkBuddy 不是“又一个AI工具”&#xff0c;而是跨行业工作流的神经中枢你刷到过多少次“WorkBuddy”这个词&#xff1f;在飞书群聊里被同事转发链接&#xff0c;在技术论坛看到有人贴出带MCP协议标识的截图&#xff0c;在Python项目文档里发现一行from workbuddy import w…

作者头像 李华
网站建设 2026/10/15 11:03:34

具身人工智能:物理因果建模与事件驱动记忆的技术实践

1. 具身人工智能不是“会动的AI”&#xff0c;而是重新定义智能边界的物理认知系统具身人工智能&#xff08;Embodied AI&#xff09;这个词最近在技术圈和产业端被反复提起&#xff0c;但很多人一听到“具身”&#xff0c;第一反应是“哦&#xff0c;就是给AI装个机器人身体吧…

作者头像 李华
网站建设 2026/10/15 11:02:46

Python空间数据分析:Shapely库核心功能与应用实践

1. 空间数据分析与Shapely库概述在地理信息系统(GIS)和空间数据分析领域&#xff0c;Python的Shapely库是一个处理几何对象的强大工具。它基于GEOS库&#xff08;GEOS是JTS拓扑套件的C端口&#xff09;&#xff0c;提供了对二维平面几何对象的创建、操作和分析功能。不同于完整…

作者头像 李华
网站建设 2026/10/15 11:02:21

AI酶发现的真相:从计算预测到湿实验验证的完整链条

1. 项目概述&#xff1a;一场被AI“发现”搅动的分子生物学信任边界最近一条科技新闻在生物信息学圈子里炸开了锅——Anthropic公司公开宣称&#xff0c;其大模型Claude在分析海量微生物基因组数据时&#xff0c;“自主发现”了一类全新的酶系统&#xff0c;命名为ART&#xff…

作者头像 李华