RabbitMq+WebSocket+Quartz+layui 实现网页端实时消息推送

讲在前面

前两天在项目中使用flowable编写流程时,项目上说在执行每一步步骤之后,需要在网页端给下一步的处理人弹一个消息框提示一下,并且这个消息推送可以在其他地方也能用到,比如发送系统公告等等,所以就写个全局消息推送功能吧。

我的思路

首先我这边业务是一个消息可能发给多个人,也可能是一个人,所以我这边封装一个消息类,保存消息的主要内容,需要发送到的list类型的用户id集合, 还有就是消息的唯一事件key,这些用户的消息属于同一个事件。

效果图

  • 每个可以单独关闭
  • 悬浮再消息框上,显示全部关闭按钮,点击全部关闭
  • 四种不同的状态情景(log,success,warn,error)

在这里插入图片描述

码云Demo地址

篇幅较长,可以直接下载源码享用 码云demo项目链接

**
另外有错欢迎指正
**

**
转载请声明作者
**

首先安装erlang

  • 安装之前我们得先安装 erlang, 因为rabbitMq是通过它去编写的,

  • 64位地址: 百度网盘地址.

  • 提取码:x56m

  • 安装 ==>下一步
    Alt

  • 默认位置 ==> 下一步 ==> install

Alt

  • 接下来配置环境变量
  • 此电脑–>鼠标右键“属性”–>高级系统设置–>环境变量–>“新建”系统环境变量
    Alt

变量名:ERLANG_HOME

变量值就是刚才erlang的安装地址,点击确定。

然后双击系统变量Path
Alt
点击“新建”,将%ERLANG_HOME%\bin加入到path中。

Alt

  • 最后windows键+R键,输入cmd,再输入erl,看到版本号就说明erlang安装成功了。

Alt

安装RabbitMq

  • 百度网盘地址 rabbitMq-server-3.7.7
  • 提取码:gz9y
  • 双击安装,安装步骤与erlang相同,一直下一步安装,
  • RabbitMQ安装好后接下来安装RabbitMQ-Plugins。打开命令行cd,输入RabbitMQ的sbin目录。
  • 我的地址是C:\Program Files\RabbitMQ Server\rabbitmq_server-3.7.7\sbin
  • 在后面输入rabbitmq-plugins enable rabbitmq_management命令进行安装

Alt

  • 访问http://localhost:15672

Alt

  • 默认用户名和密码都是guest
  • 这样rabbitMq就安装成功了

demo项目模拟发送消息

  • 新建一个springboot项目
  • yml配置如下
  • rabbitMq 默认端口是5672,用户名密码都是guest
server:
  port: 8080

spring:
  thymeleaf:
    mode: HTML
  rabbitmq:
    host: 127.0.0.1
    port: 5672
    username: guest
    password: guest
    publisher-confirms: true    #  消息发送到交换机确认机制,是否确认回调
    publisher-returns: true
    listener:
      simple:
        acknowledge-mode: manual
  • 这是我的项目目录结构
    Alt
  • 在处理后端业务逻辑的时候,需要我们同时向前端页面返回一个通知,我们通过tabbitMq来发送消息,可这消息不是在后端进行处理,而是需要返回前端去展示,所以我们就需要通过webSocket来讲我们的消息异步返回给前端处理
  • pom文件结构 引入 rabbitmqwebSocket 同时还有 quartz 后续我们会用到
  • 同时还引入了 thymeleaf 模板引擎,这样可以再html页面直接获取后端model中的值
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.1.4.RELEASE</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <groupId>com.workfeng</groupId>
    <artifactId>message</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>message</name>
    <description>Demo project for Spring Boot</description>

    <packaging>jar</packaging>

    <properties>
        <java.version>1.8</java.version>
        <thymeleaf.version>3.0.11.RELEASE</thymeleaf.version>
        <thymeleaf-layout-dialect.version>2.1.1</thymeleaf-layout-dialect.version>
    </properties>

    <dependencies>

        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>fastjson</artifactId>
            <version>1.2.47</version>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-devtools</artifactId>
            <scope>provided</scope>
            <optional>true</optional>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-thymeleaf</artifactId>
        </dependency>


        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-devtools</artifactId>
            <optional>true</optional>
        </dependency>
        <dependency>
            <groupId>org.apache.commons</groupId>
            <artifactId>commons-lang3</artifactId>
            <version>3.7</version>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-amqp</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-websocket</artifactId>
        </dependency>

        <!-- quartz定时任务 -->
        <dependency>
            <groupId>org.quartz-scheduler</groupId>
            <artifactId>quartz</artifactId>
            <version>2.2.1</version>
            <exclusions>
                <exclusion>
                    <artifactId>slf4j-api</artifactId>
                    <groupId>org.slf4j</groupId>
                </exclusion>
            </exclusions>
        </dependency>
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-context-support</artifactId>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
                <configuration>
                    <fork>true</fork><!-- 如果没有该项配置,肯呢个devtools不会起作用,即应用不会restart -->
                </configuration>
            </plugin>
        </plugins>
    </build>

