一般情况,在访问RESTful风格的API之前,可以对访问行为进行拦截,并做一些逻辑处理,本文主要介绍三种拦截方式,分别是:过滤器Filter、拦截器Interceptor以及面向切面的拦截方式AOP。
一、使用过滤器Filter进行拦截使用过滤器进行拦截主要有两种方式,第一种是将自定义的拦截器标注为Spring的Bean,在Spring Boot应用就可以对RESTful风格的API进行拦截。第二种方式往往应用在继承第三方过滤器,这时候就需要将第三方拦截器使用FilterRegistrationBean对象进行注册即可。接下来详细介绍两种方式。
将拦截器标注为Spring的Beanpackage 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(不考虑异常的发生)。如下图所示:
---来自腾讯云社区的---itlemon
微信扫一扫打赏
支付宝扫一扫打赏