第九章 监听器、拦截器

Spring Boot中使⽤监听器

1. 监听器介绍
什么是 web 监听器?web 监听器是⼀种 Servlet 中特殊的类,它们能帮助开发者监听 web 中特定的事件,⽐如ServletContext, HttpSession, ServletRequest 的创建和销毁;变量的创建、销毁和修改等。可以在某些动作前后增加处理,实现监控。
2. Spring Boot中监听器的使⽤
web 监听器的使⽤场景很多,⽐如监听 servlet 上下⽂⽤来初始化⼀些数据、监听 http session ⽤来获取当前在线的⼈数、监听客户端请求的 servlet request 对象来获取⽤户的访问信息等等。这⼀节中,我们主要通过这三个实际的使⽤场景来学习⼀下 Spring Boot 中监听器的使⽤。
2.1 监听Servlet上下⽂对象
监听 servlet 上下⽂对象可以⽤来初始化数据,⽤于缓存。什么意思呢?我举⼀个很常见的场景,⽐如⽤户在点击某个站点的⾸页时,⼀般都会展现出⾸页的⼀些信息,⽽这些信息基本上或者⼤部分时间都保持不变的,但是这些信息都是来⾃数据库。如果⽤户的每次点击,都要从数据库中去获取数据的话,⽤户量少还可以接受,如果⽤户量⾮常⼤的话,这对数据库也是⼀笔很⼤的开销。

针对这种⾸页数据,⼤部分都不常更新的话,我们完全可以把它们缓存起来,每次⽤户点击的时候,我们都直接从缓存中拿,这样既可以提⾼⾸页的访问速度,⼜可以降低服务器的压⼒。如果做的更加灵活⼀点,可以再加个定时器,定期的来更新这个⾸页缓存。就类似与 CSDN 个⼈博客⾸页中排名的变化⼀样。
下⾯我们针对这个功能,来写⼀个 demo,在实际中,读者可以完全套⽤该代码,来实现⾃⼰项⽬中的相关逻辑。⾸先写⼀个 Service,模拟⼀下从数据库查询数据:

@Service

public class UserService {

/**

* 获取用户信息

* @return

*/

public User getUser() {

// 实际中会根据具体的业务场景,从数据库中查询对应的信息

return new User(1L, "倪升武", "123456");

}

}


然后写⼀个监听器,实现 ApplicationListener<ContextRefreshedEvent> 接⼝,重写
onApplicationEvent ⽅法,将 ContextRefreshedEvent 对象传进去。如果我们想在加载或刷新应⽤上下⽂
时,也重新刷新下我们预加载的资源,就可以通过监听 ContextRefreshedEvent 来做这样的事情。如下:

/**
* 使用ApplicationListener来初始化一些数据到application域中的监听器
* @author shengni ni
* @date 2018/07/05
*/
@Component
public class MyServletContextListener implements

ApplicationListener<ContextRefreshedEvent> {
@Override
public void onApplicationEvent(ContextRefreshedEvent contextRefreshedEvent) {
// 先获取到application上下文
ApplicationContext applicationContext =
contextRefreshedEvent.getApplicationContext();
// 获取对应的service
UserService userService = applicationContext.getBean(UserService.class);
User user = userService.getUser();
// 获取application域对象,将查到的信息放到application域中
ServletContext application =
applicationContext.getBean(ServletContext.class);
application.setAttribute("user", user);
}
}




正如注释中描述的⼀样,⾸先通过 contextRefreshedEvent 来获取 application 上下⽂,再通过 application 上下
⽂来获取 UserService 这个 bean,项⽬中可以根据实际业务场景,也可以获取其他的 bean,然后再调⽤⾃⼰的
业务代码获取相应的数据,最后存储到 application 域中,这样前端在请求相应数据的时候,我们就可以直接从
application 域中获取信息,减少数据库的压⼒。下⾯写⼀个 Controller 直接从 application 域中获取 user 信息
来测试⼀下。

@RestController
@RequestMapping("/listener")
public class TestController {
@GetMapping("/user")
public User getUser(HttpServletRequest request) {
ServletContext application = request.getServletContext();
return (User) application.getAttribute("user");
}
}


