引入场景:当我们在某些情况下需要对客户端发送来的请求进行拦截分析的时候,就需要用到拦截机制,比如,我们需要对一个请求进行计时,又或者需要知道当前请求需要进入哪个控制器,哪一个方法,该请求的参数是什么等等场景下都需要用到拦截机制来处理。下面,我们来讲解一下SpringBoot的几种拦截方式以及如何使用它们来处理一定的场景需求。
一、SpringBoot的拦截机制
主要有下面三种拦截机制:
- 过滤器(filter)
- 拦截器(interceptor)
切片(aspect)
上面的这三种拦截机制有什么区别呢?
过滤器可以拦截发送请求的状态码以及信息,拦截器除了可以拦截filter可以拦截的,还可以得到当前请求进入了哪一个controller,以及映射到哪一个方法,而aspect呢,它具有上面的所有功能外,还可以得到当前请求的参数的值。
这三种拦截器的顺序是怎么样的呢?
如上图所示,当一个请求发送来的时候,filter在最外层,也最先拦截到请求,接下来就是interceptor,依次是ControllerAdvice(处理controller层异常)、aspect,最后才进入controller层去处理请求。相应的,当controller内部发生错误,抛出异常的时候,aspect最先接收到该异常,如果不对抛出的异常继续处理继续往外抛的话依次会抛到ControllerAdvice、interceptor、filter。
在简单介绍完以上三种拦截机制后,下面来看看如何使用这三种拦截机制。
二、SpringBoot的拦截机制的使用
1.过滤器(Filter)
首先,我们需要来自己定义一个MyFilter类,然后需要实现Filter接口:
package cn.shinelon.filter;
import java.io.IOException;
import java.util.Date;
import javax.servlet.Filter;
import javax.servlet.FilterChain;
import javax.servlet.FilterConfig;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import org.springframework.stereotype.Component;
/**
* @author Shinelon
*
*/
@Component
public class MyFilter implements Filter {
/* (non-Javadoc)
* @see javax.servlet.Filter#destroy()
*/
@Override
public void destroy() {
System.out.println("time is destory");
}
/* (non-Javadoc)
* @see javax.servlet.Filter#doFilter(javax.servlet.ServletRequest, javax.servlet.ServletResponse, javax.servlet.FilterChain)
*/
@Override
public void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain)
throws IOException, ServletException {
System.out.println("the filter is begin");
long start=new Date().getTime();
chain.doFilter(req, resp);
System.out.println("耗时为:"+(new Date().getTime()-start)+"ms");
System.out.println("the filter is end");
}
/* (non-Javadoc)
* @see javax.servlet.Filter#init(javax.servlet.FilterConfig)
*/
@Override
public void init(FilterConfig arg0) throws ServletException {
System.out.println("time is init");
}
}
上面的filter类会重写三个方法,这三个方法见名知意,最核心的方法是doFilter方法,该方法内部会调用chain.doFilter(req, resp);方法来调用发送来的请求,在这个方法里我们就可以拿到我们需要的一系列拦截信息。因此,在定义好该方法后我们就可以启动项目,会打印请求的耗时。
但是,有时候我们需要调用第三方框架的拦截器的时候,就不能像这个过滤器一样这样使用,因为这是j2EE中自带的过滤器。在SpringMvc中,我们可以在web.xml文件中注入我们要 引入第三方框架的过滤器,在SpringBoot中,我们需要声明一个配置类,这个类作用相当于之前的xml文件来管理bean。
下面,我们来添加一个配置类,不过上面filter中需要去掉@component注解:
package cn.shinelon.config;
import java.util.ArrayList;
import java.util.List;
import org.aopalliance.intercept.Interceptor;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Configurable;
import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter;
import cn.shinelon.filter.MyFilter;
import cn.shinelon.interceptor.TimeInteceptor;
/**
* @author Shinelon
*
*/
@Configuration
public class FilterConfig extends WebMvcConfigurerAdapter{
@Bean
public FilterRegistrationBean myFilter() {
FilterRegistrationBean filterRegistrationBean=new FilterRegistrationBean();
MyFilter myFilter=new MyFilter();
filterRegistrationBean.setFilter(myFilter);
List<String> urls=new ArrayList<String>();
urls.add("/*"); //过滤所有请求
filterRegistrationBean.setUrlPatterns(urls);
return filterRegistrationBean;
}
}
与上面不同的是,这里我们可以设置过滤的请求的格式,这里过滤所有请求。
2.拦截器interceptor
如何使用拦截器呢?与上面类似,我们还是需要自己来编写拦截器:
package cn.shinelon.interceptor;
import java.util.Date;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.springframework.stereotype.Component;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;
@Component
public class TimeInteceptor implements HandlerInterceptor {
//无论controller中是否抛出异常,都会调用该方法
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object obj, Exception ex)
throws Exception {
System.out.println("afterCompletion");
long start=new Date().getTime();
System.out.println("最后耗时为:"+(new Date().getTime()-start));
System.out.println("ex is"+ex);
}
//如果controller中抛出异常,则该方法不会被调用
@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object obj, ModelAndView view)
throws Exception {
// ModelMap map=new ModelMap();
// map.addAttribute("name", "shinelon");
System.out.println("postHandle");
long start=(long) request.getAttribute("start");
System.out.println("消耗的时间为:"+(new Date().getTime()-start));
}
//最先执行该方法
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object obj) throws Exception {
System.out.println("preHandle");
System.out.println(((HandlerMethod)obj).getBean().getClass().getName());
System.out.println(((HandlerMethod)obj).getMethod().getName());
request.setAttribute("start", new Date().getTime());
System.out.println("开始时间:"+new Date().getTime());
return true;
}
}
这个拦截器需要实现HandlerInterceptor接口,这个接口声明了三个方法,作用看上面注释。
在这里需要注意的是preHandle方法会返回一个Boolean值,这个值关系这拦截器的执行,如果返回true,就继续执行下面的方法,如果返回false则不会执行后面的业务。我们可以深入源码来看看。
我们进入到spring框架的DispatcherServlet类中有一个doDispatch方法,这个方法中有下面一段代码:
首先,它会先判断是否通过了PreHandle方法,如果通过就会真正的调用handler,将request,response等请求信息通过handle方法封装起来。因此,我们需要小心处理PreHandler方法中业务以及返回值。
与filter不同的是,它在创建interceptor之后,不能生效,我们需要创建一个配置类将该拦截器注入到spring容器中。这个配置类继承了WebMvcConfigurerAdapter类。
package cn.shinelon.config;
import java.util.ArrayList;
import java.util.List;
import org.aopalliance.intercept.Interceptor;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Configurable;
import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter;
import cn.shinelon.filter.MyFilter;
import cn.shinelon.interceptor.TimeInteceptor;
/**
* @author Shinelon
*
*/
@Configuration
public class FilterConfig extends WebMvcConfigurerAdapter{
@Autowired
public TimeInteceptor timeInteceptor;
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(timeInteceptor);
}
}
这样,我们的拦截器就会生效,我们可以在拦截器中得到发送来的请求进入哪一个控制器以及方法。
3.切片(aspect)
最后,我们来看看切片的使用,对于切片,相信使用spring的人都非常熟悉,spring的核心功能AOP,面向切面编程,这里,我们不详解aop,读者可以去查看相关资料。下面我们来看看使用切片来实现拦截机制。
首先,需要引入相关依赖:
<!-- https://mvnrepository.com/artifact/org.springframework.boot/spring-boot-starter-aop -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
下面我们来创建切面:
/**
*
*/
package cn.shinelon.aspect;
import java.util.Date;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.springframework.stereotype.Component;
/**
* @author Shinelon
*
*/
@Aspect
@Component
public class TimeAspect {
@Around("execution(* cn.shinelon.controller.UserController.*(..))")
public Object AspectHandlerMethod(ProceedingJoinPoint pjp) throws Throwable {
System.out.println("apect start");
long start=new Date().getTime();
Object[] args=pjp.getArgs();
for(Object arg:args) {
System.out.println("arg is "+arg);
}
System.out.println("最后耗时为:"+(new Date().getTime()-start));
Object obj=pjp.proceed();
System.out.println("aspect end");
return obj;
}
}
在上面,我们提到过,切面可以拿到请求的参数,它是一个Object数组对象,上面,我们将其打印。最后,它调用的proceed方法类似前面的filter和interpretor,该方法就是拦截的请求要执行的方法。返回的也是要执行的方法的返回。
下面,我们执行一个请求
我们会看到拦截器的信息会打印在控制台,如下:
上面就是SpringBoot的拦截器的使用。