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

🗺️ Deine Position im Kurs
| Tag | Thema | Status |
|---|---|---|
| 1 | Auto-Configuration & Custom Starter | ✅ Abgeschlossen |
| 2 | Spring Data JPA Basics | ✅ Abgeschlossen |
| 3 | JPA Relationships & Queries | ✅ Abgeschlossen |
| 4 | Spring Security Part 1 – Authentication | ✅ Abgeschlossen |
| 5 | Spring Security Part 2 – Authorization | ✅ Abgeschlossen |
| 6 | Spring Boot Caching & JSON | ✅ Abgeschlossen |
| → 7 | Messaging & Email | 👉 DU BIST HIER! |
| 8 | Testing & Dokumentation | 📜 Kommt als nächstes |
| 9 | Spring Boot Actuator | 🔒 Noch nicht freigeschaltet |
| 10 | Template 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:
| Komponente | Rolle | Analogie |
|---|---|---|
| Broker | Vermittelt Messages | Das Postamt |
| Producer | Sendet Messages | Der Absender |
| Consumer | Empfängt Messages | Der 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:
| Projekt | Port | Funktion |
|---|---|---|
| MyMessageBroker | 8080 / 61616 | Der Message-Vermittler |
| MessageProducer | 8081 | Sendet Bestellungen |
| MessageConsumer | 8082 | Verarbeitet Bestellungen |
| SpringMailService | 8083 | Versendet Emails |
Am Ende des Tages kannst du:
- Alle drei Messaging-Projekte gleichzeitig starten
- Eine Bestellung über den Producer senden
- Zusehen wie der Consumer sie verarbeitet
- 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?
| Aspekt | Synchron | Mit Messaging |
|---|---|---|
| Response-Zeit | 5-10 Sekunden | 200 Millisekunden |
| Bei Consumer-Ausfall | Fehler! | Message wartet in Queue |
| Skalierung | Schwierig | Einfach (mehr Consumer) |
| Entkopplung | Eng verbunden | Lose 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?
| Dependency | Zweck |
|---|---|
spring-boot-starter-web | Ermöglicht REST-Endpoints für Status-Abfragen |
spring-boot-starter-activemq | ActiveMQ Client-Bibliotheken |
activemq-broker | Der eigentliche Broker (embedded) |
activemq-kahadb-store | Persistente 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:
convert: Wandelt das Order-Objekt in eine JMS-Message umsend: 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:
- Verbindet sich mit dem Broker (URL aus Properties)
- Lauscht auf die Queue (
order-queue) - Wenn eine Message ankommt: Ruft diese Methode auf
- Konvertiert die Message automatisch zurück zum Order-Objekt
- 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
- Stoppe den Consumer (Strg+C)
- 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"
- Producer sagt: „Order sent successfully!“ – er merkt nichts vom fehlenden Consumer!
- 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
- Stoppe den Broker (Strg+C)
- 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:
| Methode | Wann verwenden? |
|---|---|
sendSimpleEmail | Einfache Benachrichtigungen, System-Alerts |
sendHtmlEmail | Willkommens-Emails, Bestellbestätigungen |
sendWelcomeEmailAsync | User-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 | @Async | JMS 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:
- ✅
@EnableAsyncin Main-Klasse? - ✅ Methode ist
public? - ✅ 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
🏋️ Mini-Challenge
Baue folgendes:
Erweitere den MessageConsumer so, dass er:
- Die Anzahl verarbeiteter Bestellungen zählt
- Bei jeder 10. Bestellung eine „Glückwunsch“-Email sendet
- 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
| Problem | Lösung |
|---|---|
| Broker startet nicht | Port 61616 belegt? Anderen Prozess beenden oder Port ändern |
| Consumer empfängt nichts | @EnableJms vergessen? Queue-Name falsch? |
| Email wird nicht gesendet | MailDev läuft? Port 1025 erreichbar? |
| @Async funktioniert nicht | @EnableAsync vergessen? Aufruf von gleicher Klasse? |
| Serialization Error | serialVersionUID unterschiedlich? Packages unterschiedlich? |
📥 Downloads & Materialien
Projekt-Downloads:
- 📦 MyMessageBroker.zip – Der Message Broker
- 📦 MessageProducer.zip – Der Producer Service
- 📦 MessageConsumer.zip – Der Consumer Service
- 📦 SpringMailService.zip – Der Email Service
Weiterführend:
📧 Feedback
Fragen zu Tag 7? Schreib mir:
- Email: elyndra.valen@java-developer.online
- Kommentar: Unten auf dieser Seite
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.
📚 Das könnte dich auch interessieren
© 2025 Java Fleet Systems Consulting | java-developer.online

