一文带你掌握java web的监听器

06-01 1091阅读

什么是监听器

我们知道java web的三大支柱分别是:servlet,filter,listener。

  1. servlet是执行业务逻辑的工具,也是spring MVC的核心,因为springMVC的核心功能的实现就是基于servlet。
  2. filter是对请求和响应做处理的工具,可以看做是项目内部的nginx,著名的开源框架spring security就是基于filter实现的。
  3. listener是用来监听消息的工具,我们可以将其当做是项目内部的MQ,用来接受各种类型的消息,分别处理。

    一文带你掌握java web的监听器

前面我们已经在一文带你掌握java web中的filter中讲过filter,也就是过滤器。今天我们来详细讲下listener,也就是监听器。学过kafka或者rabbitMQ的人肯定知道MQ的客户端分为两种,分别是:生产者和消费者,而消费者就相当于是一个监听器,用来监听MQ发过来的消息。实际上java web的监听器和MQ的监听器没什么不同,都是在监听到消息后对消息做处理。

监听器的种类

在java web中,监听器一共分为三大类,分别是:

  1. request listener: 监听request的,包括request的创建和销毁,以及request属性变化
  2. session listener: 监听session的,包括session的创建和销毁,以及session的属性变化
  3. ServletContext listener: 监听servletContext的,包括servletContext的创建和销毁,以及servletContext的属性变化。

下图是java web中所有的监听器:

一文带你掌握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登录邓布利多的账号,他是管理员角色

一文带你掌握java web的监听器

通过浏览器登录赫敏的账号,她的角色是学生

一文带你掌握java web的监听器

通过coolRequest登录哈利波特的账号,他的角色是学生

一文带你掌握java web的监听器

然后我们在用邓布利多(管理员)的账号看下当前活跃的用户:

一文带你掌握java web的监听器

的确可以看到当前的所有活跃用户,包括邓布利多,哈利波特和赫敏

然后我们使用哈利波特的账号退出登录

一文带你掌握java web的监听器

再通过邓布利多(管理员)的账号看下当前活跃用户:

一文带你掌握java web的监听器

可以看到确实看不到哈利波特这个用户了,说明他已经下线了,这说明我们查看当前活跃用户的接口是正常的。

接下来我们测试下将某个特定用户踢下线的功能,使用邓布利多的账号将赫敏这个用户给强行踢下线。

一文带你掌握java web的监听器

然后再看下当前活跃用户:

一文带你掌握java web的监听器

可以看到赫敏这个用户确实已经下线了,目前活跃的用户中只有邓布利多一个人。然后我们再通过浏览器查看下赫敏的登录状态,发现她确实是处于下线的状态,这说明我们开发的管理员将恶意用户踢下线的功能也是正常的!

一文带你掌握java web的监听器

案例中的示例代码地址

案例中的示例代码地址

一文带你掌握java web的监听器

最后的总结

这篇文章中我们讲了监听器是用来干嘛的,以及java web中的监听器有哪些类型。最后我们还通过一个管理员将恶意用户踢下线的案例带大家过了一把使用监听器的瘾,相信大家对监听器的功能和使用已经有所认识了。

觉得有收获的朋友可以点个赞,您的鼓励就是我最大的动力!

免责声明:我们致力于保护作者版权,注重分享,被刊用文章因无法核实真实出处,未能及时与作者取得联系,或有版权异议的,请联系管理员,我们会立即处理! 部分文章是来自自研大数据AI进行生成,内容摘自(百度百科,百度知道,头条百科,中国民法典,刑法,牛津词典,新华词典,汉语词典,国家院校,科普平台)等数据,内容仅供学习参考,不准确地方联系删除处理! 图片声明:本站部分配图来自人工智能系统AI生成,觅知网授权图片,PxHere摄影无版权图库和百度,360,搜狗等多加搜索引擎自动关键词搜索配图,如有侵权的图片,请第一时间联系我们。

相关阅读

目录[+]

取消
微信二维码
微信二维码
支付宝二维码