背景

公司每上线一次打点需求,均需要数据组看下数据,这样效率特别低,数组组在被占用的时间里面,只能乖乖的配合打点测试,监控日志服务器的输出或者卡夫卡消息,而日志服务器和卡夫卡的机器是数据组的,涉及到权限,没法给其他部门或者产品用,因此急需一个kafka可视化界面来测试打点消息。

这个可视化界面应该包含:

1、选择所需要的 topic

2、订阅自己关注的关键字 比如新的版本号等

3、要能实时的展示

基于这几点需求,可以考虑使用 Springboot + websocket 来实现,

以下是实现思路,还不够完善,但是已经可以用来满足基本的需求。

代码会有下载链接,先说下思路:

1、用 ajax 请求把订阅的信息的一些前台信息传给后台

2、后台启动消费线程,消费到消息通过websocket实时传给前台

3、后台需要设置一个线程,不要过多的浪费资源

4、需要监听哪些topic,怎么过滤

5、一段时间后关闭线程

一些难点

1、websocket 无法注入其他bean,提示null,

解决方法:把websocket 类里面的context上下文设置为静态变量,并给出赋值方法,然后在启动类里面设置下

  // WebSocketServer 

//此处是解决无法注入的关键
    private static ApplicationContext applicationContext;
    public static void setApplicationContext(ApplicationContext applicationContext) {
        WebSocketServer.applicationContext = applicationContext;
    }

// KafkaWebViewApplication 的 main 方法 

SpringApplication springApplication = new SpringApplication(KafkaWebViewApplication.class);
        ConfigurableApplicationContext configurableApplicationContext = springApplication.run(args);
        // 设置 bean 由于websocket 是多例模式 所以可以采取这种方法进行设置
        WebSocketServer.setApplicationContext(configurableApplicationContext);

2、当无客户端连接的时候,关闭卡夫卡的监听,可以使用事件监听来解耦这些逻辑,有个小细节,最好不要一达到0就去关闭监听,有时候只是用户刷新了页面而已,所以停留五秒钟

 // WebSocketServer 的 onclose 方法

