Kafka在Vue和Spring Boot中的使用实例

news2025/4/17 11:52:00

Kafka在Vue和Spring Boot中的使用实例

一、项目概述

本项目演示了如何在Vue前端和Spring Boot后端中集成Kafka,实现实时消息的发送和接收,以及数据的实时展示。
后端实现:springboot配置、kafka配置、消息模型和仓库、消息服务和消费者、websocket配置、REST api控制器
前端实现:vue项目创建、websocket客户端配置、api服务、消息聊天组件、统计图表组件、主页面和路由配置

1.1 功能特点

  • 前端实时发送消息到Kafka
  • 后端接收Kafka消息并处理
  • 后端发送消息到Kafka
  • 前端实时接收并展示Kafka消息
  • 消息历史记录展示
  • 消息统计图表展示

1.2 技术栈

  • 前端:Vue 3 + Element Plus + ECharts
  • 后端:Spring Boot 2.7.x + Spring Kafka
  • 消息中间件:Apache Kafka
  • 数据库:MySQL (存储消息历史)

二、环境准备

2.1 安装Kafka

# 下载Kafka
wget https://downloads.apache.org/kafka/3.5.1/kafka_2.13-3.5.1.tgz

# 解压
tar -xzf kafka_2.13-3.5.1.tgz

# 启动Zookeeper
bin/zookeeper-server-start.sh config/zookeeper.properties

# 启动Kafka
bin/kafka-server-start.sh config/server.properties

2.2 创建Topic

# 创建消息主题
bin/kafka-topics.sh --create --bootstrap-server localhost:9092 \
    --replication-factor 1 --partitions 3 --topic chat-messages

# 创建通知主题
bin/kafka-topics.sh --create --bootstrap-server localhost:9092 \
    --replication-factor 1 --partitions 3 --topic notifications

三、后端实现

3.1 添加依赖

<!-- pom.xml -->
<dependencies>
    <!-- Spring Boot Starter -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    
    <!-- Spring Kafka -->
    <dependency>
        <groupId>org.springframework.kafka</groupId>
        <artifactId>spring-kafka</artifactId>
    </dependency>
    
    <!-- MySQL -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-data-jpa</artifactId>
    </dependency>
    <dependency>
        <groupId>mysql</groupId>
        <artifactId>mysql-connector-java</artifactId>
        <scope>runtime</scope>
    </dependency>
    
    <!-- Lombok -->
    <dependency>
        <groupId>org.projectlombok</groupId>
        <artifactId>lombok</artifactId>
        <optional>true</optional>
    </dependency>
</dependencies>

3.2 配置Kafka

# application.yml
spring:
  kafka:
    bootstrap-servers: localhost:9092
    producer:
      key-serializer: org.apache.kafka.common.serialization.StringSerializer
      value-serializer: org.springframework.kafka.support.serializer.JsonSerializer
    consumer:
      group-id: chat-group
      auto-offset-reset: earliest
      key-deserializer: org.apache.kafka.common.serialization.StringDeserializer
      value-deserializer: org.springframework.kafka.support.serializer.JsonDeserializer
      properties:
        spring.json.trusted.packages: "com.example.kafkademo.model"
  
  datasource:
    url: jdbc:mysql://localhost:3306/kafka_demo?useSSL=false&serverTimezone=UTC
    username: root
    password: password
    driver-class-name: com.mysql.cj.jdbc.Driver
  
  jpa:
    hibernate:
      ddl-auto: update
    show-sql: true

3.3 创建消息模型

// Message.java
package com.example.kafkademo.model;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

import javax.persistence.*;
import java.time.LocalDateTime;

@Data
@NoArgsConstructor
@AllArgsConstructor
@Entity
@Table(name = "messages")
public class Message {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    
    private String sender;
    private String content;
    private LocalDateTime timestamp;
    
    @PrePersist
    public void prePersist() {
        if (timestamp == null) {
            timestamp = LocalDateTime.now();
        }
    }
}

3.4 创建消息仓库

// MessageRepository.java
package com.example.kafkademo.repository;

import com.example.kafkademo.model.Message;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;

import java.util.List;

public interface MessageRepository extends JpaRepository<Message, Long> {
    List<Message> findTop50ByOrderByTimestampDesc();
    
