Spring Boot Aufbau – Tag 7 von 10
Von Elyndra Valen, Senior Entwicklerin bei Java Fleet Systems Consulting


Messaging

🗺️ Deine Position im Kurs

TagThemaStatus
1Auto-Configuration & Custom Starter✅ Abgeschlossen
2Spring Data JPA Basics✅ Abgeschlossen
3JPA Relationships & Queries✅ Abgeschlossen
4Spring Security Part 1 – Authentication✅ Abgeschlossen
5Spring Security Part 2 – Authorization✅ Abgeschlossen
6Spring Boot Caching & JSON✅ Abgeschlossen
→ 7Messaging & Email👉 DU BIST HIER!
8Testing & Dokumentation📜 Kommt als nächstes
9Spring Boot Actuator🔒 Noch nicht freigeschaltet
10Template Engines & Microservices🔒 Noch nicht freigeschaltet

Modul: Spring Boot Aufbau
Gesamt-Dauer: 10 Arbeitstage (je 8 Stunden)
Dein Ziel: Email-Versand und asynchrone Kommunikation implementieren


📋 Voraussetzungen für diesen Tag

Du brauchst:

  • ✅ Java JDK 21 installiert
  • ✅ Maven 3.8+ oder deine IDE
  • ✅ Docker (für MailDev)
  • ✅ Grundlegendes Spring Boot Verständnis (Tage 1-6)

Optional (hilft beim Verständnis):

  • SMTP-Grundkenntnisse
  • Thread-Konzepte in Java

⚡ Kurze Zusammenfassung – Das Wichtigste in 30 Sekunden

Heute lernst du:

  • ✅ Was Messaging ist und warum du es brauchst
  • ✅ Die 3-Projekt-Architektur: Broker → Producer → Consumer
  • ✅ Wie Messages zwischen Services kommunizieren
  • ✅ Email-Versand mit Spring Boot Mail
  • ✅ Asynchronen Email-Versand für bessere Performance

Das Kernkonzept:

KomponenteRolleAnalogie
BrokerVermittelt MessagesDas Postamt
ProducerSendet MessagesDer Absender
ConsumerEmpfängt MessagesDer Empfänger

Warum Messaging? Der Webshop muss nicht warten, bis das Lager die Bestellung verarbeitet hat!


🖼️ Das Konzept auf einen Blick

Abbildung 1: Die drei Komponenten einer Message-Driven Architecture


👋 Elyndra: „Messaging hat mir einmal den Job gerettet!“

Hi! 👋

Elyndra hier. Lass mich dir eine Geschichte erzählen.

Vor zwei Jahren hatte ich ein Projekt: Ein Webshop mit 50.000 Bestellungen am Tag. Alles lief synchron – Bestellung rein, Lagerverwaltung verarbeitet, Email raus, Response zum Kunden. Das Problem? Die Response-Zeit lag bei 8 Sekunden.

Der Kunde war not amused. „Elyndra, das muss schneller werden. Morgen.“

Meine Lösung? Message-Driven Architecture.

Statt alles synchron zu machen, habe ich die Bestellungen in eine Queue gesteckt. Der Webshop schickt die Bestellung ab und antwortet dem Kunden sofort mit „Bestellung eingegangen!“. Im Hintergrund holt sich die Lagerverwaltung die Bestellung aus der Queue und verarbeitet sie in Ruhe.

Ergebnis? Response-Zeit: 200 Millisekunden. Der Kunde war glücklich, ich war glücklich, und ich habe etwas Wichtiges gelernt:

„Messages sind wie Briefe – der Absender muss nicht warten, bis der Empfänger gelesen hat.“

Heute zeige ich dir genau das. Wir bauen zusammen drei Projekte, die miteinander kommunizieren. Und als Bonus: Email-Versand, damit du auch „nach außen“ kommunizieren kannst.

Lass uns loslegen! 🚀


🎯 Was du heute baust

Vier Projekte, ein System:

ProjektPortFunktion
MyMessageBroker8080 / 61616Der Message-Vermittler
MessageProducer8081Sendet Bestellungen
MessageConsumer8082Verarbeitet Bestellungen
SpringMailService8083Versendet Emails

Am Ende des Tages kannst du:

  1. Alle drei Messaging-Projekte gleichzeitig starten
  2. Eine Bestellung über den Producer senden
  3. Zusehen wie der Consumer sie verarbeitet
  4. Emails synchron und asynchron versenden

🟢 GRUNDLAGEN – Teil 1: Was ist Messaging?

Das Problem verstehen

Stell dir einen Webshop vor. Ein Kunde bestellt ein Laptop. Was passiert?

Ohne Messaging (synchron):

1. Kunde klickt "Kaufen"
2. Server prüft Lagerbestand ────────────────┐
3. Server reserviert Ware                     │ Kunde wartet...
4. Server erstellt Rechnung                   │ und wartet...
5. Server sendet Bestätigungs-Email          │ und wartet...
6. Server antwortet: "Bestellung erfolgreich" ┘

