Lingmoumou's Blog

きっといつかって愿うまま

0%

SpringBoot 拦截机制

在日常的项目中,我们经常需要对请求进行拦截然后进行一些逻辑操作,拦截的方式有三种,分别是Filter、Interceptor和Aspect。

拦截顺序

Spring中的拦截机制,如果出现异常的话,异常的顺序是从里面到外面一步一步的进行处理,如果到了最外层都没有进行处理的话,就会由tomcat容器抛出异常。

Filter (过滤器)

首先,过滤器是服务端的一个组件,是基于servlet实现从客户端访问服务端web资源的一种拦截机制,对请求request和响应response都进行过滤,依赖于serverlet容器,使用时,实现Filter接口,在web.xml里配置对应的class还有mapping-url,springboot工程可以通FilterRegisteration配置后,设置要过滤的URL,注意:两种方式过滤器都是有序的,谁在前就先调用谁!定义过滤器后会重写三个方法,分别是init(),doFilter()和destory()。

  • init方法是过滤器的初始化方法,当web容器创建这个bean的时候就会执行,这个方法可以读取web.xml里面的参数
  • idoFilter方法是执行过滤的请求的核心,当客户端请求访问web资源时,这个时候我们可以拿到request里面的参数,对数据进行处理后,通过filterChain方法将请求将请求放行,放行后我们也可以通过response对响应进行处理(比如压缩响应),然后会传递到下一个过滤器
  • idestory方法是当web容器中的过滤器实例被销毁时,会被执行,释放资源

通过@Component注解配置

实现Filter方法,通过@Component将TimeFilter添加到Spring容器中。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// TimeFilter.java

@Component
public class TimeFilter implements Filter {
@Override
public void init(FilterConfig filterConfig) throws ServletException {
System.out.println("time filter init");
}

@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
System.out.println("time filter start");

long start=System.currentTimeMillis();
filterChain.doFilter(servletRequest,servletResponse);
System.out.println("time filter cost:"+(System.currentTimeMillis()-start));

System.out.println("time filter finish");
}

@Override
public void destroy() {
System.out.println("time filter destroy");
}
}

通过@Configuration注解配置

新建WebConfig类,并添加@Configuration注解,将该类配置为一个配置类。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// WebConfig.java

@Configuration
public class WebConfig {

@Bean
public FilterRegistrationBean timeFilter(){
FilterRegistrationBean registrationBean=new FilterRegistrationBean();

TimeFilter timeFilter=new TimeFilter();
registrationBean.setFilter(timeFilter);

List<String> urls=new ArrayList<>();
urls.add("/*");
registrationBean.setUrlPatterns(urls);

return registrationBean;
}

}

Interceptor (拦截器)

拦截器,顾名思义,他的作用就是拦截,这个要和过滤器区分开,过滤器依赖serverlet容器,获取request和response处理,是基于函数回调,简单说就是“去取你想取的”,拦截器是通过java反射机制,动态代理来拦截web请求,是“拒你想拒绝的”,他只拦截web请求,但不拦截静态资源。

  • preHandler(): 这个方法是在controller调用之前调用,通过返回true或者false决定是否进入Controller层
  • postHandler():在请求进入控制层之后调用,但是在处理请求抛出异常时不会调用
  • afterCompletion(): 在请求处理完成之后,也就是在DispatherServlet渲染了视图之后执行,也就是说这个方法必定是执行,包含异常信息,它的主要作用就是清理资源

拦截顺序

DispatcherServlet.java

  1. applyPreHandle方法中调用自定义拦截器中的preHandler方法,如果preHandler方法中返回的是False,则程序跳出,不继续往下进行。
  2. ha.handle真正调用handler去处理。请求中方法参数的拼装在此进行。

因此Interceptor中,无法知道请求参数的内容。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
// TimeInterceptor.java

@Component
public class TimeInterceptor implements HandlerInterceptor {

@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
System.out.println("preHandle");

System.out.println(((HandlerMethod) handler).getBean().getClass().getName());
System.out.println(((HandlerMethod) handler).getMethod().getName());

request.setAttribute("startTime",System.currentTimeMillis());
return true;
}

@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
System.out.println("postHandle");
Long start= (Long) request.getAttribute("startTime");
System.out.println("time interceptor cost:"+(System.currentTimeMillis()-start));
}

@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler,Exception ex) throws Exception {
System.out.println("afterCompletion");
Long start= (Long) request.getAttribute("startTime");
System.out.println("time interceptor cost:"+(System.currentTimeMillis()-start));
System.out.println("ex is "+ex);
}

}

Aspect (切片)

拦截顺序

AOP 简介

AOP(Aspect Oriented Programming),即为面向切面编程。在软件开发中,散布于应用中多处的功能被称为横切关注点(cross-cutting concern),通常来说,这些横切关注点从概念上是与应用的业务逻辑分离的。比如,声明式事务、日志、安全、缓存等等,都与业务逻辑无关,可以将这些东西抽象成为模块,采用面向切面编程的方式,通过声明方式定义这些功能用于何处,通过预编译方式和运行期动态代理实现这些模块化横切关注点程序功能进行统一维护,从而将横切关注点与它们所影响的对象之间分离出来,就是实现解耦。