    @Query("SELECT m.sender, COUNT(m) FROM Message m GROUP BY m.sender")
    List<Object[]> countMessagesBySender();
}

3.5 创建Kafka配置类

// KafkaConfig.java
package com.example.kafkademo.config;

import com.example.kafkademo.model.Message;
import org.apache.kafka.clients.admin.AdminClientConfig;
import org.apache.kafka.clients.admin.NewTopic;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.kafka.config.ConcurrentKafkaListenerContainerFactory;
import org.springframework.kafka.core.*;
import org.springframework.kafka.support.serializer.JsonDeserializer;
import org.springframework.kafka.support.serializer.JsonSerializer;

import java.util.HashMap;
import java.util.Map;

@Configuration
public class KafkaConfig {
    
    @Value("${spring.kafka.bootstrap-servers}")
    private String bootstrapServers;
    
    @Bean
    public KafkaAdmin kafkaAdmin() {
        Map<String, Object> configs = new HashMap<>();
        configs.put(AdminClientConfig.BOOTSTRAP_SERVERS_CONFIG, bootstrapServers);
        return new KafkaAdmin(configs);
    }
    
    @Bean
    public NewTopic chatTopic() {
        return new NewTopic("chat-messages", 3, (short) 1);
    }
    
    @Bean
    public NewTopic notificationTopic() {
        return new NewTopic("notifications", 3, (short) 1);
    }
    
    @Bean
    public ProducerFactory<String, Message> producerFactory() {
        Map<String, Object> configProps = new HashMap<>();
        configProps.put(org.apache.kafka.clients.producer.ProducerConfig.BOOTSTRAP_SERVERS_CONFIG, bootstrapServers);
        configProps.put(org.apache.kafka.clients.producer.ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, org.apache.kafka.common.serialization.StringSerializer.class);
        configProps.put(org.apache.kafka.clients.producer.ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG, JsonSerializer.class);
        return new DefaultKafkaProducerFactory<>(configProps);
    }
    
    @Bean
    public KafkaTemplate<String, Message> kafkaTemplate() {
        return new KafkaTemplate<>(producerFactory());
    }
    
    @Bean
    public ConsumerFactory<String, Message> consumerFactory() {
        Map<String, Object> props = new HashMap<>();
        props.put(org.apache.kafka.clients.consumer.ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG, bootstrapServers);
        props.put(org.apache.kafka.clients.consumer.ConsumerConfig.GROUP_ID_CONFIG, "chat-group");
        props.put(org.apache.kafka.clients.consumer.ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG, org.apache.kafka.common.serialization.StringDeserializer.class);
        props.put(org.apache.kafka.clients.consumer.ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG, JsonDeserializer.class);
        props.put(JsonDeserializer.TRUSTED_PACKAGES, "com.example.kafkademo.model");
        return new DefaultKafkaConsumerFactory<>(props, 
                new org.apache.kafka.common.serialization.StringDeserializer(),
                new JsonDeserializer<>(Message.class));
    }
    
    @Bean
    public ConcurrentKafkaListenerContainerFactory<String, Message> kafkaListenerContainerFactory() {
        ConcurrentKafkaListenerContainerFactory<String, Message> factory = 
                new ConcurrentKafkaListenerContainerFactory<>();
        factory.setConsumerFactory(consumerFactory());
        return factory;
    }
}

3.6 创建消息服务

// MessageService.java
package com.example.kafkademo.service;

import com.example.kafkademo.model.Message;
import com.example.kafkademo.repository.MessageRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.kafka.core.KafkaTemplate;
import org.springframework.stereotype.Service;

import java.util.List;

@Service
@RequiredArgsConstructor
public class MessageService {
    
    private final KafkaTemplate<String, Message> kafkaTemplate;
    private final MessageRepository messageRepository;
    
    public void sendMessage(Message message) {
        // 发送消息到Kafka
        kafkaTemplate.send("chat-messages", message);
        
        // 保存消息到数据库
        messageRepository.save(message);
    }
    
    public void sendNotification(String content) {
        Message notification = new Message();
        notification.setSender("系统");
        notification.setContent(content);
        kafkaTemplate.send("notifications", notification);
    }
    
