【SpringBoot+自定义注解+Aop+长轮询】实现简单的实时消息推送


前言

小白摸索,大佬勿喷

我们平常在网站上都可以看到右上角的消息上未读消息数量,以及有消息时右下角实时弹出的例子

我公司项目刚好也有这个需求,但是由于甲方要求不能使用第三方服务,于是便有了本文的解决思路


一、应用场景

该思路适用于一些无法使用第三方服务的场景,如果可以使用第三方服务仍旧是使用第三方的服务比较好

二、使用步骤

1.引入依赖

这里我用到的依赖有:aspectj、mybatis-plus、jjwt、hutool等,大家可以自行选择

        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatis-plus-boot-starter</artifactId>
            <version>3.3.2</version>
        </dependency>
        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatis-plus-generator</artifactId>
            <version>3.3.2</version>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-freemarker</artifactId>
            <version>2.7.3</version>
        </dependency>
        <dependency>
            <groupId>org.aspectj</groupId>
            <artifactId>aspectjrt</artifactId>
            <version>1.9.6</version>
        </dependency>
        <dependency>
            <groupId>org.aspectj</groupId>
            <artifactId>aspectjweaver</artifactId>
            <version>1.9.6</version>
        </dependency>
        <dependency>
            <groupId>io.jsonwebtoken</groupId>
            <artifactId>jjwt</artifactId>
            <version>0.9.1</version>
        </dependency>

        <dependency>
            <groupId>cn.hutool</groupId>
            <artifactId>hutool-all</artifactId>
            <version>5.7.20</version>
        </dependency>

2.数据库建立

消息表:

