摘要:Servlet规范中有3个非常特殊的Servlet,分别是过滤器、监听器和拦截器,如果使用得当,利用它们可以简单地完成一般Servlet才能实现的烦琐功能。下面分别介绍过滤器、监听器和拦截器。
Servlet规范中有3个非常特殊的Servlet,分别是过滤器、监听器和拦截器,如果使用得当,利用它们可以简单地完成一般Servlet才能实现的烦琐功能。下面分别介绍过滤器、监听器和拦截器。
过滤器(Filter)放在Web资源之前,可以在前端请求抵达Web资源之前被截获,并且还可以在资源返回客户之前截获输出的请求。过滤器是用来拦截请求的,处于客户端与被请求资源之间,目的是重用代码。在一个项目中可以配置多个过滤器,一个请求会依次通过配置的所有过滤器。
Web项目常用的过滤器有以下3种:
用户授权的过滤器:负责检查用户请求,根据请求信息过滤用户的非法请求。
日志过滤器:详细记录某些特殊的用户请求。
负责编码/解码的过滤器:对请求参数的编码和解码。
Java中的过滤器是一种特殊的Servlet,它不能处理用户请求,也不能为客户端生成响应信息,它主要用于对HttpServletRequest进行前处理,也可以对HttpServletResponse进行后处理,是一个典型的处理链程序。
监听器也是一种特殊的servlet,能够监听Web项目中特定的事件。例如,监听ServletContext、HttpSession和ServlerRequest对象的创建和销毁及各种变量的创建、销毁和修改等,还可以在一些请求前后增加监听处理,实现监听。
监听器的父接口为java.util.EventListener,所有监听器都需要实现此接口。常见的监听器有:HttpSessionListener用来监听Session,ContextLoaderListener是在启动Web容器时自动监听装配ApplicationContext的配置信息,RequestContextListener监听请求的处理。
拦截器(Interceptor)有点类似于Servlet中的过滤器,它主要用于拦截用户发送的请求并进行相应的处理。拦截器可以在项目中进行权限验证、记录请求信息的日志、判断用户是否登录等。
通过前面小节的介绍可知,过滤器和拦截器在功能上有部分重叠,定义也很相似,一些功能既可以通过过滤器实现,也可以通过拦截器实现,但是它们还是有区别的:
拦截器是基于Java的反射机制,而过滤器是基于函数回调。
拦截器的使用不依赖于Servlet容器,而过滤器依赖于Servlet容器。
拦截器只能对Controller请求起作用,而过滤器则可以对几乎所有的请求(包括静态资源和文件等)起作用。
拦截器可以访问请求的上下文、值栈里的对象,而过滤器不能访问。
在一个请求的生命周期中,可以设置多个拦截器依次运行,而过滤器只能在容器初始化时被调用一次。
拦截器可以获取Spring IoC容器中的各个Bean,而过滤器却获取不到,在拦截器中可以注入业务service,处理业务逻辑。
过滤器是在请求Servlet之前拦截请求,对请求进行预处理。请求结束返回也是在Servlet处理完后再返回给前端。而拦截器是在请求处理之前进行拦截处理。
Spring Web提供了很多过滤器,这些过滤器都在org.springframework.web.filter包中,全部实现了javax.servlet.Filter接口。如果项目中需要自定义过滤器,有以下4种实现方式:直接实现Filter接口。
继承抽象类GenericFilterBean,此类已经实现了javax.servlet.Filter接口,是普通的Filter实现。
继承抽象类OncePerRequestFilter,此类是GenericFilterBean的直接子类,主要用于对请求参数的处理。
继承抽象类AbstractRequestLoggingFilter,该类为OncePerRequestFilter的直接子类,主要用于处理日志过滤。
下面实现一个自定义的过滤器,过滤请求URL为/test1的请求。如果是这个请求,则打印这个请求的所有参数并直接返回不再继续处理业务,否则直接放行。首先实现一个自定义的Filter,代码如下:
package com.example.thymeleafdemo.filter;
import org.springframework.stereotype.Component;
import javax.servlet.*;
import javax.servlet.http.HttpServletRequest;
import java.io.IOException;
import java.util.Arrays;
import java.util.Map;
/**
* 自定义过滤器的实现
*/
@Component
public class MyHttpFilter implements Filter {
/**
* 过滤方法
*/
@Override
public void doFilter(ServletRequest request, ServletResponse
response,
FilterChain chain) throws IOException, Servlet
Exception {
HttpServletRequest servletRequest = (HttpServletRequest)
request;
String requestURI = servletRequest.getRequestURI;
if ("/test1".equals(requestURI)) { Mapmap =servletRequest.getParameterMap;
for (Map.Entryentry : map.entrySet) {System.out.println("请求的参数名字是:" + entry.getKey
+ " ,请求的值是:" +
Arrays.toString(entry.getValue));
return;
}
}
//放行,继续后面的业务处理
chain.doFilter(servletRequest,response);
}
}
在UserController.java文件中添加test1方法的代码如下:
@ResponseBody
@GetMapping("/test1")
public String test1{
return "success";
}
启动Spring Boot项目,打开浏览器访问http://localhost:8080/test1?name=cc,页面返回结果如图4.5所示,没有出现业务代码返回的成功提示,查看控制台发现已经打印出入参的参数,如图4.6所示。至此就完成了自定义过滤器过滤请求信息的操作。
综合以上的自定义过滤器,总结过滤器的使用场景如下:
执行目标资源之前执行预处理,如设置编码;
通过条件判断是否放行,如校验当前用户是否已经登录,或某些用户的IP是否被禁用;
目标资源执行后,后续的特殊处理工作,如处理目标资源输出的数据。
要使用Spring MVC中的拦截器,首先需要对拦截器类进行定义和配置。
通常,拦截器类可以通过两种方式来定义。
通过实现HandlerInterceptor接口或继承HandlerInterceptor接口的实现类(如HandlerInterceptorAdapter)来定义拦截器。
通过实现WebRequestInterceptor接口或继承WebRequestInterceptor接口的实现类来定义拦截器,此接口专门用于处理Web请求。
在项目开发中,一个常见的需求就是打印所有的请求入参,方便以后问题的定位和接口的调试。下面我们自定义一个拦截器来处理所有的请求,并且把所有请求的URL和日志都打印出来,具体代码如下:
package com.example.thymeleafdemo.inter;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.HandlerInterceptor;import org.springframework.web.servlet.ModelAndView;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.util.Arrays;
import java.util.Map;
/**
* 自定义拦截器
*/
@Slf4j
@Component
public class MyHandlerInterceptor implements HandlerInterceptor {
/**
* 在业务代码处理之前进行参数记录
*/
@Override
public boolean preHandle(HttpServletRequest request, HttpServlet
Response response, Object handler) throws Exception {
System.out.println("拦截器: preHandle在控制器的处理请求方法调用之后解
析视图之前执行");
String requestURI = request.getRequestURI;
MapparameterMap = request.getParameterMap;StringBuilder sb = new StringBuilder;
for (Map.Entryentry :parameterMap.entrySet) {
sb.append(entry.getKey).append("=").append(Arrays.toString
(entry.getValue));
sb.append(",");
}
log.info("拦截器: 请求的url是:{},请求的参数是:
{}",requestURI,sb.toString);
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,
HttpServlet Response response,
Object handler, Exception ex) throws
Exception {
System.out.println("拦截器: afterCompletion方法在控制器的处理请求方
法执行完成后执行," + "即视图渲染结束之后执行");
}
}
完成拦截的具体方法后,配置拦截器拦截哪些URL,放行静态资源的请求,拦截剩下的URL。
package com.example.thymeleafdemo.inter;
import org.Springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.Interceptor
Registration;
Registry;
import org.springframework.web.servlet.config.annotation.WebMvc
Configurer;
@Configuration
public class MyHandlerInterceptorConfig implements WebMvcConfigurer {
@Autowired
private MyHandlerInterceptor myHandlerInterceptor;
@Override
public void addInterceptors(InterceptorRegistry registry) { //注册TestInterceptor拦截器
InterceptorRegistration registration = registry.addInterceptor
(myHandlerInterceptor);
//所有路径都被拦截
registration.addPathPatterns("/**");
//添加不拦截路径
registration.excludePathPatterns(
"/**/*.html", //HTML静态资源
"/**/*.js", //JS静态资源
"/**/*.css" //CSS静态资源
);
}
}
启动项目,访问http://localhost:8080/addUser能看到结果页面,如图4.7所示。同时IDEA的控制台中打印出了请求日志,如图4.8所示,至此已经成功完成了拦截器请求参数的拦截打印。
根据以上自定义拦截器的实现代码,总结拦截器的执行步骤如下:
(1)根据请求的URL,找到可以处理请求的处理器和所有拦截器。
(2)按照配置顺序执行所有拦截器的preHandle方法。如果当前拦截器的preHandle方法返回true,则执行下一个拦截器的preHandle方法(执行下一个拦截器)。如果当前拦截器返回false,倒序执行所有已经执行了的拦截器的afterCompletion。
(3)如果任何一个拦截器返回false,则执行返回,不执行目标方法。
(4)所有拦截器都返回true,则执行目标方法。
(5)倒序执行所有拦截器的postHandle方法。
注:前面的步骤有任何异常都会触发倒序执行afterCompletion方法。
(6)页面成功渲染后,再倒序执行afterCompletion方法。
Java中的事件提供监听、订阅的功能,其内部实现原理是观察者设计模式,设计时间的发布和监听的目的也是为了系统业务逻辑之间的解耦,提高系统的可扩展性和可维护性。事件包括3个重要部分:EventObject、EventListener和Source。
EventObject:java.util.EventObject是事件状态对象的基类,它封装了事件源对象以及和事件相关的信息,所有Java的事件类都需要继承该类。
EventListener:java.util.EventListener是一个标记接口,所有事件监听器都需要实现该接口。事件监听器注册在事件源中,如果事件源的属性或状态发生改变,则会调用相应监听器内的回调方法。
Source:事件源不需要实现或继承任何接口或类,它是事件最初发生的源头。
Java的事件机制是一个典型的观察者模式。当对象间存在一对多关系时,则使用观察者模式(Observer Pattern)。当一个对象被修改时,则会自动通知依赖它的对象。观察者模式属于行为型模式,其类图如图4.9所示。
观察者模式使用3个类:Subject目标类、Observer观察者类和ConcreteSubject通知观察者类。Spring Boot框架对事件也做了支持,其对Java事件的实现、发布与监听一共分为3步。
(1)事件定义。
自定义的事件类继承自ApplicationEvent类,从而可以方便地重写发送事件的方法。
(2)事件发布。
注入事件发布类ApplicationEventPublisher使用publishEvent方法发布相应的事件。
(3)事件监听。
实现ApplicationListener接口,重写onApplicationEvent方法或者使用注解 @Event- Listener进行事件监听。根据上面的步骤自定义实现一个事件的发布和监听。新建一个事件来监听Spring Boot项目的启动,代码如下:
package com.example.thymeleafdemo.event;
import
org.springframework.boot.context.event.ApplicationStartingEvent;
import org.springframework.context.ApplicationListener;
import java.text.SimpleDateFormat;
import java.util.Date;
public class MyEventListener implements
ApplicationListener{@Override
public void onApplicationEvent(ApplicationStartingEvent event) {
SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd
HH:MM:ss");
Date date = new Date(event.getTimestamp);
System.out.println("ApplicationStartingEvent事件发布,时间是:" +
format.format(date));
}
}
同时修改Spring Boot的启动类:
package com.example.thymeleafdemo;
import com.example.thymeleafdemo.event.MyEventListener;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class ThymeleafDemoApplication {
public static void main(String args) {
SpringApplication application = new
SpringApplication(Thymeleaf DemoApplication.class); //加入自定义的监听类
application.addListeners(new MyEventListener);
application.run(args);
//SpringApplication.run(ThymeleafDemoApplication.class, args);
}
}
启动项目即可看到监听的Spring Boot启动事件,并在控制台上打印了日志,如图4.10所示。
完成了Spring Boot启动的监听后,下面新建一个Controller,通过程序来发布事件并且进行监听,代码如下:
package com.example.thymeleafdemo.event;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.web.bind.annotation.*;
/**
* 模拟触发事件
*/
@RestController
@RequestMapping("/event")
@Slf4jpublic class EventDemoController {
/**
* 注入 事件发布类
*/
@Autowired
ApplicationEventPublisher eventPublisher;
@GetMapping("/pushObject")
public String pushObject(@RequestParam("code") int code,
@RequestParam("message") String message) {
log.info("发布对象事件:{},{}", code, message);
Result result = new Result;result.setCode(code);
result.setMessage(message);
eventPublisher.publishEvent(result);
return "对象事件发布成功!";
}
}
配置当前Controller的事件监听:
package com.example.thymeleafdemo.event;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.event.EventListener;
/**
* 监听配置类
*/
@Configuration
@Slf4j
public class EventListenerConfig {
@EventListener
public void handleEvent(Object event) {
log.info("事件:{}", event); }
/**
* 监听 code为cc的事件
*/
@EventListener(condition = "#myCustomEvent.result.code == 'cc'")
public void handleCustomEventByCondition(MyCustomEvent customEvent)
{
//监听 MyCustomEvent事件
log.info("监听到code为'cc'的MyCustomEvent事件," +
"消息为:{}, 发布时间:{}", customEvent.getResult,
customEvent.getTimestamp);
}
@EventListener
public void handleObjectEvent(Result result) {
log.info("监听到对象事件,消息为:{}", result);
}
}
定义发布的事件和结果代码如下:
package com.example.thymeleafdemo.event;
import org.springframework.context.ApplicationEvent;
public class MyCustomEvent extends ApplicationEvent {
private Result result;
public MyCustomEvent(Object source, Result result) {
super(source);
this.result = result;
}
public Result getResult {
return this.result;
}
}
package com.example.thymeleafdemo.event;import lombok.Data;
import lombok.ToString;
@ToString
public class Resultprivate String message;
private int code;
private T data;
}
启动当前项目,访问http://localhost:8080/event/pushObject?code=1&message=cc,查看IDEA控制台可以看到打印的Controller被访问的事件监听信息,如图4.11所示。
至此就完成了Spring Boot的事件发布和监听。事件的发布和监听通常用在要接收消息进行业务处理,但消息的来源不固定,触发的事件也不固定的场景。最简单的方式是通过写接口然后二次调用接口的方式来实现,但二次调用接口多了一次操作,会降低性能,因此通过事件监听的方式来实现。
来源:程序员高级码农II一点号