Das Problem? Der Kunde wartet 5-10 Sekunden. Bei jedem Schritt.

Mit Messaging (asynchron):

1. Kunde klickt "Kaufen"
2. Server legt Bestellung in Queue ──────────┐
3. Server antwortet SOFORT: "Eingegangen!"   │ 200ms!
                                              │
   Im Hintergrund:                            │
   - Lagerverwaltung holt Bestellung         │
   - Verarbeitet in eigenem Tempo            │
   - Email-Service sendet Bestätigung        ┘

Das Ergebnis? Der Kunde bekommt sofort Feedback. Die Verarbeitung läuft im Hintergrund.

Die drei Rollen verstehen

1. Der Broker (Das Postamt)

Der Broker ist die zentrale Vermittlungsstelle. Er:

  • Empfängt Messages von Producern
  • Speichert sie in Queues
  • Liefert sie an Consumer aus

Wichtig: Der Broker speichert Messages. Wenn ein Consumer offline ist, wartet die Message geduldig.

2. Der Producer (Der Absender)

Der Producer erstellt Messages und schickt sie zum Broker. Er:

  • Weiß nicht, wer die Message empfängt
  • Wartet nicht auf Verarbeitung
  • Kümmert sich nur ums Absenden

3. Der Consumer (Der Empfänger)

Der Consumer holt Messages vom Broker und verarbeitet sie. Er:

  • Lauscht auf bestimmte Queues
  • Verarbeitet Messages in eigenem Tempo
  • Bestätigt erfolgreiche Verarbeitung (ACK)

Warum ist das besser?

AspektSynchronMit Messaging
Response-Zeit5-10 Sekunden200 Millisekunden
Bei Consumer-AusfallFehler!Message wartet in Queue
SkalierungSchwierigEinfach (mehr Consumer)
EntkopplungEng verbundenLose gekoppelt

🟢 GRUNDLAGEN – Teil 2: Projekt 1 – Der Message Broker

Schritt 1: Broker-Projekt erstellen

Erstelle ein neues Maven-Projekt oder lade das fertige Projekt herunter:

📦 Download: MyMessageBroker.zip

pom.xml:

xml

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0">
    <modelVersion>4.0.0</modelVersion>
    
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>3.3.0</version>
    </parent>
    
    <groupId>de.javafleet.messaging</groupId>
    <artifactId>message-broker</artifactId>
    <version>1.0.0</version>
    
    <properties>
        <java.version>21</java.version>
    </properties>
    
    <dependencies>
        <!-- Spring Boot Web für Health-Endpoint -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        
        <!-- ActiveMQ Classic -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-activemq</artifactId>
        </dependency>
        
        <!-- Embedded Broker -->
        <dependency>
            <groupId>org.apache.activemq</groupId>
            <artifactId>activemq-broker</artifactId>
        </dependency>
        
        <!-- Persistenz für Messages -->
        <dependency>
            <groupId>org.apache.activemq</groupId>
            <artifactId>activemq-kahadb-store</artifactId>
        </dependency>
    </dependencies>
</project>

Was macht jede Dependency?

DependencyZweck
spring-boot-starter-webErmöglicht REST-Endpoints für Status-Abfragen
spring-boot-starter-activemqActiveMQ Client-Bibliotheken
activemq-brokerDer eigentliche Broker (embedded)
activemq-kahadb-storePersistente Speicherung für Messages

Schritt 2: Broker-Konfiguration erstellen

BrokerConfig.java:

java

package de.javafleet.messaging.config;

import org.apache.activemq.broker.BrokerService;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class BrokerConfig {

    @Value("${broker.url:tcp://localhost:61616}")
    private String brokerUrl;
    
    @Value("${broker.data.directory:./data/activemq}")
    private String dataDirectory;

    @Bean
    public BrokerService brokerService() throws Exception {
        BrokerService broker = new BrokerService();
        
        // TCP-Connector für externe Verbindungen
        broker.addConnector(brokerUrl);
        
        // Persistenz aktivieren
        broker.setPersistent(true);
        
        // Datenverzeichnis
        broker.setDataDirectory(dataDirectory);
        
        // Broker-Name
        broker.setBrokerName("JavaFleetBroker");
        
        return broker;
    }
}

Was passiert hier?

Diese Klasse konfiguriert unseren eingebetteten ActiveMQ Broker. Lass mich das Zeile für Zeile erklären:

@Configuration – Sagt Spring: „Diese Klasse enthält Bean-Definitionen.“

@Value("${broker.url:tcp://localhost:61616}") – Liest die URL aus application.properties. Der Teil nach dem Doppelpunkt ist der Default-Wert, falls die Property nicht gesetzt ist.