启动项⽬,在浏览器中输⼊ http://localhost:8080/listener/user 测试⼀下即可,如果正常返回 user 信息,那么说明数据已经缓存成功。不过 application 这种是缓存在内存中,对内存会有消耗,后⾯的课程中我会讲到 redis,到时候再给⼤家介绍⼀下 redis 的缓存。
2.2 监听HTTP会话 Session对象
监听器还有⼀个⽐较常⽤的地⽅就是⽤来监听 session 对象,来获取在线⽤户数量,现在有很多开发者都有⾃⼰的⽹站,监听 session 来获取当前在下⽤户数量是个很常见的使⽤场景,下⾯来介绍⼀下如何来使⽤。

 

/**
* 使用HttpSessionListener统计在线用户数的监听器
* @author shengwu ni
* @date 2018/07/05
*/

@Component
public class MyHttpSessionListener implements HttpSessionListener {
private static final Logger logger =
LoggerFactory.getLogger(MyHttpSessionListener.class);
/**
* 记录在线的用户数量
*/
public Integer count = 0;
@Override
public synchronized void sessionCreated(HttpSessionEvent httpSessionEvent) {
logger.info("新用户上线了");
count++;
httpSessionEvent.getSession().getServletContext().setAttribute("count",
count);
}
@Override
public synchronized void sessionDestroyed(HttpSessionEvent httpSessionEvent) {
logger.info("用户下线了");
count--;
httpSessionEvent.getSession().getServletContext().setAttribute("count",
count);
}
}
 

 

可以看出,⾸先该监听器需要实现 HttpSessionListener 接⼝,然后重写 sessionCreated 和
sessionDestroyed ⽅法,在 sessionCreated ⽅法中传递⼀个 HttpSessionEvent 对象,然后将当前 session
中的⽤户数量加1,sessionDestroyed ⽅法刚好相反,不再赘述。然后我们写⼀个 Controller 来测试⼀下。

@RestController
@RequestMapping("/listener")
public class TestController {
/**
* 获取当前在线人数,该方法有bug
* @param request
* @return
*/
@GetMapping("/total")
public String getTotalUser(HttpServletRequest request) {
Integer count = (Integer)
request.getSession().getServletContext().getAttribute("count");
return "当前在线人数:" + count;
}
}


该 Controller 中是直接获取当前 session 中的⽤户数量,启动服务器,在浏览器中输⼊
localhost:8080/listener/total 可以看到返回的结果是1,再打开⼀个浏览器,请求相同的地址可以看到
count 是 2 ,这没有问题。但是如果关闭⼀个浏览器再打开,理论上应该还是2,但是实际测试却是 3。原因是
session 销毁的⽅法没有执⾏(可以在后台控制台观察⽇志打印情况),当重新打开时,服务器找不到⽤户原来
的 session,于是⼜重新创建了⼀个 session,那怎么解决该问题呢?我们可以将上⾯的 Controller ⽅法改造⼀
下:


@GetMapping("/total2")
public String getTotalUser(HttpServletRequest request, HttpServletResponse
response) {
Cookie cookie;
try {
// 把sessionId记录在浏览器中
cookie = new Cookie("JSESSIONID",
URLEncoder.encode(request.getSession().getId(), "utf-8"));
cookie.setPath("/");
//设置cookie有效期为2天,设置长一点
cookie.setMaxAge( 48*60 * 60);
response.addCookie(cookie);
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
}
Integer count = (Integer)
request.getSession().getServletContext().getAttribute("count");
return "当前在线人数:" + count;
}

 

可以看出,该处理逻辑是让服务器记得原来那个 session,即把原来的 sessionId 记录在浏览器中,下次再打开
时,把这个 sessionId 传过去,这样服务器就不会重新再创建了。重启⼀下服务器,在浏览器中再次测试⼀
下,即可避免上⾯的问题。
2.3 监听客户端请求Servlet Request对象
使⽤监听器获取⽤户的访问信息⽐较简单,实现 ServletRequestListener 接⼝即可,然后通过 request 对象获取
⼀些信息。如下:

/**
* 使用ServletRequestListener获取访问信息
* @author shengwu ni
* @date 2018/07/05
*/
@Component
public class MyServletRequestListener implements ServletRequestListener {
private static final Logger logger =
LoggerFactory.getLogger(MyServletRequestListener.class);
@Override
public void requestInitialized(ServletRequestEvent servletRequestEvent) {
HttpServletRequest request = (HttpServletRequest)
servletRequestEvent.getServletRequest();
logger.info("session id为:{}", request.getRequestedSessionId());
logger.info("request url为:{}", request.getRequestURL());
request.setAttribute("name", "倪升武");
}
@Override
public void requestDestroyed(ServletRequestEvent servletRequestEvent) {
logger.info("request end");
HttpServletRequest request = (HttpServletRequest)
servletRequestEvent.getServletRequest();
logger.info("request域中保存的name值为:{}", request.getAttribute("name"));
}
}

这个⽐较简单,不再赘述,接下来写⼀个 Controller 测试⼀下即可。


@GetMapping("/request")
public String getRequestInfo(HttpServletRequest request) {
System.out.println("requestListener中的初始化的name数据:" +
request.getAttribute("name"));
return "success";
}



3. Spring Boot中⾃定义事件监听
在实际项⽬中,我们往往需要⾃定义⼀些事件和监听器来满⾜业务场景,⽐如在微服务中会有这样的场景:微服务 A 在处理完某个逻辑之后,需要通知微服务 B 去处理另⼀个逻辑,或者微服务 A 处理完某个逻辑之后,需要将数据同步到微服务 B,这种场景⾮常普遍,这个时候,我们可以⾃定义事件以及监听器来监听,⼀旦监听
到微服务 A 中的某事件发⽣,就去通知微服务 B 处理对应的逻辑。
3.1 ⾃定义事件
⾃定义事件需要继承 ApplicationEvent 对象,在事件中定义⼀个 User 对象来模拟数据,构造⽅法中将 User 对
象传进来初始化。如下:

/**
* 自定义事件
* @author shengwu ni
* @date 2018/07/05
*/
public class MyEvent extends ApplicationEvent {SpringBoot经典学习笔记.md
2020/6/15
71 / 122
private User user;
public MyEvent(Object source, User user) {
super(source);
this.user = user;
}
// 省去get、set方法
}


3.2 ⾃定义监听器
接下来,⾃定义⼀个监听器来监听上⾯定义的 MyEvent 事件,⾃定义监听器需要实现 ApplicationListener
接⼝即可。如下:

/**
* 自定义监听器,监听MyEvent事件
* @author shengwu ni
* @date 2018/07/05
*/
@Component
public class MyEventListener implements ApplicationListener<MyEvent> {
@Override
public void onApplicationEvent(MyEvent myEvent) {
// 把事件中的信息获取到
User user = myEvent.getUser();
// 处理事件,实际项目中可以通知别的微服务或者处理其他逻辑等等
System.out.println("用户名:" + user.getUsername());
System.out.println("密码:" + user.getPassword());
}
}



然后重写 onApplicationEvent ⽅法,将⾃定义的 MyEvent 事件传进来,因为该事件中,我们定义了 User 对象(该对象在实际中就是需要处理的数据,在下⽂来模拟),然后就可以使⽤该对象的信息了。
OK,定义好了事件和监听器之后,需要⼿动发布事件,这样监听器才能监听到,这需要根据实际业务场景来触发,针对本⽂的例⼦,我写个触发逻辑,如下:

/**
* UserService
* @author shengwu ni
*/
@Service
public class UserService {
@Resource
private ApplicationContext applicationContext;SpringBoot经典学习笔记.md
2020/6/15
72 / 122
/**
* 发布事件
* @return
*/
public User getUser2() {
User user = new User(1L, "倪升武", "123456");
// 发布事件
MyEvent event = new MyEvent(this, user);
applicationContext.publishEvent(event);
return user;
}
}

 

