java-web开发

Java Servlet 入门指南

一、引言:为什么需要 Servlet?

在 Java Web 开发中,直接编写 HTTP 服务器需要处理 TCP 连接、解析 HTTP 协议等底层细节,效率低下且容易出错。Servlet API 的出现将这些底层工作交给 Web 服务器(如 Tomcat),开发者只需专注于业务逻辑的实现,极大提升了开发效率。

核心优势

  • 封装 HTTP 请求 / 响应,无需手动处理 TCP 连接
  • 基于 JavaEE 标准,兼容性强
  • 支持多线程处理请求,性能高效

二、快速上手:编写第一个 Servlet

2.1 代码示例:Hello World Servlet

java 复制代码
// 使用 @WebServlet 注解映射 URL 路径
@WebServlet(urlPatterns = "/hello") 
public class HelloServlet extends HttpServlet {
    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) 
            throws ServletException, IOException {
        // 设置响应类型为 HTML
        resp.setContentType("text/html"); 
        // 获取输出流
        try (PrintWriter pw = resp.getWriter()) {
            pw.write("<h1>Hello, World!</h1>");
        }
    }
}

2.2 关键组件解析

  1. HttpServlet :所有 Servlet 的基类,提供 doGet/doPost 等请求处理方法
  2. HttpServletRequest:封装 HTTP 请求数据(参数、头信息等)
  3. HttpServletResponse:封装 HTTP 响应逻辑(状态码、响应体等)
  4. 注解驱动 :通过 @WebServlet 直接映射 URL,替代传统 web.xml 配置(高版本 Servlet 无需此文件)

三、Maven 配置与项目结构

3.1 pom.xml 依赖配置

xml 复制代码
<project>
    <packaging>war</packaging> <!-- 打包为 WAR 文件 -->
    <dependencies>
        <!-- Servlet API 5.0+ 版本(Jakarta EE 标准) -->
        <dependency>
            <groupId>jakarta.servlet</groupId>
            <artifactId>jakarta.servlet-api</artifactId>
            <version>5.0.0</version>
            <scope>provided</scope> <!-- 运行时由服务器提供 -->
        </dependency>
    </dependencies>
</project>

3.2 项目目录结构

plaintext 复制代码
project-root/
├── pom.xml
└── src/main/
    ├── java/               # Servlet 代码
    ├── resources/          # 资源文件
    └── webapp/             # Web 资源(HTML/CSS/JS)
        └── WEB-INF/        # 存放类文件和依赖(自动生成)

四、部署与运行:Tomcat 服务器实战

4.1 环境准备

  • Tomcat 版本选择

    • Servlet 4.0 及以下:Tomcat 9.x 或更低
    • Servlet 5.0 及以上:Tomcat 10.x 或更高(推荐)

4.2 部署步骤

  1. 打包项目 :执行 mvn clean package 生成 xxx.war 文件

  2. 复制 WAR 包 :将 war 文件放入 Tomcat 的 webapps 目录

  3. 启动服务器

    • Linux/macOS:./bin/startup.sh
    • Windows:bin/startup.bat
  4. 访问应用

    • 默认路径:http://localhost:8080/项目名/路径
    • 设为根路径:将 WAR 重命名为 ROOT.war,访问 http://localhost:8080/

五、核心概念与最佳实践

5.1 Servlet 生命周期

  1. 初始化:服务器启动时创建 Servlet 实例(单例模式)
  2. 请求处理 :多线程调用 doGet/doPost 方法处理请求
  3. 销毁 :服务器关闭时销毁实例(可选 destroy() 方法)

5.2 线程安全注意事项

  • 避免实例变量: Servlet 为单例,实例变量会被多线程共享,可能引发线程安全问题
  • 安全使用 ThreadLocal:若使用线程局部变量,需确保请求结束后清理状态
  • 请求对象线程安全HttpServletRequestHttpServletResponse 为线程本地变量,无需同步

六、版本兼容与常见问题

6.1 Servlet 版本差异

特性 Servlet 4.0(javax) Servlet 5.0(jakarta)
包名 javax.servlet.* jakarta.servlet.*
Tomcat 版本 9.x 及以下 10.x 及以上
Spring 兼容性 Spring 5 及以下支持 Spring 6+ 支持

6.2 常见错误与解决

  1. 404 错误

    • 检查 WAR 包是否正确部署
    • 确认 @WebServlet 注解的 urlPatterns 与访问路径一致
  2. 500 内部错误

    • 检查代码是否有空指针异常(如未处理 null 参数)
    • 确保 Tomcat 版本与 Servlet API 版本匹配
  3. Maven 打包失败

    手动指定 maven-war-plugin 版本:

    xml 复制代码
    <plugin>
        <groupId>org.apache.maven.plugins</groupId>
        <artifactId>maven-war-plugin</artifactId>
        <version>3.3.2</version>
    </plugin>

嵌入式 Tomcat 开发 Servlet:告别传统部署的高效方案

在 Java Web 开发中,Servlet 作为基础组件,传统开发流程往往需要繁琐的 WAR 包部署和 Tomcat 配置。

传统 Servlet 开发的痛点