横切关注点可以被模块化为特殊的类,这些类被称为切面(aspect)。这样做有两个优点:

  1. 每个关注点都集中于一个地方,而不是分散到多处代码中;
  2. 服务模块更简洁,因为它们只包含主要的关注点的代码(核心业务逻辑),

Spring AOP

而次要关注点的代码(日志,事务,安全等)都被转移到切面中。

AOP术语

Advice(通知)

切面类有自己要完成的工作,切面类的工作就称为通知。通知定义了切面是做什么以及何时使用。

  • “做什么”:即切面类中定义的方法是干什么的;
  • “何时使用”:即5种通知类型,是在目标方法执行前,还是目标方法执行后等等;
  • “何处做”:即通知定义了做什么,何时使用,但是不知道用在何处,而切点定义的就是告诉通知应该用在

哪个类的哪个目标方法上,从而完美的完成横切点功能。
Spring切面定义了5种类型通知:

  1. 前置通知(Before):在目标方法被调用之前调用通知功能。
  2. 后置通知(After):在目标方法完成之后调用通知,不会关心方法的输出是什么。
  3. 返回通知(After-returning): 在目标方法成功执行之后调用通知。
  4. 异常通知(After-throwing):在目标方法抛出异常后调用通知。
  5. 环绕通知(Around):通知包裹了被通知的方法,在被通知的方法调用之前和之后执行自定义的行为。

Join point(连接点)

在我们的应用程序中有可能有数以万计的时机可以应用通知,而这些时机就被称为连接点。

连接点是在应用执行过程中能够插入切面的一个点。这个点可以是调用方法时、抛出异常时、甚至修改一个字段时。切面代码可以利用这些点插入到应用的正常流程之中,并添加新的行为。
连接点是一个虚概念,可以把连接点看成是切点的集合。

Poincut(切点)

连接点谈的是一个飘渺的大范围,而切点是一个具体的位置,用于缩小切面所通知的连接点的范围。前面说过,通知定义的是切面的”要做什么”和”在何时做”,是不是没有去哪里做,而切点就定义了”去何处做”。

切点的定义会匹配通知所要织入的一个或多个连接点。我们通常使用明确的类和方法名称,或者是使用正则表达式定义所匹配的类和方法名称来指定切点。说白了,切点就是让通知找到”发泄的地方”。

切点的结构

Aspect(切面)

切面是通知和切点的结合,通知和切点共同定义了切面的全部内容。因为通知定义的是切面的”要做什么”和”在何时做”,而切点定义的是切面的”在何地做”。将两者结合在一起,就可以完美的展现切面在何时,何地,做什么(功能)。

切入点表达式

  • 包名切面
    对 com.app.controller 包中所有的类的所有方法切面
    @Pointcut("execution(public * com.app.controller.*.*(..))")

  • 包名及子包切面
    对 com.app.controller 及其子包中所有的类的所有方法切面
    @Pointcut("execution(public * com.app.controller..*.*(..))")

  • 类名切面
    只针对 StudentController 类切面
    @Pointcut("execution(public * com.app.controller.StudentController.*(..))")

Introduction(引入)

引入这个概念就比较高大尚,引入允许我们向现有的类添加新方法或属性。

用来给一个类型声明额外的方法或属性(也被称为连接类型声明(inter-type declaration))。Spring允许引入新的接口(以及一个对应的实现)到任何被代理的对象。例如,你可以使用引入来使一个bean实现IsModified接口,以便简化缓存机制。

主要目的是想在无需修改A的情况下,引入B的行为和状态。

Weaving(织入)

织入是把切面应用到目标对象并创建新的代理对象的过程。切面在指定的连接点被织入到目标对象中。

把切面连接到其它的应用程序类型或者对象上,并创建一个被通知的对象。这些可以在编译时(例如使用AspectJ编译器),类加载时和运行时完成。Spring和其他纯Java AOP框架一样,在运行时完成织入。

Spring相关常用注解

切片编程,面对的是处理过程中的方法或者阶段,以获得各部分的低耦合性的隔离效果,它是基于动态代理,它关注的是行为和过程,它常用的注解如下:

  • @Aspect:声明一个切面
  • @Before:相当于拦截器preHandler,在方法执行前调用
  • @After:相当于拦截器的afterComplement()在方法执行后调用
  • @AfterThrowing:方法抛出异常时调用
  • @AfterReturning:当方法返回时调用
  • @Around:包含以上方的执行顺序
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// TimeAspect.java

@Aspect
@Component
public class TimeAspect {

@Around("execution(* net.ling.security.web.controller.UserController.*(..))")
public Object handlerControllerMethod(ProceedingJoinPoint pjp) throws Throwable {

System.out.println("time aspect start");
Object[] args=pjp.getArgs();
for(Object arg:args){
System.out.println("arg is "+arg);
}
long start=System.currentTimeMillis();

Object object=pjp.proceed();

System.out.println("time aspect cost:"+(System.currentTimeMillis()-start));

System.out.println("time aspect end");
return object;
}
}

区别

  • Filter:与Spring 无关,基于Servlet, 可以获取request 和 response,但是具体处理方法及相关参数获取不到;
  • Interceptor:与Spring相关,可以获取request 和 response 以及具体处理方法,但是获取不到具体方法参数的值;
  • Aspect: 与Spring相关,不可获取request和response , 可以获取具体方法及具体方法参数的值;

参考文献