在 service 中注⼊ ApplicationContext,在业务代码处理完之后,通过 ApplicationContext 对象⼿动发布
MyEvent 事件,这样我们⾃定义的监听器就能监听到,然后处理监听器中写好的业务逻辑。
最后,在 Controller 中写⼀个接⼝来测试⼀下:

@GetMapping("/request")
public String getRequestInfo(HttpServletRequest request) {
System.out.println("requestListener中的初始化的name数据:" +
request.getAttribute("name"));
return "success";
}

 

在浏览器中输⼊ http://localhost:8080/listener/publish,然后观察⼀下控制台打印的⽤户名和密码,
即可说明⾃定义监听器已经⽣效。
4. 总结
本课系统的介绍了监听器原理,以及在 Spring Boot 中如何使⽤监听器,列举了监听器的三个常⽤的案例,有很好的实战意义。最后讲解了项⽬中如何⾃定义事件和监听器,并结合微服务中常见的场景,给出具体的代码模型,均能运⽤到实际项⽬中去,希望读者认真消化。

 

2、Spring Boot中使⽤拦截器

拦截器的原理很简单,是 AOP 的⼀种实现,专门拦截对动态资源的后台请求,即拦截对控制层的请求。使⽤场景⽐较多的是判断⽤户是否有权限请求后台,更拔⾼⼀层的使⽤场景也有,⽐如拦截器可以结合 websocket ⼀起使⽤,⽤来拦截 websocket 请求,然后做相应的处理等等。拦截器不会拦截静态资源,Spring Boot 的默认
静态⽬录为 resources/static,该⽬录下的静态页⾯、js、css、图⽚等等,不会被拦截(也要看如何实现,有些情况也会拦截,我在下⽂会指出)。
1. 拦截器的快速使⽤
使⽤拦截器很简单,只需要两步即可:定义拦截器和配置拦截器。在配置拦截器中,Spring Boot 2.0 以后的版本和之前的版本有所不同,我会重点讲解⼀下这⾥可能出现的坑。
1.1 定义拦截器
定义拦截器,只需要实现 HandlerInterceptor 接⼝,HandlerInterceptor 接⼝是所有⾃定义拦截器或者Spring Boot 提供的拦截器的⿐祖,所以,⾸先来了解下该接⼝。该接⼝中有三个⽅法: preHandle(……)、postHandle(……) 和 afterCompletion(……) 。

preHandle(……) ⽅法:该⽅法的执⾏时机是,当某个 url 已经匹配到对应的 Controller 中的某个⽅法,且在这个⽅法执⾏之前。所以 preHandle(……) ⽅法可以决定是否将请求放⾏,这是通过返回值来决定的,返回 true 则放⾏,返回 false 则不会向后执⾏。
postHandle(……) ⽅法:该⽅法的执⾏时机是,当某个 url 已经匹配到对应的 Controller 中的某个⽅法,且在执⾏完了该⽅法,但是在 DispatcherServlet 视图渲染之前。所以在这个⽅法中有个ModelAndView 参数,可以在此做⼀些修改动作。
afterCompletion(……) ⽅法:顾名思义,该⽅法是在整个请求处理完成后(包括视图渲染)执⾏,这时做⼀些资源的清理⼯作,这个⽅法只有在 preHandle(……) 被成功执⾏后并且返回 true 才会被执⾏。

了解了该接⼝,接下来⾃定义⼀个拦截器。

/**
* 自定义拦截器
* @author shengwu ni
* @date 2018/08/03
*/
public class MyInterceptor implements HandlerInterceptor {
private static final Logger logger =
LoggerFactory.getLogger(MyInterceptor.class);
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse
response, Object handler) throws Exception {
HandlerMethod handlerMethod = (HandlerMethod) handler;
Method method = handlerMethod.getMethod();
String methodName = method.getName();
logger.info("====拦截到了方法:{},在该方法执行之前执行====", methodName);
// 返回true才会继续执行,返回false则取消当前请求
return true;
}
@Override
public void postHandle(HttpServletRequest request, HttpServletResponse
response, Object handler, ModelAndView modelAndView) throws Exception {
logger.info("执行完方法之后进执行(Controller方法调用之后),但是此时还没进行视图
渲染");
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {

logger.info("整个请求都处理完咯,DispatcherServlet也渲染了对应的视图咯,此时我
可以做一些清理的工作了");
}
}


OK,到此为⽌,拦截器已经定义完成,接下来就是对该拦截器进⾏拦截配置。

1.2 配置拦截器
在 Spring Boot 2.0 之前,我们都是直接继承 WebMvcConfigurerAdapter 类,然后重写 addInterceptors ⽅法来实现拦截器的配置。但是在 Spring Boot 2.0 之后,该⽅法已经被废弃了(当然,也可以继续⽤),取⽽代之的是 WebMvcConfigurationSupport ⽅法,如下:

@Configuration
public class MyInterceptorConfig extends WebMvcConfigurationSupport {
@Override
protected void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new MyInterceptor()).addPathPatterns("/**");
super.addInterceptors(registry);
}
}


在该配置中重写 addInterceptors ⽅法,将我们上⾯⾃定义的拦截器添加进去,addPathPatterns ⽅法是添加要拦截的请求,这⾥我们拦截所有的请求。这样就配置好拦截器了,接下来写⼀个 Controller 测试⼀下:

@Controller
@RequestMapping("/interceptor")
public class InterceptorController {
@RequestMapping("/test")
public String test() {
return "hello";
}
}

让其跳转到 hello.html 页⾯,直接在 hello.html 中输出 hello interceptor 即可。启动项⽬,在浏览器中输⼊ localhost:8080/interceptor/test 看⼀下控制台的⽇志:

====拦截到了方法:test,在该方法执行之前执行====
执行完方法之后进执行(Controller方法调用之后),但是此时还没进行视图渲染
整个请求都处理完咯,DispatcherServlet也渲染了对应的视图咯,此时我可以做一些清理的工作了

可以看出拦截器已经⽣效,并能看出其执⾏顺序。

1.3 解决静态资源被拦截问题
上⽂中已经介绍了拦截器的定义和配置,但是这样是否就没问题了呢?其实不然,如果使⽤上⾯这种配置的话,我们会发现⼀个缺陷,那就是静态资源被拦截了。可以在 resources/static/ ⽬录下放置⼀个图⽚资源或者
html ⽂件,然后启动项⽬直接访问,即可看到⽆法访问的现象。也就是说,虽然 Spring Boot 2.0 废弃了WebMvcConfigurerAdapter,但是 WebMvcConfigurationSupport ⼜会导致默认的静态资源被拦截,这就需要我们⼿动将静态资源放开。
如何放开呢?除了在 MyInterceptorConfig 配置类中重写 addInterceptors ⽅法外,还需要再重写⼀个⽅法:addResourceHandlers,将静态资源放开:

/**
* 用来指定静态资源不被拦截,否则继承WebMvcConfigurationSupport这种方式会导致静态资源无
法直接访问
* @param registry
*/
@Override
protected void addResourceHandlers(ResourceHandlerRegistry registry) {
registry.addResourceHandler("/**").addResourceLocations("classpath:/static/");
super.addResourceHandlers(registry);
}

这样配置好之后,重启项⽬,静态资源也可以正常访问了。如果你是个善于学习或者研究的⼈,那肯定不会⽌步于此,没错,上⾯这种⽅式的确能解决静态资源⽆法访问的问题,但是,还有更⽅便的⽅式来配置。
我们不继承 WebMvcConfigurationSupport 类,直接实现 WebMvcConfigurer 接⼝,然后重写addInterceptors ⽅法,将⾃定义的拦截器添加进去即可,如下:

@Configuration
public class MyInterceptorConfig implements WebMvcConfigurer {
@Override
public void addInterceptors(InterceptorRegistry registry) {
// 实现WebMvcConfigurer不会导致静态资源被拦截
registry.addInterceptor(new MyInterceptor()).addPathPatterns("/**");
}
}