传统 Servlet 开发流程存在以下问题:

  1. 编写 Servlet 后需打包为 WAR 文件
  2. 手动复制到 Tomcat 的 webapps 目录
  3. 每次调试需重启 Tomcat 或配置远程调试
  4. 初学者常卡在 IDE 与 Tomcat 的集成配置

这种方式不仅效率低下,还打断了开发调试的流畅性。

嵌入式 Tomcat 的核心优势

通过在 Java 程序中直接启动 Tomcat,我们可以:

  • 无需独立安装 Tomcat 服务器
  • 在 IDE 中直接断点调试 Servlet
  • 开发与生产环境一致的部署方式
  • 无缝集成 Maven 构建流程

实战:从零搭建嵌入式 Tomcat 项目

1. 创建 Maven 项目并配置依赖

xml 复制代码
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>com.itranswarp.learnjava</groupId>
    <artifactId>web-servlet-embedded</artifactId>
    <version>1.0-SNAPSHOT</version>
    <packaging>war</packaging>

    <properties>
        <java.version>17</java.version>
        <tomcat.version>10.1.1</tomcat.version>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.apache.tomcat.embed</groupId>
            <artifactId>tomcat-embed-core</artifactId>
            <version>${tomcat.version}</version>
            <scope>provided</scope>
        </dependency>
        <dependency>
            <groupId>org.apache.tomcat.embed</groupId>
            <artifactId>tomcat-embed-jasper</artifactId>
            <version>${tomcat.version}</version>
            <scope>provided</scope>
        </dependency>
    </dependencies>
</project>

2. 编写 Servlet 示例

java 复制代码
@WebServlet(urlPatterns = "/")
public class HelloServlet extends HttpServlet {
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        resp.setContentType("text/html");
        String name = req.getParameter("name");
        if (name == null) {
            name = "world";
        }
        PrintWriter pw = resp.getWriter();
        pw.write("<h1>Hello, " + name + "!</h1>");
        pw.flush();
    }
}

3. 实现嵌入式 Tomcat 启动类

java 复制代码
public class Main {
    public static void main(String[] args) throws Exception {
        Tomcat tomcat = new Tomcat();
        tomcat.setPort(8080);
        tomcat.getConnector();
        
        Context ctx = tomcat.addWebapp("", new File("src/main/webapp").getAbsolutePath());
        WebResourceRoot resources = new StandardRoot(ctx);
        resources.addPreResources(
                new DirResourceSet(resources, "/WEB-INF/classes", 
                new File("target/classes").getAbsolutePath(), "/"));
        ctx.setResources(resources);
        
        tomcat.start();
        tomcat.getServer().await();
    }
}

4. 运行与访问

直接运行Main类的 main 方法,即可启动 Tomcat 服务器,访问http://localhost:8080/查看效果。如需传递参数,可访问http://localhost:8080/?name=Java

生成可执行 WAR 包

1. 配置 Maven 打包插件

xml 复制代码
<build>
    <finalName>hello</finalName>
    <plugins>
        <plugin>
            <groupId>org.apache.maven.plugins</groupId>
            <artifactId>maven-war-plugin</artifactId>
            <version>3.3.2</version>
            <configuration>
                <webResources>
                    <resource>
                        <directory>${project.build.directory}/classes</directory>
                    </resource>
                </webResources>
                <archiveClasses>true</archiveClasses>
                <archive>
                    <manifest>
                        <addClasspath>true</addClasspath>
                        <classpathPrefix>tmp-webapp/WEB-INF/lib/</classpathPrefix>
                        <mainClass>com.itranswarp.learnjava.Main</mainClass>
                    </manifest>
                </archive>
            </configuration>
        </plugin>
    </plugins>
</build>

2. 完善启动类支持 WAR 包运行

java 复制代码
public class Main {
    public static void main(String[] args) throws Exception {
        String jarFile = Main.class.getProtectionDomain().getCodeSource().getLocation().getFile();
        boolean isJarFile = jarFile.endsWith(".war") || jarFile.endsWith(".jar");
        String webDir = isJarFile ? "tmp-webapp" : "src/main/webapp";
        
        if (isJarFile) {
            // 解压WAR包到临时目录
            Path baseDir = Paths.get(webDir).normalize().toAbsolutePath();
            if (Files.isDirectory(baseDir)) Files.delete(baseDir);
            Files.createDirectories(baseDir);
            
            try (JarFile jar = new JarFile(jarFile)) {
                jar.stream().sorted(Comparator.comparing(JarEntry::getName))
                    .forEach(entry -> {
                        try {
                            Path res = baseDir.resolve(entry.getName());
                            if (!entry.isDirectory()) {
                                Files.createDirectories(res.getParent());
                                Files.copy(jar.getInputStream(entry), res);
                            }
                        } catch (IOException e) {
                            e.printStackTrace();
                        }
                    });
            }
            
            // JVM退出时清理临时文件
            Runtime.getRuntime().addShutdownHook(new Thread(() -> {
                try {
                    Files.walk(baseDir).sorted(Comparator.reverseOrder())
                        .map(Path::toFile).forEach(File::delete);
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }));
        }
        
        TomcatRunner.run(webDir, isJarFile ? "tmp-webapp" : "target/classes");
    }
}

class TomcatRunner {
    public static void run(String webDir, String baseDir) throws Exception {
        Tomcat tomcat = new Tomcat();
        tomcat.setPort(8080);
        tomcat.getConnector();
        
        Context ctx = tomcat.addWebapp("", new File(webDir).getAbsolutePath());
        WebResourceRoot resources = new StandardRoot(ctx);
        resources.addPreResources(
            new DirResourceSet(resources, "/WEB-INF/classes", 
            new File(baseDir).getAbsolutePath(), "/"));
        ctx.setResources(resources);
        
        tomcat.start();
        tomcat.getServer().await();
    }
}

3. 打包与运行

执行mvn clean package生成 WAR 包,通过java -jar hello.war即可启动应用。

开发注意事项

