Servlet指南

引子:Web世界的“工业革命”

在Web开发的“黑暗时代”,一种名为CGI (通用网关接口) 的技术统治着动态内容的生成。它的工作模式简单粗暴:每当服务器收到一个动态请求,就启动一个全新的操作系统进程来处理,处理完毕后立即销毁。这就像每次点外卖,平台都为你新开一家饭店,做完你的菜后就立刻倒闭。这种模式在高并发下,服务器资源会迅速耗尽,性能极其低下。

Java Servlet的诞生,正是为了终结这场混乱,它在Web世界掀起了一场“工业革命”。Servlet用轻量级的Java线程取代了重量级的操作系统进程,通过实例常驻内存、多线程处理请求的高效模式,将Web应用的性能和可扩展性提升到了一个全新的高度。

所有现代Java Web框架,无论它们看起来多么华丽和高级(如Spring MVC),其最底层的“发动机”依然是Servlet。因此,理解Servlet,就是理解所有Java Web应用的起点和心跳。这份指南,将带你从零开始,彻底解剖这台强大的“发动机”。


第一部分:Servlet的核心本质与生命周期

1. Servlet是什么?
  • 定义: Servlet (Server Applet) 并非一个具体的程序,而是由Java官方制定的一套Web技术规范(一组Java接口)
  • 角色: 它是一个运行在Web容器/Servlet容器(如Tomcat, Jetty)内部的Java组件,负责接收和响应来自客户端的HTTP请求。
  • 核心优势:
    • 高性能: 容器启动时创建Servlet实例并使其常驻内存。对于每个请求,容器只创建一个轻量级的线程来处理,而不是重量级的进程。这使得Servlet能够轻松应对高并发场景。
    • 平台无关: 一次编写,可以部署在任何实现了Servlet规范的Web容器上。
2. Servlet的生命周期 (The Lifecycle)

Servlet从诞生到消亡,其整个生命由Web容器严格管理。理解这个过程至关重要。

一个Servlet的核心生命周期由javax.servlet.Servlet接口中的三个方法定义:

  1. init(ServletConfig config) - 初始化阶段 (只执行一次)

    • 何时调用?:在Servlet实例第一次被创建时调用(通常是第一次被访问或服务器启动时)。
    • 作用: 执行一次性的、昂贵的初始化操作。例如:加载配置文件、创建数据库连接池、初始化缓存等。
    • 类比: 相当于一个类的构造函数,但它是在Web容器的环境下被调用的,可以获取到容器提供的配置信息。
  2. service(ServletRequest req, ServletResponse res) - 服务阶段 (可执行无数次)

    • 何时调用?每当有一个HTTP请求匹配到这个Servlet时,容器就会在一个新的线程中调用这个方法。
    • 作用: 这是Servlet的心脏,所有业务逻辑都在这里处理。它负责从ServletRequest中读取请求信息,处理后通过ServletResponse将响应内容写回浏览器。
    • 线程安全警告: 由于多个线程会同时访问同一个Servlet实例,因此service方法(以及它调用的doGet/doPost必须是线程安全的严禁在Servlet类中定义可被修改的成员变量(实例变量)来存储与特定请求相关的数据,否则会产生数据错乱。
  3. destroy() - 销毁阶段 (只执行一次)

    • 何时调用?:当Web应用被卸载或服务器关闭时,容器会调用此方法。
    • 作用: 用于释放init方法中创建的资源。例如:关闭数据库连接池、停止后台定时任务等,确保应用的优雅关闭。

生命周期图解:(图来源:菜鸟教程)
在这里插入图片描述


第二部分:Servlet实战:编写与部署

2.1 环境与依赖准备
  • IDE: IntelliJ IDEA Ultimate
  • Web容器: Apache Tomcat
  • Maven依赖 (pom.xml):
    <dependency>
            <groupId>jakarta.servlet</groupId>
            <artifactId>jakarta.servlet-api</artifactId>
            <version>5.0.0</version> <!-- 注意版本号也变了 -->
            <scope>provided</scope>
        </dependency>
    
2.2 HttpServlet:更方便的起点

通常,我们不直接实现Servlet接口,而是继承javax.servlet.http.HttpServlet这个抽象类。它已经帮我们实现了service方法,并根据HTTP请求的类型(GET, POST, PUT, DELETE等)将请求分发到对应的doXxx()方法。我们只需要重写我们关心的doGet()doPost()即可。

2.3 动手实践:一个功能完备的HelloServlet

【操作】: 创建一个HelloServlet.java。这份代码包含了生命周期方法、GET/POST处理、参数获取和响应构建。

package com.example.servlet;

import jakarta.servlet.annotation.WebServlet;

import jakarta.servlet.ServletConfig;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServlet;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.PrintWriter;
import java.util.Date;

// 【核心注解】@WebServlet("/hello")
// Servlet 3.0+规范,用注解替代了在web.xml中配置<servlet>和<servlet-mapping>。
// 它告诉Tomcat:当浏览器访问路径为 "/hello" 时,就由这个Servlet来处理。
@WebServlet("/hello")
public class HelloServlet extends HttpServlet {

    @Override
    public void init(ServletConfig config) throws ServletException {
        super.init(config); // 必须调用父类的init
        System.out.println("--- HelloServlet 的 init() 方法被调用了!(只在第一次访问时出现) ---");
    }

    // 处理GET请求
    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        System.out.println("--- HelloServlet 的 doGet() 方法被调用了! ---");

        // 1. 从HttpServletRequest中获取请求信息
        String clientName = req.getParameter("name"); // 获取URL查询参数, 如 /hello?name=Tom
        if (clientName == null || clientName.isEmpty()) {
            clientName = "Guest";
        }
        
        // 2. 设置HttpServletResponse的响应头
        // 告诉浏览器,我将返回的是HTML格式的内容,并且使用UTF-8编码
        resp.setContentType("text/html;charset=UTF-8");

        // 3. 获取一个用于向浏览器输出内容的 PrintWriter 对象
        PrintWriter out = resp.getWriter();

        // 4. 生成HTML响应内容
        out.println("<html><head><title>Hello Servlet</title></head><body>");
        out.println("<h1>你好</h1>" + clientName);
        out.println("<p>这是由Servlet在服务器端动态生成的内容。</p>");
        out.println("<p>服务器当前时间是: " + new Date() + "</p>");
        out.println("</body></html>");
    }
    
    // 处理POST请求
    @Override
    protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        System.out.println("--- HelloServlet 的 doPost() 方法被调用了! ---");
        // 为了处理POST请求体中可能存在的中文乱码,必须在获取任何参数前设置
        req.setCharacterEncoding("UTF-8");
        
        String username = req.getParameter("username");
        String password = req.getParameter("password");

        resp.setContentType("text/html;charset=UTF-8");
        PrintWriter out = resp.getWriter();
        out.println("<h1>POST 请求已收到</h1>");
        out.println("<p>用户名: " + username + "</p>");
        out.println("<p>密码: " + password + "</p>");
    }

    @Override
    public void destroy() {
        System.out.println("--- HelloServlet 的 destroy() 方法被调用了!(在服务器关闭时出现) ---");
    }
}
2.4 部署与运行
  1. 配置Tomcat: 在IDEA中配置好本地的Tomcat服务器。
  2. 部署项目: 将你的Web应用打包成WAR文件(或直接以exploded形式)部署到Tomcat。
  3. 启动Tomcat并访问 http://localhost:8080/你的项目名/hello?name=World

