一.Filter(过滤器)
1.简介
过滤器(Filter)是 Java Servlet 技术中的一个重要部分,主要用于在 Servlet 处理请求之前或响应之后对数据进行某些处理。过滤器可以实现诸如日志记录、请求数据修改、响应数据修改、权限控制等功能。在 Java EE 或 Spring 应用中,过滤器的使用非常广泛。
2.基本原理
过滤器工作在 Servlet 容器中,它拦截客户端的请求和服务器的响应。过滤器链(Filter Chain)是多个过滤器按照一定的顺序执行的集合,一个请求可以依次通过多个过滤器,然后到达目标 Servlet,响应也会按相反的顺序经过这些过滤器返回给客户端。
- 生命周期管理:
- Servlet 容器负责过滤器的生命周期管理。过滤器的生命周期方法包括
init
(初始化)、doFilter
(执行过滤操作)和destroy
(销毁)。
- Servlet 容器负责过滤器的生命周期管理。过滤器的生命周期方法包括
- 请求处理流程:
- 当一个请求到达 Servlet 容器时,容器会根据部署描述符(web.xml)或注解配置,决定是否以及如何调用过滤器链。
- 过滤器链是多个过滤器按照一定的顺序执行的集合。容器按照这个顺序依次调用每个过滤器的
doFilter
方法。
- doFilter 方法:
- 在
doFilter
方法中,开发者可以实现自定义的处理逻辑,比如修改请求头、记录日志等。 doFilter
方法中必须调用FilterChain
的doFilter
方法,这样请求才能继续传递给下一个过滤器或目标资源(如 Servlet)。如果不调用,请求处理流程将会停止。
- 在
- 工作机制:
- 过滤器可以修改请求和响应,但它们通常不会生成响应或结束请求,因为这通常是 Servlet 或其他资源的职责。
3.实现步骤
-
创建过滤器类:
- 实现
javax.servlet.Filter
接口。 - 重写
init
、doFilter
和destroy
方法。
- 实现
-
配置过滤器:
- 使用注解
@WebFilter
进行声明和配置。 - 或者在 web.xml 文件中配置。
- 使用注解
-
编写过滤逻辑:
- 在
doFilter
方法中实现具体的过滤逻辑。
- 在
4.示例代码
下面是一个简单的过滤器实现示例:
import javax.servlet.*;
import javax.servlet.annotation.WebFilter;
import java.io.IOException;
@WebFilter("/example/*") // 过滤器应用于 URL 模式 "/example/*"
public class ExampleFilter implements Filter {
@Override
public void init(FilterConfig filterConfig) throws ServletException {
// 初始化代码,例如资源加载
}
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException {
// 在请求处理之前执行的代码
System.out.println("Before Servlet processing");
chain.doFilter(request, response); // 将请求传递给下一个过滤器或目标资源
// 在请求处理之后执行的代码
System.out.println("After Servlet processing");
}
@Override
public void destroy() {
// 清理代码,例如释放资源
}
}
5.过滤器的使用场景
- 权限检查:在过滤器中检查用户是否已经登录或是否有权限访问某些资源。
- 日志和审计:记录请求的详细信息,如 IP 地址、访问时间、请求参数等。
- 请求数据处理:修改或包装请求中的数据。
- 响应内容处理:修改响应数据,如添加额外的响应头、压缩响应内容等。
- 资源管理:如实现缓存机制,减少对实际资源的访问次数。
6.注意事项
- 过滤器执行顺序:如果应用中有多个过滤器,它们的执行顺序可能会影响处理逻辑。可以通过
@WebFilter
注解的order
属性或在 web.xml 中定义的顺序来控制。 - 资源释放:确保在
destroy
方法中释放过滤器使用的资源。 - 异常处理:在过滤器中妥善处理异常,避免因为异常而中断整个请求处理流程。
二.Interceptor(拦截器)
1.简介
拦截器(Interceptor)是 Spring MVC 框架中的一个核心组件,用于在处理 HTTP 请求的过程中进行拦截和处理。拦截器主要用于实现跨切面(cross-cutting)的逻辑,如日志记录、性能统计、安全控制、事务处理等。
2.基本原理
拦截器工作在 Spring 的 DispatcherServlet 和具体的 Controller 之间。当一个请求被发送到 Spring MVC 应用时,DispatcherServlet 首先接收到这个请求,然后根据配置的拦截器链对请求进行预处理,最后将请求转发到相应的 Controller 进行处理。
- 实现机制:
- 在Spring框架中,拦截器(Interceptor)的实现机制是基于Java动态代理和AOP(面向切面编程)的。具体来说,Spring框架通过代理模式和AOP技术来实现拦截器的功能。
- 当一个请求到达被拦截的方法或Controller时,Spring框架会利用动态代理生成一个代理对象,该代理对象包含了目标对象(被拦截的方法或Controller)以及拦截器的逻辑。在代理对象中,拦截器的逻辑会在目标方法执行前、执行后或抛出异常时被调用,从而实现对请求的拦截和处理。
- 这种基于代理的拦截器实现方式,使得开发者可以通过简单的配置将拦截器应用到需要的方法或Controller上,而无需修改原有的业务逻辑代码。同时,利用AOP技术,可以灵活地定义切点和通知,实现对请求处理过程的精细控制。
- 总的来说,拦截器的底层实现依赖于Spring框架提供的代理机制和AOP功能,通过代理对象对请求进行拦截和处理,实现了拦截器的功能。这种实现方式保持了代码的清晰性和可维护性,同时提供了灵活的扩展性,使得开发者能够方便地定制和应用拦截器功能。
- 集成于 Spring MVC:
- 拦截器是与 Spring MVC 的
DispatcherServlet
集成的。当一个请求被DispatcherServlet
接收后,它会处理和调度这个请求到相应的处理器(Controller)。
- 拦截器是与 Spring MVC 的
- 处理器映射(Handler Mapping):
- 在请求到达 Controller 之前,Spring 会根据请求的 URL 查找对应的 Handler Mapping,然后确定相应的 Controller。
- 拦截器链:
- 在请求达到 Controller 之前,以及响应返回给客户端之前,拦截器链中的拦截器会被依次调用。
- 开发者可以自定义拦截器,并通过实现
WebMvcConfigurer
接口的addInterceptors
方法将它们添加到应用中。
- 方法级别的拦截:
- 拦截器可以更精细地控制处理流程,它们能够在 Controller 方法执行前后以及请求完成后进行拦截处理。
- 上下文访问:
- 拦截器可以访问请求的上下文,包括请求和响应对象、处理器(Controller)的信息等。这使得拦截器可以执行如权限检查、日志记录等更复杂的任务。
3.实现步骤
-
创建拦截器类:
- 实现
HandlerInterceptor
接口或继承HandlerInterceptorAdapter
类。 - 重写
preHandle
、postHandle
和afterCompletion
方法。
- 实现
-
注册拦截器:
- 创建一个配置类,实现
WebMvcConfigurer
接口。 - 重写
addInterceptors
方法来添加拦截器。
- 创建一个配置类,实现
-
编写拦截逻辑:
- 在
preHandle
、postHandle
和afterCompletion
方法中实现具体的拦截逻辑。
- 在
4.示例代码
下面是一个简单的拦截器实现示例:
import org.springframework.web.servlet.HandlerInterceptor;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
public class MyInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
// 请求处理前的逻辑
return true; // 返回 true 继续流程,返回 false 中断流程
}
@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) {
// 请求处理后的逻辑,但在视图渲染前
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) {
// 请求处理完毕后的逻辑
}
}
@Configuration
public class WebMvcConfig implements WebMvcConfigurer {
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new MyInterceptor()).addPathPatterns("/**"); // 应用于所有路径
}
}
4.拦截器的使用场景
- 日志记录和审计:在请求处理前后记录日志,进行审计。
- 权限验证:检查用户是否登录或是否有权限访问某些资源。
- 性能监控:记录请求的处理时间,用于性能监控。
- 通用行为处理:如字符集设置、内容协商等。
- 事务管理:在 Spring 环境中管理事务。
5.注意事项
- 返回值处理:
preHandle
方法的返回值决定是否继续执行后续的拦截器和请求处理。如果返回false
,则流程中断。 - 拦截器链:Spring 允许配置多个拦截器,形成一个拦截器链。这些拦截器会按顺序执行。
- 异常处理:应妥善处理拦截器中可能发生的异常,以免影响整个请求处理流程。
三.过滤器和拦截器的相同点和不同点
过滤器(Filter)和拦截器(Interceptor)虽然都用于处理 HTTP 请求和响应,但它们在 Java Web 应用中有各自的特点和使用场景。让我们来详细对比一下它们的相同点和不同点。
1.相同点
- 目的相似:都用于在请求到达目标资源(如 Servlet 或 Controller)之前或响应返回给客户端之后进行某些处理。
- 处理能力:都能够实现请求的预处理、后处理,比如日志记录、权限检查、请求数据修改等。
- 配置方式:可以通过注解或 XML 配置的方式进行配置。
2.不同点
-
所属框架/规范:
- 过滤器是基于 Java Servlet 规范的,它是 Servlet 容器(如 Tomcat)的一部分。
- 拦截器则是 Spring MVC 框架的一部分,依赖于 Spring 的上下文环境。
-
执行层次:
- 过滤器更接近于底层,它在 Servlet 请求之前和响应之后进行处理。
- 拦截器则更接近于业务层,它在 DispatcherServlet 处理请求的过程中执行,即在请求到达 Controller 之前和处理完毕之后。
-
功能范围和依赖:
- 过滤器通常用于对请求和响应的预处理和后处理,依赖于 Servlet API。
- 拦截器可以利用 Spring MVC 提供的丰富功能,如访问控制器的元数据、处理模型和视图等。
-
可控制的粒度:
- 过滤器的粒度比较粗,通常对所有请求或特定 URL 模式的请求生效。
- 拦截器的粒度更细,可以控制到具体的控制器方法。
-
异常处理:
- 过滤器中的异常处理通常需要自己实现。
- 拦截器中的异常可以被 Spring 的全局异常处理器捕获和处理。
-
参数访问:
- 过滤器直接使用 Servlet API,所以只能访问到请求和响应对象。
- 拦截器可以访问更多的上下文数据,如控制器方法的签名、模型数据等。
3.使用选择
- 当需要处理所有请求,并且与 Spring 上下文无关时,可以选择过滤器。
- 当需要在 Spring MVC 的上下文中处理请求,并且需要更细粒度的控制或利用 Spring 提供的其他功能时,拦截器是更好的选择。
四.日志记录案例
1.数据库表
-- 操作日志表
create table operate_log(
id int unsigned primary key auto_increment comment 'ID',
class_name varchar(100) comment '操作的类名',
method_name varchar(100) comment '操作的方法名',
method_desc varchar(100) comment '方法用途',
method_params varchar(1000) comment '方法参数',
return_value varchar(2000) comment '返回值',
operate_user int unsigned comment '操作人ID',
operate_time datetime comment '操作时间',
cost_time bigint comment '方法执行耗时, 单位:ms'
) comment '操作日志表';
2.自定义注解
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface LogAnno {
/**
* 用于标记方法用途的注解
* @return
*/
String value() default "";
}
3.Controller方法标记注解
@PostMapping
@LogAnno("添加员工")
public Result insert(@RequestBody Emp emp){
log.info("emp:{}",emp);
return empService.insert(emp);
}
4.基于AOP实现的日志记录
@Component
@Aspect // 表示这个类是一个切面类
@Slf4j
public class LogAspect {
private static final String TOKEN = "token";
@Autowired
private HttpServletRequest request;
@Autowired
private OperateLogMapper operateLogMapper;
// 切点
@Pointcut("execution(* com.itheima.tlias.web.controller.*.*(..)) && @annotation(com.itheima.tlias.anno.LogAnno)")
public void pt() {
}
// 切面(环绕通知)
@Around("pt()")
public Object around(ProceedingJoinPoint pjp) {
log.info("日志环绕通知开始了:{}", pjp);
// 声明变量用来存储目标方法的返回值
Object result = null;
// 记录开始操作时间
long start = System.currentTimeMillis();
OperateLog operateLog = new OperateLog();
try {
// 解析token获取ID
String token = request.getHeader(TOKEN);
Object empId = JwtUtils.parseJwt(token).get("id");
// 获取操作的类名
String className = pjp.getTarget().getClass().getName();
operateLog.setClassName(className);
// 获取操作的方法名
MethodSignature methodSignature = (MethodSignature) pjp.getSignature();
Method method = methodSignature.getMethod();
String methodName = method.getName();
operateLog.setMethodName(methodName);
// 获取方法用途
String methodDesc = method.getAnnotation(LogAnno.class).value();
operateLog.setMethodDesc(methodDesc);
// 获取方法参数
Object[] args = pjp.getArgs();
operateLog.setMethodParams(Arrays.toString(args));
// 获取返回值
result = pjp.proceed();
operateLog.setReturnValue(JSON.toJSONString(result));
// 获取操作人ID
operateLog.setOperateUser(Integer.valueOf(empId.toString()));
// 获取操作时间
operateLog.setOperateTime(LocalDateTime.now());
} catch (Throwable e) {
e.printStackTrace();
operateLog.setReturnValue("出异常了,返回值为异常消息:" + e.getMessage()); //返回值
} finally {
// 记录结束操作时间
long end = System.currentTimeMillis();
operateLog.setCostTime(end - start);
}
operateLogMapper.save(operateLog);
return result;
}
}
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Autowired
private LogInterceptor logInterceptor;
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(logInterceptor).addPathPatterns("/**"); // 应用于所有路径
}
}
5.基于Spring MVC 拦截器实现日志记录
@Component
@Slf4j
public class LogInterceptor implements HandlerInterceptor {
@Autowired
private OperateLogMapper operateLogMapper;
// 在请求处理之前执行
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
// 记录开始时间
long start = System.currentTimeMillis();
request.setAttribute("startTime", start);
// 仅对方法调用进行处理
if (handler instanceof HandlerMethod) {
HandlerMethod handlerMethod = (HandlerMethod) handler;
Method method = handlerMethod.getMethod();
// 检查是否有 LogAnno 注解
if (method.isAnnotationPresent(LogAnno.class)) {
// 初始化操作日志对象
OperateLog operateLog = new OperateLog();
// 获取操作的类名和方法名
operateLog.setClassName(method.getDeclaringClass().getName());
operateLog.setMethodName(method.getName());
// 获取方法描述
LogAnno logAnno = method.getAnnotation(LogAnno.class);
operateLog.setMethodDesc(logAnno.value());
// 存储操作日志对象以便在 postHandle 中使用
request.setAttribute("operateLog", operateLog);
}
}
return true;
}
// 在请求处理之后执行
@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
long endTime = System.currentTimeMillis();
long startTime = (Long) request.getAttribute("startTime");
// 检查是否有日志对象
if (request.getAttribute("operateLog") != null) {
OperateLog operateLog = (OperateLog) request.getAttribute("operateLog");
// 设置操作时间和耗时
operateLog.setOperateTime(LocalDateTime.now());
operateLog.setCostTime(endTime - startTime);
// 解析token获取ID
String token = request.getHeader("token");
Object empId = JwtUtils.parseJwt(token).get("id");
operateLog.setOperateUser(Integer.valueOf(empId.toString()));
// 保存操作日志
operateLogMapper.save(operateLog);
}
}
// 在请求完成后执行
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
// 可以处理异常信息,例如记录异常日志
if (ex != null && request.getAttribute("operateLog") != null) {
OperateLog operateLog = (OperateLog) request.getAttribute("operateLog");
operateLog.setReturnValue("出异常了,返回值为异常消息:" + ex.getMessage());
operateLogMapper.save(operateLog);
}
}
}
五.疑问
1.当一个方法上同时定义了拦截器(Interceptor)和 AOP(Aspect-Oriented Programming)切面时,他的执行顺序是什么?
-
拦截器的
preHandle
方法:在请求进入 Controller 方法之前执行。这是拦截器链的第一个环节,用于预处理操作。 -
AOP 前置通知(Before Advice):在具体的方法执行之前执行,但在拦截器的
preHandle
方法之后。 -
Controller 中的方法执行:执行你的业务逻辑。
-
AOP 后置通知(After Returning Advice):在方法执行之后、返回结果之前执行。
-
拦截器的
postHandle
方法:在 Controller 方法执行完毕后、DispatcherServlet 进行视图返回渲染之前调用。 -
拦截器的
afterCompletion
方法:在整个请求完成之后,也就是在 DispatcherServlet 渲染了对应的视图之后执行。这个方法的执行时间点是在 AOP 后置通知之后。 -
AOP 环绕通知(Around Advice):这个通知可以在方法执行前后都执行,它的执行顺序取决于其内部调用
proceed()
方法的位置。如果在proceed()
之前执行,那么它将在上述所有步骤之前执行;如果在proceed()
之后执行,那么它将在所有步骤之后执行。 -
AOP 异常通知(After Throwing Advice):在方法抛出异常后执行。如果 Controller 方法中发生异常,则这个通知将在
afterCompletion
方法之前执行。 -
AOP 最终通知(After (finally) Advice):无论方法正常执行还是发生异常,这个通知都将执行。在拦截器的
afterCompletion
方法之后执行。
总结
- 拦截器的
preHandle
→ AOP Before → 方法执行 → AOP After Returning → 拦截器的postHandle
→ 拦截器的afterCompletion
→ AOP After。 - AOP 环绕通知的执行顺序取决于
proceed()
的调用位置。 - AOP 异常通知执行取决于是否有异常产生,且在拦截器的
afterCompletion
方法之前。