本项目演示了如何在Vue前端和Spring Boot后端中集成Kafka,实现实时消息的发送和接收,以及数据的实时展示。
后端实现:springboot配置、kafka配置、消息模型和仓库、消息服务和消费者、websocket配置、REST api控制器
前端实现:vue项目创建、websocket客户端配置、api服务、消息聊天组件、统计图表组件、主页面和路由配置
# 下载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
# 创建消息主题
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
<dependencies>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-webartifactId>
dependency>
<dependency>
<groupId>org.springframework.kafkagroupId>
<artifactId>spring-kafkaartifactId>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-data-jpaartifactId>
dependency>
<dependency>
<groupId>mysqlgroupId>
<artifactId>mysql-connector-javaartifactId>
<scope>runtimescope>
dependency>
<dependency>
<groupId>org.projectlombokgroupId>
<artifactId>lombokartifactId>
<optional>trueoptional>
dependency>
dependencies>
# 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
// 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();
}
}
}
// 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();
}
// 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;
}
}
// 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();
}
}
// 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);
}
}
// 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();
}
}
// 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());
}
}
# 创建Vue项目
npm create vue@latest kafka-vue-demo
# 进入项目目录
cd kafka-vue-demo
# 安装依赖
npm install
# 安装额外依赖
npm install element-plus axios sockjs-client @stomp/stompjs echarts
// 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();
// 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`);
}
};
Kafka实时聊天演示
欢迎使用Kafka实时聊天
请输入用户名开始聊天
// 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;
// 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');
# 进入后端项目目录
cd kafka-spring-demo
# 启动Spring Boot应用
./mvnw spring-boot:run
# 进入前端项目目录
cd kafka-vue-demo
# 启动开发服务器
npm run dev
打开浏览器,访问 http://localhost:8080 即可使用应用。
// 在Message类中添加消息类型字段
private String type; // 消息类型:text, image, file等
// 在Message类中添加已读状态字段
private boolean read;
// 在Message类中添加接收者字段
private String receiver; // 私聊接收者,为空表示群聊
// 在MessageRepository中添加搜索方法
List<Message> findByContentContainingOrSenderContaining(String content, String sender);
本项目演示了如何在Vue前端和Spring Boot后端中集成Kafka,实现实时消息的发送和接收。通过WebSocket和Kafka的结合,我们实现了一个功能完善的实时聊天应用。