    public List<Message> getRecentMessages() {
        return messageRepository.findTop50ByOrderByTimestampDesc();
    }
    
    public List<Object[]> getMessageStats() {
        return messageRepository.countMessagesBySender();
    }
}

3.7 创建Kafka消费者

// KafkaConsumer.java
package com.example.kafkademo.kafka;

import com.example.kafkademo.model.Message;
import lombok.extern.slf4j.Slf4j;
import org.springframework.kafka.annotation.KafkaListener;
import org.springframework.messaging.simp.SimpMessagingTemplate;
import org.springframework.stereotype.Component;

@Component
@Slf4j
public class KafkaConsumer {
    
    private final SimpMessagingTemplate messagingTemplate;
    
    public KafkaConsumer(SimpMessagingTemplate messagingTemplate) {
        this.messagingTemplate = messagingTemplate;
    }
    
    @KafkaListener(topics = "chat-messages", groupId = "chat-group")
    public void listenChatMessages(Message message) {
        log.info("收到聊天消息: {}", message);
        // 通过WebSocket发送消息到前端
        messagingTemplate.convertAndSend("/topic/messages", message);
    }
    
    @KafkaListener(topics = "notifications", groupId = "chat-group")
    public void listenNotifications(Message message) {
        log.info("收到通知: {}", message);
        // 通过WebSocket发送通知到前端
        messagingTemplate.convertAndSend("/topic/notifications", message);
    }
}

3.8 创建WebSocket配置

// WebSocketConfig.java
package com.example.kafkademo.config;

import org.springframework.context.annotation.Configuration;
import org.springframework.messaging.simp.config.MessageBrokerRegistry;
import org.springframework.web.socket.config.annotation.EnableWebSocketMessageBroker;
import org.springframework.web.socket.config.annotation.StompEndpointRegistry;
import org.springframework.web.socket.config.annotation.WebSocketMessageBrokerConfigurer;

@Configuration
@EnableWebSocketMessageBroker
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {
    
    @Override
    public void configureMessageBroker(MessageBrokerRegistry config) {
        config.enableSimpleBroker("/topic");
        config.setApplicationDestinationPrefixes("/app");
    }
    
    @Override
    public void registerStompEndpoints(StompEndpointRegistry registry) {
        registry.addEndpoint("/ws")
                .setAllowedOrigins("http://localhost:8080")
                .withSockJS();
    }
}

3.9 创建控制器

// MessageController.java
package com.example.kafkademo.controller;

import com.example.kafkademo.model.Message;
import com.example.kafkademo.service.MessageService;
import lombok.RequiredArgsConstructor;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;

import java.util.List;

@RestController
@RequestMapping("/api/messages")
@RequiredArgsConstructor
@CrossOrigin(origins = "http://localhost:8080")
public class MessageController {
    
    private final MessageService messageService;
    
    @PostMapping
    public ResponseEntity<Message> sendMessage(@RequestBody Message message) {
        messageService.sendMessage(message);
        return ResponseEntity.ok(message);
    }
    
    @GetMapping
    public ResponseEntity<List<Message>> getRecentMessages() {
        return ResponseEntity.ok(messageService.getRecentMessages());
    }
    
    @GetMapping("/stats")
    public ResponseEntity<List<Object[]>> getMessageStats() {
        return ResponseEntity.ok(messageService.getMessageStats());
    }
}

四、前端实现

4.1 创建Vue项目

# 创建Vue项目
npm create vue@latest kafka-vue-demo

# 进入项目目录
cd kafka-vue-demo

# 安装依赖
npm install

# 安装额外依赖
npm install element-plus axios sockjs-client @stomp/stompjs echarts

4.2 配置WebSocket客户端

// src/utils/websocket.js
import SockJS from 'sockjs-client';
import { Stomp } from '@stomp/stompjs';

class WebSocketClient {
  constructor() {
    this.stompClient = null;
    this.connected = false;
  }

  connect() {
    const socket = new SockJS('http://localhost:8081/ws');
    this.stompClient = Stomp.over(socket);
    this.stompClient.connect({}, this.onConnected, this.onError);
  }