</project>

RabbitMq配置

  • 接下类就是rabbitMq配置,
  • 首先我们配置一个 RabbitMqConfig类,来声明队列,和绑定交换机与路由
  • 现有一个RabbitConstant 类,声明名称
package com.workfeng.message.config.constant;

public class RabbitConstant {
    //交换机名称
    public final static String EXCHANGE = "exchange_test";
    //队列
    public final static String QUEUE_TRANSACTION = "queue_transaction";
    public final static String QUEUE_CONTRACT = "queue_contract";
    public final static String QUEUE_QUALIFICATION = "queue_qualification";
    //路由key
    public final static String RK_TRANSACTION = "transaction";
    public final static String RK_CONTRACT = "contract";
    public final static String RK_QUALIFICATION = "qualification";
}

  • 接下来是 RabbitMqConfig 配置类,
package com.workfeng.message.config.rabbitmq;

import com.workfeng.message.config.constant.RabbitConstant;
import org.springframework.amqp.core.Binding;
import org.springframework.amqp.core.BindingBuilder;
import org.springframework.amqp.core.DirectExchange;
import org.springframework.amqp.core.Queue;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class RabbitMqConfig {

    /**
     * 声明队列
     *
     * @return
     */
    @Bean
    public Queue queueTransaction() {
        // true表示持久化该队列
        return new Queue(RabbitConstant.QUEUE_TRANSACTION, true);
    }

    @Bean
    public Queue queueContract() {
        // true表示持久化该队列
        return new Queue(RabbitConstant.QUEUE_CONTRACT, true);
    }

    @Bean
    public Queue queueQualification() {
        // true表示持久化该队列
        return new Queue(RabbitConstant.QUEUE_QUALIFICATION, true);
    }

    /**
     * 声明交互器
     *
     * @return
     */
    @Bean
    DirectExchange directExchange() {
        return new DirectExchange(RabbitConstant.EXCHANGE);
    }

    /**
     * 绑定
     *
     * @return
     */
    @Bean
    public Binding bindingTransaction() {
        return BindingBuilder.bind(queueTransaction()).to(directExchange()).with(RabbitConstant.RK_TRANSACTION);
    }

    /**
     * 绑定
     *
     * @return
     */
    @Bean
    public Binding bindingContract() {
        return BindingBuilder.bind(queueContract()).to(directExchange()).with(RabbitConstant.RK_CONTRACT);
    }

    /**
     * 绑定
     *
     * @return
     */
    @Bean
    public Binding bindingQualification() {
        return BindingBuilder.bind(queueQualification()).to(directExchange()).with(RabbitConstant.RK_QUALIFICATION);
    }

}

WebSocket配置

  • 接下来就是去监听 RabbitMq 队列,如果有消息发送了,就通过webSocket发送到前端页面,给出弹出框提示,所以我们先来配置webSocket,再去处理监听事件。

  • 这是我的前端资源文件摆放位置,消息弹框组件组要依赖jQuery,引了一个jquery.js
    在这里插入图片描述

  • 这边我只用了一个index页面,我们先用controller访问index页面

  • 模拟一个用户登陆,在model中放入一个用户id,到index页面获取