broker.addConnector(brokerUrl) – Der Broker lauscht auf dieser URL. Producer und Consumer verbinden sich hierüber.

broker.setPersistent(true)Das ist wichtig! Mit true werden Messages auf der Festplatte gespeichert. Wenn der Broker neu startet, sind die Messages noch da.

broker.setDataDirectory(...) – Wo die Messages gespeichert werden. Hier wird eine KahaDB-Datenbank angelegt.

Schritt 3: application.properties

properties

spring.application.name=message-broker
server.port=8080

# Broker-Konfiguration
broker.url=tcp://localhost:61616
broker.data.directory=./data/activemq
broker.persistent=true

# ActiveMQ Client (falls der Broker selbst Messages senden will)
spring.activemq.broker-url=tcp://localhost:61616
spring.activemq.user=admin
spring.activemq.password=admin
spring.activemq.packages.trust-all=true

Schritt 4: Main Application

java

package de.javafleet.messaging;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class MessageBrokerApplication {

    public static void main(String[] args) {
        System.out.println("""
            
            ╔══════════════════════════════════════════════════════════════╗
            ║   🚀 Java Fleet Message Broker                               ║
            ║   ActiveMQ läuft auf: tcp://localhost:61616                  ║
            ║   Web-Console:        http://localhost:8080                  ║
            ╚══════════════════════════════════════════════════════════════╝
            
            """);
        
        SpringApplication.run(MessageBrokerApplication.class, args);
    }
}

Schritt 5: Broker starten und testen

bash

cd MyMessageBroker
mvn spring-boot:run

Erwartete Ausgabe:

╔══════════════════════════════════════════════════════════════╗
║   🚀 Java Fleet Message Broker                               ║
║   ActiveMQ läuft auf: tcp://localhost:61616                  ║
║   Web-Console:        http://localhost:8080                  ║
╚══════════════════════════════════════════════════════════════╝

... INFO ... BrokerService - Using Persistence Adapter: KahaDBPersistenceAdapter
... INFO ... BrokerService - Apache ActiveMQ ... is starting
... INFO ... TransportConnector - Connector tcp://localhost:61616 started

✅ Checkpoint:

  • Broker startet ohne Fehler?
  • „Connector tcp://localhost:61616 started“ erscheint?
  • Port 61616 ist belegt? (netstat -an | grep 61616)

Broker läuft! Lass ihn laufen und öffne ein neues Terminal für den Producer.


🟢 GRUNDLAGEN – Teil 3: Projekt 2 – Der Message Producer

Der Producer sendet Bestellungen

Der Producer simuliert einen Webshop. Wenn eine Bestellung eingeht, schickt er sie an die Queue.

📦 Download: MessageProducer.zip

Schritt 1: Order-Model erstellen

java

package de.javafleet.messaging.model;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.io.Serializable;
import java.time.LocalDateTime;
import java.util.UUID;

@Data
@NoArgsConstructor
@AllArgsConstructor
public class Order implements Serializable {
    
    private static final long serialVersionUID = 1L;
    
    private String orderId;
    private String customerName;
    private String product;
    private int quantity;
    private LocalDateTime orderTime;
    private String status;

    public static Order create(String customerName, String product, int quantity) {
        return new Order(
            UUID.randomUUID().toString().substring(0, 8),
            customerName,
            product,
            quantity,
            LocalDateTime.now(),
            "PENDING"
        );
    }
}

Warum Serializable?

Das ist wichtig! Damit die Bestellung als Message verschickt werden kann, muss sie serialisierbar sein. ActiveMQ wandelt das Objekt in Bytes um, schickt es über das Netzwerk, und der Consumer baut es wieder zusammen.

Schritt 2: OrderProducer Service

java

package de.javafleet.messaging.service;

import de.javafleet.messaging.model.Order;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.jms.core.JmsTemplate;
import org.springframework.stereotype.Service;

@Service
@Slf4j
public class OrderProducer {

    private final JmsTemplate jmsTemplate;
    private final String queueName;

    public OrderProducer(
            JmsTemplate jmsTemplate, 
            @Value("${app.queue.orders}") String queueName) {
        this.jmsTemplate = jmsTemplate;
        this.queueName = queueName;
    }

    public void sendOrder(Order order) {
        log.info("📤 Sending order to queue '{}': {}", queueName, order.getOrderId());
        
        try {
            jmsTemplate.convertAndSend(queueName, order);
            log.info("✅ Order {} sent successfully!", order.getOrderId());
        } catch (Exception e) {
            log.error("❌ Failed to send order {}: {}", order.getOrderId(), e.getMessage());
            throw new RuntimeException("Could not send order to queue", e);
        }
    }
}

Was passiert hier im Detail?

JmsTemplate – Das ist Spring’s Helfer für JMS-Operationen. Er kümmert sich um Verbindung, Session, Message-Erstellung – alles automatisch.