  onConnected = () => {
    this.connected = true;
    console.log('WebSocket连接成功');
    
    // 订阅消息主题
    this.stompClient.subscribe('/topic/messages', this.onMessageReceived);
    this.stompClient.subscribe('/topic/notifications', this.onNotificationReceived);
  }

  onError = (error) => {
    console.error('WebSocket连接错误:', error);
    this.connected = false;
  }

  onMessageReceived = (payload) => {
    const message = JSON.parse(payload.body);
    console.log('收到消息:', message);
    // 触发消息接收事件
    window.dispatchEvent(new CustomEvent('messageReceived', { detail: message }));
  }

  onNotificationReceived = (payload) => {
    const notification = JSON.parse(payload.body);
    console.log('收到通知:', notification);
    // 触发通知接收事件
    window.dispatchEvent(new CustomEvent('notificationReceived', { detail: notification }));
  }

  disconnect() {
    if (this.stompClient) {
      this.stompClient.disconnect();
      this.connected = false;
    }
  }
}

export default new WebSocketClient();

4.3 创建API服务

// src/api/messageApi.js
import axios from 'axios';

const API_URL = 'http://localhost:8081/api';

export default {
  sendMessage(message) {
    return axios.post(`${API_URL}/messages`, message);
  },
  
  getRecentMessages() {
    return axios.get(`${API_URL}/messages`);
  },
  
  getMessageStats() {
    return axios.get(`${API_URL}/messages/stats`);
  }
};

4.4 创建消息组件

<!-- src/components/MessageChat.vue -->
<template>
  <div class="message-chat">
    <el-card class="chat-container">
      <template #header>
        <div class="card-header">
          <h2>实时聊天</h2>
        </div>
      </template>
      
      <div class="message-list" ref="messageList">
        <div v-for="message in messages" :key="message.id" class="message-item" :class="{ 'message-self': message.sender === username }">
          <div class="message-sender">{{ message.sender }}</div>
          <div class="message-content">{{ message.content }}</div>
          <div class="message-time">{{ formatTime(message.timestamp) }}</div>
        </div>
      </div>
      
      <div class="message-input">
        <el-input
          v-model="newMessage"
          placeholder="输入消息..."
          @keyup.enter="sendMessage"
        >
          <template #append>
            <el-button type="primary" @click="sendMessage">发送</el-button>
          </template>
        </el-input>
      </div>
    </el-card>
  </div>
</template>

<script>
import { ref, onMounted, onUnmounted, nextTick } from 'vue';
import { ElMessage } from 'element-plus';
import messageApi from '../api/messageApi';
import websocket from '../utils/websocket';

export default {
  name: 'MessageChat',
  props: {
    username: {
      type: String,
      required: true
    }
  },
  setup(props) {
    const messages = ref([]);
    const newMessage = ref('');
    const messageList = ref(null);
    
    // 加载历史消息
    const loadMessages = async () => {
      try {
        const response = await messageApi.getRecentMessages();
        messages.value = response.data;
        scrollToBottom();
      } catch (error) {
        console.error('加载消息失败:', error);
        ElMessage.error('加载消息失败');
      }
    };
    
    // 发送消息
    const sendMessage = async () => {
      if (!newMessage.value.trim()) return;
      
      const message = {
        sender: props.username,
        content: newMessage.value,
        timestamp: new Date()
      };
      
      try {
        await messageApi.sendMessage(message);
        newMessage.value = '';
      } catch (error) {
        console.error('发送消息失败:', error);
        ElMessage.error('发送消息失败');
      }
    };
    
    // 处理接收到的消息
    const handleMessageReceived = (event) => {
      const message = event.detail;
      messages.value.push(message);
      scrollToBottom();
    };
    
    // 处理接收到的通知
    const handleNotificationReceived = (event) => {
      const notification = event.detail;
      ElMessage.info(notification.content);
    };
    
    // 滚动到底部
    const scrollToBottom = async () => {
      await nextTick();
      if (messageList.value) {
        messageList.value.scrollTop = messageList.value.scrollHeight;
      }
    };
    
    // 格式化时间
    const formatTime = (timestamp) => {
      if (!timestamp) return '';
      const date = new Date(timestamp);
      return date.toLocaleTimeString();
    };
    
    onMounted(() => {
      loadMessages();
      websocket.connect();
      window.addEventListener('messageReceived', handleMessageReceived);
      window.addEventListener('notificationReceived', handleNotificationReceived);
    });
    
    onUnmounted(() => {
      websocket.disconnect();
      window.removeEventListener('messageReceived', handleMessageReceived);
      window.removeEventListener('notificationReceived', handleNotificationReceived);
    });
    
    return {
      messages,
      newMessage,
      messageList,
      sendMessage,
      formatTime
    };
  }
};
</script>