CREATE TABLE `t_message` (
  `id` int NOT NULL AUTO_INCREMENT,
  `content` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '内容',
  `from_uid` int DEFAULT NULL COMMENT '发起人id',
  `to_uid` int DEFAULT NULL COMMENT '接收人id',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;

部门表:

CREATE TABLE `t_dept` (
  `id` int NOT NULL AUTO_INCREMENT,
  `dept_name` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '部门名称',
  `dept_code` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '部门编码',
  `leader_uid` int DEFAULT NULL COMMENT '部门领导id',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=21 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;

用户表:

CREATE TABLE `t_user` (
  `id` int NOT NULL AUTO_INCREMENT,
  `username` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '用户名',
  `password` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '密码',
  `real_name` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '真实姓名',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;

3.编写业务代码

为了方便演示我这里使用的是MybatisPlus生成后自带的方法去添加,@RealTimeMessage为自定义注解,下面会提到

/**
     * 添加部门
     *
     * @param dept 部门实体类
     * @return String
     */
    @RealTimeMessage("添加部门")
    @PostMapping("/add")
    public String addDept(@RequestBody Dept dept) {
        boolean save = deptService.save(dept);
        if (save) {
            return "SUCCESS";
        } else {
            return "FAILED";
        }
    }

4.Token相关

JwtUtils:

@Component
public class JwtUtils {

    private final String KEY = "abcdefghijklmnopqrstuvwxyz";

    /**
     * 创建token
     *
     * @param user 用户实体
     * @return String
     */
    public String createToken(User user) {
        Map<String, Object> claims = BeanUtil.beanToMap(user);
        JwtBuilder jwtBuilder = Jwts.builder()
                .setClaims(claims)
                .setIssuedAt(new Date())
                .signWith(SignatureAlgorithm.HS512, KEY);
        return jwtBuilder.compact();
    }

    /**
     * 解析token
     *
     * @param token token
     * @return User
     */
    public User parseToken(String token) {
        Claims claims;
        try {
            claims = Jwts.parser()
                    .setSigningKey(KEY)
                    .parseClaimsJws(token)
                    .getBody();
        } catch (JwtException e) {
            claims = null;
            e.printStackTrace();
        }
        return BeanUtil.toBean(claims, User.class);
    }

}

TokenService及实现类:

// TokenService
public interface TokenService {
	/**
	* 获取当前登录用户的信息
	* @return User 
	*/
    User getUserByToken();
}


// TokenService的实现类
@Service
public class TokenServiceImpl implements TokenService {

    @Resource
    private JwtUtils jwtUtils;

    @Override
    public User getUserByToken() {
        HttpServletRequest request = getRequest();
        String token = request.getHeader("token");
        return jwtUtils.parseToken(token);
    }

    public HttpServletRequest getRequest(){
        return ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();
    }

5.编写队列相关方法

@Component
public class MessageQueue {

    /**
     * 存放消息的map集合
     */
    private static final Map<User, Queue<Message>> MSG_MAP = new ConcurrentHashMap<>();
    private static final Map<User, Integer> RETURN_TIME = new ConcurrentHashMap<>();

    /**
     * 注册当前请求用户到map中
     *
     * @param user 当前用户
     */
    public void register(User user) {
        MSG_MAP.put(user, new ConcurrentLinkedDeque<>());
        RETURN_TIME.put(user, 0);
    }

    /**
     * 销毁已注册的用户
     *
     * @param user 当前用户
     */
    public void remove(User user) {
        MSG_MAP.remove(user);
        RETURN_TIME.remove(user);
    }

    /**
     * 将消息存入对应的接收方队列中
     *
     * @param message 要接收的消息
     */
    public void push(Message message) {
        MSG_MAP.keySet().forEach(a -> {
            synchronized (a) {
                if (a.getId().equals(message.getToUid())) {
                    MSG_MAP.get(a).add(message);
                    a.notifyAll();
                }
            }
        });
    }

    /**
     * 推送消息
     *
     * @param user 当前用户实体
     * @return List<Message>
     */
    public List<Message> pollAll(User user) {
        Map<User,Queue<Message>> map1 = MSG_MAP;
        Map<User,Integer> map2 = RETURN_TIME;
        List<Message> messageList = new ArrayList<>();
        Queue<Message> messageQueue = MSG_MAP.get(user);
        //如果队列不为空,则将消息添加到对应的队列中  否则将该用户注册到map中
        if (messageQueue != null) {
            messageQueue.forEach(a -> {
                Message poll = messageQueue.poll();
                if (poll != null) {
                    messageList.add(poll);
                }
            });
        } else {
            register(user);
        }

        //如果消息为空则将服务阻塞 不响应前端请求 直到map中有新消息则返回消息 
        //此时需要与前端配合设置阻塞时间 可根据实际需求更改
        if (messageList.isEmpty() && RETURN_TIME.get(user) < 3) {
            try {
                Integer time = RETURN_TIME.get(user);
                RETURN_TIME.put(user, time + 1);
                synchronized (user) {
                    user.wait(10000);
                }
                return pollAll(user);
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
            }
        }
        return messageList;

    }

}

6.自定义注解+AOP

自定义注解,value用来存放操作名称

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface RealTimeMessage {

    String value();
}

编写切面去拦截添加了自定义注解的方法

@Aspect
@Component
public class RealTimeMessageAop {

    @Resource
    private MessageService messageService;

    @Resource
    private UserService userService;

    @Resource
    private DeptService deptService;

    @Resource
    private TokenService tokenService;

	//
    @Resource
    private MessageQueue messageQueue;

    @Pointcut("@annotation(com.example.validator.anno.RealTimeMessage)")
    public void realTimeMessage() {
    }

    @Around("realTimeMessage()")
    public Object check(ProceedingJoinPoint point) throws Throwable {
        Object obj;
        //获取当前请求用户对象
        User user = tokenService.getUserByToken();
        //获取自定义注解
        Method method=((MethodSignature)point.getSignature()).getMethod();
        Signature signature = point.getSignature();
        Method realMethod = point.getTarget().getClass().getDeclaredMethod(signature.getName(), method.getParameterTypes());
        RealTimeMessage anno = realMethod.getAnnotation(RealTimeMessage.class);
        //获取前端传参,并转为map集合
        Object[] args = point.getArgs();
        Dept dept = BeanUtil.toBean(args[0], Dept.class);
        //继续执行原有方法
        obj = point.proceed();
        //获取返回消息
        String result = String.valueOf(obj);
        //如果请求成功则弹出消息 否则不做操作
        if ("SUCCESS".equals(result)) {
            //添加消息到数据库中
            Message build = Message.builder()
                    .content(anno.value())
                    .fromUid(user.getId())
                    .toUid(dept.getLeaderUid())
                    .build();
            messageService.save(build);
            //发送实时消息
            messageQueue.push(build);
        }
        return obj;
    }

}

此处我想实现的效果为当管理员添加部门,并指定该部门的领导时,将通知实时发送给管理员设置的领导

7.长轮询

该接口登陆后由前端调用,到达约定时间或返回结果后,前端再次调用该接口实现长轮询

@RestController
@RequestMapping("/message")
public class MessageController {

    @Resource
    private MessageQueue messageQueue;

    @Resource
    private TokenService tokenService;

	/**
	* 拉取实时消息
	* @return List<Message> 
	*/
    @GetMapping("/realTimeMsg")
    public List<Message> getRealTimeMsg() {
        //获取当前请求用户信息
        User user = tokenService.getUserByToken();
        try {
            //注册该用户
            messageQueue.register(user);
            //推送消息
            return messageQueue.pollAll(user);
        } catch (Exception e) {
            throw new IllegalStateException();
        } finally {
            //销毁注册
            messageQueue.remove(user);
        }
    }

功能测试

自此我们的代码已经全部编写完成,接下来就启动项目看一下效果

然后打开我们的接口调试工具把接口与数据填进去
添加部门接口调试

拉取实时消息接口调试

我们还需要Token的参与,为了方便演示我们就直接在生成所需要的Token

Token生成单元测试
接下来我们把管理员Token填入添加单位的接口的Header
添加管理员Token到添加部门接口

然后我们把任命领导的Token填入拉取消息的Header

添加领导Token到拉取消息接口

我们首先调用拉取实时消息的接口,接口在没有消息的时候会一直阻塞,直到到达约定时间,如果到了约定时间仍无新消息则直接返回空列表,效果如下:
请求阻塞调用拉取实时消息接口

接着我们重新调用拉取实时消息的接口,并发送添加部门的请求,这时我们可以看到消息已经推送过来了,效果如下:
调用添加部门接口
拉取实时消息接口

自此我们的全部功能就已经完成啦


总结

本文提供了一种简单实时消息推送的一种思路,在使用时可根据实际需求进行修改,该思路主要工作在后端实现,前端只需要轮番调用拉取消息的接口即可


推荐

关注博客和公众号获取最新文章

Bummon’s BlogBummon’s Home公众号

  • 1
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Bummon.

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值