Published on

Brewing Real-time Notifications: A Magician Guide to Spring Boot, Kafka, ReactJS, and Docker

Authors

Why hello there fellow Java enthusiast! Ready to dive into a mad jumble of code and coffee? Excellent! Make sure to don your coding helmets (it's a thing, trust me!) and remember - we're about to wrangle some Kafka and Spring Boot to implement a killer notification feature for your web and mobile clients. So let's get those creative Java juices flowing!

First things first, let's talk about what we're dealing with here: Kafka and Spring Boot.

Kafka is a bit like a super-efficient postman, who's guzzling energy drinks and always on the move. It's a distributed streaming platform that allows apps to publish and subscribe to streams of records. Spring Boot, on the other hand, is like your trusty toolbox - a comfy zone filled with everything you need for application development.

Step 1: Conjure a new Spring Boot Project with Spring Initializr

Spring Initializr is like a magic potion for creating Spring Boot projects. It's the secret recipe of seasoned Java sorcerers!

  1. Zip on over to Spring Initializr.
  2. Fill out the Group and Artifact details to your heart's content.
  3. Choose your dependencies like a pro - for this mission, we'll need Spring Web, Spring for Apache Kafka, and Spring WebSocket.
  4. Click 'Generate' with a flourish, and poof! Your zipped project is ready for download.
  5. Once downloaded, unzip it and open it in your IDE. I'm going to use IntelliJ IDEA, because, well, it's the IDE of champions!

Step 2: The Kafka Potion - Producer and Consumer Configuration

Now that we have our Spring Boot project, let's bring Kafka into the mix.

  1. Kafka Producer Configuration is the wizardry that sends our notifications. Here's the code to whip up this concoction:
@Configuration
public class KafkaProducerConfig {
    
    @Value("${kafka.bootstrapAddress}")
    private String bootstrapAddress;

    @Bean
    public ProducerFactory<String, String> producerFactory() {
        Map<String, Object> configProps = new HashMap<>();
        configProps.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG, bootstrapAddress);
        configProps.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, StringSerializer.class);
        configProps.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG, StringSerializer.class);
        return new DefaultKafkaProducerFactory<>(configProps);
    }

    @Bean
    public KafkaTemplate<String, String> kafkaTemplate() {
        return new KafkaTemplate<>(producerFactory());
    }
}
  1. Let's create a NotificationService that uses this Kafka producer to send the notification:
@Service
public class NotificationService {
    
    @Autowired
    private KafkaTemplate<String,String> kafkaTemplate;

    public void sendNotification(String message){
        kafkaTemplate.send("notifications", message);
    }
}
  1. But who will listen to these messages? Enter Kafka Consumer Configuration:
@Configuration
public class KafkaConsumerConfig {
    
    @Value("${kafka.bootstrapAddress}")
    private String bootstrapAddress;

    @Value("${kafka.groupId}")
    private String groupId;

    @Bean
    public ConsumerFactory<String, String> consumerFactory() {
        Map<String, Object> configProps = new HashMap<>();
        configProps.put(ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG, bootstrapAddress);
        configProps.put(ConsumerConfig.GROUP_ID_CONFIG, groupId);
        configProps.put(ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class);
        configProps.put(ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class);
        return new DefaultKafkaConsumerFactory<>(configProps);
    }

    @Bean
    public ConcurrentKafkaListenerContainerFactory<String, String> kafkaListenerContainerFactory() {
        ConcurrentKafkaListenerContainerFactory<String, String> factory = new ConcurrentKafkaListenerContainerFactory<>();
        factory.setConsumerFactory(consumerFactory());
        return factory;
    }
}
  1. Now, create a NotificationConsumerService that listens to the messages and then sends them to our clients:
@Service
public class NotificationConsumerService {
    
    @Autowired
    private SimpMessagingTemplate template;

    @K

afkaListener(topics = "notifications", groupId = "group_id")
    public void consume(String message){
        template.convertAndSend("/topic/notifications", message);
    }
}

Step 3: Brewing the React.js Frontend Potion

This one's for our web clients. Create a new React.js application and add this NotificationComponent to see the notifications on the webpage. It uses a WebSocket to listen to notifications from our server and updates the UI whenever it receives a notification.

import React, { Component } from 'react';
import SockJS from 'sockjs-client';
import Stomp from 'stompjs';

class NotificationComponent extends Component {
    constructor(props) {
        super(props);
        this.state = { notifications: [] };
    }

    componentDidMount() {
        this.connect();
    }

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

    onConnected = () => {
        this.stompClient.subscribe('/topic/notifications', this.onMessageReceived);
    };

    onMessageReceived = (payload) => {
        let notification = JSON.parse(payload.body);
        this.setState((prevState) => ({
            notifications: [...prevState.notifications, notification],
        }));
    };

    onError = (error) => {
        console.log("Could not connect you to the server. Please, try again later!");
    };

    render() {
        return (
            <div className="notificationComponent">
                {this.state.notifications.map((notification, i) => (
                    <div key={i} className="notification">
                        {notification}
                    </div>
                ))}
            </div>
        );
    }
}

export default NotificationComponent;

Step 4: Bottle Up Your App With Docker

Now that our potions are all brewed, let's bottle them up.

  1. Create a Dockerfile in the root of your Spring Boot project:
FROM openjdk:11
ARG JAR_FILE=target/*.jar
COPY ${JAR_FILE} app.jar
ENTRYPOINT ["java","-jar","/app.jar"]
  1. Build your Docker image:
docker build -t my-spring-boot-app .
  1. Next, create a Dockerfile for your React.js application:
FROM node:14 as build
WORKDIR /app
COPY package*.json ./
RUN npm install
COPY . .
RUN npm run build

FROM nginx:stable-alpine
COPY --from=build /app/build /usr/share/nginx/html
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]
  1. Build this Docker image:
docker build -t my-react-app .
  1. Now that you have your Docker images, you can use Docker Compose to start your whole stack:
docker-compose up

And voila! You've just weaved together Spring Boot, Kafka, React.js, and Docker to create a real-time notification feature. Your Java mastery knows no bounds! May the Force of Java be with you, always!