<style scoped>
.message-chat {
  height: 100%;
  display: flex;
  flex-direction: column;
}

.chat-container {
  height: 100%;
  display: flex;
  flex-direction: column;
}

.card-header {
  display: flex;
  justify-content: space-between;
  align-items: center;
}

.message-list {
  flex: 1;
  overflow-y: auto;
  padding: 10px;
  margin-bottom: 10px;
  height: 400px;
}

.message-item {
  margin-bottom: 10px;
  padding: 10px;
  border-radius: 5px;
  background-color: #f5f7fa;
  max-width: 80%;
}

.message-self {
  margin-left: auto;
  background-color: #ecf5ff;
}

.message-sender {
  font-weight: bold;
  margin-bottom: 5px;
}

.message-time {
  font-size: 12px;
  color: #909399;
  text-align: right;
  margin-top: 5px;
}

.message-input {
  margin-top: 10px;
}
</style>

4.5 创建统计图表组件

<!-- src/components/MessageStats.vue -->
<template>
  <div class="message-stats">
    <el-card>
      <template #header>
        <div class="card-header">
          <h2>消息统计</h2>
        </div>
      </template>
      
      <div class="chart-container" ref="chartContainer"></div>
    </el-card>
  </div>
</template>

<script>
import { ref, onMounted, onUnmounted } from 'vue';
import { ElMessage } from 'element-plus';
import * as echarts from 'echarts';
import messageApi from '../api/messageApi';

export default {
  name: 'MessageStats',
  setup() {
    const chartContainer = ref(null);
    let chart = null;
    
    // 加载统计数据
    const loadStats = async () => {
      try {
        const response = await messageApi.getMessageStats();
        const stats = response.data;
        
        const senders = stats.map(item => item[0]);
        const counts = stats.map(item => item[1]);
        
        updateChart(senders, counts);
      } catch (error) {
        console.error('加载统计数据失败:', error);
        ElMessage.error('加载统计数据失败');
      }
    };
    
    // 更新图表
    const updateChart = (senders, counts) => {
      if (!chart) return;
      
      const option = {
        title: {
          text: '用户消息数量统计'
        },
        tooltip: {
          trigger: 'axis',
          axisPointer: {
            type: 'shadow'
          }
        },
        xAxis: {
          type: 'category',
          data: senders
        },
        yAxis: {
          type: 'value'
        },
        series: [
          {
            name: '消息数量',
            type: 'bar',
            data: counts
          }
        ]
      };
      
      chart.setOption(option);
    };
    
    // 初始化图表
    const initChart = () => {
      if (chartContainer.value) {
        chart = echarts.init(chartContainer.value);
        loadStats();
      }
    };
    
    // 处理窗口大小变化
    const handleResize = () => {
      if (chart) {
        chart.resize();
      }
    };
    
    onMounted(() => {
      initChart();
      window.addEventListener('resize', handleResize);
    });
    
    onUnmounted(() => {
      if (chart) {
        chart.dispose();
      }
      window.removeEventListener('resize', handleResize);
    });
    
    return {
      chartContainer
    };
  }
};
</script>

<style scoped>
.message-stats {
  height: 100%;
}

.chart-container {
  height: 400px;
}
</style>

4.6 创建主页面

