JavaWeb 核心深度解析:Servlet 全家桶(原理 + 实战 + 踩坑)


作为 JavaWeb 的底层基石,Servlet 及周边组件的细节、原理、坑点直接决定了后端代码的稳定性。本文从底层原理、实战进阶、踩坑指南三个维度,把这些知识点讲透,覆盖企业开发的核心需求。

一、Servlet 配置:不止是 “贴名牌”,更是 “规则博弈”

Servlet 的配置核心是URL 映射规则——Tomcat 如何根据请求路径找到对应的 Servlet,这是极易踩坑的细节。

1. URL 映射的 3 种规则(及优先级)

Tomcat 的 URL 匹配遵循精确匹配 > 路径匹配 > 扩展名匹配的优先级,实战中必须明确:

  • 精确匹配:路径完全一致,比如@WebServlet("/user"),仅匹配/user
  • 路径匹配:以/*结尾,比如@WebServlet("/user/*"),匹配/user/1/user/detail
  • 扩展名匹配:以.xxx结尾,比如@WebServlet("*.do"),匹配/login.do/order.do
  • 默认匹配@WebServlet("/"),匹配所有未被其他规则匹配的请求(注意:会覆盖 Tomcat 默认的静态资源处理器,导致 CSS/JS 无法访问)。

2. 进阶配置:提前加载 Servlet

默认情况下,Servlet 在第一次请求时才会实例化,但可以通过load-on-startup让 Servlet 在 Tomcat 启动时就加载(适合初始化耗时的组件,比如加载配置文件):

java

运行

// 注解版:loadOnStartup值越小,优先级越高(≥0)
@WebServlet(value = "/init", loadOnStartup = 1)
public class InitServlet extends HttpServlet {
    @Override
    public void init() throws ServletException {
        // Tomcat启动时执行:加载全局配置、初始化连接池等
        System.out.println("Tomcat启动时,我就初始化了!");
    }
}

// web.xml版:
<servlet>
    <servlet-name>InitServlet</servlet-name>
    <servlet-class>com.yourpackage.InitServlet</servlet-class>
    <load-on-startup>1</load-on-startup> <!-- 启动时加载 -->
</servlet>

3. 实战踩坑:URL 匹配冲突

如果同时配置了@WebServlet("/user/*")@WebServlet("/user/detail"),请求/user/detail会匹配哪个?→ 优先匹配精确匹配/user/detail,而非路径匹配的/user/*

二、Servlet 生命周期:从 “出生” 到 “死亡” 的底层逻辑

Servlet 的生命周期由Tomcat 容器管理,核心是 “单例多线程”—— 一个 Servlet 类仅实例化一次,多请求共享同一个对象(因此 Servlet 类不能有线程不安全的成员变量)。

1. 生命周期的 4 个阶段(深化版)

阶段触发时机核心方法 & 作用
加载实例化Tomcat 启动(load-on-startup)或第一次请求调用构造方法:创建 Servlet 对象(仅 1 次)
初始化实例化后立即执行调用init(ServletConfig config):初始化资源(如连接池),ServletConfig可获取配置参数
服务每次请求调用service(HttpServletRequest req, HttpServletResponse resp):分发到doGet/doPost
销毁Tomcat 关闭或应用卸载调用destroy():释放资源(如关闭连接池)

2. 进阶:通过 ServletConfig 获取配置参数

可以给 Servlet 配置专属参数,在init中通过ServletConfig获取:

java

运行

// 注解版:
@WebServlet(value = "/config", initParams = {
    @WebInitParam(name = "maxCount", value = "100"),
    @WebInitParam(name = "timeout", value = "5000")
})
public class ConfigServlet extends HttpServlet {
    private int maxCount;
    private int timeout;

    @Override
    public void init(ServletConfig config) throws ServletException {
        super.init(config); // 必须调用父类方法,否则getServletConfig()会返回null
        // 获取初始化参数
        maxCount = Integer.parseInt(config.getInitParameter("maxCount"));
        timeout = Integer.parseInt(config.getInitParameter("timeout"));
        System.out.println("maxCount=" + maxCount + ", timeout=" + timeout);
    }
}

3. 实战踩坑:Servlet 的线程安全

错误写法(线程不安全):

java

运行

@WebServlet("/unsafe")
public class UnsafeServlet extends HttpServlet {
    // 成员变量:多线程共享,会被并发修改
    private String username;

    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException {
        username = req.getParameter("name");
        // 模拟耗时操作,此时其他请求可能修改username
        try { Thread.sleep(1000); } catch (InterruptedException e) {}
        resp.getWriter().write("Hello " + username);
    }
}

→ 正确做法:避免成员变量,用局部变量或ThreadLocal存储请求专属数据。

三、请求 & 响应:HTTP 协议的 “具象化”

HttpServletRequest/HttpServletResponse 是对 HTTP 协议的封装,底层是TCP 连接中的字节流——Tomcat 帮我们解析了 HTTP 报文,封装成对象。

1. 请求的进阶用法:处理复杂参数

(1)多值参数(复选框):getParameterMap

java

运行

// 前端表单:<input type="checkbox" name="hobby" value="篮球">篮球
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws IOException {
    // 获取多值参数(返回Map<String, String[]>)
    Map<String, String[]> paramMap = req.getParameterMap();
    String[] hobbies = paramMap.get("hobby");
    if (hobbies != null) {
        String hobbyStr = String.join(",", hobbies);
        resp.getWriter().write("你的爱好:" + hobbyStr);
    }
}
(2)请求体数据(JSON/XML):getReader

如果前端传的是 JSON(而非表单),需要读取请求体:

java

运行

@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws IOException {
    // 读取请求体(JSON字符串)
    BufferedReader reader = req.getReader();
    String line;
    StringBuilder json = new StringBuilder();
    while ((line = reader.readLine()) != null) {
        json.append(line);
    }
    System.out.println("前端传的JSON:" + json); // 如:{"name":"张三","age":20}
}

2. 响应的进阶用法:状态码 & 文件下载

(1)手动设置状态码

java

运行

@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException {
    String id = req.getParameter("id");
    if (id == null) {
        resp.setStatus(400); // 400:请求参数错误
        resp.getWriter().write("缺少id参数");
        return;
    }
    // 其他逻辑...
}
(2)文件下载响应

java

运行

@WebServlet("/download")
public class DownloadServlet extends HttpServlet {
    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException {
        // 1. 设置响应头:告诉浏览器这是文件下载
        resp.setContentType("application/octet-stream");
        resp.setHeader("Content-Disposition", "attachment;filename=test.txt");

        // 2. 读取服务器文件,写入响应流
        ServletContext context = getServletContext();
        InputStream in = context.getResourceAsStream("/WEB-INF/test.txt");
        OutputStream out = resp.getOutputStream();
        byte[] buffer = new byte[1024];
        int len;
        while ((len = in.read(buffer)) != -1) {
            out.write(buffer, 0, len);
        }
        in.close();
    }
}

3. 底层原理:请求 / 响应的生命周期

  • 请求对象(HttpServletRequest):每个请求对应一个新的对象,请求结束后被 Tomcat 回收;
  • 响应对象(HttpServletResponse):与请求一一对应,响应完成后(out.close())不能再操作,否则会抛IllegalStateException

四、转发 & 重定向:底层流程与场景抉择

这是面试高频题,核心是 **“一次请求” vs “两次请求”** 的底层差异。

1. 底层流程对比(可视化)

操作流程数据传递
转发(Forward)客户端→Tomcat→ServletA→Tomcat 内部转发→ServletB→Tomcat→客户端request 域(同一个请求对象)
重定向(Redirect)客户端→Tomcat→ServletA→Tomcat 返回 302 + 新地址→客户端→Tomcat→ServletB→客户端Session/URL 参数(新请求对象)

2. 实战场景抉择

  • 用转发:需要传递大量数据(用 request 域)、跳转后地址栏不变(比如表单提交后跳转到结果页,避免刷新重复提交);
  • 用重定向:表单提交成功后跳转(避免刷新重复提交)、跳转到外部网站、需要清空 request 域(比如登录成功后跳首页)。

3. 进阶:重定向的 URL 编码(解决中文乱码)

如果重定向的 URL 包含中文,需要编码:

java

运行

String name = "张三";
// 编码中文参数
String encodedName = URLEncoder.encode(name, "UTF-8");
// 重定向
resp.sendRedirect(req.getContextPath() + "/index.jsp?name=" + encodedName);

// 接收时解码:
String name = URLDecoder.decode(req.getParameter("name"), "UTF-8");

五、过滤器:“链式拦截” 的执行逻辑与高级用法

过滤器(Filter)是AOP 思想的底层实现,核心是 “对请求 / 响应进行预处理 / 后处理”。

1. 过滤器链的执行顺序

过滤器链的执行顺序由配置顺序决定

  • web.xml 版:按<filter-mapping>的配置顺序执行;
  • 注解版:按类名字母顺序执行(比如AFilterBFilter先执行)。

代码示例(过滤器链):

java

运行

// Filter1:
@WebFilter("/*")
public class Filter1 implements Filter {
    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
        System.out.println("Filter1:请求前处理");
        chain.doFilter(request, response); // 放行到下一个过滤器/Servlet
        System.out.println("Filter1:响应后处理");
    }
}

// Filter2:
@WebFilter("/*")
public class Filter2 implements Filter {
    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
        System.out.println("Filter2:请求前处理");
        chain.doFilter(request, response);
        System.out.println("Filter2:响应后处理");
    }
}

执行结果

plaintext

Filter1:请求前处理
Filter2:请求前处理
(Servlet处理逻辑)
Filter2:响应后处理
Filter1:响应后处理

2. 高级用法:防止 XSS 攻击的过滤器

XSS 攻击是前端注入恶意脚本,过滤器可以对请求参数进行转义:

java

运行

@WebFilter("/*")
public class XssFilter implements Filter {
    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
        // 包装request,重写getParameter方法
        XssHttpServletRequestWrapper wrapper = new XssHttpServletRequestWrapper((HttpServletRequest) request);
        chain.doFilter(wrapper, response);
    }

    // 自定义Request包装类,转义参数
    static class XssHttpServletRequestWrapper extends HttpServletRequestWrapper {
        public XssHttpServletRequestWrapper(HttpServletRequest request) {
            super(request);
        }

        @Override
        public String getParameter(String name) {
            String value = super.getParameter(name);
            return value == null ? null : escape(value);
        }

        // 转义HTML特殊字符
        private String escape(String value) {
            value = value.replaceAll("<", "&lt;");
            value = value.replaceAll(">", "&gt;");
            return value;
        }
    }
}

3. 实战踩坑:过滤静态资源

如果过滤器配置了/*,会过滤 CSS/JS/ 图片等静态资源,导致页面样式失效。解决方法:在过滤器中排除静态资源:

java

运行

@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
    HttpServletRequest req = (HttpServletRequest) request;
    String path = req.getRequestURI().toLowerCase();
    // 排除静态资源
    if (path.endsWith(".css") || path.endsWith(".js") || path.endsWith(".png")) {
        chain.doFilter(request, response);
        return;
    }
    // 其他过滤逻辑...
}

六、监听器:“事件驱动” 的全场景覆盖

监听器(Listener)是观察者模式的实现,覆盖 Web 应用的所有核心事件。

1. 常用监听器分类

监听器类型作用场景核心方法示例
ServletContextListener应用启动 / 销毁时执行(加载配置、初始化连接池)contextInitialized(ServletContextEvent e)
ServletRequestListener每个请求的创建 / 销毁时执行(记录请求日志)requestInitialized(ServletRequestEvent e)
HttpSessionListenerSession 创建 / 销毁时执行(统计在线人数)sessionCreated(HttpSessionEvent e)
HttpSessionAttributeListenerSession 属性增 / 删 / 改时执行(监控用户状态)attributeAdded(HttpSessionBindingEvent e)

2. 实战:用 ServletContextListener 加载全局配置

应用启动时加载config.properties文件到 ServletContext 域:

java

运行

@WebListener
public class ConfigListener implements ServletContextListener {
    @Override
    public void contextInitialized(ServletContextEvent sce) {
        ServletContext context = sce.getServletContext();
        // 读取WEB-INF下的配置文件
        InputStream in = context.getResourceAsStream("/WEB-INF/config.properties");
        Properties props = new Properties();
        try {
            props.load(in);
            // 存到ServletContext域(全局共享)
            context.setAttribute("config", props);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

// 其他Servlet中获取配置:
Properties props = (Properties) getServletContext().getAttribute("config");
String dbUrl = props.getProperty("db.url");

七、Cookie&Session:状态管理的 “底层逻辑与坑点”

HTTP 是无状态协议,Cookie 和 Session 是解决 “状态丢失” 的核心方案,但二者的安全、性能、分布式问题是企业开发的重点。

1. Cookie 的底层限制与安全

Cookie 的本质是 “客户端浏览器存储的文本文件”,存在以下限制:

  • 大小限制:每个 Cookie≤4KB;
  • 数量限制:每个域名下≤20 个 Cookie(不同浏览器略有差异);
  • 跨域限制:Cookie 默认不跨域(可通过setDomain设置主域名共享,比如setDomain(".example.com")a.example.comb.example.com共享);
  • 安全限制
    • setHttpOnly(true):禁止 JS 读取 Cookie,防止 XSS 攻击;
    • setSecure(true):仅在 HTTPS 协议下传输 Cookie,防止明文泄露。

2. Session 的底层实现与分布式问题

Session 的本质是 “服务器端的内存对象”,通过Cookie 中的 JSESSIONID关联客户端:

plaintext

客户端请求 → Tomcat生成Session(内存中)→ 向客户端写Cookie:JSESSIONID=XXXX → 客户端后续请求携带JSESSIONID → Tomcat根据JSESSIONID找到对应的Session
(1)Session 的超时配置
  • web.xml 版:

    xml

    <session-config>
        <session-timeout>30</session-timeout> <!-- 单位:分钟 -->
    </session-config>
    
  • 代码版:

    java

    运行

    HttpSession session = req.getSession();
    session.setMaxInactiveInterval(1800); // 单位:秒(30分钟)
    
(2)分布式 Session 问题

如果项目部署在多台 Tomcat(集群),默认情况下 Session 不共享(每台 Tomcat 的 Session 是独立的),导致用户在 A 服务器登录后,访问 B 服务器又需要重新登录。

解决方案:

  • Session 复制:Tomcat 集群开启 Session 复制(性能低,仅适用于小规模集群);
  • Redis 共享 Session:将 Session 序列化后存储到 Redis(主流方案,比如 Spring Session 框架);
  • Token 替代 Session:用 JWT 等 Token 机制,完全脱离 Session。

3. 实战踩坑:Cookie 中文乱码

Cookie 不支持直接存储中文,需要编码:

java

运行

// 编码:
String value = URLEncoder.encode("中文内容", "UTF-8");
Cookie cookie = new Cookie("key", value);
resp.addCookie(cookie);

// 解码:
Cookie[] cookies = req.getCookies();
if (cookies != null) {
    for (Cookie cookie : cookies) {
        if ("key".equals(cookie.getName())) {
            String value = URLDecoder.decode(cookie.getValue(), "UTF-8");
        }
    }
}

八、JSP:从 “翻译为 Servlet” 到 “现代替代方案”

JSP 的本质是 “Servlet 的模板”——Tomcat 会将 JSP 文件翻译为.java 文件,再编译为.class 文件执行。

1. JSP 的翻译过程(可视化)

比如index.jsp

jsp

<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<body>
    Hello <%= request.getParameter("name") %>
</body>
</html>

Tomcat 会将其翻译为index_jsp.java,核心代码是:

java

运行

public final class index_jsp extends org.apache.jasper.runtime.HttpJspBase {
    public void _jspService(HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException {
        response.setContentType("text/html;charset=UTF-8");
        javax.servlet.jsp.JspWriter out = pageContext.getOut();
        out.write("<html>\n<body>\n    Hello ");
        out.print( request.getParameter("name") );
        out.write("\n</body>\n</html>");
    }
}

2. JSP 的 9 大内置对象(自动创建,直接使用)

内置对象名类型作用
requestHttpServletRequest请求对象
responseHttpServletResponse响应对象
sessionHttpSession会话对象
applicationServletContext应用全局对象
pageContextPageContext页面上下文(域对象,作用域最小)
outJspWriter输出流(替代 resp.getWriter ())
pageObject当前 JSP 对应的 Servlet 对象(极少用)
configServletConfigServlet 配置对象
exceptionThrowable异常对象(仅在page errorPage="true"时可用)

3. 现代替代方案:模板引擎

JSP 的缺点是 “前后端代码混编”、“性能低”,现在企业开发更多用Thymeleaf(SpringBoot 默认)、Freemarker 等模板引擎,比如 Thymeleaf 的语法:

html

预览

<!-- Thymeleaf替代JSP的EL表达式 -->
<div th:text="${user.name}">用户名</div>
<ul>
    <li th:each="item : ${list}" th:text="${item}"></li>
</ul>

总结:JavaWeb 的 “底层逻辑” 与 “实战落地”

Servlet 及周边组件是 JavaWeb 的 “地基”——SpringMVC 的@Controller本质是 Servlet 的封装,SpringBoot 的自动配置本质是简化了 web.xml/ServletConfig 的配置。

掌握本文的原理、进阶用法、踩坑指南,不仅能独立开发传统 JavaWeb 项目,更能理解框架的底层逻辑,在遇到问题时快速定位根因。

最后,给新手的建议:先写原生 Servlet 项目,再用框架—— 框架是 “工具”,底层原理是 “内功”,内功扎实才能用好工具。

评论 1
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值