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 关键组件解析
HttpServlet
:所有 Servlet 的基类,提供doGet
/doPost
等请求处理方法HttpServletRequest
:封装 HTTP 请求数据(参数、头信息等)HttpServletResponse
:封装 HTTP 响应逻辑(状态码、响应体等)- 注解驱动 :通过
@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 部署步骤
-
打包项目 :执行
mvn clean package
生成xxx.war
文件 -
复制 WAR 包 :将
war
文件放入 Tomcat 的webapps
目录 -
启动服务器:
- Linux/macOS:
./bin/startup.sh
- Windows:
bin/startup.bat
- Linux/macOS:
-
访问应用:
- 默认路径:
http://localhost:8080/项目名/路径
- 设为根路径:将 WAR 重命名为
ROOT.war
,访问http://localhost:8080/
- 默认路径:
五、核心概念与最佳实践
5.1 Servlet 生命周期
- 初始化:服务器启动时创建 Servlet 实例(单例模式)
- 请求处理 :多线程调用
doGet
/doPost
方法处理请求 - 销毁 :服务器关闭时销毁实例(可选
destroy()
方法)
5.2 线程安全注意事项
- 避免实例变量: Servlet 为单例,实例变量会被多线程共享,可能引发线程安全问题
- 安全使用
ThreadLocal
:若使用线程局部变量,需确保请求结束后清理状态 - 请求对象线程安全 :
HttpServletRequest
和HttpServletResponse
为线程本地变量,无需同步
六、版本兼容与常见问题
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 常见错误与解决
-
404 错误:
- 检查 WAR 包是否正确部署
- 确认
@WebServlet
注解的urlPatterns
与访问路径一致
-
500 内部错误:
- 检查代码是否有空指针异常(如未处理
null
参数) - 确保 Tomcat 版本与 Servlet API 版本匹配
- 检查代码是否有空指针异常(如未处理
-
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 开发流程存在以下问题:
- 编写 Servlet 后需打包为 WAR 文件
- 手动复制到 Tomcat 的 webapps 目录
- 每次调试需重启 Tomcat 或配置远程调试
- 初学者常卡在 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
即可启动应用。
开发注意事项
- IDEA 配置 :运行时需勾选
Include dependencies with "Provided" scope
- 类加载顺序:Main 类不能依赖解压后才加载的类(如 Tomcat 相关类)
- 临时文件清理:确保程序退出时能正确清理解压的临时文件
- 版本兼容性: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
方法。 HttpServletRequest
和HttpServletResponse
是线程隔离的局部变量,无需担心线程安全。
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 错误。
📌 核心总结
- Servlet 映射 :通过
@WebServlet
注解配置路径,/
可处理所有未匹配请求; - 请求处理 :覆写
doGet/doPost
等方法处理不同 HTTP 方法,依赖HttpServletRequest
获取请求信息; - 响应构建 :通过
HttpServletResponse
设置头信息和输出 Body,注意flush()
的调用时机; - 多线程安全: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 练习场景
实现一个用户登录系统:
- 未登录时访问受限页面重定向到登录页
- 登录成功后根据权限转发到不同首页
- 处理登录参数的传递
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 间共享数据
一文搞懂 Java Web 开发中的 Session 与 Cookie 机制
在 Web 应用开发中,跟踪用户状态是一个基础且重要的需求
一、Web 会话管理基础
HTTP 协议是无状态的,这意味着服务器无法自动识别两次请求是否来自同一个浏览器。为了解决这个问题,Web 开发中引入了会话管理机制,其中最核心的就是Session 和Cookie。
核心概念对比
特性 | 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 │
└────────────┘
解决方案:
- Session 复制:在服务器间同步 Session 数据(消耗带宽,内存利用率低)
- 粘滞会话(Sticky Session) :反向代理根据 JSESSIONID 固定转发到某台服务器(需代理支持)
- 分布式 Session 存储:使用 Redis/Memcached 集中存储 Session(大型应用首选)
三、Cookie 机制详解
1. Cookie 核心属性
属性 | 说明 |
---|---|
name=value |
键值对数据 |
path |
生效路径(如/user/ 表示仅该路径下生效) |
maxAge |
有效期(秒),-1 表示浏览器关闭时失效 |
secure |
仅 HTTPS 请求时发送 |
HttpOnly |
禁止 JavaScript 访问(防范 XSS 攻击) |
2. 创建与读取 Cookie
设置用户语言偏好 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. 安全与规范设计
-
JSP 文件保护 :
将 JSP 放在
/WEB-INF/
目录下,利用 Web 容器机制防止直接访问(如http://localhost/user.jsp
) -
数据转义处理 :
上述示例未处理 HTML 转义,实际开发中应使用
org.apache.commons.text.StringEscapeUtils
等工具防止 XSS 攻击:jsp<%= StringEscapeUtils.escapeHtml4(user.name) %>
-
职责严格分离:
- 控制器不涉及 HTML 生成
- 视图不包含 SQL 查询等业务逻辑
2. MVC 架构流程图解
plaintext
┌───────────────────────┐
┌────▶│Controller: UserServlet│
│ └───────────────────────┘
│ │
┌───────┐ │ ┌─────┴─────┐
│Browser│────┘ │Model: User│
│ │◀───┐ └─────┬─────┘
└───────┘ │ │
│ ▼
│ ┌───────────────────────┐
└─────│ View: user.jsp │
└───────────────────────┘
请求流程解析:
- 浏览器发送请求到控制器 Servlet
- 控制器处理业务逻辑并生成模型数据
- 控制器将模型放入请求作用域并转发到 JSP
- JSP 从请求中获取模型并渲染为 HTML
- 渲染结果返回给浏览器
四、MVC 模式的核心优势
- 开发效率提升 :
前端与后端可并行开发,控制器、模型、视图可由不同团队负责 - 可维护性增强 :
问题定位更精准,如页面显示异常优先检查视图层 - 复用性提高 :
同一模型可适配多个视图(如 HTML 页面 / 移动端 API) - 测试便利性 :
模型层可独立进行单元测试,无需依赖 UI 环境
Java Web 开发必备:Filter 过滤器原理与实战应用
引言
在 Java Web 开发中,当我们需要对多个 Servlet 的公共逻辑进行统一处理时,Filter 过滤器是一种非常有效的解决方案。
为什么需要 Filter?
以一个论坛应用为例,我们有多个 Servlet 处理不同功能:
- IndexServlet:浏览帖子
- SignInServlet:登录
- ProfileServlet:修改用户资料(需登录)
- PostServlet:发帖(需登录)
如果直接在每个需要登录验证的 Servlet 中重复编写登录检查逻辑,会导致:
-
代码冗余
-
维护困难
-
功能扩展不便
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);
}
}
核心要点解析:
- 必须实现
Filter
接口 doFilter
方法是核心处理逻辑@WebFilter
注解用于指定过滤的 URL 模式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│
└──────────────┘ └─────────┘ └────────┘
顺序控制注意事项
- 使用
@WebFilter
注解时,Servlet 规范未定义执行顺序 - 若需精确控制顺序,需在
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>
核心注意事项
-
请求传递关键 :
若希望请求继续处理,必须在
doFilter
中调用chain.doFilter(request, response)
,否则会返回空白页 -
URL 模式匹配:
/*
:匹配所有路径/user/*
:匹配以/user/
开头的路径*.do
:匹配所有.do 结尾的路径
-
中断请求处理 :
若在 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 应用的全局上下文
核心特性解析
-
全局唯一性:每个 Web 应用对应唯一的 ServletContext 实例
-
生命周期一致性:与 Web 应用生命周期完全一致
-
数据共享中心:
- 通过
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");
}
// 销毁方法略
}