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