一般情况,在访问
RESTful
风格的API
之前,可以对访问行为进行拦截,并做一些逻辑处理,本文主要介绍三种拦截方式,分别是:过滤器Filter
、拦截器Interceptor
以及面向切面的拦截方式AOP
。
一、使用过滤器Filter进行拦截
使用过滤器进行拦截主要有两种方式,第一种是将自定义的拦截器标注为Spring
的Bean
,在Spring Boot
应用就可以对RESTful
风格的API
进行拦截。第二种方式往往应用在继承第三方过滤器,这时候就需要将第三方拦截器使用FilterRegistrationBean
对象进行注册即可。接下来详细介绍两种方式。
- 将拦截器标注为
Spring
的Bean
package com.lemon.security.web.filter;
import org.springframework.stereotype.Component;
import javax.servlet.*;
import java.io.IOException;
/**
* @author lemon
* @date 2018/4/1 下午10:19
*/
@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 request, ServletResponse response, FilterChain chain)
throws IOException, ServletException {
System.out.println("time filter start.");
long startTime = System.currentTimeMillis();
chain.doFilter(request, response);
System.out.println("time filter 耗时: " (System.currentTimeMillis() - startTime));
System.out.println("time filter finish.");
}
@Override
public void destroy() {
System.out.println("time filter destroy.");
}
}
启动Spring Boot
应用的时候,上面的拦截器就会起作用,当访问每一个服务的时候,都会进入这个拦截器中。初始化方法init
和销毁方法destroy
只会调用一次,分别是应用启动时候调用init
方法,应用关闭时候调用destroy
方法。而doFilter
方法则在每次都会调用。
- 将拦截器作为第三方拦截器进行注册
使用的类还是上面的同一个类,只不过这次不需要@Component
注解,这时候我们需要自己写一个配置类,将过滤器注册到Spring
容器中。推荐使用这种方式,因为这种方式我们可以自己设置需要拦截的API
,否则第一种方式是拦截所有的API
。
package com.lemon.security.web.config;
import com.lemon.security.web.filter.TimeFilter;
import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import java.util.ArrayList;
import java.util.List;
/**
* @author lemon
* @date 2018/4/1 下午10:34
*/
@Configuration
public class WebConfig {
@Bean
public FilterRegistrationBean timeFilter() {
FilterRegistrationBean filterRegistrationBean = new FilterRegistrationBean();
TimeFilter timeFilter = new TimeFilter();
filterRegistrationBean.setFilter(timeFilter);
List<String> urls = new ArrayList<>();
urls.add("/*");
filterRegistrationBean.setUrlPatterns(urls);
return filterRegistrationBean;
}
}
这里我设置的仍然是拦截所有的API
,可以设置为自定义的方式对API
进行拦截。
二、使用拦截器Interceptor进行拦截
这里需要定义一个拦截器类,并实现HandlerInterceptor接口,这个接口有三个方法需要实现,分别是:
代码语言:javascript复制boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler)
throws Exception;
void postHandle(
HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView)
throws Exception;
void afterCompletion(
HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex)
throws Exception;
下面对三个方法进行一一解释:
-
preHandle
方法的第三个参数是具体的API
处理方法的Method
对象,我们可以将其强转为HandlerMethod
,然后就可以获取该Method
的一些属性,比如方法名,方法所在类的类名等信息。preHandle
是当访问API
之前,都要进入这个方法,由这个方法进行一些逻辑处理,如果处理完结果返回true
,那么将继续进入到具体的API
中,否则将就地结束访问,逻辑不会进入API
方法中。 -
postHandle
方法是在API
方法访问完成之后立即进入的方法,可以处理一些逻辑,比如将API
中的数据封装到ModelAndView
中,如果前面的preHandle
方法返回false
,将不会执行该方法,如果API
方法发生了异常,也将不会调用此方法。 -
afterCompletion
方法的调用只要preHandle
方法通过之后就会调用它,不论API
方法是否出现了异常。如果出现了异常,将被封装到Exception
对象中。
下面,写一个自定义的类来实现上述接口:
代码语言:javascript复制package com.lemon.security.web.interceptor;
import org.springframework.stereotype.Component;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
/**
* @author lemon
* @date 2018/4/1 下午10:39
*/
@Component
public class TimeInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
System.out.println("preHandler");
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("postHandler");
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
System.out.println("afterCompletion");
System.out.println("TimeInterceptor耗时:" (System.currentTimeMillis() - (Long) request.getAttribute("startTime")));
}
}
这里需要将其标注为Spring
的Bean
,但是仅仅标注为Bean
还是不够的,需要在配置类中进行配置。代码如下:
package com.lemon.security.web.config;
import com.lemon.security.web.interceptor.TimeInterceptor;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter;
/**
* @author lemon
* @date 2018/4/1 下午10:34
*/
@Configuration
public class WebConfig extends WebMvcConfigurerAdapter {
@Autowired
private TimeInterceptor timeInterceptor;
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(timeInterceptor);
}
}
这个配置类需要继承WebMvcConfigurerAdapter
,并重写添加拦截器的方法addInterceptors
,将自定义拦截器添加到应用中。这时候拦截器就生效了。
三、使用AOP进行拦截
其实是有拦截器Interceptor
对API
进行拦截的时候是有缺陷的,因为无法获取前端访问API
的时候所携带的参数的,为什么会这么说?从Spring MVC
的DispatcherServlet
的源代码中可以发现,找到doDispatch
方法,也就是请求分发的方法,有一段代码如下:
如果我们自定的Interceptor
的preHandler
方法返回的是false
,分发任务就会截止,不再继续执行下面的代码,而下面的一行代码正是将前端携带的参数进行映射的逻辑,也就是说,preHandler
方法不会接触到前端携带来的参数,也就是说拦截器无法处理参数。所以这里引进AOP
进行拦截。
AOP的核心概念解释:
描述AOP常用的一些术语有通知(Adivce
)、切点(Pointcut
)、连接点(Join point
)、切面(Aspect
)、引入(Introduction
)、织入(Weaving
)
- 通知(
Advice
)
通知分为五中类型:
Before
:在方法被调用之前调用
After
:在方法完成后调用通知,无论方法是否执行成功
After-returning
:在方法成功执行之后调用通知
After-throwing
:在方法抛出异常后调用通知
Around
:通知了好、包含了被通知的方法,在被通知的方法调用之前后调用之后执行自定义的行为
- 连接点(
Join point
)
连接点是一个应用执行过程中能够插入一个切面的点。比如:方法调用、方法执行、字段设置/获取、异常处理执行、类初始化、甚至是for
循环中的某个点。理论上, 程序执行过程中的任何时点都可以作为作为织入点, 而所有这些执行时点都是Joint point
,但 Spring AOP
目前仅支持方法执行 (method execution
)。
- 切点(
Pointcut
)
通知(advice
)定义了切面何时,那么切点就是定义切面“何处” 描述某一类 Joint points
, 比如定义了很多 Joint point
, 对于 Spring AOP
来说就是匹配哪些方法的执行。
- 切面(
Aspect
)
切面是切点和通知的结合。通知和切点共同定义了关于切面的全部内容,它是什么时候,在何时和何处完成功能。
- 引入(
Introduction
)
引用允许我们向现有的类添加新的方法或者属性
- 织入(
Weaving
)
组装方面来创建一个被通知对象。这可以在编译时完成(例如使用AspectJ
编译器),也可以在运行时完成。Spring
和其他纯Java AOP
框架一样,在运行时完成织入。
上面的概念有点生涩难懂,总结一个核心内容:切面 = 切点 通知
。
现在通过代码来编写一个切面:
package com.lemon.security.web.aspect;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.springframework.stereotype.Component;
/**
* @author lemon
* @date 2018/4/2 上午10:40
*/
@Aspect
@Component
public class TimeAspect {
@Around("execution(* com.lemon.security.web.controller.UserController.*(..))")
public Object handleTime(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {
System.out.println("time aspect is start.");
for (Object object : proceedingJoinPoint.getArgs()) {
System.out.println(object);
}
long startTime = System.currentTimeMillis();
Object obj = proceedingJoinPoint.proceed();
System.out.println("time aspect 耗时:" (System.currentTimeMillis() - startTime));
System.out.println("time aspect finish.");
return obj;
}
}
@Around
定义了环绕通知,也就是定义了何时使用切面,表达式"execution(* com.lemon.security.web.controller.UserController.*(..))"
定义了再哪里使用。ProceedingJoinPoint
对象的proceed()
方法表示执行被拦截的方法,它有一个Object
类型的返回值,是原有方法的返回值,后期使用的时候往往需要强转。关于切点的表达式,可以访问Spring官方文档。
对于上面三种拦截方式,他们的执行有一个基本的顺序,进入的顺序是Filter-->Interceptor-->Aspect-->Controller-->Aspect-->Interceptor-->Filter
(不考虑异常的发生)。如下图所示: