十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

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

深入理解Servlet:JavaWeb开发的基石与实战指南

深入理解Servlet:JavaWeb开发的基石与实战指南 搞 JavaWeb 这些年我带过不少新人发现一个特别常见的现象大家用 Spring Boot 用得飞起一问 Servlet 是什么多半支支吾吾。这不能怪初学者毕竟现在框架把底层细节藏得太好了很多人写接口连HttpServletRequest长什么样都没见过。但 Servlet 恰恰是整个 JavaWeb 链路里最不该跳过的一块地基理解了它你再看框架、查线上问题、做性能调优都会通透很多。这篇文章我不打算念教科书而是按我当年入门到熟练的实际路径把 Servlet 的核心知识点掰开揉碎讲清楚包括手写 web.xml 配置、HTTP 请求响应的读写、生命周期和线程模型、Session 和 Filter 的使用以及最后如何把这份理解迁移到 SpringMVC。目标只有一个让你读完就能上手踩坑时知道去哪排查。1. 为什么今天仍要学 Servlet框架下面那层真实存在的基石很多人的疑问很直接“我写 Spring Boot 项目Controller 里一个注解就接请求了为什么还要学 Servlet”这个问题其实本身就代表了答案——Controller 能接请求是因为框架底层帮你接住了 HTTP 协议而那个“接住”的角色本质上就是 Servlet。1.1 Servlet 在一条 JavaWeb 请求链路里的真实位置浏览器发一个 HTTP 请求到服务端真实经历是TCP 连接建立、HTTP 报文解析、请求分发、业务处理、响应序列化、连接关闭。这些事如果让你自己用原生 Java 写 Socket 来实现工作量大得吓人。Tomcat 这类 Web 容器把前面这些脏活累活全干了然后把一个已经解析好的“请求对象”和“响应对象”交给你的代码。这个交接点就是 Servlet。换句话说Tomcat 是 Servlet 容器它负责加载和管理 Servlet 实例把一个请求映射到对应的 Servlet 上并调用它的方法。Servlet 本身是一组接口和抽象类约定了一个 Java 类怎么去接收请求、处理业务、写回响应。它是契约Tomcat 是执行者业务代码是打工人。我习惯用餐厅打比方Tomcat 是餐厅大堂负责接客、点单、上菜Servlet 是后厨的一个个灶台每个灶台有自己的拿手菜一个请求进来大堂按菜单url-pattern把单子派到对应灶台厨师炒完菜服务员再端出去。没有灶台大堂再热闹也出不了菜。1.2 框架再方便绕不开的底层约定Spring MVC 里你写的Controller标记的类本质上是框架帮你把请求拦截下来再通过反射调用你的方法但这个“拦截请求”的入口也就是DispatcherServlet本身就是一个 Servlet在 web.xml 或者配置类里显式注册。Spring Boot 则是内嵌了 Tomcat自动完成了同样的注册。所以不管框架怎么变Servlet 规范定义的生命周期、线程模型、请求响应语义依然在底层运转。你在 Controller 方法里直接用HttpServletRequest参数时拿到的东西和当年 Servlet 里一模一样。对于想长期在这行走下去的人来说学 Servlet 不是学一套已经过时的写法而是在掌握一套根本性的协议处理模型。这个模型通了以后学任何 JavaWeb 框架都是降维打击面试被问到“SpringMVC 的执行流程”时也不至于背一句“DispatcherServlet 分发到 Controller”就卡壳你能说出它分发的目标其实是HandlerMethod背后围绕着 Servlet 容器在运作层次完全不同。2. 手工搭建 JavaWeb 工程从目录结构到第一个 Hello Servlet别急着用插件一键生成我建议你至少亲手搭一次传统的 Web 应用。亲手建目录、写 web.xml、编译部署的这套流程走通了你才算真正见过 JavaWeb 项目的骨架。2.1 最小项目结构长什么样一个最朴素的 JavaWeb 项目通常长这样web-demo/ ├── src │ ├── main │ │ ├── java │ │ │ └── com/example/demo │ │ │ └── HelloServlet.java │ │ └── webapp │ │ └── WEB-INF │ │ └── web.xml │ └── test └── pom.xml (如果用 Maven)如果你用的是 2023 版 IDEA新建项目时选 “Jakarta EE” 或 “Java Enterprise”再选对应的 Web Application 模板IDEA 会帮你生成这个结构。但我更推荐 Maven 项目手动补上webapp/WEB-INF/web.xml哪怕旧一点逻辑清楚方便你理解部署时到底哪些目录是干嘛的。注意到WEB-INF这个目录很特殊浏览器是直接访问不到里面东西的它用来放web.xml、编译后的classes目录和依赖的lib目录。这是 Servlet 规范的安全约定容器只把webapp根目录下的静态资源和配置好的 Servlet 映射暴露给外部。2.2 写一个 Servlet 类并配置 web.xml先看代码一个最简单的 Servletpackage com.example.demo; import javax.servlet.ServletException; import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException; import java.io.PrintWriter; public class HelloServlet extends HttpServlet { public HelloServlet() { System.out.println(HelloServlet 构造函数执行); } Override public void init() throws ServletException { System.out.println(HelloServlet init 方法执行); } Override protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { resp.setContentType(text/html; charsetUTF-8); PrintWriter writer resp.getWriter(); writer.println(h1Hello Servlet/h1); } Override public void destroy() { System.out.println(HelloServlet destroy 方法执行); } }然后是web.xml把 Servlet 类和一个 URL 路径绑起来?xml version1.0 encodingUTF-8? web-app xmlnshttp://xmlns.jcp.org/xml/ns/javaee xmlns:xsihttp://www.w3.org/2001/XMLSchema-instance xsi:schemaLocationhttp://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_4_0.xsd version4.0 servlet servlet-namehello/servlet-name servlet-classcom.example.demo.HelloServlet/servlet-class /servlet servlet-mapping servlet-namehello/servlet-name url-pattern/hello/url-pattern /servlet-mapping /web-app这套配置的逻辑核心是先声明一个 servlet 实例给它取名hello再把hello映射到/hello这个 URL 上。容器启动时读到这段配置就能在收到http://localhost:8080/项目名/hello请求时找到HelloServlet并调用它的doGet。这里有个细节值得多说一句HttpServlet这个类本身已经把 HTTP 方法分发逻辑写好了service()方法收到请求后会根据请求动词调用对应的doGet、doPost、doPut、doDelete等。所以你在子类里重写哪个方法就是在处理对应的 HTTP 方法。不重写 doPost 的话POST 请求打过来会报 405后面我会再提这个坑。2.3 部署到 Tomcat 与常见启动报错部署方式无非两种。第一种是传统外部部署把项目打包成 WAR 包复制到 Tomcat 的webapps目录下启动 Tomcat。这也是热搜词里提到的“windows 服务器 Apache Tomcat 发布 JavaWeb 项目”的做法。Tomcat 启动时会自动解压 WAR 包得到一个和包名同名的上下文路径之后的访问地址通常是http://ip:8080/项目名/hello。第二种是开发期用 IDEA 集成部署配置一个 Tomcat Server把 artifact 添加进去。这种方式的好处是能边改边热部署调试时直接打断点。新手启动时最容易面对两个报错端口被占用Tomcat 默认 8080被占用了会报Port in use。解决办法是改 Tomcat 的conf/server.xml里的 Connector 端口或者关掉占用程序。ClassNotFoundExceptionweb.xml里写的servlet-class全限定名错了或者编译后的 class 没进WEB-INF/classes。检查编译输出目录和包名对不对别嫌烦这个错我见新人犯过不下二十次。3. Servlet 生命周期与线程模型一个 Servlet 怎么服务所有请求如果说 web.xml 配置是 JavaWeb 的门面生命周期和线程模型就是内核。不懂这里你写出来的 Servlet 可能在并发稍微高一点的时候出现玄幻 bug。3.1 init、service、destroy 三个方法与执行时机Servlet 的生命周期由容器管理一共三个阶段对应三个方法构造与初始化init()第一个请求到达前容器加载 Servlet 类并创建实例然后调用init完成初始化比如读取配置文件、建立连接池。这个过程只执行一次。服务service()每来一个请求容器都会分配一个线程调用service然后由它分发到doGet或doPost。可以执行无数次。销毁destroy()容器卸载应用或停止前调用destroy做资源回收也只执行一次。我见过一个经典面试题“Servlet 是单例还是多例”答案是单例容器通常只创建一个 Servlet 实例服务所有请求。但这并不意味着它是线程安全的因为同一个实例会被多个线程同时调用。init还有个可配置的细节。默认情况下Servlet 是懒加载的第一个请求来了才初始化如果你希望应用一启动就初始化可以在servlet标签内部加上load-on-startup1/load-on-startup数字越小优先级越高。适合放一些重量级、不想等到第一个用户访问才加载的服务。3.2 单实例多线程模型下的并发隐患既然一个 Servlet 实例同时服务多个请求那成员变量就有竞争风险。比如这段代码public class CounterServlet extends HttpServlet { private int count 0; protected void doGet(HttpServletRequest req, HttpServletResponse resp) { count; // 输出 count } }在高并发下count这个操作不是原子的它包含读取、加一、写回三步。两个线程同时读到了同一个旧值分别写回导致计数丢失更新。这不是理论问题是真实存在的生产事故隐患。解决办法有三个思路尽力不要用可变的成员变量Servlet 里保存无状态的数据只声明局部变量局部变量在每个线程的栈里是独立的天然安全。用原子类或加锁比如AtomicInteger或者对操作加synchronized同步块。能用前者就别用后者锁竞争会降低吞吐。用请求作用域的数据结构把状态放到HttpServletRequest、HttpSession或ServletContext里而不是 Servlet 字段里。我当时入门时也看不起这个点直到线上用HashMap做缓存高并发下出现了诡异的死循环和线程阻塞才真正体会到“容器单例、请求多线程”这句话的分量。4. 请求与响应HttpServletRequest 与 HttpServletResponse 的常用姿势Servlet 里打交道最多的就是这两个接口。一个代表客户端发来的请求一个代表你要写回给客户端的响应。把它们的常用 API 用熟剩下就是业务逻辑的事。4.1 取参数、读请求头、处理中文乱码一个典型 GET 请求http://localhost:8080/demo/hello?namezhangsanage18。在doGet里取值String name req.getParameter(name); String age req.getParameter(age);获取请求头比如 User-AgentString userAgent req.getHeader(User-Agent);还有几个高频方法和它们的含义方法作用使用场景getParameter()获取 URL 参数或表单 POST 参数最常用getParameterValues()同名多值比如 checkbox、多选数组接收getHeader()获取请求头拿到 token、UA、Referer 等getMethod()获取请求方法做一些通用判断getRequestURI()/getRequestURL()获取路径信息日志、权限判断getSession()获取或创建会话用户状态管理中文乱码是新手问得最多的问题这里一起说清楚。乱码根源在于服务端和客户端之间的编码不一致。在 Servlet 中POST 请求的参数需要主动设置解码字符集并且必须在读取第一个参数之前设置req.setCharacterEncoding(UTF-8);GET 请求的参数在 URL 上由容器解码Tomcat 8 之后默认 URI 编码已经是 UTF-8所以 GET 乱码问题少一些。至于响应必须同时设置字符编码和 ContentTyperesp.setContentType(text/html; charsetUTF-8);setContentType要在getWriter()之前调用否则编码设置不生效。4.2 转发与重定向该怎么选Servlet 里跳页面有两条路转发和重定向。不少新人混着用经常出现页面 URL 对不上、数据丢了的困惑。转发由服务端完成对应代码req.getRequestDispatcher(/target).forward(req, resp);它是容器内部把请求转给另一个资源浏览器全程只知道一个 URL地址栏不变因为仍然是同一个请求所以request里存的 attribute 在目标资源里还能拿到。适合在 Servlet 里查询完数据后把数据req.setAttribute(list, list)塞进请求再转发到 JSP 渲染页面。重定向则是一次完整的“客户端第二次请求”对应代码resp.sendRedirect(http://localhost:8080/demo/login);服务端返回 302浏览器收到后自动发起第二个请求。地址栏会变化两个请求之间完全独立想传数据只能靠 URL 参数或者 Session。适合登录成功后跳首页、支付完成后跳回商家这种“需要让浏览器重新访问一个地址”的场景。一个容易踩的坑是路径写法。转发写的是应用内部的资源路径开头是/target不是带项目名的完整路径重定向的/login在部署时会被解析成“当前机器根路径”但跳转到另一台机器或换项目名后就容易 404。稳妥做法是动态拼接上下文路径resp.sendRedirect(req.getContextPath() /login);getContextPath()返回项目的上下文根比如/demo这样即使项目改了名只要部署名一致路径都不会写死。5. Session、Cookie 与扩展点Servlet 容器还给了你哪些能力HTTP 协议本身是无状态的但业务需要记住用户所以 Servlet 规范提供了 HttpSession 和 Cookie 机制还定义了 Filter 和 Listener 两个扩展点这些到今天都是 JavaWeb 开发的常青树。5.1 Session 怎么识别同一个用户简单理解用户第一次访问服务器时容器创建一个HttpSession对象生成一个唯一的 Session ID通过 Cookie 下发到浏览器Cookie 的 key 是JSESSIONID。后续每次请求都带着这个 Cookie容器就能对应到同一个 Session 对象。在 Servlet 里取 SessionHttpSession session req.getSession(); // 有则取无则建 HttpSession session req.getSession(false); // 有则取无则返回 null存和取数据session.setAttribute(userId, 1001); Object userId session.getAttribute(userId); session.removeAttribute(userId);Session 默认超时时间是 30 分钟可以在 web.xml 里改session-config session-timeout60/session-timeout /session-config单位是分钟。时间到了还没请求容器就销毁这个 Session。注意用户关闭浏览器不代表 Session 立刻销毁只是 JSESSIONID 这个 Cookie 失效了服务端的对象还会保留到超时。过度创建 Session 是内存泄漏的常见来源能不用就不用需要存的东西尽量精简。5.2 Filter 统一处理编码与登录检查Filter 是 Servlet 规范里非常精彩的一个设计。它可以在请求进入 Servlet 之前、响应返回客户端之前插入一段统一处理的逻辑。你可以在 web.xml 里配置多个 Filter形成一条过滤链。比如最经典的编码过滤器public class EncodingFilter implements Filter { public void init(FilterConfig filterConfig) {} public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { HttpServletRequest req (HttpServletRequest) request; req.setCharacterEncoding(UTF-8); response.setContentType(text/html; charsetUTF-8); chain.doFilter(request, response); } public void destroy() {} }web.xml 配置filter filter-nameencoding/filter-name filter-classcom.example.demo.EncodingFilter/filter-class /filter filter-mapping filter-nameencoding/filter-name url-pattern/*/url-pattern /filter-mappingurl-pattern写成/*表示拦截所有请求。注意chain.doFilter(request, response)这行是链的出口忘记调用的后果是请求永远到不了 Servlet页面直接卡住我见过不少新人查了半天代码发现是过滤器里漏了这一句。登录校验过滤器也是同样的套路在 doFilter 里检查 Session 里有没有用户信息HttpSession session req.getSession(false); if (session ! null session.getAttribute(user) ! null) { chain.doFilter(request, response); } else { req.getRequestDispatcher(/login).forward(req, resp); }这种统一拦截的好处很明显你不用在每个 Servlet 里重复写“先登录、后操作”的判断真正的代码里还应该加上对登录页面本身和静态资源的放行逻辑否则会出现死循环跳转。5.3 Listener 监听应用启动Listener 是另一个开关。ServletContextListener可以感知应用启动和销毁这对做全局初始化特别有用。比如你不想每次请求都创建数据库连接可以在应用启动时初始化连接池public class AppInitListener implements ServletContextListener { Override public void contextInitialized(ServletContextEvent sce) { System.out.println(应用启动开始初始化资源); sce.getServletContext().setAttribute(startTime, System.currentTimeMillis()); } Override public void contextDestroyed(ServletContextEvent sce) { System.out.println(应用销毁释放资源); } }web.xml 里注册listener listener-classcom.example.demo.AppInitListener/listener-class /listener这种扩展到 Spring Boot 时代虽然大部分被ApplicationRunner、Configuration取代但底层的思想一脉相承在合适的生命周期节点做合适的初始化。6. 走向框架DispatcherServlet 与 SpringMVC 的接轨点学完前面的东西你实际上已经具备理解主流框架的完整知识底座了。SpringMVC 再强大入口仍然是一个 Servlet。6.1 前端控制器模式先搞清楚在纯 Servlet 时代一个 URL 对应一个 Servlet你写十个业务接口就得配十个 Servlet 类代码越来越碎。框架的解决思路是引入“前端控制器”模式只注册一个 Servlet 作为总入口所有请求都先进这个门再由它根据 URL 去路由到具体的业务方法。SpringMVC 的这个总入口就是DispatcherServlet它的核心工作包括根据请求 URL 找出对应的Controller方法HandlerMapping解析方法参数并调用方法HandlerAdapter把返回值渲染成视图或 JSON 响应。所以当你在 SpringMVC 里写Controller public class UserController { GetMapping(/user) ResponseBody public String getUser() { return success; } }真实执行链路是Tomcat 收到请求发现 URL 已被DispatcherServlet映射于是调用这个 Servlet 的doGet/doPost框架再从 HandlerMapping 里找到getUser方法反射执行。理解了 Servlet你就能理解框架为什么能有拦截器、为什么HttpServletRequest能直接作为 Controller 方法的参数传入——因为这一切都发生在同一个 Servlet 请求生命周期里。6.2 把 Servlet 知识迁移到框架思维我建议你用这种“分层对应”的视角学框架传统 Servlet 概念框架中对应物web.xml 里的servlet-mappingWebServlet、RequestMapping路由FilterSpringMVC 拦截器 HandlerInterceptorServletContextSpring 容器ApplicationContextInitParameter配置类里的ValueHttpServletRequestController 方法参数框架自动注入迁移时最关键的变化是Servlet 时代你用if/else自己分发请求到框架里换成了注解加反射让框架替你分发但请求对象、会话、过滤器链这些底层机制全都没变。7. 排错实操一次请求在 Tomcat 里可能挂掉的环节最后用实战视角收个尾把请求在 Tomcat 里各个可能出问题的环节串一遍你会一次性获得排查思路。7.1 404、405、500 分别对应哪些配置问题这三个状态码出现的场景差异很大代表了三类不同的问题404 找不到资源。常见原因有三URL 路径打错或大小写不一致web.xml里的url-pattern和实际访问路径不匹配部署上下文路径和请求路径拼接不对。排查时先访问http://localhost:8080确认应用本身能访问再看项目名和路径。405 方法不允许。多数是只重写了doGet但请求发的是 POST或者反过来。HttpServlet默认的doPost实现直接返回 405你不重写POST 请求自然就 405。解法很简单按需重写对应方法或者直接在service里做统一处理。500 服务器内部错误。这个范围最广常见的有Servlet 代码抛异常web.xml里 Servlet 类写错导致 ClassNotFound初始化代码报错但日志被吞。这种一定要去看 Tomcat 的日志文件。7.2 定位问题的思路和看日志的正确姿势Tomcat 的日志在logs目录下按时间分类重点看两个文件catalina.日期.log记录启动信息和全局日志localhost.日期.log记录应用异常堆栈。报 500 时把localhost.2024-XX-XX.log打开拉到最底下就能看到可读性很好的异常堆栈。很多时候你一眼就能看出是空指针还是类型转换问题。如果日志不够直观我建议用 IDEA 的 Debug 模式。在 Servlet 的doGet方法第一行打断点然后通过浏览器发起请求你会发现调用栈从 Tomcat 的StandardWrapperValve一路下来最后到你的代码。顺着调用栈你就能验证容器是怎么把请求交到 Servlet 手里的这种直观感受是单纯看文档替代不了的。还有一种隐蔽问题改了代码没生效。检查是不是 Tomcat 部署的产物没同步或者浏览器缓存了旧页面。外部部署时记得先停掉 Tomcat 再替换 WAR 包别在运行中直接复制解压冲突的后果很恶心。7.3 聊一点外部部署的注意事项热搜里有“windows server Apache Tomcat 发布 JavaWeb 项目”如果你真要在生产环境这么干有三件事别忽略用独立用户跑 Tomcat别用管理员账户降低权限风险设置 JVM 内存参数在catalina.bat或setenv.bat里配置JAVA_OPTS-Xms512m -Xmx1024m否则高并发下 OOM 很正常日志切割Tomcat 默认日志会持续增长配置好滚动策略不然磁盘爆了很被动。这些虽然属于运维范畴但开发者懂一点和运维沟通起来就不用“鸡同鸭讲”。最后再分享一个我的使用习惯我建议你在学完这些基础后找个时间把身边一个最小可运行的 Spring Boot 项目扒开看看找到内嵌 Tomcat 启动时打印的行看看DispatcherServlet的注册日志。然后再用 IDEA 断点停在 Controller 方法第一行往上翻三层调用栈亲眼看一下HttpServletRequest是怎么穿越容器进入框架的。这一步做完我会恭喜你已经不是一个只会调接口的“API 搬运工”了。以后无论框架再怎么出新的底层这套“容器负责解析协议、Servlet 负责业务入口”的模型都不会过时。踩过坑、翻过日志、看过调用栈之后你也就会明白JavaWeb 入门不靠背靠的是亲手把一个请求从浏览器打通到数据库再看着它完整地回来。那一次全程跑通比看十本教材都管用。
返回列表