@Value("${app.queue.orders}") – Liest den Queue-Namen aus den Properties. So ist er nicht hardcoded und kann für verschiedene Umgebungen angepasst werden.

jmsTemplate.convertAndSend(queueName, order) – Das ist der Kern:

  1. convert: Wandelt das Order-Objekt in eine JMS-Message um
  2. send: Schickt die Message an die Queue

Die Methode returned sofort – sie wartet nicht, bis ein Consumer die Message verarbeitet hat. Das ist der Unterschied zu synchroner Kommunikation!

Schritt 3: REST Controller

java

package de.javafleet.messaging.controller;

import de.javafleet.messaging.model.Order;
import de.javafleet.messaging.service.OrderProducer;
import lombok.RequiredArgsConstructor;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import java.util.Map;

@RestController
@RequestMapping("/api/orders")
@RequiredArgsConstructor
public class OrderController {

    private final OrderProducer orderProducer;

    @GetMapping("/send")
    public ResponseEntity<Map<String, Object>> sendOrder(
            @RequestParam String product,
            @RequestParam(defaultValue = "TestKunde") String customer,
            @RequestParam(defaultValue = "1") int quantity) {
        
        Order order = Order.create(customer, product, quantity);
        orderProducer.sendOrder(order);
        
        return ResponseEntity.ok(Map.of(
            "status", "QUEUED",
            "orderId", order.getOrderId(),
            "message", "Order sent to queue!"
        ));
    }
}

Schritt 4: application.properties

properties

spring.application.name=message-producer
server.port=8081

spring.activemq.broker-url=tcp://localhost:61616
spring.activemq.user=admin
spring.activemq.password=admin
spring.activemq.packages.trust-all=true

app.queue.orders=order-queue

Schritt 5: Producer starten und testen

bash

cd MessageProducer
mvn spring-boot:run

Teste mit curl:

bash

curl "http://localhost:8081/api/orders/send?product=Laptop&customer=Max&quantity=2"

Erwartete Response:

json

{
  "status": "QUEUED",
  "orderId": "a1b2c3d4",
  "message": "Order sent to queue!"
}

In der Producer-Console:

📤 Sending order to queue 'order-queue': a1b2c3d4
✅ Order a1b2c3d4 sent successfully!

✅ Checkpoint:

  • Producer startet auf Port 8081?
  • curl-Request gibt Response zurück?
  • „Order sent successfully!“ im Log?

Die Message ist jetzt in der Queue! Sie wartet dort, bis ein Consumer sie abholt. Starte den Consumer und beobachte, was passiert.


🟢 GRUNDLAGEN – Teil 4: Projekt 3 – Der Message Consumer

Der Consumer verarbeitet Bestellungen

Der Consumer simuliert die Lagerverwaltung. Er lauscht auf die Queue und verarbeitet eingehende Bestellungen.

📦 Download: MessageConsumer.zip

Schritt 1: Order-Model (identisch zum Producer!)

Das Order-Model muss exakt identisch sein – gleiches Package, gleiche Felder, gleiche serialVersionUID. Kopiere es aus dem Producer-Projekt.

Schritt 2: OrderConsumer Service

java

package de.javafleet.messaging.service;

import de.javafleet.messaging.model.Order;
import lombok.extern.slf4j.Slf4j;
import org.springframework.jms.annotation.JmsListener;
import org.springframework.stereotype.Service;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;

@Service
@Slf4j
public class OrderConsumer {

    private final List<Order> processedOrders = 
        Collections.synchronizedList(new ArrayList<>());

    @JmsListener(destination = "${app.queue.orders}")
    public void receiveOrder(Order order) {
        log.info("📥 ======================================");
        log.info("📥 ORDER RECEIVED!");
        log.info("📥 Order ID:  {}", order.getOrderId());
        log.info("📥 Customer:  {}", order.getCustomerName());
        log.info("📥 Product:   {}", order.getProduct());
        log.info("📥 Quantity:  {}", order.getQuantity());
        log.info("📥 ======================================");
        
        processOrder(order);
    }
    
    private void processOrder(Order order) {
        log.info("⚙️ Processing order {}...", order.getOrderId());
        
        // Simuliere Verarbeitungszeit
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        }
        
        order.setStatus("COMPLETED");
        processedOrders.add(order);
        
        log.info("✅ Order {} completed! Total: {}", 
            order.getOrderId(), processedOrders.size());
    }
    
    public List<Order> getProcessedOrders() {
        return new ArrayList<>(processedOrders);
    }
}

Was macht @JmsListener?

Das ist die Magie! Spring macht folgendes automatisch:

  1. Verbindet sich mit dem Broker (URL aus Properties)
  2. Lauscht auf die Queue (order-queue)
  3. Wenn eine Message ankommt: Ruft diese Methode auf
  4. Konvertiert die Message automatisch zurück zum Order-Objekt
  5. Nach erfolgreicher Verarbeitung: Bestätigt die Message (ACK)

