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");
    }
    // 销毁方法略
}
相关推荐
Codebee5 分钟前
“自举开发“范式:OneCode如何用低代码重构自身工具链
java·人工智能·架构
星星电灯猴12 分钟前
iOS 性能调试全流程:从 Demo 到产品化的小团队实战经验
后端
掘金-我是哪吒14 分钟前
分布式微服务系统架构第158集:JavaPlus技术文档平台日更-JVM基础知识
jvm·分布式·微服务·架构·系统架构
程序无bug20 分钟前
手写Spring框架
java·后端
程序无bug22 分钟前
Spring 面向切面编程AOP 详细讲解
java·前端
JohnYan22 分钟前
模板+数据的文档生成技术方案设计和实现
javascript·后端·架构
全干engineer34 分钟前
Spring Boot 实现主表+明细表 Excel 导出(EasyPOI 实战)
java·spring boot·后端·excel·easypoi·excel导出
Da_秀37 分钟前
软件工程中耦合度
开发语言·后端·架构·软件工程
Fireworkitte43 分钟前
Java 中导出包含多个 Sheet 的 Excel 文件
java·开发语言·excel
GodKeyNet1 小时前
设计模式-责任链模式
java·设计模式·责任链模式