第三部分:核心API与数据共享机制

3.1 HttpServletRequest & HttpServletResponse
  • request (请求对象): 封装了所有来自客户端的HTTP请求信息。

    • 获取参数: getParameter(String name), getParameterValues(String name)
    • 获取请求头: getHeader(String name)
    • 获取路径信息: getRequestURI(), getContextPath(), getServletPath()
    • 获取请求方法: getMethod()
    • 数据共享 (请求域): setAttribute(String, Object), getAttribute(String)
  • response (响应对象): 用于构建并向客户端发送HTTP响应。

    • 设置响应类型: setContentType(String type) (极其重要!)
    • 获取输出流: getWriter() (用于文本), getOutputStream() (用于二进制)
    • 设置响应头: setHeader(String, String)
    • 页面跳转: sendRedirect(String location)
3.2 三大作用域:Web应用的“数据容器”

在一个Web应用中,数据可以在三个不同的范围内共享和传递。

  1. ServletContext (应用域)

    • 生命周期: 整个Web应用运行期间。
    • 范围: 全局唯一,所有用户、所有请求共享。
    • 作用: 存放全局配置、共享资源(如数据库连接池)、网站计数器等。
    • 动手实践:网站访问计数器
      修改HelloServlet.javadoGet方法:
      // ...在doGet方法内...
      ServletContext context = req.getServletContext();
      Integer counter = (Integer) context.getAttribute("hitCounter");
      
      // 第一次访问时,counter为null
      if (counter == null) {
          counter = 1;
      } else {
          counter++;
      }
      context.setAttribute("hitCounter", counter);
      
      out.println("<p>本站总访问量 (ServletContext): " + counter + "</p>");
      // ...
      
      为了在服务器启动时初始化计数器,我们可以使用监听器(见第四部分)。
  2. HttpSession (会话域)

    • 生命周期: 从用户第一次访问开始,到会话超时或手动invalidate()结束。
    • 范围: 每个用户(浏览器会话)独享
    • 作用: 跟踪单个用户的状态,如登录信息、购物车
    • 底层原理: 基于Cookie(JSESSIONID)。
    • 动手实践:实现一个简单的会话级计数器
      修改HelloServlet.javadoGet方法:
      // ...在doGet方法内...
      HttpSession session = req.getSession(); // 获取或创建session
      Integer sessionCounter = (Integer) session.getAttribute("sessionCounter");
      
      if (sessionCounter == null) {
          sessionCounter = 1;
      } else {
          sessionCounter++;
      }
      session.setAttribute("sessionCounter", sessionCounter);
      
      out.println("<p>您的会话访问次数 (HttpSession): " + sessionCounter + "</p>");
      out.println("<p>您的Session ID: " + session.getId() + "</p>");
      // ...
      
  3. HttpServletRequest (请求域)

    • 生命周期: 从一次HTTP请求到达服务器开始,到该请求的响应发送完毕结束。生命周期最短
    • 范围: 仅在同一次请求的处理链中有效
    • 作用: 在请求转发过程中,从一个Servlet向另一个Servlet或JSP传递数据。