Das Schöne: Du schreibst nur die Verarbeitungslogik – den Rest erledigt Spring!

Was wenn die Verarbeitung fehlschlägt?

Wenn eine Exception geworfen wird, wird die Message nicht bestätigt. Sie bleibt in der Queue und wird erneut zugestellt. Das ist „Guaranteed Delivery“ – eine der Stärken von JMS.

Schritt 3: application.properties

properties

spring.application.name=message-consumer
server.port=8082

spring.activemq.broker-url=tcp://localhost:61616
spring.activemq.user=admin
spring.activemq.password=admin
spring.activemq.packages.trust-all=true

app.queue.orders=order-queue

Schritt 4: Main Application

java

package de.javafleet.messaging;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.jms.annotation.EnableJms;

@SpringBootApplication
@EnableJms  // Aktiviert JMS Listener!
public class MessageConsumerApplication {

    public static void main(String[] args) {
        SpringApplication.run(MessageConsumerApplication.class, args);
    }
}

Wichtig: @EnableJms muss dabei sein, sonst werden die @JmsListener-Methoden nicht aktiviert!

Schritt 5: Consumer starten

bash

cd MessageConsumer
mvn spring-boot:run

Was passiert jetzt?

Der Consumer verbindet sich mit dem Broker und holt sich alle wartenden Messages aus der Queue. Wenn du vorher schon Bestellungen gesendet hast, siehst du jetzt:

📥 ======================================
📥 ORDER RECEIVED!
📥 Order ID:  a1b2c3d4
📥 Customer:  Max
📥 Product:   Laptop
📥 Quantity:  2
📥 ======================================
⚙️ Processing order a1b2c3d4...
✅ Order a1b2c3d4 completed! Total: 1

✅ Checkpoint:

  • Consumer startet auf Port 8082?
  • Wartende Messages werden verarbeitet?
  • „ORDER RECEIVED!“ erscheint im Log?

🟢 GRUNDLAGEN – Live-Experiment!

Jetzt wird’s spannend. Du hast drei Projekte laufen – lass uns experimentieren!

Experiment 1: Mehrere Bestellungen senden

Öffne ein neues Terminal und sende mehrere Bestellungen:

bash

curl "http://localhost:8081/api/orders/send?product=Laptop&customer=Anna"
curl "http://localhost:8081/api/orders/send?product=Maus&customer=Ben"
curl "http://localhost:8081/api/orders/send?product=Tastatur&customer=Clara"

Beobachte die Consumer-Console: Alle drei Bestellungen werden nacheinander verarbeitet!

Experiment 2: Consumer stoppen, Messages senden

  1. Stoppe den Consumer (Strg+C)
  2. Sende Bestellungen:

bash

curl "http://localhost:8081/api/orders/send?product=Monitor&customer=David"
curl "http://localhost:8081/api/orders/send?product=Webcam&customer=Eva"
  1. Producer sagt: „Order sent successfully!“ – er merkt nichts vom fehlenden Consumer!
  2. Starte den Consumer wieder:

bash

cd MessageConsumer
mvn spring-boot:run

Was passiert? Die Messages werden jetzt verarbeitet! Sie haben im Broker gewartet.

Das ist Guaranteed Delivery! 🎉

Experiment 3: Broker stoppen

  1. Stoppe den Broker (Strg+C)
  2. Versuche eine Bestellung zu senden:

bash

curl "http://localhost:8081/api/orders/send?product=Test&customer=Test"

Ergebnis: Fehler! Ohne Broker geht nichts.

Lektion: Der Broker ist das Herzstück. In Production brauchst du High Availability!


🟢 GRUNDLAGEN – Zwischencheckpoint

Kontrolliere:

  • Alle drei Projekte gestartet (Broker → Producer → Consumer)
  • Bestellung über curl gesendet
  • Message im Consumer angekommen
  • Experiment 2 durchgeführt (Consumer offline, Messages warten)
  • Du verstehst: Producer wartet nicht auf Consumer!

Alle ✅? Du verstehst die Grundlagen von Messaging!

Pause! ☕ Nimm dir 10 Minuten. Der nächste Teil ist Email-Versand.


🟢 GRUNDLAGEN – Teil 5: Projekt 4 – Email-Versand

Email ist „Messaging nach außen“

Messaging zwischen Services ist toll – aber manchmal musst du auch mit Menschen kommunizieren. Dafür gibt es Email!

📦 Download: SpringMailService.zip

MailDev starten (Test-Email-Server)

Bevor wir Emails versenden, brauchen wir einen Test-Server. MailDev ist perfekt dafür – alle Emails werden abgefangen und in einer Web-UI angezeigt.

bash

docker run -p 1080:1080 -p 1025:1025 maildev/maildev

Öffne http://localhost:1080 – das ist die MailDev-Oberfläche.

EmailService erstellen

java

