Web组件概述
JavaEE定义了三大Web组件,它们共同构成了Web应用的核心处理机制:
| 组件 | 职责 | 执行时机 |
|---|---|---|
| Servlet | 处理请求对应的业务逻辑 | 接收到请求时 |
| Listener | 监听Web应用生命周期事件 | 应用启动/关闭时 |
| Filter | 对请求/响应进行过滤处理 | Servlet前后 |
三者的执行顺序:Listener → Filter → Servlet → Filter
Listener监听器
什么是监听器
监听器(Listener)是JavaEE提供的事件监听机制。监听器会监听特定的主体(如ServletContext、HttpSession、ServletRequest等),当主体发生特定事件(初始化、销毁、属性变化等)时,自动触发对应的方法。
核心思想:通过监听机制,在关键生命周期节点自动执行初始化或清理操作。
ServletContextListener
ServletContextListener是最常用的监听器,用于监听ServletContext的生命周期:
| 事件 | 触发时机 | 对应方法 |
|---|---|---|
| 初始化 | 应用程序启动时 | contextInitialized() |
| 销毁 | 应用程序关闭时 | contextDestroyed() |
生命周期说明
应用程序启动
↓
ServletContext初始化
↓
触发contextInitialized() ← 在这里进行资源初始化
↓
应用运行中...
↓
应用程序关闭
↓
ServletContext销毁
↓
触发contextDestroyed() ← 在这里进行资源释放
执行过程图解
当应用程序启动时,Web组件按以下顺序加载:
- 首先加载:ServletContext 和 Listener
- 然后加载:loadOnStartup ≥ 0 的Servlet
应用启动
├── 创建ServletContext
├── 执行ServletContextListener.contextInitialized()
│ └── 初始化全局资源(如数据库连接池、SqlSessionFactory)
├── 初始化loadOnStartup ≥ 0的Servlet
└── 应用就绪,等待请求
配置方式
方式一:注解配置(推荐)
import javax.servlet.ServletContextEvent;
import javax.servlet.ServletContextListener;
import javax.servlet.annotation.WebListener;
@WebListener // 无需指定value,自动监听
public class CustomServletContextListener implements ServletContextListener {
@Override
public void contextInitialized(ServletContextEvent sce) {
// 应用启动时执行
System.out.println("✅ 应用程序启动,ServletContext初始化完成");
}
@Override
public void contextDestroyed(ServletContextEvent sce) {
// 应用关闭时执行
System.out.println("❌ 应用程序关闭,ServletContext销毁");
}
}
方式二:web.xml配置
<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee
http://xmlns.jcp.org/xml/ns/javaee/web-app_4_0.xsd"
version="4.0">
<!-- 监听器配置 -->
<listener>
<listener-class>com.example.listener.CustomServletContextListener</listener-class>
</listener>
</web-app>
两种方式等效,但注解配置更简洁,是Servlet 3.0+的推荐方式。
MyBatis集成优化
场景描述
之前整合MyBatis时,SqlSessionFactory在Servlet的init()方法中初始化。更好的做法是在应用启动时就完成初始化,并存入ServletContext供全局使用。
完整代码实现
监听器代码:
import org.apache.ibatis.io.Resources;
import org.apache.ibatis.session.SqlSessionFactory;
import org.apache.ibatis.session.SqlSessionFactoryBuilder;
import javax.servlet.ServletContext;
import javax.servlet.ServletContextEvent;
import javax.servlet.ServletContextListener;
import javax.servlet.annotation.WebListener;
import java.io.IOException;
import java.io.InputStream;
@WebListener
public class MyBatisInitListener implements ServletContextListener {
private static final String CONFIG_FILE = "mybatis-config.xml";
private static final String CONTEXT_KEY = "SqlSessionFactory";
@Override
public void contextInitialized(ServletContextEvent sce) {
ServletContext context = sce.getServletContext();
try (InputStream is = Resources.getResourceAsStream(CONFIG_FILE)) {
SqlSessionFactory factory = new SqlSessionFactoryBuilder().build(is);
// 存入ServletContext,全局共享
context.setAttribute(CONTEXT_KEY, factory);
System.out.println("✅ MyBatis初始化成功,SqlSessionFactory已存入ServletContext");
} catch (IOException e) {
System.err.println("❌ MyBatis初始化失败: " + e.getMessage());
throw new RuntimeException("MyBatis初始化失败", e);
}
}
@Override
public void contextDestroyed(ServletContextEvent sce) {
// 清理资源
ServletContext context = sce.getServletContext();
context.removeAttribute(CONTEXT_KEY);
System.out.println("✅ MyBatis资源已清理");
}
}
Servlet中使用:
import org.apache.ibatis.session.SqlSessionFactory;
import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
@WebServlet("/user/list")
public class UserListServlet extends HttpServlet {
private SqlSessionFactory sqlSessionFactory;
@Override
public void init() throws ServletException {
// 从ServletContext获取已初始化的工厂
sqlSessionFactory = (SqlSessionFactory) getServletContext()
.getAttribute("SqlSessionFactory");
if (sqlSessionFactory == null) {
throw new ServletException("SqlSessionFactory未初始化");
}
}
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp)
throws ServletException, IOException {
// 使用sqlSessionFactory执行业务操作
// ...
}
}
其他常用监听器
| 监听器接口 | 监听对象 | 用途 |
|---|---|---|
HttpSessionListener | Session | 统计在线人数、Session超时处理 |
ServletRequestListener | Request | 请求日志记录、性能监控 |
HttpSessionAttributeListener | Session属性 | 监听登录/登出状态变化 |
Filter过滤器
什么是过滤器
Filter(过滤器)是运行在Servlet之前的组件,可以对请求进行预处理,对响应进行后处理。
执行位置
客户端请求
↓
Filter预处理(请求过滤)
↓
Servlet业务处理
↓
Filter后处理(响应过滤)
↓
返回客户端
Filter和Servlet的映射关系
| 对比项 | Servlet | Filter |
|---|---|---|
| URL映射 | 1个URL → 1个Servlet | 1个URL → 多个Filter |
| 执行顺序 | 按URL匹配 | 按配置顺序 |
| 终止性 | 处理请求 | 可选择放行或拦截 |
重要:一个请求可以经过多个Filter,形成过滤器链(FilterChain)。