3.3 请求转发 vs 重定向
  • 请求转发 (Forward): 服务器内部的“内部交接”。地址栏不变,一次请求,共享request数据。

    • 动手实践:Servlet转发给另一个Servlet
      1. 创建DataServlet.java:
        @WebServlet("/data")
        public class DataServlet extends HttpServlet {
            @Override
            protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
                req.setAttribute("sharedMessage", "这是来自DataServlet的机密信息");
                System.out.println("DataServlet: 数据已准备好,准备转发...");
                req.getRequestDispatcher("/display").forward(req, resp);
            }
        }
        
      2. 创建DisplayServlet.java:
        @WebServlet("/display")
        public class DisplayServlet extends HttpServlet {
            @Override
            protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
                String message = (String) req.getAttribute("sharedMessage");
                resp.getWriter().println("<h1>显示页面</h1><p>收到的共享数据: " + message + "</p>");
            }
        }
        
      • 测试: 浏览器访问 /data,地址栏不变,但看到的是 /display 的内容。
  • 重定向 (Redirect): 通知浏览器的“外部跳转”。地址栏改变,两次请求,request数据不共享。

    • 动手实践:处理完POST后重定向
      HelloServletdoPost方法末尾,添加这行代码来替代原有的输出:
      // ...在doPost方法末尾...
      // 重定向到 /hello 路径 (会触发doGet)
      resp.sendRedirect(req.getContextPath() + "/hello?name=" + java.net.URLEncoder.encode(username, "UTF-8"));
      
      • req.getContextPath()获取应用上下文路径,URLEncoder对中文参数编码,都是良好实践。

第四部分:过滤器 (Filter) 与监听器 (Listener)

4.1 过滤器 (Filter):请求的“安检门”
  • 作用: 实现AOP(面向切面编程),对请求和响应进行统一的预处理和后处理。
  • 企业级应用: 全局编码、权限校验、日志记录、数据压缩。
  • 动手实践:创建一个全局编码过滤器
import jakarta.servlet.*;  // 注意包名变更
import jakarta.servlet.annotation.WebFilter;
import java.io.IOException;

@WebFilter("/*")
public class EncodingFilter implements Filter {
    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) 
            throws IOException, ServletException {
        System.out.println("--- EncodingFilter: 拦截到请求,设置编码 ---");
        request.setCharacterEncoding("UTF-8");
        response.setCharacterEncoding("UTF-8");
        
        chain.doFilter(request, response);
        
        System.out.println("--- EncodingFilter: 响应已处理完毕 ---");
    }
    
    // 建议显式实现空的init()和destroy()方法
    @Override
    public void init(FilterConfig config) throws ServletException {
        // 初始化代码(如果有)
    }
    
    @Override
    public void destroy() {
        // 清理资源代码(如果有)
    }
} 
4.2 监听器 (Listener):Web应用生命周期的“事件响应器”
  • 作用: 在Web应用的关键生命周期节点上执行代码。
  • 企业级应用: 应用启动时初始化数据库连接池、定时任务等重量级资源。
  • 动手实践:使用ServletContextListener初始化资源
import jakarta.servlet.ServletContextEvent;  // 注意包名变更
import jakarta.servlet.ServletContextListener;
import jakarta.servlet.annotation.WebListener;

@WebListener
public class AppLifecycleListener implements ServletContextListener {

    @Override
    public void contextInitialized(ServletContextEvent sce) {
        System.out.println("========= Web应用启动: contextInitialized() =========");
        // 这里是初始化数据库连接池、Spring容器等的最佳位置
        sce.getServletContext().setAttribute("hitCounter", 0);
        System.out.println("全局访问计数器已初始化。");
    }

    @Override
    public void contextDestroyed(ServletContextEvent sce) {
        System.out.println("========= Web应用关闭: contextDestroyed() =========");
        // 这里是释放资源、关闭连接池的最佳位置
    }
}

总结

学完这份终极指南,你对Servlet的理解已经达到了一个非常深入的层次。你不仅掌握了Servlet如何处理单个请求的基础,更重要的是,你构建了一个完整的Java Web架构知识图谱

  • 你知道了如何通过三大作用域在Web应用的不同层面管理数据。
  • 你明白了转发重定向的本质区别和适用场景。
  • 你学会了使用过滤器来实现横切关注点(如权限、编码),让代码更整洁。
  • 你懂得了如何利用监听器在应用启动时加载和初始化关键服务。

这些知识,是理解Spring、Spring MVC乃至整个Java EE生态系统的坚实基础。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值