  1. IDEA 配置 :运行时需勾选Include dependencies with "Provided" scope
  2. 类加载顺序:Main 类不能依赖解压后才加载的类(如 Tomcat 相关类)
  3. 临时文件清理:确保程序退出时能正确清理解压的临时文件
  4. 版本兼容性:Tomcat 版本需与项目 JDK 版本匹配

Servlet 进阶实战:从请求映射到多线程处理全解析

🔥 引言:Servlet 在 Web 开发中的角色

一个标准的 Java Web 应用由多个 Servlet 组成,它们如同 HTTP 请求的 "路由器",通过配置路径映射处理不同的客户端请求。

一、Servlet 基础配置与请求映射

1. 注解式映射替代传统配置
java 复制代码
@WebServlet(urlPatterns = "/hello")  // 直接通过注解配置路径
public class HelloServlet extends HttpServlet {
    // 处理GET请求
    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) {
        // 业务逻辑
    }
}

💡 对比传统方式 :早期需在web.xml中配置映射,而 Servlet 3.0 + 通过注解简化开发,无需 XML 配置。

2. 多 Servlet 协同工作机制
java 复制代码
@WebServlet(urlPatterns = "/hello")  // 处理/hello路径
public class HelloServlet extends HttpServlet { ... }

@WebServlet(urlPatterns = "/signin")  // 处理/signin路径
public class SignInServlet extends HttpServlet { ... }

@WebServlet(urlPatterns = "/")  // 处理未匹配的所有路径(等效于/*)
public class IndexServlet extends HttpServlet { ... }
3. Dispatcher 请求分发原理
plaintext 复制代码
Browser → Web Server(Dispatcher) → 根据URL路径分发到对应Servlet  
  • 映射为/的 Servlet 会接收所有未匹配的路径(如/abc会被IndexServlet处理)。

二、HTTP 请求与响应核心对象

1. HttpServletRequest:请求信息的 "瑞士军刀"
java 复制代码
// 获取请求核心信息
String method = request.getMethod();        // "GET"/"POST"
String uri = request.getRequestURI();        // 请求路径(不含参数)
String query = request.getQueryString();     // 请求参数(如"name=Bob")
String param = request.getParameter("name"); // 获取指定参数

// 获取请求头与Body
Cookie[] cookies = request.getCookies();     // 所有Cookie
String header = request.getHeader("User-Agent"); // 获取请求头
InputStream body = request.getInputStream();   // 读取请求Body

💡 注意getQueryString()在无参数时返回null,需做非空判断。

2. HttpServletResponse:响应构建的 "控制器"
java 复制代码
// 设置响应头(需在输出Body前调用)
response.setStatus(200);                    // 状态码
response.setContentType("text/html;charset=UTF-8"); // 内容类型
response.setHeader("Cache-Control", "no-cache");    // 自定义头

// 输出响应Body(二选一:流或字符Writer)
OutputStream os = response.getOutputStream();
os.write("Hello".getBytes("UTF-8"));
os.flush(); // 必须调用flush()发送缓冲区内容,不能调用close()!

⚠️ 关键注意事项

  • 响应头必须在输出 Body 前设置;
  • 写入完毕后调用flush()而非close(),避免关闭 TCP 连接影响复用。

三、Servlet 多线程模型与线程安全

1. 单实例多线程执行机制
  • 每个 Servlet 在服务器中仅创建一个实例 ,多个请求由多线程并发调用doGet/doPost方法。
  • HttpServletRequestHttpServletResponse线程隔离的局部变量,无需担心线程安全。
2. 实例字段的线程安全风险
java 复制代码
public class HelloServlet extends HttpServlet {
    private Map<String, String> userCache = new ConcurrentHashMap<>(); // 线程安全集合
    
    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) {
        userCache.put("key", "value"); // 并发安全操作
    }
}

⚠️ 避免陷阱 :若使用非线程安全的集合(如HashMap),多线程读写会导致数据不一致,需使用ConcurrentHashMap或同步机制。

四、实战场景与典型应用

