一文带你掌握java web的监听器
什么是监听器
我们知道java web的三大支柱分别是:servlet,filter,listener。
- servlet是执行业务逻辑的工具,也是spring MVC的核心,因为springMVC的核心功能的实现就是基于servlet。
- filter是对请求和响应做处理的工具,可以看做是项目内部的nginx,著名的开源框架spring security就是基于filter实现的。
- listener是用来监听消息的工具,我们可以将其当做是项目内部的MQ,用来接受各种类型的消息,分别处理。
前面我们已经在一文带你掌握java web中的filter中讲过filter,也就是过滤器。今天我们来详细讲下listener,也就是监听器。学过kafka或者rabbitMQ的人肯定知道MQ的客户端分为两种,分别是:生产者和消费者,而消费者就相当于是一个监听器,用来监听MQ发过来的消息。实际上java web的监听器和MQ的监听器没什么不同,都是在监听到消息后对消息做处理。
监听器的种类
在java web中,监听器一共分为三大类,分别是:
- request listener: 监听request的,包括request的创建和销毁,以及request属性变化
- session listener: 监听session的,包括session的创建和销毁,以及session的属性变化
- ServletContext listener: 监听servletContext的,包括servletContext的创建和销毁,以及servletContext的属性变化。
下图是java web中所有的监听器:
使用监听器
虽然监听器的种类繁多,但是它们的用法都是差不多的。下面我将通过一个案例带大家学习下如何使用监听器。
假设我们需要实现这样一个功能,那就是管理员可以随时知道当前有多少个用户处于在线状态,并且还可以将一些不遵守平台规则的恶意用户给踢下线。如果是你,你会怎样实现?
开发监听器
开发监听器需要实现对应功能的监听器接口,下面是我写的几个监听器:
下面是监听sessionId的变化的监听器
package com.lizemin.listener; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; import javax.servlet.http.HttpSessionEvent; import javax.servlet.http.HttpSessionIdListener; /** * @author lzm * @date 2025/4/6 12:56 * @description sessionId监听器,用于监听sessionId的变化 */ @Slf4j @Component public class MySessionIdListener implements HttpSessionIdListener { @Autowired SessionManagementListener sessionManagementListener; @Override public void sessionIdChanged(HttpSessionEvent se, String oldSessionId) { log.info("sessionId已更改,从{}变成了{}", oldSessionId, se.getSession().getId()); sessionManagementListener.refreshSession(oldSessionId, se.getSession().getId()); } }
下面是监听会话创建和销毁的监听器:
package com.lizemin.listener; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Component; import javax.servlet.http.HttpSession; import javax.servlet.http.HttpSessionEvent; import javax.servlet.http.HttpSessionListener; import java.util.ArrayList; import java.util.List; import java.util.Objects; import java.util.concurrent.ConcurrentHashMap; /** * @author lzm * @date 2025/4/6 10:48 * @description 会话管理监听器 */ @Component @Slf4j public class SessionManagementListener implements HttpSessionListener { /** * 存储所有会话信息 */ private static final ConcurrentHashMap SESSION_MAP = new ConcurrentHashMap(); @Override public void sessionCreated(HttpSessionEvent se) { log.info("会话已创建....."); HttpSession session = se.getSession(); SESSION_MAP.put(session.getId(), session); } @Override public void sessionDestroyed(HttpSessionEvent se) { log.info("会话已销毁....."); HttpSession session = se.getSession(); SESSION_MAP.remove(session.getId()); } /** * 获取所有会话信息 */ public List getAllSessions() { ArrayList sessions = new ArrayList(SESSION_MAP.values()); sessions.removeIf(session -> Objects.isNull(session.getAttribute(Constants.LOGIN_STATUS))); return sessions; } /** * 获取当前活跃的会话数量 * * @return 当前活跃的会话数量 */ public int getActiveSessionCount() { return SESSION_MAP.size(); } /** * 移除指定的session * * @param sessionId sessionId */ public void removeSession(String sessionId) { HttpSession session = SESSION_MAP.get(sessionId); if (Objects.isNull(session)) { log.info("sessionId为{}的session不存在", sessionId); return; } session.invalidate(); } /** * 刷新sessionId * @param oldSessionId 旧的sessionId * @param newSessionId 新的sessionId */ public void refreshSession(String oldSessionId, String newSessionId) { HttpSession session = SESSION_MAP.get(oldSessionId); if (Objects.isNull(session)) { log.info("sessionId为{}的session不存在, 无需处理", oldSessionId); return; } SESSION_MAP.put(newSessionId, session); SESSION_MAP.remove(oldSessionId); } }
这个监听器是实现管理员会话管理的核心,它会将所有session放到一个map中进行管理,后续会通过这个map来获取恶意用户的session,然后将其踢下线。
下面是监听User对象是否存在于sessionAttribute中的监听器:
package com.lizemin.entity; import cn.hutool.json.JSONUtil; import lombok.AllArgsConstructor; import lombok.Data; import lombok.extern.slf4j.Slf4j; import javax.servlet.http.HttpSessionBindingEvent; import javax.servlet.http.HttpSessionBindingListener; /** * @author lzm * @date 2025/4/6 11:32 * @description */ @Slf4j @Data @AllArgsConstructor public class User implements HttpSessionBindingListener { private String username; private String password; private String role; private String sessionId; @Override public void valueBound(HttpSessionBindingEvent event) { String name = event.getName(); User user = (User) event.getValue(); log.info("name={}, value={}", name, JSONUtil.toJsonStr(user)); log.info("用户名为【{}】的用户登录成功,角色为:【{}】", user.getUsername(), user.getRole()); } @Override public void valueUnbound(HttpSessionBindingEvent event) { User user = (User) event.getValue(); log.info("用户【{}】已退出登录,角色为:【{}】", user.getUsername(), user.getRole()); } }
开发项目需要的接口
下面是和登录有关的接口,一个用于用户登录,另一个用于退出登录
package com.lizemin.controller; import cn.hutool.core.util.StrUtil; import com.lizemin.constant.Constants; import com.lizemin.entity.User; import com.lizemin.service.UserService; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RestController; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpSession; import java.util.Objects; /** * @author lzm * @date 2025/4/6 11:33 * @description */ @Slf4j @RestController public class LoginController { @Autowired UserService userService; @Autowired HttpServletRequest request; /** * 登录接口 * @param username 用户名 * @param password 密码 */ @GetMapping("/login") public String login(String username, String password) { if (StrUtil.hasBlank(username, password)) { throw new RuntimeException("用户名或密码不能为空"); } log.info("用户名:{},密码:{}", username, password); HttpSession session = request.getSession(); String oldSessionId = session.getId(); log.info("原始的sessionId为:{}", oldSessionId); User user = userService.findUserByUsernameAndPassword(username, password); if (Objects.isNull(user)) { throw new RuntimeException("用户不存在或密码错误"); } // 认证成功后,修改sessionId, 防止会话固定攻击 String newSessionId = request.changeSessionId(); log.info("新的sessionId为:{}", newSessionId); // 将用户信息存入session中 session.setAttribute(Constants.LOGIN_STATUS, user); return "登录成功"; } /** * 退出登录 */ @GetMapping("/logout") public String logout() { request.getSession().invalidate(); return "已退出登录"; } }
下面是给管理员用户使用的接口:
package com.lizemin.controller; import cn.hutool.core.util.StrUtil; import com.lizemin.constant.Constants; import com.lizemin.entity.User; import com.lizemin.listener.SessionManagementListener; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RestController; import javax.servlet.http.HttpSession; import java.util.List; import java.util.Objects; import java.util.stream.Collectors; /** * @author lzm * @date 2025/4/6 9:58 * @description */ @RestController public class AdminController { @Autowired SessionManagementListener sessionManagementListener; @Autowired HttpSession httpSession; /** * 获取所有在线用户 */ @GetMapping("/getAllActiveUsers") public List getAllActiveUsers() { validatePermission(); List sessions = sessionManagementListener.getAllSessions(); return sessions.stream() .map(session -> { User user = (User) session.getAttribute(Constants.LOGIN_STATUS); user.setSessionId(session.getId()); return user; }) .collect(Collectors.toList()); } /** * 获取在线用户数 * @return 在线用户数 */ @GetMapping("/getActiveSessionCount") public int getActiveSessionCount() { validatePermission(); return sessionManagementListener.getActiveSessionCount(); } /** * 将特定用户踢下线 * * @param sessionId sessionId */ @GetMapping("/remove") public String getActiveSessionCount(String sessionId) { validatePermission(); if (StrUtil.isBlank(sessionId)) { throw new RuntimeException("sessionId不能为空"); } sessionManagementListener.removeSession(sessionId); return "success"; } /** * 验证权限 */ private void validatePermission() { User cuurentUser = (User) httpSession.getAttribute(Constants.LOGIN_STATUS); if (Objects.isNull(cuurentUser)) { throw new RuntimeException("用户未登录"); } String role = cuurentUser.getRole(); if (!StrUtil.equals(role, "admin")) { throw new RuntimeException("权限不足"); } } }
下面是用于验证用户是否处理登录状态的接口:
package com.lizemin.controller; import com.lizemin.constant.Constants; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RestController; import javax.servlet.http.HttpSession; import java.util.Objects; /** * @author lzm * @date 2025/4/6 12:59 * @description */ @RestController public class TestController { @GetMapping("/validateLoginStatus") public String validateLoginStatus(HttpSession session) { Object user = session.getAttribute(Constants.LOGIN_STATUS); if (Objects.isNull(user)) { throw new RuntimeException("用户未登录"); } return "在线状态"; } }
测试
下面我们使用多个客户端模拟不同用户的登录行为:
使用postman登录邓布利多的账号,他是管理员角色
通过浏览器登录赫敏的账号,她的角色是学生
通过coolRequest登录哈利波特的账号,他的角色是学生
然后我们在用邓布利多(管理员)的账号看下当前活跃的用户:
的确可以看到当前的所有活跃用户,包括邓布利多,哈利波特和赫敏
然后我们使用哈利波特的账号退出登录
再通过邓布利多(管理员)的账号看下当前活跃用户:
可以看到确实看不到哈利波特这个用户了,说明他已经下线了,这说明我们查看当前活跃用户的接口是正常的。
接下来我们测试下将某个特定用户踢下线的功能,使用邓布利多的账号将赫敏这个用户给强行踢下线。
然后再看下当前活跃用户:
可以看到赫敏这个用户确实已经下线了,目前活跃的用户中只有邓布利多一个人。然后我们再通过浏览器查看下赫敏的登录状态,发现她确实是处于下线的状态,这说明我们开发的管理员将恶意用户踢下线的功能也是正常的!
案例中的示例代码地址
案例中的示例代码地址
最后的总结
这篇文章中我们讲了监听器是用来干嘛的,以及java web中的监听器有哪些类型。最后我们还通过一个管理员将恶意用户踢下线的案例带大家过了一把使用监听器的瘾,相信大家对监听器的功能和使用已经有所认识了。
觉得有收获的朋友可以点个赞,您的鼓励就是我最大的动力!