这样就⾮常⽅便了,实现 WebMvcConfigure 接⼝的话,不会拦截 Spring Boot 默认的静态资源。这两种⽅式都可以,具体他们之间的细节,感兴趣的读者可以做进⼀步的研究,由于这两种⽅式的不同,继承
WebMvcConfigurationSupport 类的⽅式可以⽤在前后端分离的项⽬中,后台不需要访问静态资源(就不需要放开静态资源了);实现 WebMvcConfigure 接⼝的⽅式可以⽤在⾮前后端分离的项⽬中,因为需要读取⼀些图⽚、css、js⽂件等等。

2. 拦截器使⽤实例
2.1 判断⽤户有没有登录

⼀般⽤户登录功能我们可以这么做,要么往 session 中写⼀个 user,要么针对每个 user ⽣成⼀个 token,第⼆种要更好⼀点,那么针对第⼆种⽅式,如果⽤户登录成功了,每次请求的时候都会带上该⽤户的 token,如果未登录,则没有该 token,服务端可以检测这个 token 参数的有⽆来判断⽤户有没有登录,从⽽实现拦截功
能。我们改造⼀下 preHandle ⽅法,如下:

@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response,
Object handler) throws Exception {
HandlerMethod handlerMethod = (HandlerMethod) handler;
Method method = handlerMethod.getMethod();
String methodName = method.getName();
logger.info("====拦截到了方法:{},在该方法执行之前执行====", methodName);
// 判断用户有没有登陆,一般登陆之后的用户都有一个对应的token
String token = request.getParameter("token");
if (null == token || "".equals(token)) {
logger.info("用户未登录,没有权限执行……请登录");
return false;
}
// 返回true才会继续执行,返回false则取消当前请求
return true;
}

重启项⽬,在浏览器中输⼊ localhost:8080/interceptor/test 后查看控制台⽇志,发现被拦截,如果在
浏览器中输⼊ localhost:8080/interceptor/test?token=123 即可正常往下⾛。

2.2 取消拦截操作
根据上⽂,如果我要拦截所有 /admin 开头的 url 请求的话,需要在拦截器配置中添加这个前缀,但是在实际项⽬中,可能会有这种场景出现:某个请求也是 /admin 开头的,但是不能拦截,⽐如 /admin/login 等等,这
样的话⼜需要去配置。那么,可不可以做成⼀个类似于开关的东⻄,哪⾥不需要拦截,我就在哪⾥弄个开关上去,做成这种灵活的可插拔的效果呢?
是可以的,我们可以定义⼀个注解,该注解专门⽤来取消拦截操作,如果某个 Controller 中的⽅法我们不需要拦截掉,即可在该⽅法上加上我们⾃定义的注解即可,下⾯先定义⼀个注解:

/**
* 该注解用来指定某个方法不用拦截
*/
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface UnInterception {
}

然后在 Controller 中的某个⽅法上添加该注解,在拦截器处理⽅法中添加该注解取消拦截的逻辑,如下:

@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response,
Object handler) throws Exception {
HandlerMethod handlerMethod = (HandlerMethod) handler;
Method method = handlerMethod.getMethod();
String methodName = method.getName();
logger.info("====拦截到了方法:{},在该方法执行之前执行====", methodName);
// 通过方法,可以获取该方法上的自定义注解,然后通过注解来判断该方法是否要被拦截
// @UnInterception 是我们自定义的注解
UnInterception unInterception = method.getAnnotation(UnInterception.class);
if (null != unInterception) {
return true;
}
// 返回true才会继续执行,返回false则取消当前请求
return true;
}

Controller 中的⽅法代码可以参见源码,重启项⽬在浏览器中输⼊
http://localhost:8080/interceptor/test2?token=123 测试⼀下,可以看出,加了该注解的⽅法不会被拦截。

3. 总结
本节主要介绍了 Spring Boot 中拦截器的使⽤,从拦截器的创建、配置,到拦截器对静态资源的影响,都做了详细的分析。Spring Boot 2.0 之后拦截器的配置⽀持两种⽅式,可以根据实际情况选择不同的配置⽅式。最后结合实际中的使⽤,举了两个常⽤的场景,希望读者能够认真消化,掌握拦截器的使⽤。

 

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值