package de.javafleet.messaging.service;

import jakarta.mail.MessagingException;
import jakarta.mail.internet.MimeMessage;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.mail.SimpleMailMessage;
import org.springframework.mail.javamail.JavaMailSender;
import org.springframework.mail.javamail.MimeMessageHelper;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Service;
import org.thymeleaf.context.Context;
import org.thymeleaf.spring6.SpringTemplateEngine;

@Service
@RequiredArgsConstructor
@Slf4j
public class EmailService {

    private final JavaMailSender mailSender;
    private final SpringTemplateEngine templateEngine;
    
    @Value("${spring.mail.from:noreply@java-developer.online}")
    private String fromEmail;

    // Einfache Text-Email
    public void sendSimpleEmail(String to, String subject, String text) {
        log.info("📧 Sending simple email to: {}", to);
        
        SimpleMailMessage message = new SimpleMailMessage();
        message.setFrom(fromEmail);
        message.setTo(to);
        message.setSubject(subject);
        message.setText(text);
        
        mailSender.send(message);
        log.info("✅ Email sent to: {}", to);
    }

    // HTML-Email mit Template
    public void sendHtmlEmail(String to, String subject, String username, String email) {
        log.info("📧 Sending HTML email to: {}", to);
        
        try {
            Context context = new Context();
            context.setVariable("username", username);
            context.setVariable("email", email);
            
            String htmlContent = templateEngine.process("email/welcome", context);
            
            MimeMessage mimeMessage = mailSender.createMimeMessage();
            MimeMessageHelper helper = new MimeMessageHelper(mimeMessage, true, "UTF-8");
            
            helper.setFrom(fromEmail);
            helper.setTo(to);
            helper.setSubject(subject);
            helper.setText(htmlContent, true);
            
            mailSender.send(mimeMessage);
            log.info("✅ HTML email sent to: {}", to);
            
        } catch (MessagingException e) {
            log.error("❌ Failed to send email: {}", e.getMessage());
            throw new RuntimeException("Email sending failed", e);
        }
    }

    // ASYNCHRON - der Game-Changer!
    @Async
    public void sendWelcomeEmailAsync(String to, String subject, String username) {
        log.info("📧 [ASYNC] Starting async email to: {}", to);
        
        try {
            Thread.sleep(2000);  // Simuliert lange Verarbeitung
            sendHtmlEmail(to, subject, username, to);
            log.info("✅ [ASYNC] Email sent!");
        } catch (Exception e) {
            log.error("❌ [ASYNC] Failed: {}", e.getMessage());
        }
    }
}

Drei Methoden, drei Anwendungsfälle:

MethodeWann verwenden?
sendSimpleEmailEinfache Benachrichtigungen, System-Alerts
sendHtmlEmailWillkommens-Emails, Bestellbestätigungen
sendWelcomeEmailAsyncUser-Registrierung (Response sofort!)

application.properties

properties

spring.application.name=spring-mail-service
server.port=8083

# MailDev SMTP
spring.mail.host=localhost
spring.mail.port=1025
spring.mail.from=noreply@java-developer.online

# Thymeleaf
spring.thymeleaf.cache=false

Main Application mit @EnableAsync

java

@SpringBootApplication
@EnableAsync  // WICHTIG!
public class SpringMailServiceApplication {
    public static void main(String[] args) {
        SpringApplication.run(SpringMailServiceApplication.class, args);
    }
}

Testen

bash

# Einfache Email
curl "http://localhost:8083/api/email/send?to=test@test.de"

# HTML Email
curl "http://localhost:8083/api/email/html?to=test@test.de&name=Max"

# Async Email (Response ist SOFORT da!)
curl "http://localhost:8083/api/email/async?to=test@test.de&name=Max"

Öffne http://localhost:1080 – dort siehst du alle Emails!


🟡 PROFESSIONALS – @Async vs JMS: Wann was?

Abbildung 2: Entscheidungshilfe zwischen @Async und JMS

Die Entscheidungsmatrix

Kriterium@AsyncJMS Messaging
Setup✅ Eine Annotation⚠️ Broker + Config
Persistenz❌ Verloren bei Restart✅ Messages überleben
Retry❌ Manuell implementieren✅ Automatisch
Microservices❌ Nur innerhalb einer App✅ Zwischen Services
Performance⚡ Sehr schnell🐢 Etwas Overhead

Meine Faustregel

Nutze @Async für:

  • Email-Versand (nicht geschäftskritisch)
  • Logging, Analytics
  • Einfache Background-Tasks

Nutze JMS für:

  • Bestellungen, Zahlungen (geschäftskritisch!)
  • Kommunikation zwischen Services
  • Alles, was bei Fehlern wiederholt werden muss

Best Practice: Thread Pool konfigurieren

java

@Configuration
public class AsyncConfig {
    
