一般情况,在访问
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(不考虑异常的发生)。如下图所示:



