一般情况,在访问
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接口,这个接口有三个方法需要实现,分别是:
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
对象中。
下面,写一个自定义的类来实现上述接口:
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
(不考虑异常的发生)。如下图所示:
Spring Security技术栈开发企业级认证与授权系列文章列表:
Spring Security技术栈学习笔记(一)环境搭建
Spring Security技术栈学习笔记(二)RESTful API详解
Spring Security技术栈学习笔记(三)表单校验以及自定义校验注解开发
Spring Security技术栈学习笔记(四)RESTful API服务异常处理
Spring Security技术栈学习笔记(五)使用Filter、Interceptor和AOP拦截REST服务
Spring Security技术栈学习笔记(六)使用REST方式处理文件服务
Spring Security技术栈学习笔记(七)使用Swagger自动生成API文档
Spring Security技术栈学习笔记(八)Spring Security的基本运行原理与个性化登录实现
Spring Security技术栈学习笔记(九)开发图形验证码接口
Spring Security技术栈学习笔记(十)开发记住我功能
Spring Security技术栈学习笔记(十一)开发短信验证码登录
Spring Security技术栈学习笔记(十二)将短信验证码验证方式集成到Spring Security
Spring Security技术栈学习笔记(十三)Spring Social集成第三方登录验证开发流程介绍
Spring Security技术栈学习笔记(十四)使用Spring Social集成QQ登录验证方式
Spring Security技术栈学习笔记(十五)解决Spring Social集成QQ登录后的注册问题
Spring Security技术栈学习笔记(十六)使用Spring Social集成微信登录验证方式
示例代码下载地址:
项目已经上传到码云,欢迎下载,内容所在文件夹为
chapter005
。
更多干货分享,欢迎关注我的微信公众号:爪哇论剑(微信号:itlemon)