    @Bean(name = "taskExecutor")
    public Executor taskExecutor() {
        ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
        executor.setCorePoolSize(2);      // Minimum 2 Threads
        executor.setMaxPoolSize(5);       // Maximum 5 Threads
        executor.setQueueCapacity(100);   // 100 Tasks in Warteschlange
        executor.setThreadNamePrefix("async-email-");
        executor.initialize();
        return executor;
    }
}

Warum? Ohne Konfiguration erstellt @Async unbegrenzt viele Threads. Bei 1000 Emails = 1000 Threads = Server-Crash!


🟡 PROFESSIONALS – Sync vs Async Email im Vergleich

Bild anzeigen

Abbildung 3: Warum asynchroner Email-Versand 100x schneller ist

Das Problem mit synchronem Versand

java

// ❌ SCHLECHT: User wartet 5 Sekunden
public User registerUser(UserDTO dto) {
    User user = repository.save(dto.toEntity());
    emailService.sendHtmlEmail(user.getEmail(), ...);  // BLOCKIERT!
    return user;
}

Die Lösung mit @Async

java

// ✅ GUT: User wartet 200ms
public User registerUser(UserDTO dto) {
    User user = repository.save(dto.toEntity());
    emailService.sendWelcomeEmailAsync(user.getEmail(), ...);  // SOFORT!
    return user;
}

Häufiger Fehler: @Async funktioniert nicht

Symptom: Email wird synchron versendet, nicht async.

Checkliste:

  1. @EnableAsync in Main-Klasse?
  2. ✅ Methode ist public?
  3. ✅ Methode wird von anderer Klasse aufgerufen?

Der dritte Punkt ist wichtig! @Async funktioniert nur bei Aufrufen von außen:

java

// ❌ Funktioniert NICHT:
public void method1() {
    this.asyncMethod();  // Gleiche Klasse!
}

// ✅ Funktioniert:
public void method1() {
    otherService.asyncMethod();  // Andere Klasse!
}

🔵 BONUS – Messaging + Email kombinieren

Real-World-Szenario: Bestellung mit Email-Bestätigung

Was wenn du beides kombinieren willst? Der Consumer verarbeitet die Bestellung und schickt dann eine Email!

Erweiterter Consumer:

java

@Service
@RequiredArgsConstructor
@Slf4j
public class OrderConsumer {

    private final EmailService emailService;

    @JmsListener(destination = "${app.queue.orders}")
    public void receiveOrder(Order order) {
        log.info("📥 Order received: {}", order.getOrderId());
        
        // Bestellung verarbeiten
        processOrder(order);
        
        // Email senden (async!)
        emailService.sendOrderConfirmationAsync(
            order.getCustomerEmail(),
            order.getOrderId()
        );
        
        log.info("✅ Order processed, email triggered!");
    }
}

Das Beste aus beiden Welten:

  • JMS für zuverlässige Bestellverarbeitung
  • @Async für nicht-blockierenden Email-Versand

💬 Real Talk: Elyndras Messaging-Fehler

Nova kam letztens zu mir:

„Elyndra, ich hab ein Problem. Meine Bestellungen gehen manchmal verloren!“

Ich schaute in ihren Code… und fand das:

java

// Novas Code
@Async
public void processOrder(Order order) {
    // Bestellung verarbeiten
    database.save(order);
    // Email senden
    emailService.send(...);
}

Das Problem? Sie hat @Async für geschäftskritische Logik verwendet! Bei einem Server-Restart waren alle ausstehenden Bestellungen weg.

Meine Antwort: „Nova, für Bestellungen brauchst du JMS. @Async ist für Email okay – aber nicht für Geld!“

Wir haben es auf JMS umgestellt. Seitdem: Keine verlorene Bestellung mehr.

Die Lektion:

„Die Frage ist nicht ‚Async oder nicht?‘ – sondern ‚Was passiert, wenn es schiefgeht?'“


✅ Checkpoint: Hast du es verstanden?

Spring Boot Aufbau - Tag 7

Messaging & Email

Frage 1 von 10

Was ist MailDev?

Frage 2 von 10

Was macht JavaMailSender?

Frage 3 von 10

Was ist der Vorteil von asynchronem Email-Versand?

Frage 4 von 10

Wie aktiviert man asynchrone Ausführung?

Frage 5 von 10

Was macht MimeMessageHelper?

Frage 6 von 10

Warum Thymeleaf für Email-Templates?

Frage 7 von 10

Was ist JMS?

Frage 8 von 10

Was ist Artemis?

Frage 9 von 10

Was macht @JmsListener?

Frage 10 von 10

Was ist der Unterschied zwischen Queue und Topic?


🏋️ Mini-Challenge

Baue folgendes:

Erweitere den MessageConsumer so, dass er:

  1. Die Anzahl verarbeiteter Bestellungen zählt
  2. Bei jeder 10. Bestellung eine „Glückwunsch“-Email sendet
  3. Die Statistik über einen REST-Endpoint anzeigt