<!-- src/App.vue -->
<template>
  <div class="app-container">
    <el-container>
      <el-header>
        <h1>Kafka实时聊天演示</h1>
        <div class="user-info" v-if="!username">
          <el-input v-model="inputUsername" placeholder="请输入用户名" />
          <el-button type="primary" @click="login">登录</el-button>
        </div>
        <div class="user-info" v-else>
          <span>欢迎, {{ username }}</span>
          <el-button type="danger" @click="logout">退出</el-button>
        </div>
      </el-header>
      
      <el-main v-if="username">
        <el-row :gutter="20">
          <el-col :span="16">
            <MessageChat :username="username" />
          </el-col>
          <el-col :span="8">
            <MessageStats />
          </el-col>
        </el-row>
      </el-main>
      
      <el-main v-else class="login-container">
        <el-card class="login-card">
          <h2>欢迎使用Kafka实时聊天</h2>
          <p>请输入用户名开始聊天</p>
        </el-card>
      </el-main>
    </el-container>
  </div>
</template>

<script>
import { ref } from 'vue';
import MessageChat from './components/MessageChat.vue';
import MessageStats from './components/MessageStats.vue';

export default {
  name: 'App',
  components: {
    MessageChat,
    MessageStats
  },
  setup() {
    const username = ref('');
    const inputUsername = ref('');
    
    const login = () => {
      if (inputUsername.value.trim()) {
        username.value = inputUsername.value;
        inputUsername.value = '';
      }
    };
    
    const logout = () => {
      username.value = '';
    };
    
    return {
      username,
      inputUsername,
      login,
      logout
    };
  }
};
</script>

<style>
.app-container {
  height: 100vh;
}

.el-header {
  background-color: #409eff;
  color: white;
  display: flex;
  justify-content: space-between;
  align-items: center;
  padding: 0 20px;
}

.user-info {
  display: flex;
  align-items: center;
  gap: 10px;
}

.login-container {
  display: flex;
  justify-content: center;
  align-items: center;
}

.login-card {
  width: 400px;
  text-align: center;
  padding: 20px;
}

.el-main {
  padding: 20px;
}
</style>

4.7 配置路由

// src/router/index.js
import { createRouter, createWebHistory } from 'vue-router';
import App from '../App.vue';

const routes = [
  {
    path: '/',
    name: 'Home',
    component: App
  }
];

const router = createRouter({
  history: createWebHistory(import.meta.env.BASE_URL),
  routes
});

export default router;

4.8 配置主入口

// src/main.js
import { createApp } from 'vue';
import ElementPlus from 'element-plus';
import 'element-plus/dist/index.css';
import App from './App.vue';
import router from './router';

const app = createApp(App);

app.use(ElementPlus);
app.use(router);

app.mount('#app');

五、运行项目

5.1 启动后端

# 进入后端项目目录
cd kafka-spring-demo

# 启动Spring Boot应用
./mvnw spring-boot:run

5.2 启动前端

# 进入前端项目目录
cd kafka-vue-demo

# 启动开发服务器
npm run dev

5.3 访问应用

打开浏览器,访问 http://localhost:8080 即可使用应用。

六、功能演示

6.1 发送和接收消息

  1. 打开多个浏览器窗口,分别以不同用户名登录
  2. 在任意窗口发送消息,所有窗口都能实时接收到消息
  3. 消息会显示发送者、内容和时间

6.2 消息统计

  1. 发送多条消息后,统计图表会显示每个用户发送的消息数量
  2. 图表会自动更新,反映最新的统计数据

6.3 系统通知

  1. 后端可以发送系统通知,所有用户都能收到
  2. 通知会以Element Plus的消息提示形式显示

七、扩展功能

7.1 添加消息类型

// 在Message类中添加消息类型字段
private String type; // 消息类型:text, image, file等

7.2 添加消息已读状态

// 在Message类中添加已读状态字段
private boolean read;

7.3 添加私聊功能

// 在Message类中添加接收者字段
private String receiver; // 私聊接收者,为空表示群聊

7.4 添加消息搜索功能

// 在MessageRepository中添加搜索方法
List<Message> findByContentContainingOrSenderContaining(String content, String sender);

八、总结

本项目演示了如何在Vue前端和Spring Boot后端中集成Kafka,实现实时消息的发送和接收。通过WebSocket和Kafka的结合,我们实现了一个功能完善的实时聊天应用。

8.1 技术要点

  • Kafka消息的发送和接收
  • WebSocket实时通信
  • Vue组件化开发
  • Spring Boot后端服务
  • 数据可视化

8.2 项目亮点

  • 实时消息推送
  • 消息历史记录
  • 消息统计图表
  • 系统通知功能
  • 响应式设计

