在Spring Boot中,拦截器可以分为两种类型:
- 一是WebMVC,负责拦截请求,类似于过滤器,对用户的请求在Controller接收前进行处理,在Controller处理完成后加工结果等。使用时需实现HandlerInterceptor接口。
- 一是AOP,拦截指定类型的方法,通过动态代理模式实现,可以在方法的调用前和调用后添加功能处理。使用时需要实现MethodInterceptor接口。
拦截器(Interceptor)和过滤器(Filter)对比
相同点:
- 都可以对请求进行提前处理和响应内容加工。
- 都支持多个拦截器/过滤器的链路传递。
不同点:
- 拦截器由Spring提供,过滤器由Servlet提供。
HandlerInterceptor
HandlerInterceptor接口属于顶级接口,里面一共有三个default方法(这是jdk8的新特性,用来在接口中编写带有方法体的方法。实现接口可以不重写default方法,默认调用的仍是接口中的default方法体)
public interface HandlerInterceptor { default boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { return true; } default void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, @Nullable ModelAndView modelAndView) throws Exception { } default void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, @Nullable Exception ex) throws Exception { } }
boolean preHandle:会在controller处理前调用该方法,方法返回true则进入对应的controller。方法返回false则不会进入controller。可以用来编码,安全控制,权限校验等。
void postHandle:在controller处理完成返回ModelAndView后执行。此时还没有进行视图渲染,还可以修改ModelAndView。
void afterCompletion:整个请求已经处理完成了,可能返回了正常的请求结果,也可能返回一个异常。
下面我们来做一个简单的demo。
先定义一个controller:
@RestController public class UserController { @GetMapping("users/{id}") public String getUser(@PathVariable("id") String id) { System.out.println("controller[url=users/" + id + "]"); return "testUser"; } @GetMapping("users/login") public String test(User user, Model model) { System.out.println("controller[url=users/login]"); model.addAttribute("id", user.getId()); model.addAttribute("name", user.getName()); model.addAttribute("password", user.getPassword()); model.addAttribute("mail", user.getMail()); return "index"; } }
里面提供了两个请求链接:/users/{id}和/users/login,这里为了简单方便,我们均将请求方式设置为get。
定义一个拦截器UserInteceptor(这里先测试第一个方法preHandle):
@Component public class UserInteceptor implements HandlerInterceptor { @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { System.out.println("preHandle;"); Map<String, String[]> map = request.getParameterMap(); map.forEach((k, v) -> { System.out.println("[Key=" + k + ";Value=" + StringUtils.join(v) + "];"); }); return true; } @Override public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception { System.out.println("postHandle"); } @Override public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception { System.out.println("afterCompletion"); } }
然后将拦截器添加进容器中,设定它的拦截路径为/users/**:
@Configuration public class InteceptorConfig implements WebMvcConfigurer { @Autowired private UserInteceptor userInteceptor; @Override public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(userInteceptor).addPathPatterns("/users/*"); } }
就此一个简单的项目已经构成,我们启动程序,然后访问/users/login?id=1&username=yxf&password=123&mail=5@qq.com
可以看到访问结果:
preHandle; //----------------------------调用了preHandle方法 [Key=id;Value=1]; //---------------------打印request中的id [Key=name;Value=yxf]; //-----------------打印request中的name [Key=password;Value=123]; //-------------打印request中的password [Key=mail;Value=5@qq.com]; //------------打印request中的mail controller[url=users/login] //-----------在preHandle之后,这里进入UserController的test方法。 postHandle //----------------------------controller处理完成后调用PostHandle方法。
//---------------在这中间其实还有dispatchServlet调用视图解析器对View的解析等------------------ afterCompletion //-----------------------postHandle处理完成后调用afterCompletion。
顺序是按照preHandle→Controller→postHandle→视图渲染器→afterCompletion的顺序执行。
MethodInterceptor
MethodInterceptor继承关系:
在MethodInterceptor接口中,只提供了一个方法
Object invoke(MethodInvocation invocation) throws Throwable;
首先分析方法的传入参数MethodInvocation
MethodInvocation对象继承关系如下
相关方法说明
Method getMethod(); //获取Java反射类Method Object[] getArguments(); // 获取方法的传入参数 Object proceed() throws Throwable; // 继续执行方法 Object getThis(); // 获取方法所在的对象 AccessibleObject getStaticPart(); // 获取的也是Java反射类Method
测试Demo
为了方便一部分功能展示,我们这里定义一个注解用来后续操作
@Target({ElementType.METHOD}) @Retention(RetentionPolicy.RUNTIME) @Documented public @interface DemoAnnotation { String value() default "test"; }
定义我们的方法过滤器,从参数对象获取到被拦截的方法的相关信息,这里为了简单直接将信息打印在控制台(实际情况因具体业务而异)。
public class DemoInterceptor implements MethodInterceptor { @Override public Object invoke(MethodInvocation methodInvocation) throws Throwable { System.out.println("--------------------------------------------------"); // 参数 Object[] object = methodInvocation.getArguments(); for (Object obj : object) { System.out.println(obj.getClass().getName()); if (obj instanceof String) { System.out.println("param = " + ((String) obj).toString()); } } // 对象 Object obj = methodInvocation.getThis(); System.out.println(obj.getClass().getName()); // 方法 Method method = methodInvocation.getMethod(); // 继续执行 obj = methodInvocation.proceed(); System.out.println(obj.getClass().getName()); // 获取注解 DemoAnnotation d = method.getAnnotation(DemoAnnotation.class); if (d == null) { System.out.println("当前类没有DemoAnnotation注解"); } else { System.out.println(d.value()); } System.out.println("--------------------------------------------------"); return null; } }
配置过滤器。(方法过滤器实际可以算是实现AOP的一种方式,我们需要定义一个切面,配置它的切点和增强)
@Bean public DefaultPointcutAdvisor defaultPointcutAdvisor() { // 声明切点 // JdkRegexpMethodPointcut pointcut = new JdkRegexpMethodPointcut(); // pointcut.setPatterns("com.example.*"); AspectJExpressionPointcut pointcut =new AspectJExpressionPointcut(); pointcut.setExpression("execution(* com.example..*(*))"); // 拦截com.example包和子包下带一个参数的任何方法 // 声明增强 DemoInterceptor interceptor = new DemoInterceptor(); // 配置切面 DefaultPointcutAdvisor advisor = new DefaultPointcutAdvisor(); advisor.setPointcut(pointcut); advisor.setAdvice(interceptor); return advisor; }
定义调用类,这里为了简单,也没有写service层,直接在controller层调用了。
@RestController public class DemoController { @GetMapping("/demo/{id}") public String demo(@PathVariable("id") String id) { return "test demo"; } @GetMapping("/demo2/{id}") @DemoAnnotation public String demo2(@PathVariable("id") String id) { return "test demo"; } }
运行程序,分别访问上面的两个地址,打印如下
/demo/1
-------------------------------------------------- java.lang.String param = 1 com.example.demo.controller.DemoController java.lang.String 当前类没有DemoAnnotation注解 --------------------------------------------------
/demo2/2
-------------------------------------------------- java.lang.String param = 2 com.example.demo.controller.DemoController java.lang.String test --------------------------------------------------
结束