从零到一搞懂JavaWeb:Servlet、Cookie、Session、JSP、Filter、Ajax一次讲透

最近在做一个企业内部管理系统的时候,前端同事问我:"你后端接口返回的状态码是啥意思?我怎么判断用户有没有登录?"我当时愣了一下,发现虽然用了很久的Spring Boot,但对底层的Servlet、Cookie、Session这些JavaWeb基础知识,理解得并不扎实。于是花了一段时间系统梳理了一遍,今天把学习笔记整理出来,希望能帮到有同样困惑的朋友。


一、JavaEE是什么?和JavaWeb有什么关系?

在正式开始之前,先搞清楚一个基本概念:JavaEE(Java Enterprise Edition),Java企业版,是一个用于企业级Web开发的平台规范。最早由Sun公司制定,后来由Oracle负责维护。

JavaEE平台规范了13个技术标准,包括:JDBC、JNDI、EJB、RMI、ServletJSP、XML、JMS、Java IDL、JPA、JTA、JavaMail和JAF。

我们常说的"JavaWeb开发",其实就是在JavaEE规范下,使用Servlet、JSP等技术来构建Web应用。只不过现在大家都用Spring Boot了,底层其实还是这些技术在撑着。

简单理解:JavaEE是规范,JavaWeb是基于这个规范的实际开发。就像"交通法规"和"开车上路"的关系一样。


二、Tomcat:JavaWeb应用的"家"

2.1 什么是Tomcat?

Tomcat是Apache开源组织使用Java开发的一款Web容器(也叫Web服务器)。它实现了JavaEE平台下的部分技术标准,比如Servlet、JSP、JNDI等。最关键的是——免费开源

对比一下其他服务器:

服务器 厂商 是否免费
Weblogic Oracle 收费
WebSphere IBM 收费
JBoss RedHat 收费
Tomcat Apache 免费
Jetty Eclipse 免费

2.2 Tomcat的目录结构

下载Tomcat后解压,目录结构是这样的:

apache-tomcat-9.0.x/
├── bin/        # 启动和关闭的命令文件(.bat是Windows的,.sh是Linux的)
├── conf/       # 配置文件(server.xml、web.xml等)
├── lib/        # Tomcat运行需要的jar包
├── logs/       # 运行日志
├── temp/       # 临时文件(清空不影响运行)
├── webapps/    # 存放Web应用的地方(重点!)
└── work/       # JSP编译后的文件

小提示:安装路径不要包含中文、空格和特殊符号,这是个好习惯。

2.3 Tomcat的配置

Tomcat的核心配置文件有4个:

  • server.xml:Tomcat的核心配置,可以修改端口号、并发数等
  • web.xml:Web应用的默认配置
  • context.xml:公用环境配置
  • tomcat-users.xml:配置访问Tomcat Manager的用户

修改端口号(默认8080):

<!-- conf/server.xml -->
<Connector port="8080" protocol="HTTP/1.1"
           connectionTimeout="20000"
           redirectPort="8443" />

80是HTTP协议的默认端口,如果改成80,访问时就可以省略端口号。

配置并发数

<Connector port="8080" protocol="HTTP/1.1"
           minSpareThreads="100"      <!-- 初始化线程数 -->
           maxSpareThreads="500"      <!-- 最大空闲线程数 -->
           maxThreads="1000"          <!-- 最大并发数 -->
           acceptCount="700"          <!-- 等待队列长度 -->
           connectionTimeout="20000"
           redirectPort="8443" />

2.4 Tomcat处理请求的过程

当用户访问 http://localhost:8080/test/index.jsp 时,Tomcat内部是这样处理的:

  1. Connector(连接器)监听8080端口,接收到HTTP请求
  2. Connector把请求交给Engine(引擎)
  3. Engine匹配Host(虚拟主机),找到 localhost
  4. Host根据路径 /test 匹配Context(Web应用)
  5. Context处理 /index.jsp,生成 HttpServletRequest 和 HttpServletResponse
  6. 业务逻辑执行完毕,响应层层返回,最终到达浏览器

