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<ContextRefreshedEvent> { |
---|
正如注释中描述的⼀样,⾸先通过 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 来获取当前在下⽤户数量是个很常见的使⽤场景,下⾯来介绍⼀下如何来使⽤。
/** @Component |
---|
可以看出,⾸先该监听器需要实现 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 才会被执⾏。
了解了该接⼝,接下来⾃定义⼀个拦截器。
/** logger.info("整个请求都处理完咯,DispatcherServlet也渲染了对应的视图咯,此时我 |
---|
OK,到此为⽌,拦截器已经定义完成,接下来就是对该拦截器进⾏拦截配置。
1.2 配置拦截器
在 Spring Boot 2.0 之前,我们都是直接继承 WebMvcConfigurerAdapter 类,然后重写 addInterceptors ⽅法来实现拦截器的配置。但是在 Spring Boot 2.0 之后,该⽅法已经被废弃了(当然,也可以继续⽤),取⽽代之的是 WebMvcConfigurationSupport ⽅法,如下:
@Configuration |
---|
在该配置中重写 addInterceptors ⽅法,将我们上⾯⾃定义的拦截器添加进去,addPathPatterns ⽅法是添加要拦截的请求,这⾥我们拦截所有的请求。这样就配置好拦截器了,接下来写⼀个 Controller 测试⼀下:
@Controller |
---|
让其跳转到 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 类,直接实现 WebMvcConfigurer 接⼝,然后重写addInterceptors ⽅法,将⾃定义的拦截器添加进去即可,如下:
@Configuration |
---|
这样就⾮常⽅便了,实现 WebMvcConfigure 接⼝的话,不会拦截 Spring Boot 默认的静态资源。这两种⽅式都可以,具体他们之间的细节,感兴趣的读者可以做进⼀步的研究,由于这两种⽅式的不同,继承
WebMvcConfigurationSupport 类的⽅式可以⽤在前后端分离的项⽬中,后台不需要访问静态资源(就不需要放开静态资源了);实现 WebMvcConfigure 接⼝的⽅式可以⽤在⾮前后端分离的项⽬中,因为需要读取⼀些图⽚、css、js⽂件等等。
2. 拦截器使⽤实例
2.1 判断⽤户有没有登录
⼀般⽤户登录功能我们可以这么做,要么往 session 中写⼀个 user,要么针对每个 user ⽣成⼀个 token,第⼆种要更好⼀点,那么针对第⼆种⽅式,如果⽤户登录成功了,每次请求的时候都会带上该⽤户的 token,如果未登录,则没有该 token,服务端可以检测这个 token 参数的有⽆来判断⽤户有没有登录,从⽽实现拦截功
能。我们改造⼀下 preHandle ⽅法,如下:
@Override |
---|
重启项⽬,在浏览器中输⼊ localhost:8080/interceptor/test 后查看控制台⽇志,发现被拦截,如果在
浏览器中输⼊ localhost:8080/interceptor/test?token=123 即可正常往下⾛。
2.2 取消拦截操作
根据上⽂,如果我要拦截所有 /admin 开头的 url 请求的话,需要在拦截器配置中添加这个前缀,但是在实际项⽬中,可能会有这种场景出现:某个请求也是 /admin 开头的,但是不能拦截,⽐如 /admin/login 等等,这
样的话⼜需要去配置。那么,可不可以做成⼀个类似于开关的东⻄,哪⾥不需要拦截,我就在哪⾥弄个开关上去,做成这种灵活的可插拔的效果呢?
是可以的,我们可以定义⼀个注解,该注解专门⽤来取消拦截操作,如果某个 Controller 中的⽅法我们不需要拦截掉,即可在该⽅法上加上我们⾃定义的注解即可,下⾯先定义⼀个注解:
/** |
---|
然后在 Controller 中的某个⽅法上添加该注解,在拦截器处理⽅法中添加该注解取消拦截的逻辑,如下:
@Override |
---|
Controller 中的⽅法代码可以参见源码,重启项⽬在浏览器中输⼊
http://localhost:8080/interceptor/test2?token=123 测试⼀下,可以看出,加了该注解的⽅法不会被拦截。
3. 总结
本节主要介绍了 Spring Boot 中拦截器的使⽤,从拦截器的创建、配置,到拦截器对静态资源的影响,都做了详细的分析。Spring Boot 2.0 之后拦截器的配置⽀持两种⽅式,可以根据实际情况选择不同的配置⽅式。最后结合实际中的使⽤,举了两个常⽤的场景,希望读者能够认真消化,掌握拦截器的使⽤。