// 当没有在线用户的时候 关掉消费者
        if(onlineCount <= 0){
            // 5秒钟 后再判断 防止是刷新引起的 多关闭一次服务器
            try {
                Thread.sleep(5000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            if(onlineCount <= 0){
                applicationContext.publishEvent(OpenCloseEvent.builder().command("stop").build());
                CheckCenterController.startFlag = false;
            }

 // 使用 spring 的事件 来实现解耦

import lombok.Builder;
import lombok.Getter;

@Getter
@Builder(toBuilder = true)
public class OpenCloseEvent {
    // 监听命令名字
    private String command;
}
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.event.EventListener;
import org.springframework.stereotype.Component;

@Slf4j
@Component
public class OpenCloseEventListener {

    @Autowired
    private KafkaConsumer server;

    @EventListener
    public void handleOrderEvent(OpenCloseEvent event) {
        log.info("监听到 OpenCloseEvent 命令:" + event.getCommand());
        if("start".equals(event.getCommand())){
            server.startListener("durable1");
        }
        else {
            server.shutDownListener("durable1");
        }
    }
}

 3、消费者监听类

import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.kafka.annotation.KafkaListener;
import org.springframework.kafka.config.ConcurrentKafkaListenerContainerFactory;
import org.springframework.kafka.config.KafkaListenerEndpointRegistry;
import org.springframework.kafka.core.ConsumerFactory;
import org.springframework.stereotype.Component;

import java.io.IOException;


@Slf4j
@Component
public class KafkaConsumer{

    // 客户端连接数
    private static final int CLIENT_COUNT = 8;

    // 需要监控的关键字 支持多个 以 "&&" 分开
    static String[] keywordArray = new String[CLIENT_COUNT];

    // 订阅关键字的分隔符
    private static final String SEPTERATOR ="&&";
    // 字段之间的分隔符
    private static final String WORD_SEPTERATOR ="@";


    @Autowired
    private KafkaListenerEndpointRegistry registry;

    @Autowired
    private ConsumerFactory consumerFactory;

    @Bean
    public ConcurrentKafkaListenerContainerFactory delayContainerFactory() {
        ConcurrentKafkaListenerContainerFactory container = new ConcurrentKafkaListenerContainerFactory();
        container.setConsumerFactory(consumerFactory);
        //禁止自动启动
        container.setAutoStartup(false);
        return container;
    }

    @KafkaListener(id = "durable1", topics = "#{'${spring.kafkaListenerList}'.split(',')}",containerFactory = "delayContainerFactory")
    public void durableListener1(String data) {
        durableListenerCommon(data,0);
    }

    // 如果有多个消费者 可以把下面注释放开
//    @KafkaListener(id = "durable2", topics = "#{'${spring.kafkaListenerList}'.split(',')}",containerFactory = "delayContainerFactory")
//    public void durableListener2(String data) { durableListenerCommon(data,1); }
//    @KafkaListener(id = "durable3", topics = "#{'${spring.kafkaListenerList}'.split(',')}",containerFactory = "delayContainerFactory")
//    public void durableListener3(String data) {
//        durableListenerCommon(data,2);
//    }
//    @KafkaListener(id = "durable4", topics = "#{'${spring.kafkaListenerList}'.split(',')}",containerFactory = "delayContainerFactory")
//    public void durableListener4(String data) {
//        durableListenerCommon(data,3);
//    }
//    @KafkaListener(id = "durable5", topics = "#{'${spring.kafkaListenerList}'.split(',')}",containerFactory = "delayContainerFactory")
//    public void durableListener5(String data) {
//        durableListenerCommon(data,4);
//    }
//    @KafkaListener(id = "durable6", topics = "#{'${spring.kafkaListenerList}'.split(',')}",containerFactory = "delayContainerFactory")
//    public void durableListener6(String data) {
//        durableListenerCommon(data,5);
//    }
//    @KafkaListener(id = "durable7", topics = "#{'${spring.kafkaListenerList}'.split(',')}",containerFactory = "delayContainerFactory")
//    public void durableListener7(String data) {
//        durableListenerCommon(data,6);
//    }
//    @KafkaListener(id = "durable8", topics = "#{'${spring.kafkaListenerList}'.split(',')}",containerFactory = "delayContainerFactory")
//    public void durableListener8(String data) {
//        durableListenerCommon(data,7);
//    }



    // 开启消费监听
    void startListener(String listenerId) {
        log.info("开启监听");
        //判断监听容器是否启动,未启动则将其启动
        if (!registry.getListenerContainer(listenerId).isRunning()) {
            registry.getListenerContainer(listenerId).start();
        }
        registry.getListenerContainer(listenerId).resume();
    }

    //关闭消费监听
    void shutDownListener(String listenerId) {
        log.info("关闭监听");
        registry.getListenerContainer(listenerId).pause();
    }


    /**
     * 通用消息消费方法
     * @param data 监听到的数据
     * @param index 索引值
     */
    public void durableListenerCommon(String data,int index1){
        //这里做数据持久化的操作
        log.info("topic.quick.durable receive : " + data);

        for(int index =0; index <CLIENT_COUNT; index ++){
            String arrayData = keywordArray[index];
            if(arrayData != null ){
                String[] keyArray = arrayData.split(WORD_SEPTERATOR);
                String[] words = keyArray[1].split(SEPTERATOR);
                String topic = words[0];
                String key1 = "";
                String key2 = "";
                key1 = words[1];
                // 首先过滤 topic
                if(data.contains(topic)){
                    if(words.length > 2){
                        key2 = words[2];
                        if(data.contains(key1) && data.contains(key2)){
                            log.info("twokey:"+data);
                            try {
                                WebSocketServer.sendInfo(data,keyArray[2]);
                            } catch (IOException e) {
                                log.error("twokey发送失败");
                                this.shutDownListener("durable"+keyArray[0]);
                                e.printStackTrace();
                            }
                        }
                    }
                    else {
                        if(data.contains(key1)){
                            log.info("onekey:"+data);
                            try {
                                WebSocketServer.sendInfo(data,keyArray[2]);
                            } catch (IOException e) {
                                log.error("onekey发送失败");
                                this.shutDownListener("durable"+keyArray[0]);
                                e.printStackTrace();
                            }
                        }  // end if data contains key1
                    } // end else
                } // end if contains topic

            } // end if arrayData null

        } // end for

    }

}

项目源代码

遇到的问题

Caused by: java.lang.IllegalStateException: javax.websocket.server.ServerContainer not available

使用这个项目打包的时候会出现上述报错问题,因为spring boot内带tomcat,tomcat中的websocket会有冲突出现问题,因此跳过test编译打包,

修改配置文件,然后打包的时候 加上  -DskipTests 即可

<dependency>
   <groupId>org.springframework.boot</groupId>
   <artifactId>spring-boot-starter-websocket</artifactId>
   <scope>provided</scope>
</dependency><dependency>
   <groupId>org.eclipse.jetty.websocket</groupId>
   <artifactId>websocket-server</artifactId>
   <version>9.4.7.v20170914</version>
   <scope>test</scope>
</dependency>
 mvn package  -DskipTests

运行界面截图

kafka可视化工具 docker 安装 kafka可视化界面_消息可视化展示平台