这个过程就像快递:Connector是收件员,Engine是分拣中心,Host是区域站点,Context是具体派送员。


三、HTTP协议:浏览器和服务器的"对话规则"

3.1 什么是HTTP协议?

HTTP(HyperText Transfer Protocol,超文本传输协议)是万维网数据通信的基础。简单说,它定义了浏览器怎么向服务器发请求,服务器怎么给浏览器回响应

HTTP协议有几个重要特点:

  • 支持客户端/服务器模式:必须用浏览器(客户端)主动请求
  • 简单快速:只需要传请求方法和路径
  • 灵活:可以传输任意类型的数据(通过Content-Type标识)
  • 无连接:每次请求完成后就断开(HTTP/1.1默认支持长连接)
  • 无状态:服务器不记录之前的请求信息(这是Cookie和Session出现的原因!)

3.2 HTTP请求的组成

一个HTTP请求分为三部分:请求行请求头请求体

POST /login HTTP/1.1              ← 请求行(请求方式 + 路径 + 协议版本)
Host: localhost:8080              ← 请求头(键值对)
Content-Type: application/json
                                  ← 空行
{"username":"admin","pwd":"123"}  ← 请求体(POST才有)

常见请求头

请求头 作用
Host 指定服务器域名和端口
Connection 连接方式(keep-alive/close)
User-Agent 浏览器身份标识
Accept 接受的响应数据类型
Cookie 携带之前服务器设置的Cookie
Content-Type 请求体的数据格式

3.3 GET和POST的区别(面试高频!)

对比项 GET POST
参数位置 URL中 请求体中
数据量 有限制(URL长度) 理论上无限制
安全性 参数暴露在URL中 相对安全
缓存 浏览器会主动缓存 不会缓存
书签 可以收藏 不可以
编码 只支持ASCII 支持多种编码
回退 无害 会重新提交

一句话总结:GET适合查询,POST适合提交数据。敏感信息(密码等)一定用POST。

3.4 HTTP响应的组成

响应也分三部分:响应行响应头响应体

HTTP/1.1 200 OK                   ← 响应行(协议 + 状态码 + 描述)
Content-Type: text/html           ← 响应头
Content-Length: 1024
                                  ← 空行
<html>...</html>                  ← 响应体

常见状态码

状态码 含义
200 请求成功
301 永久重定向
302 临时重定向
304 资源未修改,使用缓存
400 请求语法错误
401 未授权
403 无权限访问
404 资源不存在
500 服务器内部错误

四、Servlet:JavaWeb的核心

4.1 什么是Servlet?

Servlet(Server Applet)是JavaEE平台下的技术标准,运行在服务器端的Java程序。它的核心功能是:接收HTTP请求,处理业务逻辑,返回HTTP响应

静态资源:HTML、CSS、JS等,每次访问内容不变。
动态资源:Servlet、JSP等,每次访问可能返回不同的内容。

在MVC模式中,Servlet扮演的是**Controller(控制器)**的角色。

4.2 第一个Servlet

// 继承HttpServlet是开发Servlet的标准方式
public class MyServlet extends HttpServlet {

    @Override
    protected void service(HttpServletRequest request, HttpServletResponse response)
            throws ServletException, IOException {
        // 动态生成数据
        int num = new Random().nextInt(100);
        String message = num % 2 == 0 ? "happy birthday" : "happy new year";

        // 获取输出流,向浏览器写响应
        PrintWriter writer = response.getWriter();
        writer.write(message);
    }
}

然后在 web.xml 中配置映射:

<servlet>
    <servlet-name>myServlet</servlet-name>
    <servlet-class>com.example.MyServlet</servlet-class>
</servlet>
<servlet-mapping>
    <servlet-name>myServlet</servlet-name>
    <url-pattern>/myServlet.do</url-pattern>
</servlet-mapping>

访问 http://localhost:8080/项目名/myServlet.do 就能看到效果了。

4.3 Servlet的继承结构

Servlet接口(定义了init、service、destroy等方法)
    ↓
GenericServlet抽象类(与协议无关的通用实现)
    ↓