过滤器链执行流程
单个Filter执行流程
请求 → Filter.doFilter()前半部分
↓
chain.doFilter() ← 放行
↓
Servlet处理
↓
Filter.doFilter()后半部分 → 响应
多个Filter执行流程
假设配置了FilterA、FilterB、FilterC三个过滤器:
请求 → FilterA → FilterB → FilterC → Servlet
↓
响应 ← FilterA ← FilterB ← FilterC ← 处理完成
执行顺序遵循先进后出(类似栈结构):
FilterA开始
FilterB开始
FilterC开始
Servlet处理
FilterC结束
FilterB结束
FilterA结束
关键方法说明
// 放行方法:必须调用才能继续后续流程
filterChain.doFilter(request, response);
如果不调用
chain.doFilter(),请求将被拦截,不会到达Servlet。
Filter生命周期
| 阶段 | 方法 | 执行次数 | 用途 |
|---|---|---|---|
| 初始化 | init(FilterConfig) | 1次 | 读取配置参数 |
| 过滤 | doFilter() | 每次请求 | 执行过滤逻辑 |
| 销毁 | destroy() | 1次 | 释放资源 |
配置方式
方式一:注解配置(推荐)
import javax.servlet.Filter;
import javax.servlet.FilterChain;
import javax.servlet.FilterConfig;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.annotation.WebFilter;
import java.io.IOException;
@WebFilter("/*") // 过滤所有请求
public class LoggingFilter implements Filter {
@Override
public void init(FilterConfig filterConfig) throws ServletException {
// 初始化操作
String encoding = filterConfig.getInitParameter("encoding");
System.out.println("Filter初始化,编码参数: " + encoding);
}
@Override
public void doFilter(ServletRequest request, ServletResponse response,
FilterChain chain) throws IOException, ServletException {
// 前置处理
System.out.println("请求到达Filter");
// 放行
chain.doFilter(request, response);
// 后置处理
System.out.println("响应离开Filter");
}
@Override
public void destroy() {
// 销毁操作
System.out.println("Filter销毁");
}
}
常用URL模式:
| 模式 | 含义 | 示例 |
|---|---|---|
/* | 所有请求 | 全局字符编码 |
/user/* | 以/user/开头的请求 | 登录验证 |
/admin/* | 以/admin/开头的请求 | 权限控制 |
*.jsp | 所有JSP页面 | 页面权限 |
方式二:web.xml配置
<web-app>
<!-- Filter定义 -->
<filter>
<filter-name>CharacterEncodingFilter</filter-name>
<filter-class>com.example.filter.CharacterEncodingFilter</filter-class>
<init-param>
<param-name>encoding</param-name>
<param-value>UTF-8</param-value>
</init-param>
</filter>
<!-- Filter映射 -->
<filter-mapping>
<filter-name>CharacterEncodingFilter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
</web-app>
多个Filter时,web.xml中的配置顺序决定了执行顺序;注解配置时,Filter名称的字典序影响顺序(建议使用web.xml精确控制)。
常见问题与解决方案
Filter相关问题
Filter不生效怎么办?
排查步骤:
- 检查URL模式:确认请求的URL匹配Filter的url-pattern
- 检查配置方式:注解和web.xml不要重复配置,可能冲突
- 检查是否放行:确认调用了
chain.doFilter() - 检查执行顺序:可能被前面的Filter拦截了
多个Filter的执行顺序怎么控制?
解决方案:
| 配置方式 | 顺序控制方法 |
|---|---|
| web.xml | 按<filter-mapping>的配置顺序 |
| 注解 | 无法控制(按类名字典序),建议使用web.xml |
推荐在web.xml中集中配置Filter顺序:
<!-- 先执行字符编码 -->
<filter-mapping>
<filter-name>CharacterEncodingFilter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
<!-- 再执行登录验证 -->
<filter-mapping>
<filter-name>LoginCheckFilter</filter-name>
<url-pattern>/user/*</url-pattern>
</filter-mapping>
Filter中如何获取Spring容器中的Bean?
// 通过WebApplicationContext获取
WebApplicationContext context = WebApplicationContextUtils
.getRequiredWebApplicationContext(request.getServletContext());
UserService userService = context.getBean(UserService.class);
Listener相关问题
contextInitialized执行两次?
原因:Tomcat配置了多个Context,或IDEA热部署导致。
解决:在方法中添加判断,避免重复初始化:
@Override
public void contextInitialized(ServletContextEvent sce) {
ServletContext context = sce.getServletContext();
// 检查是否已初始化
if (context.getAttribute("SqlSessionFactory") != null) {
return; // 已存在,跳过
}
// 初始化代码...
}
Listener中获取不到数据库连接?
原因:数据库驱动在Listener初始化时还未加载。
解决:确保数据库驱动JAR在WEB-INF/lib中,或使用延迟加载。
其他常见问题
如何设置Filter的初始化参数?
注解方式:
@WebFilter(
value = "/*",
initParams = {
@WebInitParam(name = "encoding", value = "UTF-8"),
@WebInitParam(name = "forceEncoding", value = "true")
}
)
public class CharacterEncodingFilter implements Filter {
private String encoding;
@Override
public void init(FilterConfig config) throws ServletException {
this.encoding = config.getInitParameter("encoding");
}
}
如何排除特定路径不进行过滤?
@Override
public void doFilter(ServletRequest request, ServletResponse response,
FilterChain chain) throws IOException, ServletException {
HttpServletRequest httpRequest = (HttpServletRequest) request;
String path = httpRequest.getRequestURI();
// 排除静态资源
if (path.startsWith("/static/") || path.endsWith(".js") || path.endsWith(".css")) {
chain.doFilter(request, response);
return;
}
// 继续过滤逻辑...
}
总结
三大组件协作图
┌─────────────────────────────────────────────────────────┐
│ 应用程序生命周期 │
├─────────────────────────────────────────────────────────┤
│ 应用启动 │
│ ↓ │
│ ServletContext初始化 │
│ ↓ │
│ Listener.contextInitialized() ← 初始化全局资源 │
│ ↓ │
│ 等待请求... │
│ ↓ │
│ 请求到达 ───────────────────────────────────────┐ │
│ ↓ │ │
│ Filter1.doFilter()前半部分 │ │
│ ↓ │ │
│ Filter2.doFilter()前半部分 │ │
│ ↓ │ │
│ chain.doFilter() ───────────────────────────────┤───────┤
│ ↓ │ │
│ Servlet.service() │ │
│ ↓ │ │
│ Filter2.doFilter()后半部分 │ │
│ ↓ │ │
│ Filter1.doFilter()后半部分 ←────────────────────┘ │
│ ↓ │
│ 响应返回 │
│ ↓ │
│ 应用关闭 │
│ ↓ │
│ Listener.contextDestroyed() ← 清理资源 │
└─────────────────────────────────────────────────────────┘
知识点回顾
| 组件 | 核心接口 | 关键方法 | 主要用途 |
|---|---|---|---|
| Listener | ServletContextListener | contextInitialized() contextDestroyed() | 应用启动/关闭时执行初始化和清理 |
| Filter | Filter | init() doFilter() destroy() | 请求预处理、响应后处理 |
- 资源初始化:使用Listener在应用启动时初始化数据库连接池、缓存等
- 通用处理:使用Filter统一处理字符编码、登录验证、日志记录
- 顺序控制:多个Filter时使用web.xml精确控制执行顺序
- 异常处理:Filter中注意异常处理,避免影响正常请求
- 白名单机制:登录验证等场景使用白名单放行特定路径