package com.workfeng.message.controller;

import com.workfeng.message.config.rabbitmq.MessageVo;
import com.workfeng.message.config.rabbitmq.Sender;
import com.workfeng.message.utils.SuccessTip;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;

import java.util.Arrays;

@Controller
public class IndexController {

    @Autowired
    private Sender sender;

    @RequestMapping("/index")
    public String index(Model model){

        // 假设用户1登陆
        model.addAttribute("userId","1");

        return "index";
    }
}

  • 访问index页面
  • 设置一个input隐藏域保存userId

Alt

  • 然后是index页面下 webSocket 的配置信息
<script>
    layui.use(['layer',"jquery"], function(){
        var layer = layui.layer;
        $ = layui.jquery;

        var websocket = null;
        var userId = $("#userId").val();

        //判断当前浏览器是否支持WebSocket
        if ('WebSocket' in window) {
            // 连接服务端  并将userId带入
            websocket = new WebSocket("ws://localhost:8080/ws/message/"+userId);
        }
        else {
            alert('当前浏览器 不支持WebSocket')
        }

        //连接发生错误的回调方法
        websocket.onerror = function () {
            //setMessageInnerHTML("连接发生错误");
            console.log("webSocket 连接发生错误")
        };

        //连接成功建立的回调方法
        websocket.onopen = function () {
            console.log("webSocket 连接成功")
        };

        //接收到消息的回调方法,此处添加处理接收消息方法,当前是将接收到的信息显示在网页上
        websocket.onmessage = function (event) {
            setMessageInnerHTML(event.data);
        };

        //连接关闭的回调方法
        websocket.onclose = function () {
            console.log("webSocket 连接关闭,如需登录请刷新页面。")
        };

        //监听窗口关闭事件,当窗口关闭时,主动去关闭websocket连接,防止连接还没断开就关闭窗口,server端会抛异常。
        window.onbeforeunload = function () {
            websocket.close();
        };

        //将消息显示在网页上,如果不需要显示在网页上,则不调用该方法
        function setMessageInnerHTML(result) {
            var message = JSON.parse(result.replace(/'/g,'\"'));
            // 消息不为空
            if(message !== undefined && message !== null){
                // 播放通知
                //$("#chatAudio").get(0).play();
                // 消息状态 log(默认), success(成功), warn(警告), error(危险)
                var status = message.status;
                naranja()[status]({
                    title: '新消息',
                    text:  "<div class='goto' m-type='"+message.type+"' m-key='"+message.bussinessKey+"'><a href='javascript:void(0)'>"+message.message+"</a></div>",
                    timeout: 'keep',
                    buttons: [{
                        text: '我知道了',
                        click: function (e) {
                            e.closeNotification();
                        }
                    }]
                })
            }
        }
		/**
         * 链接点击事件
         */
        $(document).on("click",".goto",function () {
            // 消息类型
            var type = $(this).attr("m-type");
            // 消息key
            var key = $(this).attr("m-key");
            // 点击之后关闭当前消息框
            naranja().closeThisNotification(this);
            // 根据类型去相应页面
            if(type === "todo"){
                // 根据 type请求不同的页面,key可作为参数携带
            }
        });
    });
</script>
  • 接下来就是服务端的webSocket配置,要和js中的对应起来,首先是config类
package com.workfeng.message.config.webSocket;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.socket.server.standard.ServerEndpointExporter;

@Configuration
public class WebSocketConfig {

    @Bean
    public ServerEndpointExporter serverEndpointExporter() {
        return new ServerEndpointExporter();
    }

}
  • 然后是连接网页端的配置类
package com.workfeng.message.config.webSocket;

import com.alibaba.fastjson.JSONObject;
import com.workfeng.message.config.rabbitmq.MessageVo;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;

import javax.websocket.*;
import javax.websocket.server.PathParam;
import javax.websocket.server.ServerEndpoint;
import java.io.IOException;
import java.util.Iterator;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

/**
 * ServerEndpoint
 * <p>
 * 使用springboot的唯一区别是要@Component声明下,而使用独立容器是由容器自己管理websocket的,但在springboot中连容器都是spring管理的。
 * <p>
 * 虽然@Component默认是单例模式的,但springboot还是会为每个websocket连接初始化一个bean,所以可以用一个静态set保存起来。
 */
@ServerEndpoint("/ws/message/{userId}") //WebSocket客户端建立连接的地址
@Component
public class WebSocketServerEndpoint {

    private final static Logger log = LoggerFactory.getLogger(WebSocketServerEndpoint.class);
    /**
     * 存活的session集合(使用线程安全的map保存)
     */
    private static Map<String, Session> livingSessions = new ConcurrentHashMap<>();
    /**
     * 未发送出的消息
     */
    public static Map<String,Map<String,String>> unSensMessages = new ConcurrentHashMap<>();
    /**
     * 建立连接的回调方法
     *
     * @param session 与客户端的WebSocket连接会话
     * @param userId  用户名,WebSocket支持路径参数
     */
    @OnOpen
    public void onOpen(Session session, @PathParam("userId") String userId) {
        // 建立连接时 保存当前登陆人到已登录集合中
        livingSessions.put(userId, session);
        // 判断当前登陆人是否有未读的消息  有则发送
        Iterator<Map.Entry<String, Map<String, String>>> iterator = unSensMessages.entrySet().iterator();
        while (iterator.hasNext()){
            Map.Entry<String, Map<String, String>> next = iterator.next();
            Map<String, String> nextValue = next.getValue();
            Iterator<Map.Entry<String, String>> iterator1 = nextValue.entrySet().iterator();
            while (iterator1.hasNext()){
                Map.Entry<String, String> next1 = iterator1.next();
                if(next1.getKey().equals(userId)){
                    sendMessage(session,next1.getValue());
                    //移除已发送的消息
                    iterator1.remove();
                }
            }
            if(next.getValue().size() == 0){
                // 当前事件已全部发送 移除该事件
                iterator.remove();
            }
        }
        log.info(userId + "进入连接");
    }

    @OnMessage
    public void onMessage(String message, Session session, @PathParam("userId") String userId) {
        log.info(userId + " : " + message);
        //sendMessageToAll(userId + " : " + message);
    }


    @OnError
    public void onError(Session session, Throwable error) {
        log.info("发生错误");
        log.error(error.getStackTrace() + "");
    }


    @OnClose
    public void onClose(Session session, @PathParam("userId") String userId) {
        livingSessions.remove(userId);
        log.info(userId + " 关闭连接");
    }

    /**
     * 单独发送消息
     *
     * @param session
     * @param message
     */
    public void sendMessage(Session session, String message) {
        try {
            session.getBasicRemote().sendText(message.replace('\"', '\''));
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    /**
     * 群发消息
     * userIds 是需要接收消息的用户id集合 可单发,可群发
     *
     * @param message
     */
    public void sendMessageToAll(String message) {
        // 将json字符串转为message类
        MessageVo messageVo = JSONObject.parseObject(message, MessageVo.class);

        // 需要发送的人可能未上线 将未发送的消息保存到未发送集合中
        Map<String,String> unSendsUsers = new ConcurrentHashMap<>();
        // 遍历需要发送到的人
        for (Integer userId : messageVo.getUserIds()) {
            // 当前已登录的人
            if(livingSessions.get(String.valueOf(userId)) != null){
                //当前user已登录,发送消息
                sendMessage(livingSessions.get(String.valueOf(userId)), message);
            }else{
                // 当前user未登录,保存到集合中
                unSendsUsers.put(String.valueOf(userId),message);
            }
        };
        // 这些消息属于同一个事件,放入集合中
        unSensMessages.put(messageVo.getBussinessKey(),unSendsUsers);
    }

}

监听消息队列

  • 最后就是监听消息队列,通过webSocke发送到前端页面
  • 先封装一个 message 类,保存我们需要发送的消息
package com.workfeng.message.config.rabbitmq;

import org.springframework.stereotype.Component;

import java.io.Serializable;
import java.util.List;

/**
 * 公共消息类
 */
@Component
public class MessageVo implements Serializable {

    //消息的唯一标识
    private String bussinessKey;
    //消息内容
    private String message;
    //可以收到的用户id
    private List<Integer> userIds;

    public String getBussinessKey() {
        return bussinessKey;
    }

    public void setBussinessKey(String bussinessKey) {
        this.bussinessKey = bussinessKey;
    }

    public String getMessage() {
        return message;
    }

    public void setMessage(String message) {
        this.message = message;
    }

    public List<Integer> getUserIds() {
        return userIds;
    }

    public void setUserIds(List<Integer> userIds) {
        this.userIds = userIds;
    }
}

  • 然后是监听类
package com.workfeng.message.config.rabbitmq;

import com.rabbitmq.client.Channel;
import com.workfeng.message.config.webSocket.WebSocketServerEndpoint;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.amqp.core.AcknowledgeMode;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.core.Queue;
import org.springframework.amqp.rabbit.annotation.EnableRabbit;
import org.springframework.amqp.rabbit.annotation.RabbitListenerConfigurer;
import org.springframework.amqp.rabbit.connection.ConnectionFactory;
import org.springframework.amqp.rabbit.listener.RabbitListenerEndpointRegistrar;
import org.springframework.amqp.rabbit.listener.SimpleMessageListenerContainer;
import org.springframework.amqp.rabbit.listener.api.ChannelAwareMessageListener;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.messaging.converter.MappingJackson2MessageConverter;
import org.springframework.messaging.handler.annotation.support.DefaultMessageHandlerMethodFactory;

import java.io.IOException;


@Configuration
@EnableRabbit
public class ConsumerConfig implements RabbitListenerConfigurer {

    private final static Logger log = LoggerFactory.getLogger(ConsumerConfig.class);

    @Autowired
    private ConnectionFactory connectionFactory;
    @Autowired
    private WebSocketServerEndpoint webSocketServerEndpoint;

    @Bean
    public DefaultMessageHandlerMethodFactory handlerMethodFactory() {
        DefaultMessageHandlerMethodFactory factory = new DefaultMessageHandlerMethodFactory();
        factory.setMessageConverter(new MappingJackson2MessageConverter());
        return factory;
    }

    /**
     * 监听消息发送
     * @param transaction
     * @param contract
     * @param qualification
     * @return
     */
    @Bean
    public SimpleMessageListenerContainer messageContainer(@Qualifier("queueTransaction") Queue transaction, @Qualifier("queueContract") Queue contract, @Qualifier("queueQualification") Queue qualification) {
        SimpleMessageListenerContainer container = new SimpleMessageListenerContainer(connectionFactory);
        container.setQueues(transaction, contract, qualification);
        container.setMaxConcurrentConsumers(1);
        container.setConcurrentConsumers(1);
        container.setAcknowledgeMode(AcknowledgeMode.MANUAL); //设置确认模式手工确认
        container.setMessageListener(new ChannelAwareMessageListener() {
            @Override
            public void onMessage(Message message, Channel channel) throws Exception {
                // 获取消息体
                byte[] body = message.getBody();
                log.info("receive msg : " + new String(body));
                try {
                    // 通过websocket向前端发送消息
                    webSocketServerEndpoint.sendMessageToAll(new String(body));
                    channel.basicAck(message.getMessageProperties().getDeliveryTag(), false);//确认消息成功消费
                } catch (IOException e) {
                    log.error("消息推送前台出错:" + e.getMessage() + "/r/n重新发送");
                    channel.basicNack(message.getMessageProperties().getDeliveryTag(), false, true); //重新发送
                }
            }
        });
        return container;
    }

    @Override
    public void configureRabbitListeners(RabbitListenerEndpointRegistrar rabbitListenerEndpointRegistrar) {
        rabbitListenerEndpointRegistrar.setMessageHandlerMethodFactory(handlerMethodFactory());
    }
}

模拟发送消息

  • ==============================
  • 接下来我们模拟向用户1 发送消息
  • ==============================
	@RequestMapping("/hello")
    @ResponseBody
    public Object hello(){

        // 发送给用户1的消息
        Integer[] ids = {1};
        String[] status = {"log","success","warn","error"};

        for(int i = 0; i< status.length;i++){
            MessageVo vo = new MessageVo();
            vo.setBussinessKey("00000"+i); // 消息唯一标识,将事件id作为消息唯一标识
            vo.setMessage("这是发送给用户 "+ids[0]+" 的 "+status[i]+" 状态的消息"); // 消息内容
            vo.setUserIds(Arrays.asList(ids)); //收消息的人
            vo.setStatus(status[i]); //警告
            vo.setType("toDo"); // 待办事项
            sender.send(vo);
        }

        return new SuccessTip();
    }
  • 我们先访问 /index 模拟用户1登陆,
  • 再访问 /hello ,这个时候我们前端就受到了四条消息
    在这里插入图片描述

消息发送逻辑详解

  • 我们来看看这中间都做了什么
  • 1,用户登陆

Alt

  • 2,前端html页面获取到userId, js向后端发起socket连接

Alt

  • 3,连接到 *WebSocketServerEndpoint 类,触发 onOpen() 方法
    Alt
  • 4,onOpen() 方法将当前登陆人的id保存到已登录人的集合中,并判断当前登陆人是否有未读事件的消息,有则发送。注释已经写了。想看的可以看看

Alt

  • 5,没有消息,就啥都没有反应啦。。。
  • 所以我们就主动发一条

Alt

  • 6 看一下send方法

Alt

  • 7,这里就通过rabbitMq发送了一条消息
  • 所以下一步就到了消息监听那里

Alt

  • 8 再接下来就是webSocket那边给前端发送消息了
  • 这边就是先遍历我需要发送的人,再查看我发送的人是否已登录,已登录就发送,未登陆就保存到集合中,下次登陆时就可以读到(第四步)
    Alt
    Alt
  • 9,接下类我们测试一下先发消息,在登陆看看是否可以读到消息
  • 我们先发消息,直接访问 /hello

Alt

  • 10,发送成功 ,此时用户1是有一条未读消息的
    Alt
  • 11 我们 “登陆” 用户1,访问 /index

Alt

  • 成功读到!!!
    在这里插入图片描述

定时循环发送消息

  • 这边的定时任务我用的是 quartz ,可以动态生成定时任务,和动态删除,正好可以用到。
  • pom 文件我们之前引过了
  • 接下来就是配置
  • 首先是jobBean工厂
package com.workfeng.message.config.quartz;

import org.quartz.spi.TriggerFiredBundle;
import org.springframework.beans.factory.config.AutowireCapableBeanFactory;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.scheduling.quartz.SpringBeanJobFactory;

/**
 * 自动注入的jobBean工厂
 */
public class AutowiringSpringBeanJobFactory extends SpringBeanJobFactory
        implements ApplicationContextAware {

    private transient AutowireCapableBeanFactory beanFactory;

    @Override
    public void setApplicationContext(final ApplicationContext context) {

        beanFactory = context.getAutowireCapableBeanFactory();
    }

    @Override
    protected Object createJobInstance(final TriggerFiredBundle bundle) throws Exception {
        final Object job = super.createJobInstance(bundle);

        beanFactory.autowireBean(job);

        return job;
    }
}

  • 配置一个监听类,可以在控制台看到任务的动态log信息
package com.workfeng.message.config.quartz;

import org.quartz.JobExecutionContext;
import org.quartz.JobExecutionException;
import org.quartz.JobListener;

public class SchedulerListener implements JobListener {
    public static final String LISTENER_NAME = "QuartSchedulerListener";

    @Override
    public String getName() {
        return LISTENER_NAME; //must return a name
    }

    //任务被调度前
    @Override
    public void jobToBeExecuted(JobExecutionContext context) {

        String jobName = context.getJobDetail().getKey().toString();
        System.out.println("jobToBeExecuted");
        System.out.println("Job : " + jobName + " is going to start...");

    }

    //任务调度被拒了
    @Override
    public void jobExecutionVetoed(JobExecutionContext context) {
        System.out.println("jobExecutionVetoed");
        //可以做一些日志记录原因

    }

    //任务被调度后
    @Override
    public void jobWasExecuted(JobExecutionContext context,
                               JobExecutionException jobException) {
        System.out.println("jobWasExecuted");

        String jobName = context.getJobDetail().getKey().toString();
        System.out.println("Job : " + jobName + " is finished...");

        if (jobException!=null&&!jobException.getMessage().equals("")) {
            System.out.println("Exception thrown by: " + jobName
                    + " Exception: " + jobException.getMessage());
        }

    }
}

  • 创建动态任务的管理类
  • startJob() 方法创建定时任务,供外部调用
  • scheduleJob() 启动任务
package com.workfeng.message.config.quartz;

import com.alibaba.fastjson.JSONObject;
import com.workfeng.message.config.rabbitmq.MessageVo;
import org.quartz.*;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.scheduling.quartz.SchedulerFactoryBean;
import org.springframework.stereotype.Component;

@Component
public class SchedulerManager {

    @SuppressWarnings("SpringJavaInjectionPointsAutowiringInspection")
    @Autowired
    private SchedulerFactoryBean schedulerFactoryBean;
    private JobListener scheduleListener;

    /**
     * 开始定时任务
     * @param jobName
     * @param jobGroup
     * @throws SchedulerException
     */
    public void startJob(String cron, String jobName, String jobGroup, Class<? extends Job> jobClass, MessageVo messageVo) throws SchedulerException
    {
        Scheduler scheduler = schedulerFactoryBean.getScheduler();
        if(scheduleListener==null){
            scheduleListener=new SchedulerListener();
            scheduler.getListenerManager().addJobListener(scheduleListener);
        }
        JobKey jobKey=new JobKey(jobName,jobGroup);
        if(!scheduler.checkExists(jobKey))
        {
            scheduleJob(cron,scheduler,jobName,jobGroup,jobClass,messageVo);
        }
    }

    /**
     * 移除定时任务
     * @param jobName
     * @param jobGroup
     * @throws SchedulerException
     */
    public void deleteJob(String jobName,String jobGroup) throws SchedulerException
    {
        Scheduler scheduler = schedulerFactoryBean.getScheduler();
        JobKey jobKey=new JobKey(jobName,jobGroup);
        scheduler.deleteJob(jobKey);
    }
    /**
     * 暂停定时任务
     * @param jobName
     * @param jobGroup
     * @throws SchedulerException
     */
    public void pauseJob(String jobName,String jobGroup) throws SchedulerException
    {
        Scheduler scheduler = schedulerFactoryBean.getScheduler();
        JobKey jobKey=new JobKey(jobName,jobGroup);
        scheduler.pauseJob(jobKey);
    }
    /**
     * 恢复定时任务
     * @param jobName
     * @param jobGroup
     * @throws SchedulerException
     */
    public void resumeJob(String jobName,String jobGroup) throws SchedulerException
    {
        Scheduler scheduler = schedulerFactoryBean.getScheduler();
        JobKey triggerKey=new JobKey(jobName,jobGroup);
        scheduler.resumeJob(triggerKey);
    }
    /**
     * 清空所有当前scheduler对象下的定时任务【目前只有全局一个scheduler对象】
     * @throws SchedulerException
     */
    public void clearAll() throws SchedulerException {
        Scheduler scheduler = schedulerFactoryBean.getScheduler();
        scheduler.clear();
    }



    /**
     * 动态创建Job
     * @param scheduler
     * @throws SchedulerException
     */
    private void scheduleJob(String cron,Scheduler scheduler,String jobName,String jobGroup,Class<? extends Job> jobClass,MessageVo messageVo) throws SchedulerException{
		// 将消息key,和消息体作为参数发送
        JobDetail jobDetail = JobBuilder
                .newJob(jobClass).withIdentity(jobName, jobGroup)
                .usingJobData(messageVo.getBussinessKey(), JSONObject.toJSON(messageVo).toString())
                .usingJobData("businessKey",messageVo.getBussinessKey())
                .build();
        // 每5s执行一次
        CronScheduleBuilder scheduleBuilder = CronScheduleBuilder.cronSchedule(cron);
        CronTrigger cronTrigger = TriggerBuilder.newTrigger().withIdentity(jobName, jobGroup).withSchedule(scheduleBuilder).build();

        scheduler.scheduleJob(jobDetail,cronTrigger);
    }

}

  • 这边定时任务还没有处理我们的业务逻辑(定时向前端推送消息)
  • 下边我们就处理
package com.workfeng.message.config.quartz;

import com.workfeng.message.config.util.DateUtil;
import com.workfeng.message.config.webSocket.WebSocketServerEndpoint;
import org.jboss.logging.Logger;
import org.quartz.*;
import org.springframework.beans.factory.annotation.Autowired;

import java.util.Date;

/**
 * 自定义定时任务
 */
public class ScheduledJob implements Job {

    private static final Logger logger= Logger.getLogger(ScheduledJob.class);

    @Autowired
    private WebSocketServerEndpoint webSocketServerEndpoint;

    @Override
    public void execute(JobExecutionContext jobExecutionContext) throws JobExecutionException {

        // 获取 jobDataMap
        JobDetail jobDetail = jobExecutionContext.getJobDetail();
        JobDataMap jobDataMap = jobDetail.getJobDataMap();
        // 获取消息businessKey,
        String businessKey = jobDataMap.getString("businessKey");
        // 再根据key获取消息内容
        String message = jobDataMap.getString(businessKey);

        //发送消息  执行任务逻辑....
        webSocketServerEndpoint.sendMessageToAll(message);

        logger.info("时间: "+ DateUtil.format(new Date(),"yyyy-MM-dd HH:mm:ss")+" 执行定时任务: key: "+ businessKey +",message: " + message);
    }
}

测试定时发送消息

  • 这样定时任务就配置的差不多了
  • 我们测试一下
  • 发消息是启动定时任务,每分钟发送一次
  • 修改 /hello
	// 注入
	@Autowired
    public SchedulerManager myScheduler;

	@RequestMapping("/hello")
    @ResponseBody
    public Object hello(){

        // 发送给用户1的消息
        Integer[] ids = {1};
        String[] status = {"log","success","warn","error"};

        for(int i = 0; i< status.length;i++){
            MessageVo vo = new MessageVo();
            vo.setBussinessKey("00000"+i); // 消息唯一标识,将事件id作为消息唯一标识
            vo.setMessage("这是发送给用户 "+ids[0]+" 的 "+status[i]+" 状态的消息"); // 消息内容
            vo.setUserIds(Arrays.asList(ids)); //收消息的人
            vo.setStatus(status[i]); //警告
            vo.setType("toDo"); // 待办事项
            sender.send(vo);

            //定时任务
            try {
                myScheduler.startJob("0 0/1 * * * ?", vo.getBussinessKey(),"messages", ScheduledJob.class,vo);//每五分钟执行一次
            } catch (SchedulerException e) {
                e.printStackTrace();
            }
        }
        return new SuccessTip();
    }
  • 重新访问之后,控制台打印

Alt

  • 删除定时任务
	@RequestMapping("/deletejob")
    @ResponseBody
    public Object deleteJob(){
        //删除定时任务
        try {
        	// 根据 事件唯一key 来删除
        	// 删除 messages 组
            myScheduler.deleteJob("000001","messages");
            System.out.println("定时消息任务key: "+ "000001" + " 已删除");
        } catch (SchedulerException e) {
            e.printStackTrace();
        }
        return new SuccessTip();
    }
  • 访问后控制台打印
    Alt

quartz 定时任务实现逻辑步骤

  • 1,启动定时任务

Alt

  • 2,启动任务

Alt

  • 3,发起时设置参数,携带message
    Alt
  • 4, 监听任务执行

Alt
**
另外有错欢迎指正
**

**
转载请声明作者
**

========================= end ===============

  • 8
    点赞
  • 39
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值