HttpServlet(针对HTTP协议,区分GET/POST等请求方式)
    ↓
我们自定义的Servlet

实际开发中:直接继承HttpServlet,重写 doGet()doPost() 方法,或者直接重写 service() 方法处理所有请求。

注意:要么重写 doGet/doPost,要么重写 service,二选一,必须重写一个。

4.4 Servlet的生命周期(面试重点!)

Servlet的生命周期由容器(Tomcat)管理,分为四个阶段:

阶段 次数 时机
实例化 1次 第一次请求时(或配置load-on-startup)
初始化 1次 实例化之后,调用init()
执行服务 多次 每次请求都调用service()
销毁 1次 服务器停止时,调用destroy()
public class MyServlet4 extends HttpServlet {

    // 构造方法:创建Servlet对象
    public MyServlet4() {
        System.out.println("构造方法调用");
    }

    // 初始化:只调用一次
    @Override
    public void init() throws ServletException {
        System.out.println("init调用");
    }

    // 服务方法:每次请求都调用
    @Override
    protected void service(HttpServletRequest req, HttpServletResponse resp)
            throws ServletException, IOException {
        System.out.println("service调用");
    }

    // 销毁:服务器停止时调用
    @Override
    public void destroy() {
        System.out.println("destroy调用");
    }
}

重要:Servlet是单实例多线程的!一个Servlet类只会创建一个对象,但每次请求会开一个新线程。所以不要在Servlet中使用成员变量,否则会有线程安全问题!

4.5 HttpServletRequest:获取请求信息

HttpServletRequest 对象封装了客户端发来的所有请求信息。

获取请求行信息

req.getRequestURL()    // 完整URL:http://localhost:8080/demo/servlet1.do
req.getRequestURI()    // 资源路径:/demo/servlet1.do
req.getRemoteAddr()    // 客户端IP
req.getLocalAddr()     // 服务器IP
req.getLocalPort()     // 服务器端口
req.getMethod()        // 请求方式:GET/POST
req.getContextPath()   // 项目部署名

获取请求参数

// 获取单个参数
String username = req.getParameter("username");

// 获取多个同名参数(比如复选框)
String[] hobbies = req.getParameterValues("hobby");

// 获取所有参数名
Enumeration<String> names = req.getParameterNames();

// 获取所有参数(Map结构)
Map<String, String[]> params = req.getParameterMap();

处理中文乱码

// POST请求设置编码(必须在获取参数之前调用!)
req.setCharacterEncoding("UTF-8");

4.6 HttpServletResponse:返回响应信息

HttpServletResponse 对象用于向客户端返回响应。

设置响应类型

// 返回HTML
resp.setContentType("text/html;charset=UTF-8");

// 返回JSON(现在最常用)
resp.setContentType("application/json;charset=UTF-8");

// 返回纯文本
resp.setContentType("text/plain;charset=UTF-8");

// 返回图片
resp.setContentType("image/jpeg");

输出响应内容

// 字符型响应
PrintWriter writer = resp.getWriter();
writer.write("{\"code\":200,\"message\":\"success\"}");

// 字节型响应(文件下载)
OutputStream os = resp.getOutputStream();

文件下载示例

// 设置响应头,告诉浏览器这是一个需要下载的文件
resp.setHeader("Content-Disposition",
    "attachment;filename=" + new String(fileName.getBytes("gbk"), "iso-8859-1"));

4.7 ServletConfig和ServletContext

ServletConfig:对应web.xml中单个Servlet的配置,每个Servlet独享一个。

// 获取Servlet的初始化参数
ServletConfig config = this.getServletConfig();
String brand = config.getInitParameter("brand");  // 对应<init-param>

ServletContext:整个Web应用全局唯一,所有Servlet共享。

// 获取ServletContext
ServletContext context = this.getServletContext();
// 或者
ServletContext context = req.getServletContext();

// 获取项目部署名
String contextPath = context.getContextPath();

// 相对路径转绝对路径(文件上传下载时很有用)
String realPath = context.getRealPath("upload");

