1. 概述
过滤器、拦截器与AOP均是用于对目标功能代码进行过滤、拦截、增强的技术手段,可实现对API接口或方法执行过程的扩展与增强。它们的核心作用是在代码执行的特定阶段,注入开发者自定义的业务逻辑,从而满足多种场景需求。
具体而言,过滤器和拦截器主要用于对较为粗粒度的功能单元进行过滤拦截以及增强,通常作用于Controller
层的访问请求。这些请求可能涉及多个业务逻辑或方法调用。而AOP(面向切面编程)则更加精细化,其目标是对单个具体方法进行切入,允许在方法执行的前后插入用户定义的代码,从而实现对方法级别的灵活扩展与控制。
工具 | 作用对象 | 拦截 | 增强 | 修改出入参 |
AOP | 函数方法 | 不支持 | 支持 | 支持 |
Filter | 访问请求接口 | 支持 | 支持 | 支持 |
Interceptor | 访问请求接口 | 支持 | 支持 | 支持 |
2. AOP的实现
2.1 面向切面编程介绍
AOP:Aspect Oriented Programming面向切面编程
AOP可以说是OOP(Object Oriented Programming,面向对象编程)的补充和完善。OOP引入封装、继承、多态等概念来建立一种对象层次结构,用于模拟公共行为的一个集合。不过OOP允许开发者定义纵向的关系,但并不适合定义横向的关系,例如日志功能。日志代码往往横向地散布在所有对象层次中,而与它对应的对象的核心功能毫无关系对于其他类型的代码,如安全性、异常处理和透明的持续性也都是如此,这种散布在各处的无关的代码被称为横切(cross cutting),在OOP设计中,它导致了大量代码的重复,而不利于各个模块的重用。
AOP技术恰恰相反,它利用一种称为"横切"的技术,剖解开封装的对象内部,并将那些影响了多个类的公共行为封装到一个可重用模块,并将其命名为"Aspect",即切面。所谓"切面",简单说就是那些与业务无关,却为业务模块所共同调用的逻辑或责任封装起来,便于减少系统的重复代码,降低模块之间的耦合度,并有利于未来的可操作性和可维护性。
使用AOP,可以在不修改原来代码的基础上添加新功能。
2.2 相关注解介绍
a. 增强业务代码的编写
注解:
@Order(100): 优先级注解,作用于类上,数字越小,优先级越高,越先执行,套到最外层
@Component: 容器注解,作用于类上,告诉spring要把这个类装入容器中
@Aspect :切面类注解,作用于类上,告诉spring这是一个切面类
@Before:前置通知注解,作用于方法上,被注解的方法在目标函数执行前执行
@After:后置通知注解,作用于方法上,被注解的方法在目标函数执行后执行,无论目标函数执行成功与否
@AfterReturning: 返回通知注解,作用于方法上,被注解的方法在目标函数执行正常返回结果后执行
@AfterThrowing:异常通知注解,作用于方法上,被注解的方法在目标函数执行发生异常后执行
@Around:环绕通知注解,作用于方法上,被注解的方法通过try catch 包装目标函数,并在代码中实现以上四种通知注解的功能,可以控制目标方法是否执行,修改目标方法参数、执行结果等。
b. 配置需要切面的方法
注解:
@Pointcut 切入点注解,作用于空方法上,在其注解中赋值切入点表达式,使其方法名等同于切入点注解中的切入点表达式。
2.2 目标函数前后添加增强方法
这里AOP的业务代码有两种方式实现,一种是通过零散的通知注解实现,但这种方式对目标函数操作的功能有限,仅能对目标函数进行增强,即在目标函数执行前后进行添加额外的业务逻辑代码功能执行,并不能拦截目标函数,也无法修改目标函数的入参和出参返回值。具体代码如下:
@Order(10000) //数字越小,优先级越高,数字越大,优先级越低; 数字越小,越先执行,就必须套到最外层
@Component
@Aspect //告诉spring这个组件是个切面
public class LogAspect {
@Pointcut("execution(int com.hongshan.spring02aop.calculator.MathCalculator.*(..))")
public void pointCut(){};
@Before("execution(int com.hongshan.spring02aop.calculator.MathCalculator.*(..))")
public void logStart(JoinPoint joinPoint){ // 方法名、方法修饰符、方法出参都可以随便写,但入参不能随便写
//1、拿到方法全签名 (也就是execution中的东西就叫方法全签名)
MethodSignature signature = (MethodSignature) joinPoint.getSignature();
//方法名
String name = signature.getName();
//目标方法传来的参数值
Object[] args = joinPoint.getArgs();
System.out.println("【切面 - 日志】【"+name+"】开始:参数列表:【"+ Arrays.toString(args) +"】");
}
@After("pointCut()")
public void logEnd(JoinPoint joinPoint){
MethodSignature signature = (MethodSignature) joinPoint.getSignature();
String name = signature.getName();
System.out.println("【切面 - 日志】【"+name+"】后置...");
}
@AfterReturning(value = "pointCut()",
returning = "result") //returning="result" 获取目标方法返回值
public void logReturn(JoinPoint joinPoint,Object result){
MethodSignature signature = (MethodSignature) joinPoint.getSignature();
String name = signature.getName();
System.out.println("【切面 - 日志】【"+name+"】返回:值:"+result);
}
@AfterThrowing(value = "pointCut()")
public void logExceptionN(JoinPoint joinPoint){
MethodSignature signature = (MethodSignature) joinPoint.getSignature();
String name = signature.getName();
System.out.println("【切面 - 日志】【"+name+"】异常");
}
(1)切入点表达式:
execution(方法的全签名):
全写法:[public] int [com.atguigu.spring.aop.calculator.MathCalculator].add(int,int) [throws ArithmeticException]
省略写法:int add(int i,int j)
通配符:
*:表示任意字符
..:
1)、参数位置:表示多个参数,任意类型 ;(..) 括号两个点代表任意多个参数
2)、类型位置:代表多个层级
最省略: * *(..)
(2)通知方法的执行顺序:
a. 正常链路: 前置通知->目标方法->返回通知->后置通知
b. 异常链路: 前置通知->目标方法->异常通知->后置通知
(3)JoinPoint: 包装了当前目标方法的所有信息
2.3 修改目标函数的入参和出参
AOP的另一种实现方法就是通过环绕通知,这个功能强大,不仅能在目标函数前后切入额外的业务代码功能,还能对目标函数进行拦截,甚至对目标函数的入参和出参进行修改。
@Component
@Aspect // 切面的注解没有集成包含 Component
public class AroundAspect {
@Pointcut("execution(int com.hongshan.spring02aop.calculator.MathCalculator.*(..))")
public void pointCut(){};
/**
* 环绕通知固定写法如下:
* Object: 返回值
* ProceedingJoinPoint: 可以继续推进的切点
*/
@Around("pointCut()")
public Object aroundAdvice(ProceedingJoinPoint joinPoint) throws Throwable{
Object[] args = joinPoint.getArgs(); // 获取目标方法的参数
// 修改入参
args[1] = 12;
// 前置
System.out.println("环绕 - 前置通知" + Arrays.toString(args));
Object proceed = null;
try{
// 接受传入参数的 proceed,实现修改目标方法执行用的参数
proceed = joinPoint.proceed(args);// 继续执行目标方法;类似于反射 method.invoke()
System.out.println("环绕 - 返回通知" + proceed);
}catch (Exception e){
System.out.println("环绕 - 异常通知" + e.getMessage());
throw e;
}finally {
System.out.println("环绕 - 后置通知");
}
// 修改返回值
return Integer.valueOf(proceed.toString()) - 10;
}
}
值得注意的是:
a. 环绕通知在目标方法执行前后进行拦截,如果在环绕通知中捕获了异常,这个异常默认不会抛出去,而是在环绕通知内部进行处理。也可以在catch中手动抛出异常。
b. 异常通知会捕获这个异常并默认将其抛出,使得外部的代码可以感知到这个异常并进行相应的处理
3. FIlter的实现
3.1 过滤器的介绍
使用Filter完整的流程是:Filter对用户请求进行预处理,接着将请求交给Servlet进行处理并生成响应,最后Filter再对服务器响应进行后处理。
1)Filter接口定义了过滤器的开发规范,所有的过滤器都要实现该接口;
2)Filter的工作位置是项目中所有目标资源之前,容器在创建HttpServletRequest和
HttpServletResponse对象后,会先调用Filter的doFilter方法;
3)Filter的doFilter方法可以控制请求是否继续,如果放行,则请求继续,如果拒绝,则请求到此为
止,由过滤器本身做出响应;
4)Filter不仅可以对请求做出过滤,也可以在目标资源做出响应前,对响应再次进行处理;
@Component 作用于过滤器类,告诉spring放入容器中
@Order(100): 优先级注解,作用于FIlter类上,数字越小,优先级越高,越先执行,套到最外层
3.2 请求前后增强方法
首先需要编写Filter类,此类需实现Filter接口,并重写 init()、destroy()、doFilter() 三个方法即可
阶段 | 对应方法 | 执行时机 | 执行次数 |
创建对象 | 构造器 | web应用启动时 | 1 |
初始化方法 |
void init(FilterConfig filterConfig)
| 构造完毕 | 1 |
过滤请求 |
void doFilter(ServletRequest servletRequest, ServletResponse
servletResponse, FilterChain filterChain)
| 每次请求 | 多次 |
销毁 |
default void destroy()
| web应用关闭时 | 1 |
@Component
public class MyFilter implements Filter {
@Override
public void init(FilterConfig filterConfig) throws ServletException {
System.out.println("MyFilter name is: " + filterConfig.getFilterName());
Filter.super.init(filterConfig);
}
@Override
public void destroy() {
Filter.super.destroy();
}
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
System.out.println("filter 前置.....");
filterChain.doFilter(servletRequest, servletResponse);
System.out.println("filter 后置.....");
}
}
在filterChain.doFilter(servletRequest, servletResponse)这行代码前后可添加功能代码对访问请求进行前置增强和后置增强。
3.3 对请求进行拦截放行
其次需要对需要拦截过滤的请求进行配置
@Configuration // 专门对 springMVC 底层做一些配置
public class MySpringMVCConfig implements WebMvcConfigurer{
@Autowired
private MyFilter myFilter;
@Bean
public FilterRegistrationBean getFilter1Registration() {
FilterRegistrationBean filterRegistrationBean = new FilterRegistrationBean();
filterRegistrationBean.setFilter(this.myFilter);
//设置过滤器名称和路径,在过滤器类写了的话,这里不用重复写
filterRegistrationBean.setName("filter");
filterRegistrationBean.addUrlPatterns("/api/*");
//设置过滤器执行顺序,数字越小,越早进行过滤,也可设置为负数
filterRegistrationBean.setOrder(1);
return filterRegistrationBean;
}
}
1)doFilter方法中的请求和响应对象是以父接口的形式声明的,实际传入的实参就是HttpServletRequest和HttpServletResponse子接口级别的,可以安全强转;
2)filterChain.doFilter(request,response); 这行代码的功能是放行请求,如果没有这一行代码,则
请求到此为止;
3)filterChain.doFilter(request,response);在放行时需要传入request和response,意味着请求和响
应对象要继续传递给后续的资源,这里没有产生新的request和response对象;可以对 request,response进行
3.4 修改接口的请求和响应
4. Interceptor的实现
4.1 拦截器的介绍
拦截器 Springmvc VS 过滤器 javaWeb:
- 相似点
- 拦截:必须先把请求拦住,才能执行后续操作
- 过滤:拦截器或过滤器存在的意义就是对请求进行统一处理
- 放行:对请求执行了必要操作后,放请求过去,让它访问原本想要访问的资源
- 不同点
- 工作平台不同
- 过滤器工作在 Servlet 容器中
- 拦截器工作在 SpringMVC 的基础上
- 拦截的范围
- 过滤器:能够拦截到的最大范围是整个 Web 应用
- 拦截器:能够拦截到的最大范围是整个 SpringMVC 负责的请求
- IOC 容器支持
- 过滤器:想得到 IOC 容器需要调用专门的工具方法,是间接的
- 拦截器:它自己就在 IOC 容器中,所以可以直接从 IOC 容器中装配组件,也就是可以直接得到 IOC 容器的支持
选择:功能需要如果用 SpringMVC 的拦截器能够实现,就不使用过滤器。
4.2 请求前后增强方法
通过实现一个拦截器接口的类来构建拦截器类,并重写 preHandle()、postHandle()、afterCompletion() 三个方法:
preHandle():前置增强,在处理请求的目标 handler 方法前执行
postHandle():在目标 handler 方法之后,handler报错不执行!
afterCompletion():渲染视图之后执行(最后),一定执行!
@Component // 拦截器还需要配置 告诉springMVC 这个拦截器主要拦截什么请求
public class MyHandlerInterceptor0 implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
System.out.println("MyHandlerInterceptor0...preHandle...");
// response.getWriter().write("no permission");
// return false; // 拦截
return true; // 放行
}
@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
System.out.println("MyHandlerInterceptor0...postHandle...");
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
System.out.println("MyHandlerInterceptor0...afterCompletion...");
}
}
4.3 对请求进行拦截放行
首先需要在spring配置类配置需要拦截的URL请求,代码如下:
@Configuration // 专门对 springMVC 底层做一些配置
public class MySpringMVCConfig implements WebMvcConfigurer{
@Autowired
private MyHandlerInterceptor0 myHandlerInterceptor0;
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(myHandlerInterceptor0)
.addPathPatterns("/api/**").order(1); //拦截所有请求
}
}
并且重写preHandle()方法,如果return的值是true 则放行,false 则拦截
其中链式表达式中的order表示执行优先级,数字越小越先执行。