8.3 后续优化方向

  • 添加消息加密
  • 实现消息撤回功能
  • 添加文件上传功能
  • 优化移动端适配
  • 添加用户认证和授权

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2330706.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Flutter:Flutter SDK版本控制,fvm安装使用

1、首先已经安装了Dart&#xff0c;cmd中执行 dart pub global activate fvm2、windows配置系统环境变量 fvm --version3、查看本地已安装的 Flutter 版本 fvm releases4、验证当前使用的 Flutter 版本&#xff1a; fvm flutter --version5、切换到特定版本的 Flutter fvm use …

碰一碰发视频源头开发技术服务商

碰一碰发视频系统 随着短视频平台的兴起&#xff0c;用户的创作与分享需求日益增长。而如何让视频分享更加便捷、有趣&#xff0c;则成为各大平台优化的重点方向之一。抖音作为国内领先的短视频平台&#xff0c;在2023年推出了“碰一碰”功能&#xff0c;通过近距离通信技术实…

Oracle 23ai Vector Search 系列之4 VECTOR数据类型和基本操作

文章目录 Oracle 23ai Vector Search 系列之4 VECTOR数据类型和基本操作VECTOR 数据类型基本语法Vector 维度限制和向量大小向量存储格式&#xff08;DENSE vs SPARSE&#xff09;1. DENSE存储2. SPARSE存储3. 内部存储与空间计算 Oracle VECTOR数据类型的声明格式VECTOR基本操…

C++day8

思维导图 牛客练习 练习 #include <iostream> #include <cstring> #include <cstdlib> #include <unistd.h> #include <sstream> #include <vector> #include <memory> using namespace std; class user{ public: …

MySQL的进阶语法8(SQL优化——insert、主键、order by、group by、limit、count和update)

目录 一、插入数据 1.1 insert 1.2 大批量插入数据 二、主键优化 2.1 数据组织方式 2.2 页分裂 2.2.1 主键顺序插入效果 2.2.2 主键乱序插入效果 2.3 页合并 2.4 索引设计原则 三、order by优化 3.1 执行以下两条语句&#xff08;无索引&#xff09; 3.2 创建索引…

自然语言处理利器NLTK:从入门到核心功能解析

文章目录 一、NLP领域的基石工具包二、NLTK核心模块全景解析1 数据获取与预处理2 语言特征发现3 语义与推理 三、设计哲学与架构优势1 四维设计原则2 性能优化策略 四、典型应用场景1 学术研究2 工业实践 五、生态系统与未来演进 一、NLP领域的基石工具包 自然语言工具包&…

使用Docker安装及使用最新版本的Jenkins

1. 拉取镜像 通过Windows powerShell执行命令行&#xff08;2选1&#xff09;&#xff1a; -- 长期支持版 docker pull jenkins/jenkins:lts-- 最新版 docker pull jenkins/jenkins:latest 2. 创建并执行容器 你可以通过以下命令来运行Jenkins容器&#xff0c;执行命令&…

15-产品经理-维护需求

一、提研发需求 在产品–研发需求列表页&#xff0c;点击“提研发需求”按钮&#xff0c; 在提研发需求页面&#xff0c;可以选择已有的计划。也可以在计划页面里进行关联。 未编辑完的需求可以点击【存为草稿】按钮&#xff0c;保存为草稿状态&#xff0c;待编辑完成再选择提…

js前端对时间进行格式处理

时间格式处理 通过js前端&#xff0c;使用dayjs库进行格式化 安装dayjs库 npm install dayjs 封装成日期格式化工具类 formatter.ts // 导入 dayjs&#xff0c;先安装依赖 npm install dayjs import dayjs from "dayjs"; import utc from "dayjs/plugin/utc…

如何拿到iframe中嵌入的游戏数据

在 iframe 中嵌入的游戏数据是否能被获取&#xff0c;取决于以下几个关键因素&#xff1a; 1. 同源策略 浏览器的同源策略是核心限制。如果父页面和 iframe 中的内容同源&#xff08;即协议、域名和端口号完全相同&#xff09;&#xff0c;那么可以直接通过 JavaScript 访问 …

Chrome 135 版本新特性

