1、认识过滤器(Filter)
1.1、过滤器的定义
过滤器是JavaWeb的三大组件之一,是实现Filter接口的Java类。
过滤器是实现对请求资源(jsp、servlet、html)的过滤功能,是一个运行在服务器的程序,优先于请求资源(jsp、servlet、html)之前执行。
当浏览器发送请求给服务器的时候,先执⾏过滤器,然后才访问Web的资源。服务器响应Response,从Web资源抵达浏览器之前,也会途径过滤器。
在很多Web开发中,都会用到过滤器(Filter),如参数过滤、防止SQL注入、防止页面攻击、过滤敏感字符、解决网站乱码、空参数矫正、Token验证、Session验证、点击率统计等。
1.2、为什么要使用过滤器
在Web开发中,经常会有这样的需求:在所有接口中去除用户输入的非法字符,以防止引起业务异常。要实现这个功能,可以有很多方法,如:
- 在前端参数传入时进行校验,先过滤非法字符,然后返回用户界面提示用户重新输入。
- 后端接收前端没有过滤的数据,然后过滤非法字符。
- 利用filter处理项目中所有非法字符。
很明显,前两种实现方法会存在重复代码,因为每个前端页面或后端都需要处理,这样会导致代码很难维护。如果用过滤器来实现,则只需要用过滤器对所有接口进行过滤处理。这样非常方便,同时不会出现冗余代码。
1.3、使用Filter的步骤(以SpringBoot项目为例)
(1)新建类,实现Filter抽类类。
(2)重写init、doFilter、destroy方法。
(3)在Spring Boot入口类中添加注解@ServletComponentScan,以注册Filter。
init():该方法在容器启动初始化过滤器时被调用,它在Filter的整个生命周期只会被调用一次,这个方法必须执行成功,否则过滤器会不起作用。
doFilter():容器中的每一次请求都会调用该方法,FilterChain用来调用下一个过滤器Filter。
destroy():容器销毁时被调用。一般在方法中销毁或关闭资源,也只会被调用一次。
注意的是doFilter()方法:
@Override public void doFilter(ServletRequest var1, ServletResponse var2, FilterChain var3) throws IOException, ServletException{ //请求(request)处理逻辑 //请求(request)封装逻辑 //chain重新写回request和response }
上面的FilterChain是一个接口,里面又定义了doFilter()方法,这是因为在Java中使⽤了链式结构。把所有的过滤器都放在FilterChain⾥边,如果符合条件,就执⾏下⼀个过滤器(如果没有过滤器了,就执⾏⽬标资源)。
1.4、SpringBoot实现一个简单的过滤器
(1)首先随便写一个控制器Controller
@RestController @Slf4j @RequestMapping("/api/filter") public class FilterUserController { @GetMapping("/getUserList") public List<String> getUser() { log.info("开始业务逻辑处理。"); List<String> list = new ArrayList<>(); list.add("张三"); list.add("李四"); list.add("王五"); log.info("业务逻辑处理结束。"); return list; } }
(2)在启动类添加一个注解,找到定义的拦截器。
@ServletComponentScan(basePackages = "com.binlog.study.filter")
(3)写一个过滤器,实现Filter
@Slf4j @Order(1) //如果有多个Filter,则序号越小,越早被执行 //@Component//无需添加此注解,在启动类添加@ServletComponentScan注解后,会自动将带有@WebFilter的注解进行注入! //这里的urlPatterns为接口里的路径过滤条件 @WebFilter(filterName = "timeFilter", urlPatterns = "/api/filter/*") public class TimeFilter implements Filter { @Override public void init(FilterConfig filterConfig) throws ServletException { log.info("初始化过滤器:{}", filterConfig.getFilterName()); } @Override public void destroy() { log.info("销毁过滤器"); } @Override public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException { log.info("开始执行"); long startTime = System.currentTimeMillis(); filterChain.doFilter(servletRequest, servletResponse); long endTime = System.currentTimeMillis(); log.info("请求:{},耗时:{}ms", getUrlFrom(servletRequest), (endTime - startTime)); log.info("结束执行"); } private String getUrlFrom(ServletRequest servletRequest) { if (servletRequest instanceof HttpServletRequest) { return ((HttpServletRequest) servletRequest).getRequestURL().toString(); } return ""; } }
(4)项目启动后,控制台输出结果为:
项目启动后,就已经初始化过滤器了。
浏览器页面调用一下接口:http://localhost:8060/api/filter/getUserList
关闭项目后,过滤器也销毁了。
2、认识拦截器(Interceptor)
2.1、拦截器的定义
SpringMVC的处理器拦截器类似于Servlet开发中的过滤器Filter,用于对处理器进行预处理和后处理。开发者可以自己定义一些拦截器来实现特定的功能。
拦截器它是链式调用,一个应用中可以同时存在多个拦截器Interceptor,一个请求也可以触发多个拦截器,而每个拦截器的调用会依据它的声明顺序依次执行。
2.2、拦截器的核心API
SpringMVC拦截器提供三个方法分别是preHandle、postHandle、afterCompletion,我们就是通过重写这几个方法来对用户的请求进行拦截处理的。
- preHandle() :这个方法将在请求处理之前进行调用。「注意」:如果该方法的返回值为false ,将视为当前请求结束,不仅自身的拦截器会失效,还会导致其他的拦截器也不再执行。
- postHandle():只有在 preHandle() 方法返回值为true 时才会执行。会在Controller 中的方法调用之后,DispatcherServlet 返回渲染视图之前被调用。「有意思的是」:postHandle() 方法被调用的顺序跟 preHandle() 是相反的,先声明的拦截器 preHandle() 方法先执行,而postHandle()方法反而会后执行。
- afterCompletion():只有在 preHandle() 方法返回值为true 时才会执行,在整个请求结束之后, DispatcherServlet 渲染了对应的视图之后执行。
2.3、SpringBoot实现一个登录拦截器
预想:用户在访问首页接口,先判断一下session,如果session中有user的信息,说明用户已经登录过了,能正常访问首页接口,否则跳转到登录页面,让用户进行登录。
(1)首先随便定义一个实体类
@Data public class InterceptorUserEntity { private Integer id; private String name; }
(2)在controller里写3个接口
@RestController @Slf4j @RequestMapping("/api/interceptor") public class InterceptorUserController { @GetMapping("/setSession") @ResponseBody public Object setSession(HttpServletRequest request) { //将用户信息存放到session中 InterceptorUserEntity user = new InterceptorUserEntity(); user.setId(001); user.setName("张三"); request.getSession().setAttribute("user", user); return "已进行登录!"; } /** * 用户登录后跳转到首页 * * @return */ @GetMapping("/index") public Object index() { return "这里是首页!"; } /** * 登录页面 * * @return */ @GetMapping("/login") public Object login() { return "请进行登录!"; } }
(3)编写拦截器,可以通过要定义的Interceptor类实现handlerInterceptor接口。
@Component @Slf4j public class UserInterceptor implements HandlerInterceptor { @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { //业务拦截相关规则 //从session中获取用户的信息 InterceptorUserEntity user = (InterceptorUserEntity) request.getSession().getAttribute("user"); //判断用户是否登录 if (null == user) { response.sendRedirect(request.getContextPath() + "/api/interceptor/login"); return false; } //需要返回true,否则请求不会被控制器处理 return true; } @Override public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception { log.info("请求处理之后进行调用,但是在视图被渲染之前(Controller方法调用之后),如果异常发生,则该方法不会被调用"); } @Override public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception { log.info("在整个请求结束之后被调用,也就是在DispatcherServlet渲染了对应的视图之后执行(主要是用于进行资源清理工作)"); } }
(4)使用@Configuration注解写一个拦截器的配置文件。
@Configuration public class InterceptorConfig implements WebMvcConfigurer { @Autowired private UserInterceptor userInterceptor; @Override public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(userInterceptor).addPathPatterns("/api/interceptor/**").excludePathPatterns("/**/login", "/**/setSession"); } }
.addPathPatterns表示作用范围。(只在这个interceptor下的所有接口进行拦截)
.excludePathPatterns表示放行。这里把登录页面和已登录完成(setSession)放行。
(其它接口都会被拦截,然后跳转到login页面)
在用户没有请求过 /interceptor/setsession的时候,如果用户请求了 /interceptor/拦截器就会发挥作用, 把它跳转到/user/login的接口上去,如果用户请求过/interceptor/setsession的话, 再去请求/user/index拦截器就会放行,请求到相应的结果。
3、过滤器与拦截器的区别
相同点:
过滤器与拦截器都体现了AOP的编程思想,都可以实现例如日志、登录鉴权等功能。
不同点:
①:拦截器是基于java的反射机制(动态代理)的实现,而过滤器是基于函数的回调。
②:拦截器不依赖于servlet容器,而过滤器依赖于servlet容器。
③:拦截器只对Controller请求起作用,而过滤器则可以对几乎所有的请求起作用。
④:拦截器可以访问Controller上下文、值、栈里面的对象,而过滤器不可以。
⑤:在spring容器的生命周期中,拦截器可以多次被调用,而过滤器只能在容器初始化时被调用一次。
⑥:拦截器可以获取IOC容器中的各个bean,而过滤器不行,这点很重要,在拦截器里注入一个service,可以调用业务逻辑。
触发机制不同
过滤器Filter是在请求进入容器后,但在进入servlet之前进行预处理,请求结束是在servlet处理完以后。
拦截器 Interceptor 是在请求进入servlet后,在进入Controller之前进行预处理的,Controller 中渲染了对应的视图之后请求结束。
过滤器几乎可以对所有进入容器的请求起作用,而拦截器只会对Controller中请求或访问static目录下的资源请求起作用。
附:ChatGpt的回答
4.为什么说Filter是基于函数回调的?
相信经常查阅博客的朋友们都知道,我们去查阅Filter和Interceptor的区别的文章时,经常有这么一句话:
- 过滤器,依赖于servlet容器,在实现上基于函数回调。
- 拦截器,依赖于依赖于web框架,在实现上基于Java的反射机制,属于面向切面编
- 程(AOP)的一种运用。
那么为什么说Filter是基于函数回调的呢?函数回调又是怎么回事呢?
函数回调
A callback is a function that is passed as an argument to another function and is executed after its parent function has completed。
翻译:回调是作为参数传递给另一个函数的函数,在父函数完成后执行。
其实简单点说就是有2个类,A和B,A持有B的引用,当A中调用B的方法时就是回调 即:A(B)
接下来我们就看看Filter的工作图:
FilterChain是Filter的链路,每次url请求过来的时候就会进入链路然后根据事先设定好的Filter顺序按照顺序过滤下去,每次Filter完成了自己的操作后就会调用filterChain.doFilter(servletRequest, servletResponse);将请求发到FilterChain中的下一个Filter处理,如果没有下一个,就直接放行,然后进入servlet获取资源
Filter代码示例:
package com.hxkj.rbac.filter; import javax.servlet.*; import java.io.IOException; /** 字符编码过滤器 * Create by wangbin * 2019-03-28-14:52 */ public class EncodingFilter implements Filter { private String encode; @Override public void init(FilterConfig filterConfig) throws ServletException { //Filter初始化 // 读取web.xml中Filter配置的初始化参数 encode = filterConfig.getInitParameter("encode"); } @Override public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException { // 设置初始化的参数encode servletRequest.setCharacterEncoding(encode); servletResponse.setCharacterEncoding(encode); servletResponse.setContentType("text/html; charset=UTF-8"); filterChain.doFilter(servletRequest, servletResponse); // 放行,转到下一个过滤器 } @Override public void destroy() { //在Filter销毁前,完毕某些资源的回收 } }
从Filter的 doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) 这个方法中可以看到,FilterChain作为一个参数传递了进来,
当前的字符过滤Filter将字符编码设置好后,调用了FilterChain的 filterChain.doFilter(servletRequest, servletResponse); 让FilterChain去找下一个Filter进行新的处理,如果没有下一个Filter了,就直接放行,访问servlet获取资源,这就是典型的一个函数回调:
Filter就相当于类A FilterChain就相当于类B A(B)
FilterChain作为参数传递给了Filter的doFilter()方法,然后在该方法中又调用了FilterChain的doFilter()方法,即为:函数回调,这也就是为什么说Filter是基于函数回调的原因了