Hinweise:

  • Du brauchst einen Zähler (AtomicInteger?)
  • Email-Service aus Projekt 4 integrieren
  • Modulo-Operator für „jede 10.“

Lösung: Morgen in Tag 8 oder im GitHub-Repository!


❓ FAQ

Q: Mein Broker startet nicht – Port 61616 belegt? A: Ein anderer Prozess nutzt den Port. netstat -an | grep 61616 zeigt wer. Oder ändere den Port in application.properties.

Q: Messages kommen nicht an – was tun? A: Checkliste: (1) Broker läuft? (2) Queue-Name identisch bei Producer und Consumer? (3) @EnableJms vorhanden? (4) packages.trust-all=true gesetzt?

Q: Wie sicher ist packages.trust-all=true? A: Für Development okay. In Production: Nur spezifische Packages erlauben! spring.activemq.packages.trusted=de.javafleet.messaging.model

Q: Kann ich mehrere Consumer haben? A: Ja! Starte einfach mehrere Instanzen (unterschiedliche Ports). ActiveMQ verteilt die Messages automatisch.

Q: Wann nehme ich RabbitMQ statt ActiveMQ? A: RabbitMQ ist moderner und hat bessere UI. Für diesen Kurs ist ActiveMQ einfacher zu starten. In Production: Evaluiere beide!

Q: @Async Email wird nicht gesendet – warum? A: Exceptions in @Async-Methoden werden „geschluckt“! Du musst sie im catch-Block loggen. Ohne Try-Catch siehst du den Fehler nicht!

Q: Gibt es bei euch im Team auch manchmal… zwischenmenschliche Spannungen? A: lacht Frag mal nach „private logs“ in der Suche oben. Manche Geschichten passen nicht in Tech-Blogs… 📖



🎉 Tag 7 geschafft!

Was du heute gelernt hast:

✅ Message-Driven Architecture verstehen (Broker, Producer, Consumer) ✅ ActiveMQ als Message Broker einrichten ✅ Messages zwischen Services senden und empfangen ✅ Email-Versand mit Spring Boot Mail ✅ Synchron vs Asynchron (@Async) ✅ Wann JMS, wann @Async?

Du hast vier Projekte gebaut, die zusammenarbeiten! Das ist echte Microservices-Kommunikation.


🔮 Wie geht’s weiter?

Tag 8: Testing & Dokumentation

  • Unit Testing mit JUnit 5 & Mockito
  • Integration Testing mit @SpringBootTest
  • API-Dokumentation mit OpenAPI/Swagger
  • Test-Driven Development (TDD)

Das wird dein Game-Changer für professionelle Entwicklung! 🚀

Brauchst du eine Pause? Mach sie! Der Content von heute braucht Zeit zum Setzen.

Tipp für heute Abend: Starte alle drei Messaging-Projekte und spiel damit rum. Sende 100 Bestellungen, stoppe den Consumer, starte ihn wieder. Beobachte, was passiert!


🛠️ Troubleshooting

ProblemLösung
Broker startet nichtPort 61616 belegt? Anderen Prozess beenden oder Port ändern
Consumer empfängt nichts@EnableJms vergessen? Queue-Name falsch?
Email wird nicht gesendetMailDev läuft? Port 1025 erreichbar?
@Async funktioniert nicht@EnableAsync vergessen? Aufruf von gleicher Klasse?
Serialization ErrorserialVersionUID unterschiedlich? Packages unterschiedlich?

📥 Downloads & Materialien

Projekt-Downloads:

Weiterführend:


📧 Feedback

Fragen zu Tag 7? Schreib mir:

Bis morgen bei Tag 8! 🚀

Keep coding, keep learning!


Tag 8 erscheint morgen. Bis dahin: Happy Messaging!


Tags: #SpringBoot #Messaging #JMS #ActiveMQ #Email #Async #Tag7 #Kurs


Autor

Elyndra Valen
Senior Entwicklerin bei Java Fleet Systems Consulting

28 Jahre alt, frisch zur Senior Entwicklerin befördert nach 4 Jahren intensiver Java-Entwicklung. Elyndra kennt die wichtigsten Frameworks und Patterns, beginnt aber gerade erst, die tieferen Zusammenhänge und Architektur-Entscheidungen zu verstehen. Sie ist die Brücke zwischen Junior- und Senior-Welt im Team.


© 2025 Java Fleet Systems Consulting | java-developer.online

Autor

  • Elyndra Valen

    28 Jahre alt, wurde kürzlich zur Senior Entwicklerin befördert nach 4 Jahren intensiver Java-Entwicklung. Elyndra kennt die wichtigsten Frameworks und Patterns, beginnt aber gerade erst, die tieferen Zusammenhänge und Architektur-Entscheidungen zu verstehen. Sie ist die Brücke zwischen Junior- und Senior-Welt im Team.