// 获取全局配置参数(对应<context-param>)
String username = context.getInitParameter("username");

// 全局容器(所有Servlet共享)
context.setAttribute("key", value);      // 存数据
Object value = context.getAttribute("key");  // 取数据
context.removeAttribute("key");          // 删数据

注意:ServletContext生命周期很长(从服务器启动到关闭),不建议存放业务数据。

4.8 URL匹配规则

Servlet的URL匹配有四种方式:

匹配方式 配置示例 说明
精确匹配 /demo.do 必须完全一致
扩展名匹配 *.do 只看扩展名,不看路径
路径匹配 /suibian/* 匹配该路径及子路径
匹配所有 /* 匹配所有请求

优先级:精确路径 > 最长路径 > 扩展名

举个例子:

Servlet1 → /abc/*
Servlet2 → /*
Servlet3 → /abc
Servlet4 → *.do

请求 /abc/a.html  → 匹配Servlet1(/abc/* 比 /* 更精确)
请求 /abc         → 匹配Servlet3(精确匹配优先)
请求 /abc/a.do    → 匹配Servlet1(/abc/* 比 *.do 优先级高)
请求 /a.do        → 匹配Servlet2(/* 比 *.do 优先级高)

4.9 注解方式开发Servlet

Servlet 3.0之后支持注解开发,不用再写web.xml配置了:

@WebServlet(urlPatterns = "/servlet1.do")
public class Servlet1 extends HttpServlet {
    // ...
}

// 带初始化参数
@WebServlet(
    urlPatterns = "/servlet2.do",
    initParams = {
        @WebInitParam(name = "brand", value = "ASUS"),
        @WebInitParam(name = "screen", value = "三星")
    }
)
public class Servlet2 extends HttpServlet {
    // ...
}

五、Cookie和Session:解决"无状态"的利器

5.1 为什么需要Cookie和Session?

HTTP协议是无状态的——服务器不记得你之前来过。但很多时候我们需要记录用户状态,比如:

  • 用户登录后,后续请求要能识别是同一个用户
  • 购物车功能,用户添加的商品要能保留
  • 记住用户偏好设置

这就需要会话管理技术:Cookie和Session。

5.2 Cookie:客户端的"小本本"

Cookie是保存在浏览器端的少量数据。工作流程:

  1. 第一次请求时,服务器通过响应头 Set-Cookie 给浏览器一些数据
  2. 浏览器把这些数据保存下来
  3. 之后每次请求,浏览器都会自动通过请求头 Cookie 把这些数据带上

Cookie的特点

  • 使用Key-Value结构存储数据
  • 单个Cookie大小限制4097字节
  • 每个域名最多50个Cookie(Chrome)
  • 与域名绑定,不支持跨一级域名
  • 分为状态Cookie(内存,关闭浏览器消失)和持久化Cookie(磁盘)

创建和读取Cookie

@WebServlet(urlPatterns = "/cookie.do")
public class CookieServlet extends HttpServlet {
    @Override
    protected void service(HttpServletRequest req, HttpServletResponse resp)
            throws ServletException, IOException {

        // ===== 创建Cookie并响应给浏览器 =====
        Cookie c1 = new Cookie("username", "zhangsan");
        Cookie c2 = new Cookie("age", "25");

        // 设置持久化Cookie(存活60秒)
        c2.setMaxAge(60);

        resp.addCookie(c1);
        resp.addCookie(c2);

        // ===== 读取浏览器带来的Cookie =====
        Cookie[] cookies = req.getCookies();
        if (cookies != null) {
            for (Cookie cookie : cookies) {
                System.out.println(cookie.getName() + "=" + cookie.getValue());
            }
        }
    }
}

Cookie不支持中文的处理(Servlet 4.0之前):

// 存储时编码
Cookie c = new Cookie("name", URLEncoder.encode("张三", "UTF-8"));

// 读取时解码
String name = URLDecoder.decode(cookie.getValue(), "UTF-8");

安全提醒:Cookie是明文存储的,安全性很低,不要在Cookie中存放敏感数据

5.3 Cookie实战:记录访问次数

@WebServlet(urlPatterns = "/visitCount.do")
public class VisitCountServlet extends HttpServlet {
    @Override
    protected void service(HttpServletRequest req, HttpServletResponse resp)
            throws ServletException, IOException {

        Cookie[] cookies = req.getCookies();
        boolean found = false;

        if (cookies != null) {
            for (Cookie cookie : cookies) {
                if ("visitCount".equals(cookie.getName())) {
                    // 找到记录,次数+1
                    int count = Integer.parseInt(cookie.getValue()) + 1;
                    Cookie newCookie = new Cookie("visitCount", String.valueOf(count));
                    resp.addCookie(newCookie);
                    resp.getWriter().write("欢迎您第" + count + "次访问!");
                    found = true;
                    break;
                }
            }
        }

        if (!found) {
            // 第一次访问
            Cookie newCookie = new Cookie("visitCount", "1");
            resp.addCookie(newCookie);
            resp.getWriter().write("欢迎您第一次访问!");
        }
    }
}

5.4 Session:服务端的"用户档案"

Session(HttpSession)是保存在服务器端的会话信息。工作流程:

  1. 第一次请求时,服务器创建Session对象,生成一个唯一的 JSESSIONID
  2. 把JSESSIONID通过Cookie发给浏览器
  3. 之后每次请求,浏览器通过Cookie带上JSESSIONID
  4. 服务器根据JSESSIONID找到对应的Session对象

Session的特点

  • 保存在服务器端,安全性高
  • 可以存储任何类型的数据
  • 使用Key-Value结构,value是Object类型
  • 存储大小无限制
  • 默认超时时间30分钟

Session的使用

@WebServlet(urlPatterns = "/session.do")
public class SessionServlet extends HttpServlet {
    @Override
    protected void service(HttpServletRequest req, HttpServletResponse resp)
            throws ServletException, IOException {

        // 获取Session(没有就创建新的)
        HttpSession session = req.getSession();

        // 存储数据
        session.setAttribute("username", "zhangsan");
        session.setAttribute("role", "admin");

        // 读取数据
        String username = (String) session.getAttribute("username");

        // 获取Session ID
        String sessionId = session.getId();
        System.out.println("SessionID: " + sessionId);

        // 删除数据
        session.removeAttribute("role");

        // 销毁整个Session
        // session.invalidate();
    }
}

Session的销毁方式

<!-- web.xml中配置超时时间(单位:分钟) -->
<session-config>
    <session-timeout>30</session-timeout>
</session-config>
// 或者手动销毁
session.invalidate();

5.5 Cookie和Session的区别(面试必问!)

对比项 Cookie Session
存储位置 浏览器端 服务器端
安全性 低(明文存储)
数据大小 限制4KB 无限制
数量限制 每域名50个 无限制
生命周期 可设置MaxAge 默认30分钟超时
数据类型 只能字符串 任意类型

使用建议:Session安全性高,适合存放用户登录信息。但Session生命周期不固定,不建议存放大量业务数据。

5.6 四大域对象

在JavaWeb中有四个"域对象",用来在不同范围内共享数据:

域对象 范围 说明
PageContext 当前页面 JSP中使用
HttpServletRequest 一次请求 请求转发时共享
HttpSession 一次会话 跨请求共享
ServletContext 整个应用 所有用户共享

六、JSP:HTML和Java的"混血儿"

6.1 什么是JSP?

JSP(Java Server Pages)是一种动态网页技术,本质上就是Servlet。它的目的是把Java代码嵌入到HTML中,让页面展示更方便。

第一次访问JSP时:Tomcat会把JSP文件编译成一个Java文件(Servlet),然后编译成class文件执行。后续访问如果JSP没修改,就直接执行编译好的class。

6.2 JSP的本质

JSP文件最终会被转译成一个继承自 HttpServlet 的Java类。所以JSP本质上就是Servlet,只是写法更像HTML。

JSP的执行过程:

  1. 转译:.jsp → .java(Servlet源代码)
  2. 编译:.java → .class(字节码)
  3. 执行:运行class,生成响应

JSP为什么慢? 不仅是因为第一次需要转译和编译,更因为JSP内部通过大量IO流输出页面内容,IO是重量级操作。

6.3 JSP中嵌入Java代码

JSP提供了三种方式嵌入Java代码:

<%-- JSP注释,不会出现在转译后的Java代码中 --%>

<!-- 1. 脚本片段:在_JSPService方法中 -->
<%
    int num = (int) (Math.random() * 100) + 1;
    String grade = num >= 90 ? "优秀" : "及格";
%>

<!-- 2. 表达式:输出值到页面 -->
<p>分数:<%= num %></p>
<p>等级:<%= grade %></p>

<!-- 3. 声明:作为类的成员(不推荐,会有线程安全问题) -->
<%!
    private int count = 0;  // 成员变量,不推荐!
%>

建议:不要在JSP中定义成员变量,因为JSP本质是Servlet,Servlet是单实例多线程的。

6.4 JSP九大内置对象

JSP内置了9个可以直接使用的对象,不用new:

对象 类型 说明
request HttpServletRequest 请求对象
response HttpServletResponse 响应对象
session HttpSession 会话对象
application ServletContext 全局应用对象
out JspWriter 输出对象
pageContext PageContext 页面上下文
config ServletConfig 配置对象
page Object 当前页面(this)
exception Throwable 异常对象

这些对象其实就是Servlet中那些常用对象的"快捷方式"。

6.5 JSP指令标签

JSP有三种指令标签:

page指令:设置页面属性

<%@ page language="java"
         contentType="text/html; charset=UTF-8"
         pageEncoding="UTF-8"
         import="java.util.*"
         session="true"
         errorPage="error.jsp"  %>

include指令:包含其他文件

<%@ include file="header.jsp" %>

taglib指令:引入标签库

<%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c" %>

6.6 JSP的使用建议

JSP和Servlet本质相同,功能可以互换。但JSP更适合展示数据(在HTML中嵌入少量Java代码),Servlet更适合处理业务逻辑

在现代开发中,JSP已经很少使用了,前后端分离架构下,后端只提供JSON接口,前端用Vue/React渲染页面。但理解JSP对理解JavaWeb的历史演进很有帮助。


七、MVC模式:分层的艺术

7.1 什么是MVC?

MVC(Model-View-Controller)是一种架构模式,把应用分成三层:

  • Model(模型):业务逻辑和数据(JavaBean、Service、DAO)
  • View(视图):页面展示(JSP、HTML)
  • Controller(控制器):协调模型和视图(Servlet)
用户请求 → Controller → Model(处理业务) → View(展示页面) → 用户

7.2 MVC的优势

  • 职责分离:各层独立修改,互不影响
  • 代码复用:同一个Model可以被多个View使用
  • 团队协作:后端写Model和Controller,前端写View
  • 易于维护:改页面不用动业务逻辑,改业务不用动页面

在Spring MVC中,Controller就是 @Controller 注解的类,Model就是Service和DAO层,View就是JSP或模板引擎。现在流行的Spring Boot + Vue前后端分离架构,View层完全交给了前端。


八、Filter和Listener:Servlet的"左膀右臂"

8.1 Filter过滤器

Filter可以拦截请求和响应,在Servlet处理之前和之后做一些通用处理。典型应用:

  • 统一编码处理
  • 登录验证
  • 权限控制
  • 敏感词过滤
  • 压缩响应

Filter的工作流程

浏览器请求 → Filter(预处理) → Servlet处理 → Filter(后处理) → 浏览器响应

开发一个Filter

@WebFilter(urlPatterns = "/*")  // 拦截所有请求
public class EncodingFilter implements Filter {

    // 初始化(服务器启动时调用一次)
    @Override
    public void init(FilterConfig filterConfig) throws ServletException {
        System.out.println("Filter初始化");
    }

    // 核心方法:每次请求都会调用
    @Override
    public void doFilter(ServletRequest request, ServletResponse response,
                         FilterChain chain) throws IOException, ServletException {
        // 预处理:设置编码
        request.setCharacterEncoding("UTF-8");
        response.setCharacterEncoding("UTF-8");
        response.setContentType("text/html;charset=UTF-8");

        System.out.println("Filter预处理");

        // 放行:交给下一个Filter或Servlet
        chain.doFilter(request, response);

        // 后处理:Servlet执行完毕后
        System.out.println("Filter后处理");
    }

    // 销毁(服务器停止时调用一次)
    @Override
    public void destroy() {
        System.out.println("Filter销毁");
    }
}

Filter的生命周期

阶段 次数 时机
init 1次 服务器启动时
doFilter 多次 每次请求
destroy 1次 服务器停止时

多个Filter的执行顺序:按Filter类名的字母顺序执行。如果需要控制顺序,可以在 web.xml 中配置顺序。

8.2 登录验证Filter实战

@WebFilter(urlPatterns = "/*")
public class LoginFilter implements Filter {

    // 不需要登录就能访问的路径
    private static final Set<String> WHITE_LIST = Set.of(
        "/login.html", "/login.do", "/register.html", "/register.do"
    );

    @Override
    public void doFilter(ServletRequest req, ServletResponse resp,
                         FilterChain chain) throws IOException, ServletException {

        HttpServletRequest request = (HttpServletRequest) req;
        HttpServletResponse response = (HttpServletResponse) resp;

        // 获取请求路径
        String uri = request.getRequestURI();
        String path = uri.substring(uri.lastIndexOf("/"));

        // 白名单直接放行
        if (WHITE_LIST.contains(path)) {
            chain.doFilter(request, response);
            return;
        }

        // 检查是否已登录(Session中有用户信息)
        HttpSession session = request.getSession(false);
        if (session != null && session.getAttribute("user") != null) {
            // 已登录,放行
            chain.doFilter(request, response);
        } else {
            // 未登录,跳转到登录页
            response.sendRedirect(request.getContextPath() + "/login.html");
        }
    }
}

8.3 Listener监听器

Listener可以监听Web应用中的各种事件,比如:

  • Session的创建和销毁
  • Session中属性的添加、删除、修改
  • Request的创建和销毁
  • ServletContext的创建和销毁

Session域监听器(最常用):

@WebListener
public class MySessionListener implements HttpSessionListener {

    @Override
    public void sessionCreated(HttpSessionEvent se) {
        System.out.println("Session创建: " + se.getSession().getId());
        // 可以在这里统计在线用户数
    }

    @Override
    public void sessionDestroyed(HttpSessionEvent se) {
        System.out.println("Session销毁: " + se.getSession().getId());
        // 可以在这里清理用户相关数据
    }
}

Session属性监听器

@WebListener
public class MySessionAttributeListener implements HttpSessionAttributeListener {

    @Override
    public void attributeAdded(HttpSessionBindingEvent event) {
        System.out.println("Session添加属性: " + event.getName()
                         + " = " + event.getValue());
    }

    @Override
    public void attributeRemoved(HttpSessionBindingEvent event) {
        System.out.println("Session删除属性: " + event.getName());
    }

    @Override
    public void attributeReplaced(HttpSessionBindingEvent event) {
        System.out.println("Session修改属性: " + event.getName());
    }
}

九、Ajax:页面不刷新的魔法

9.1 什么是Ajax?

Ajax(Asynchronous JavaScript And XML)是一种异步请求技术,可以在不刷新整个页面的情况下,与服务器交换数据并更新部分页面内容。

你刷微博的时候,往下拉自动加载新内容,页面并没有刷新——这就是Ajax在背后工作。

9.2 Ajax的核心对象

Ajax的核心是 XMLHttpRequest 对象:

// 1. 创建XMLHttpRequest对象
var xhr = new XMLHttpRequest();

// 2. 配置请求(GET请求)
xhr.open("GET", "/api/user?id=1", true);

// 3. 设置回调函数
xhr.onreadystatechange = function() {
    if (xhr.readyState === 4 && xhr.status === 200) {
        // 请求完成且成功
        var data = xhr.responseText;
        console.log(data);
    }
};

// 4. 发送请求
xhr.send();

readyState状态说明

状态 说明
0 UNSENT 未初始化
1 OPENED 已调用open
2 HEADERS_RECEIVED 已收到响应头
3 LOADING 正在接收响应体
4 DONE 请求完成

9.3 Ajax实战:用户名校验

前端页面

<!DOCTYPE html>
<html>
<head>
    <meta charset="UTF-8">
    <title>用户名校验</title>
</head>
<body>
    <form>
        用户名:<input type="text" id="username" onblur="checkUsername()">
        <span id="msg"></span>
    </form>

    <script>
        function checkUsername() {
            var username = document.getElementById("username").value;
            var xhr = new XMLHttpRequest();

            xhr.open("GET", "/checkUsername.do?username=" + username, true);

            xhr.onreadystatechange = function() {
                if (xhr.readyState === 4 && xhr.status === 200) {
                    var result = xhr.responseText;
                    var msgSpan = document.getElementById("msg");
                    if (result === "available") {
                        msgSpan.style.color = "green";
                        msgSpan.innerText = "用户名可用";
                    } else {
                        msgSpan.style.color = "red";
                        msgSpan.innerText = "用户名已存在";
                    }
                }
            };

            xhr.send();
        }
    </script>
</body>
</html>

后端Servlet

@WebServlet(urlPatterns = "/checkUsername.do")
public class CheckUsernameServlet extends HttpServlet {
    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp)
            throws ServletException, IOException {

        String username = req.getParameter("username");

        // 模拟数据库查询
        boolean exists = "admin".equals(username) || "root".equals(username);

        resp.setContentType("text/plain;charset=UTF-8");
        resp.getWriter().write(exists ? "exists" : "available");
    }
}

9.4 Ajax的状态码问题

在开发中,有时候Ajax请求会遇到状态码为0的情况。常见原因:

  • 跨域请求被浏览器拦截
  • 请求被取消(页面跳转、用户刷新)
  • 网络不通(服务器未启动)
  • 使用了file://协议(本地直接打开HTML文件)

十、请求转发和响应重定向

这是JavaWeb中两个非常重要的概念,面试经常问。

10.1 请求转发(Forward)

// 在Servlet中转发到另一个资源
request.getRequestDispatcher("/target.jsp").forward(request, response);

特点:

  • 一次请求,浏览器地址栏不变
  • 可以通过request域共享数据
  • 只能转发到项目内部资源

10.2 响应重定向(Redirect)

// 重定向到另一个资源
response.sendRedirect("/项目名/target.jsp");

特点:

  • 两次请求,浏览器地址栏会变
  • 不能通过request域共享数据
  • 可以重定向到外部资源

10.3 对比

对比项 请求转发 响应重定向
请求次数 1次 2次
地址栏 不变 变为新地址
数据共享 可以共享request域 不能共享
效率 低(多一次请求)
范围 项目内部 可以到外部

一句话记忆:转发是"内部转手",重定向是"告诉你新地址,你自己再去"。


总结

回顾一下,JavaWeb的核心知识体系是这样的:

HTTP协议(通信规则)
    ↓
Tomcat(Web容器,管理Servlet生命周期)
    ↓
Servlet(处理请求的核心)
    ├── HttpServletRequest(获取请求信息)
    ├── HttpServletResponse(返回响应信息)
    ├── Cookie(客户端会话管理)
    ├── HttpSession(服务端会话管理)
    ├── ServletContext(全局共享)
    ├── Filter(请求拦截)
    └── Listener(事件监听)
    ↓
JSP(动态页面,本质是Servlet)
    ↓
MVC模式(分层架构思想)

虽然现在开发中直接写Servlet的机会很少了,Spring Boot帮我们封装了太多东西。但理解这些底层知识,对排查问题、理解框架原理、应对面试都非常有帮助。

就像学开车,你不需要知道发动机的每个零件怎么工作,但至少要明白油门、刹车、方向盘的基本原理。JavaWeb就是Java Web开发的"基本原理"。


Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