Chrome 135 版本新特性 一、Chrome 135 版本浏览器更新 ** 1. 第三方托管账户注册迁移到 OIDC 授权码流程** Chrome 135 将账户注册的登录页面从营销网站迁移到动态网站&#xff0c;同时也将 OpenID Connect (OIDC) 的隐式流程迁移到授权码流程。这样做的目的是进一步提升第…

【Vue-组件】学习笔记

目录 <<回到导览组件1.项目1.1.Vue Cli1.2.项目目录1.3.运行流程1.4.组件的组成1.5.注意事项 2.组件2.1.组件注册2.2.scoped样式冲突2.3.data是一个函数2.4.props详解2.5.data和prop的区别 3.组件通信3.1.父子通信3.1.1.父传子&#xff08;props&#xff09;3.1.2.子传父…

(PROFINET 转 EtherCAT)EtherCAT/Ethernet/IP/Profinet/ModbusTCP协议互转工业串口网关

型号 协议转换通信网关 PROFINET 转 EtherCAT MS-GW31 概述 MS-GW31 是 PROFINET 和 EtherCAT 协议转换网关&#xff0c;为用户提供两种不同通讯协议的 PLC 进行数据交互的解决方案&#xff0c;可以轻松容易将 EtherCAT 网络接入 PROFINET 网络中&#xff0c;方便扩展&…

关于sqlsugar实体多层List映射的问题

如上图所示&#xff0c;当一个主表&#xff08;crm_fina_pay_req&#xff09;的子表list<文件附件关系表>&#xff08; List<crm_fina_payreq_evidofpay_relation> &#xff09;中&#xff0c;还包含有sysfile&#xff08;SysFile SysFiles&#xff09;类型的文件信…

STM32 HAL库 CANFD配置工具

用法说明&#xff1a; 该工具适用于STM32HAL库&#xff0c;可一键生成CANFD的HAL库配置代码。计算依据为HAL库&#xff0c;并参考ZLG标准。 软件界面&#xff1a; 仓库地址&#xff1a; HAL CANFD Init Gen: 适用于STM32控制器的HAL库 版本说明&#xff1a; V1.2.0 &#x…

UIMeter-UI自动化软件(产品级)

前言&#xff1a;作为一个资深测试工程师&#xff0c;UI测试&#xff0c;webUI自动化测试是我们必备的技能&#xff0c;我们都知道常用的框架比如selenium、playwright、rebootframwork等等&#xff0c;但是无论哪一种框架&#xff0c;都需要测试人员去编写代码&#xff0c;进行…

企业级Java开发工具MyEclipse v2025.1——支持AI编码辅助

MyEclipse一次性提供了巨量的Eclipse插件库&#xff0c;无需学习任何新的开发语言和工具&#xff0c;便可在一体化的IDE下进行Java EE、Web和PhoneGap移动应用的开发&#xff1b;强大的智能代码补齐功能&#xff0c;让企业开发化繁为简。 立即获取MyEclipse v2025.1正式版 具…

【redis】简介及在springboot中的使用

redis简介 基本概念 Redis&#xff0c;英文全称是Remote Dictionary Server&#xff08;远程字典服务&#xff09;&#xff0c;是一个开源的使用ANSI C语言编写、支持网络、可基于内存亦可持久化的日志型、Key-Value数据库&#xff0c;并提供多种语言的API。 与MySQL数据库不…

隐私计算的崛起:数据安全的未来守护者

在信息技术&#xff08;IT&#xff09;的滚滚浪潮中&#xff0c;一种新兴技术正以惊人速度崭露头角——隐私计算&#xff08;Privacy-Preserving Computation&#xff09;。2025 年&#xff0c;随着数据泄露事件频发、全球隐私法规日益严格&#xff0c;以及企业对数据协作需求的…

【Vue-vue基础知识】学习笔记

目录 <<回到导览vue基础知识1.1.创建一个vue实例1.2.vue基础指令1.2.1.v-bind1.2.2.v-model1.2.3.常用事件1.2.4.指令修饰符 1.3.计算属性1.3.1.计算属性的完整写法1.3.2.【案例】成绩 1.4.watch1.4.1.watch属性1.4.2.翻译业务实现1.4.3.watch属性的完整写法1.4.4.【案例…