1. 路径映射最佳实践
  • 精确路径(如/api/user)优先于通配符路径(如/api/*)匹配;
  • /作为默认 Servlet 处理所有未匹配请求,避免 404 错误。
2. 请求方法处理策略
  • GET请求用于获取资源,POST用于提交数据;
  • 未覆写的方法(如doPut)会返回 405 Method Not Allowed 错误。

📌 核心总结

  1. Servlet 映射 :通过@WebServlet注解配置路径,/可处理所有未匹配请求;
  2. 请求处理 :覆写doGet/doPost等方法处理不同 HTTP 方法,依赖HttpServletRequest获取请求信息;
  3. 响应构建 :通过HttpServletResponse设置头信息和输出 Body,注意flush()的调用时机;
  4. 多线程安全:Servlet 实例单例,需确保实例字段的线程安全,请求对象无需担心线程问题。

🔥Java Web 开发核心:重定向 (Redirect) 与转发 (Forward) 深度解析

在 Java Web 开发中,重定向与转发是 Servlet 编程的基础核心概念,正确理解两者的区别与应用场景是构建高效 Web 应用的关键。

一、重定向 (Redirect) 详解

重定向是服务器告诉浏览器 "资源地址已变更,请使用新 URL 重新请求" 的过程,本质是两次独立的 HTTP 请求。

1.1 代码实现示例

java 复制代码
@WebServlet(urlPatterns = "/hi")
public class RedirectServlet extends HttpServlet {
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        // 构造重定向目标URL
        String name = req.getParameter("name");
        String redirectToUrl = "/hello" + (name == null ? "" : "?name=" + name);
        // 发送302重定向响应
        resp.sendRedirect(redirectToUrl);
    }
}

1.2 重定向流程解析

ascii 复制代码
┌───────┐   GET /hi     ┌───────────────┐
│Browser│ ────────────▶ │RedirectServlet│
│       │ ◀──────────── │               │
└───────┘   302         └───────────────┘

┌───────┐  GET /hello   ┌───────────────┐
│Browser│ ────────────▶ │ HelloServlet  │
│       │ ◀──────────── │               │
└───────┘   200 <html>  └───────────────┘

1.3 302 与 301 重定向区别

类型 状态码 含义 浏览器行为
临时重定向 302 资源临时移动 每次请求都重新获取新地址
永久重定向 301 资源永久移动 缓存重定向关系,直接请求新地址

1.4 301 永久重定向实现

java 复制代码
resp.setStatus(HttpServletResponse.SC_MOVED_PERMANENTLY); // 301状态码
resp.setHeader("Location", "/hello");

1.5 应用场景

  • 资源路径变更时保证旧链接可用
  • 登录成功后跳转至首页
  • 表单提交后避免重复提交

二、转发 (Forward) 详解

转发是服务器内部的请求处理转移,浏览器完全感知不到转发过程,仅发起一次 HTTP 请求。

2.1 代码实现示例

java 复制代码
@WebServlet(urlPatterns = "/morning")
public class ForwardServlet extends HttpServlet {
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        // 转发请求到/hello路径
        req.getRequestDispatcher("/hello").forward(req, resp);
    }
}

2.2 转发流程解析

ascii 复制代码
                          ┌────────────────────────┐
                          │      ┌───────────────┐ │
                          │ ────▶│ForwardServlet │ │
┌───────┐  GET /morning   │      └───────────────┘ │
│Browser│ ──────────────▶ │              │         │
│       │ ◀────────────── │              ▼         │
└───────┘    200 <html>   │      ┌───────────────┐ │
                          │ ◀────│ HelloServlet  │ │
                          │      └───────────────┘ │
                          │       Web Server       │
                          └────────────────────────┘

2.3 转发特点

  • 浏览器地址栏保持原 URL 不变
  • 属于服务器内部操作,性能更高
  • 可以通过 request 对象共享数据

2.4 应用场景

  • 复杂业务逻辑的模块拆分
  • 权限校验后的请求处理转移
  • 公共处理逻辑的统一入口

三、重定向与转发核心区别对比

对比项 重定向 (Redirect) 转发 (Forward)
HTTP 请求次数 2 次 1 次
地址栏变化 变更为新 URL 保持原 URL
数据共享方式 只能通过 URL、Cookie 等方式 可以通过 request.setAttribute ()
性能消耗 较高(两次网络请求) 较低(服务器内部处理)
跳转范围 可以跳转到任何 URL(包括外部网站) 只能在当前 Web 应用内跳转
实现方式 resp.sendRedirect() req.getRequestDispatcher().forward()

四、实战练习与常见问题

4.1 练习场景

实现一个用户登录系统:

  1. 未登录时访问受限页面重定向到登录页
  2. 登录成功后根据权限转发到不同首页
  3. 处理登录参数的传递

4.2 常见问题解决方案

4.2.1 IDEA 中 Servlet 运行报错
plaintext 复制代码
idea需要设置启动的工作目录为project目录
4.2.2 重定向参数丢失问题
java 复制代码
// 正确携带参数的重定向写法
String name = req.getParameter("name");
String redirectUrl = "/target?name=" + URLEncoder.encode(name, "UTF-8");
resp.sendRedirect(redirectUrl);
4.2.3 转发路径问题
java 复制代码
// 绝对路径转发(推荐)
req.getRequestDispatcher("/hello").forward(req, resp);

// 相对路径转发(容易出错)
req.getRequestDispatcher("./hello").forward(req, resp);

五、总结与最佳实践

5.1 核心总结

  • 重定向是 "浏览器层面的地址变更",适用于需要改变用户可见 URL 的场景
  • 转发是 "服务器层面的请求转移",适用于内部逻辑处理和页面跳转
  • 301 重定向用于永久资源迁移,避免 SEO 权重丢失
  • 转发时可以通过 request 对象在多个 Servlet 间共享数据

在 Web 应用开发中,跟踪用户状态是一个基础且重要的需求

一、Web 会话管理基础

HTTP 协议是无状态的,这意味着服务器无法自动识别两次请求是否来自同一个浏览器。为了解决这个问题,Web 开发中引入了会话管理机制,其中最核心的就是SessionCookie

核心概念对比

特性 Session Cookie
存储位置 服务器端 客户端
安全性 较高(数据在服务端) 较低(需防范 XSS/CSRF)
存储限制 受服务器内存限制 浏览器通常限制 4KB / 域名
典型用途 存储用户登录状态、购物车 存储用户偏好、语言设置

二、Session 机制详解

1. Session 工作原理

Session 本质上是服务器为每个用户分配的唯一标识(Session ID),通过 Cookie(默认名为JSESSIONID)传递给浏览器。浏览器后续请求会携带该 Cookie,服务器通过 ID 查找对应的会话数据。

2. 在 Servlet 中使用 Session

登录案例实现

java 复制代码
@WebServlet(urlPatterns = "/signin")
public class SignInServlet extends HttpServlet {
    private Map<String, String> users = Map.of("bob", "bob123", "alice", "alice123");

    // 处理登录请求
    protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        String name = req.getParameter("username");
        String password = req.getParameter("password");
        if (users.containsKey(name) && users.get(name).equals(password)) {
            // 登录成功,将用户信息存入Session
            req.getSession().setAttribute("user", name);
            resp.sendRedirect("/"); // 重定向到首页
        } else {
            resp.sendError(HttpServletResponse.SC_FORBIDDEN); // 认证失败
        }
    }
}

获取 Session 数据

java 复制代码
@WebServlet(urlPatterns = "/")
public class IndexServlet extends HttpServlet {
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        // 从Session获取用户信息
        String user = (String) req.getSession().getAttribute("user");
        resp.setContentType("text/html");
        resp.getWriter().write("<h1>Welcome, " + (user != null ? user : "Guest") + "</h1>");
    }
}

3. Session 集群问题与解决方案

当应用部署到多台服务器时,Session 会面临以下挑战:

集群场景问题:
plaintext 复制代码
┌─────────┐     ┌─────────────┐   ┌────────────┐
│ Browser │────▶│ Reverse Proxy │──▶│ Web Server1 │
└─────────┘     └─────────────┘   └────────────┘
                                   └────────────┐
                                        ┌────────▼────┐
                                        │ Web Server2 │
                                        └────────────┘
解决方案:
  1. Session 复制:在服务器间同步 Session 数据(消耗带宽,内存利用率低)
  2. 粘滞会话(Sticky Session) :反向代理根据 JSESSIONID 固定转发到某台服务器(需代理支持)
  3. 分布式 Session 存储:使用 Redis/Memcached 集中存储 Session(大型应用首选)

三、Cookie 机制详解

属性 说明
name=value 键值对数据
path 生效路径(如/user/表示仅该路径下生效)
maxAge 有效期(秒),-1 表示浏览器关闭时失效
secure 仅 HTTPS 请求时发送
HttpOnly 禁止 JavaScript 访问(防范 XSS 攻击)

设置用户语言偏好 Cookie

java 复制代码
@WebServlet(urlPatterns = "/pref")
public class LanguageServlet extends HttpServlet {
    private static final Set<String> LANGUAGES = Set.of("en", "zh");

    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        String lang = req.getParameter("lang");
        if (LANGUAGES.contains(lang)) {
            // 创建Cookie并设置属性
            Cookie cookie = new Cookie("lang", lang);
            cookie.setPath("/");                 // 全路径生效
            cookie.setMaxAge(60 * 60 * 24 * 100); // 100天有效期
            cookie.setHttpOnly(true);            // 禁止JS访问
            if (req.isSecure()) {
                cookie.setSecure(true); // 仅HTTPS发送
            }
            resp.addCookie(cookie);
        }
        resp.sendRedirect("/");
    }
}

读取 Cookie 数据

java 复制代码
private String parseLanguageFromCookie(HttpServletRequest req) {
    Cookie[] cookies = req.getCookies();
    if (cookies != null) {
        for (Cookie cookie : cookies) {
            if ("lang".equals(cookie.getName())) {
                return cookie.getValue();
            }
        }
    }
    return "en"; // 默认语言
}

四、实战场景优化

1. 多语言支持优化

java 复制代码
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
    String lang = parseLanguageFromCookie(req);
    resp.setContentType("text/html; charset=UTF-8");
    PrintWriter pw = resp.getWriter();
    
    // 根据Cookie中的语言显示不同页面
    if ("zh".equals(lang)) {
        pw.write("<h1>登录</h1>");
        // 中文表单...
    } else {
        pw.write("<h1>Sign In</h1>");
        // 英文表单...
    }
    pw.flush();
}

2. 登出逻辑实现

java 复制代码
@WebServlet(urlPatterns = "/signout")
public class SignOutServlet extends HttpServlet {
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        // 移除Session中的用户信息
        req.getSession().removeAttribute("user");
        // 或者使Session失效
        // req.getSession().invalidate();
        resp.sendRedirect("/");
    }
}

Java Web 开发中的 MVC 模式实践:从 Servlet 到 Spring 的架构演进

在 Java Web 开发领域,MVC(Model-View-Controller)模式是最经典的架构设计模式之一。

一、MVC 模式的核心概念

MVC 模式将应用程序分为三个核心组件:

  • Model(模型) :负责封装业务数据和逻辑,如 JavaBean
  • View(视图) :负责数据展示和用户交互,如 JSP 页面
  • Controller(控制器) :负责接收请求、处理业务逻辑并协调模型与视图

这种架构的核心优势在于职责分离

  • 控制器专注于业务流程控制
  • 模型专注于数据处理
  • 视图专注于界面渲染

三者通过松耦合的方式协作,使得团队开发时可以并行处理不同组件,后续维护时也能精准定位问题模块。

二、原生 Java 实现 MVC 的经典案例

1. 模型层设计

首先定义业务模型类,以用户信息为例:

java 复制代码
// User.java - 业务模型类
public class User {
    public long id;
    public String name;
    public School school;
    
    // 构造方法、getter/setter省略
}

// School.java - 关联模型类
public class School {
    public String name;
    public String address;
    
    // 构造方法、getter/setter省略
}

模型层遵循 JavaBean 规范,通过属性封装数据,不涉及任何展示逻辑。

2. 控制器实现

使用 Servlet 作为控制器,处理请求并组装模型:

java 复制代码
// UserServlet.java - 控制器
@WebServlet(urlPatterns = "/user")
public class UserServlet extends HttpServlet {
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) 
            throws ServletException, IOException {
        // 1. 模拟从数据库获取数据
        School school = new School("No.1 Middle School", "101 South Street");
        User user = new User(123, "Bob", school);
        
        // 2. 将模型放入请求作用域
        req.setAttribute("user", user);
        
        // 3. 转发到视图层处理
        req.getRequestDispatcher("/WEB-INF/user.jsp").forward(req, resp);
    }
}

控制器的核心职责是:

  • 接收请求参数
  • 调用业务逻辑获取数据
  • 将模型数据存入请求作用域
  • 转发到对应视图

3. 视图层渲染

使用 JSP 作为视图,专注于数据展示:

jsp 复制代码
<!-- user.jsp - 视图层 -->
<%@ page import="com.itranswarp.learnjava.bean.*"%>
<%
    User user = (User) request.getAttribute("user");
%>
<html>
<head>
    <title>Hello World - JSP</title>
</head>
<body>
    <h1>Hello <%= user.name %>!</h1>
    <p>School Name:
    <span style="color:red">
        <%= user.school.name %>
    </span>
    </p>
    <p>School Address:
    <span style="color:red">
        <%= user.school.address %>
    </span>
    </p>
</body>
</html>

视图层的特点:

  • 从请求中获取模型数据
  • 仅负责 HTML 结构与数据渲染
  • 不包含任何业务逻辑

三、MVC 架构的最佳实践

1. 安全与规范设计

  1. JSP 文件保护

    将 JSP 放在/WEB-INF/目录下,利用 Web 容器机制防止直接访问(如http://localhost/user.jsp

  2. 数据转义处理

    上述示例未处理 HTML 转义,实际开发中应使用org.apache.commons.text.StringEscapeUtils等工具防止 XSS 攻击:

    jsp 复制代码
    <%= StringEscapeUtils.escapeHtml4(user.name) %>
  3. 职责严格分离

    • 控制器不涉及 HTML 生成
    • 视图不包含 SQL 查询等业务逻辑

2. MVC 架构流程图解

plaintext 复制代码
                   ┌───────────────────────┐
             ┌────▶│Controller: UserServlet│
             │     └───────────────────────┘
             │                 │
┌───────┐    │           ┌─────┴─────┐
│Browser│────┘           │Model: User│
│       │◀───┐           └─────┬─────┘
└───────┘    │                 │
             │                 ▼
             │     ┌───────────────────────┐
             └─────│    View: user.jsp     │
                   └───────────────────────┘

请求流程解析:

  1. 浏览器发送请求到控制器 Servlet
  2. 控制器处理业务逻辑并生成模型数据
  3. 控制器将模型放入请求作用域并转发到 JSP
  4. JSP 从请求中获取模型并渲染为 HTML
  5. 渲染结果返回给浏览器

四、MVC 模式的核心优势

  1. 开发效率提升
    前端与后端可并行开发,控制器、模型、视图可由不同团队负责
  2. 可维护性增强
    问题定位更精准,如页面显示异常优先检查视图层
  3. 复用性提高
    同一模型可适配多个视图(如 HTML 页面 / 移动端 API)
  4. 测试便利性
    模型层可独立进行单元测试,无需依赖 UI 环境

Java Web 开发必备:Filter 过滤器原理与实战应用

引言

在 Java Web 开发中,当我们需要对多个 Servlet 的公共逻辑进行统一处理时,Filter 过滤器是一种非常有效的解决方案。

为什么需要 Filter?

以一个论坛应用为例,我们有多个 Servlet 处理不同功能:

  • IndexServlet:浏览帖子
  • SignInServlet:登录
  • ProfileServlet:修改用户资料(需登录)
  • PostServlet:发帖(需登录)

如果直接在每个需要登录验证的 Servlet 中重复编写登录检查逻辑,会导致:

  1. 代码冗余

  2. 维护困难

  3. 功能扩展不便

Filter 的出现正是为了解决这类问题,它可以在 HTTP 请求到达 Servlet 之前进行预处理,实现公共逻辑的集中管理。

Filter 核心概念与实现

基础实现示例

java 复制代码
@WebFilter(urlPatterns = "/*")
public class EncodingFilter implements Filter {
    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
            throws IOException, ServletException {
        System.out.println("EncodingFilter:doFilter");
        // 统一设置请求响应编码
        request.setCharacterEncoding("UTF-8");
        response.setCharacterEncoding("UTF-8");
        // 必须调用此方法让请求继续传递
        chain.doFilter(request, response);
    }
}

核心要点解析:

  1. 必须实现Filter接口
  2. doFilter方法是核心处理逻辑
  3. @WebFilter注解用于指定过滤的 URL 模式
  4. chain.doFilter()是请求继续传递的关键,若不调用将导致请求中断

Filter 应用场景实战

1. 登录验证过滤器

java 复制代码
@WebFilter("/user/*")
public class AuthFilter implements Filter {
    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
            throws IOException, ServletException {
        HttpServletRequest req = (HttpServletRequest) request;
        HttpServletResponse resp = (HttpServletResponse) response;
        
        // 检查会话中是否存在用户信息
        if (req.getSession().getAttribute("user") == null) {
            System.out.println("AuthFilter: 未登录,重定向到登录页");
            resp.sendRedirect("/signin");
        } else {
            // 已登录,继续处理请求
            chain.doFilter(request, response);
        }
    }
}

应用特点

  • 只过滤以/user/开头的路径
  • 未登录时直接重定向,阻止请求继续传递
  • 已登录时放行请求到目标 Servlet

2. 日志记录过滤器

java 复制代码
@WebFilter("/*")
public class LogFilter implements Filter {
    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
            throws IOException, ServletException {
        HttpServletRequest httpRequest = (HttpServletRequest) request;
        System.out.println("LogFilter: 处理请求 " + httpRequest.getRequestURI());
        chain.doFilter(request, response);
    }
}

应用价值

  • 统一记录所有请求的访问日志
  • 便于系统监控和问题排查
  • 不影响原有业务逻辑

Filter 链处理机制

多 Filter 执行顺序

当存在多个 Filter 时,会形成一个处理链:

plaintext 复制代码
┌──────────────┐    ┌─────────┐    ┌────────┐
│EncodingFilter│───▶│LogFilter│───▶│ServletA│
└──────────────┘    └─────────┘    └────────┘

顺序控制注意事项

  1. 使用@WebFilter注解时,Servlet 规范未定义执行顺序
  2. 若需精确控制顺序,需在web.xml中显式配置:
xml 复制代码
<filter>
    <filter-name>EncodingFilter</filter-name>
    <filter-class>com.example.EncodingFilter</filter-class>
</filter>
<filter-mapping>
    <filter-name>EncodingFilter</filter-name>
    <url-pattern>/*</url-pattern>
</filter-mapping>
<filter>
    <filter-name>LogFilter</filter-name>
    <filter-class>com.example.LogFilter</filter-class>
</filter>
<filter-mapping>
    <filter-name>LogFilter</filter-name>
    <url-pattern>/*</url-pattern>
</filter-mapping>

核心注意事项

  1. 请求传递关键

    若希望请求继续处理,必须在doFilter中调用chain.doFilter(request, response),否则会返回空白页

  2. URL 模式匹配

    • /*:匹配所有路径
    • /user/*:匹配以/user/开头的路径
    • *.do匹配所有.do 结尾的路径
  3. 中断请求处理

    若在 Filter 中决定不继续处理请求(如未授权),可不调用chain.doFilter,直接返回响应

深入理解 Java Web 开发中的 Listener 组件:从基础到实践

在 Java Web 开发领域,除了我们熟知的 Servlet 和 Filter 两大核心组件外,还有一个重要的组件常常被忽视却又在框架底层发挥关键作用 ------Listener(监听器)。

一、Listener:Web 应用的事件监听者

Listener 本质上是一种事件监听器,它能够监听 Web 应用中三大域对象(ServletContext、HttpSession、ServletRequest)的生命周期事件及属性变化事件。与 Servlet 和 Filter 不同,Listener 不直接处理请求响应,而是专注于 "感知" 应用状态的变化并执行相应的响应操作。

核心应用场景:

  • Web 应用初始化资源加载(如数据库连接池、缓存初始化)
  • 会话状态监控与统计
  • 请求级资源追踪与性能监控
  • 全局事件通知机制

二、ServletContextListener:最常用的监听器

基础实现示例

java 复制代码
@WebListener
public class AppListener implements ServletContextListener {
    // Web应用初始化完成时调用
    @Override
    public void contextInitialized(ServletContextEvent sce) {
        ServletContext context = sce.getServletContext();
        System.out.println("Web应用初始化完成,当前时间:" + new Date());
        
        // 典型应用:初始化数据库连接池
        DatabasePool.init(context.getInitParameter("dbConfigPath"));
        
        // 注册全局应用属性
        context.setAttribute("appStartDate", new Date());
    }

    // Web应用销毁时调用
    @Override
    public void contextDestroyed(ServletContextEvent sce) {
        ServletContext context = sce.getServletContext();
        System.out.println("Web应用正在销毁,运行时长:" + 
                (new Date().getTime() - ((Date)context.getAttribute("appStartDate")).getTime()) + "ms");
        
        // 典型应用:释放数据库连接池资源
        DatabasePool.shutdown();
    }
}

执行时机关键点

  • contextInitialized在 Web 容器完成 ServletContext 初始化后调用,早于任何 Servlet 请求处理
  • contextDestroyed在 Web 容器卸载 Web 应用前调用,可用于资源清理
  • 容器保证初始化方法执行完毕后才会处理用户请求

三、六大核心 Listener 接口详解

1. 生命周期监听器

接口名称 监听对象 核心方法
ServletContextListener ServletContext contextInitialized/ contextDestroyed
HttpSessionListener HttpSession sessionCreated/ sessionDestroyed
ServletRequestListener ServletRequest requestInitialized/ requestDestroyed

2. 属性变化监听器

接口名称 监听对象 核心方法
ServletContextAttributeListener ServletContext attributeAdded/ attributeRemoved/ attributeReplaced
HttpSessionAttributeListener HttpSession 同上
ServletRequestAttributeListener ServletRequest 同上

实战案例:会话活跃度统计

java 复制代码
@WebListener
public class SessionListener implements HttpSessionListener {
    private static final AtomicInteger ACTIVE_SESSIONS = new AtomicInteger(0);
    
    @Override
    public void sessionCreated(HttpSessionEvent se) {
        int count = ACTIVE_SESSIONS.incrementAndGet();
        System.out.println("新会话创建,当前活跃会话数:" + count);
    }

    @Override
    public void sessionDestroyed(HttpSessionEvent se) {
        int count = ACTIVE_SESSIONS.decrementAndGet();
        System.out.println("会话销毁,当前活跃会话数:" + count);
    }
    
    // 提供静态方法获取活跃会话数
    public static int getActiveSessionCount() {
        return ACTIVE_SESSIONS.get();
    }
}

四、ServletContext:Web 应用的全局上下文

核心特性解析

  1. 全局唯一性:每个 Web 应用对应唯一的 ServletContext 实例

  2. 生命周期一致性:与 Web 应用生命周期完全一致

  3. 数据共享中心

    • 通过setAttribute(String name, Object value)存储全局数据
    • 通过getAttribute(String name)获取全局数据
    • 通过removeAttribute(String name)移除全局数据

高级应用:动态注册组件

java 复制代码
@WebListener
public class DynamicComponentRegistrar implements ServletContextListener {
    @Override
    public void contextInitialized(ServletContextEvent sce) {
        ServletContext context = sce.getServletContext();
        
        // 动态注册Servlet
        ServletRegistration.Dynamic dynamicServlet = context.addServlet("dynamicServlet", 
                new HttpServlet() {
                    @Override
                    protected void doGet(HttpServletRequest req, HttpServletResponse resp) 
                            throws ServletException, IOException {
                        resp.getWriter().write("动态注册的Servlet响应");
                    }
                });
        dynamicServlet.addMapping("/dynamic");
        
        // 动态注册Filter
        FilterRegistration.Dynamic dynamicFilter = context.addFilter("dynamicFilter", 
                new Filter() {
                    @Override
                    public void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain) 
                            throws IOException, ServletException {
                        System.out.println("动态过滤器执行");
                        chain.doFilter(req, resp);
                    }
                    // 其他接口实现略
                });
        dynamicFilter.addMappingForServletNames(null, false, "dynamicServlet");
    }
    // 销毁方法略
}
相关推荐
程序猿追10 小时前
使用GeeLark+亮数据,做数据采集打造爆款内容
运维·服务器·人工智能·机器学习·架构
喵爸的小作坊10 小时前
StreamPanel:一个让 SSE 调试不再痛苦的 Chrome 插件
前端·后端·http
神奇小汤圆10 小时前
字符串匹配算法
后端
无限大610 小时前
为什么网站需要"域名"?——从 IP 地址到网址的演进
后端
树獭叔叔11 小时前
LangGraph Memory 机制
后端·langchain·aigc
BullSmall11 小时前
Tomcat11证书配置全指南
java·运维·tomcat
程序员Feri11 小时前
一文就可搞清楚的HarmonyOS6.0解锁模态页面的“真香”操作
架构
永不停歇的蜗牛11 小时前
K8S之创建cm指令create和 apply的区别
java·容器·kubernetes
Java编程爱好者11 小时前
OpenCVSharp:了解几种特征检测
后端