Skip to content
Starten Sie mit 100 kostenlosen Verifizierungs-Credits
Qualisend
Alle Artikel
Engineering / 9. Mai 2026

E-Mail-Adresse in Spring Boot validieren

8 minutes read

Qualisend team
Ein Spring-Boot-Codefenster, das eine E-Mail über die Schichten Format, DNS und Postfach bis zu einem Zustellbarkeitsurteil validiert

Eine E-Mail-Adresse in Spring Boot zu validieren, sind in Wirklichkeit drei Prüfungen, die sich hinter einer einzigen Annotation verbergen. Setzen Sie @Email auf ein DTO-Feld, verdrahten Sie @Valid im Controller, und Spring bestätigt bereitwillig, dass der String wie eine Adresse geformt ist — was das am wenigsten nützliche der drei Dinge ist, die Sie eigentlich wissen wollen. Echte Validierung ist geschichtet: eine günstige Formatprüfung, ein DNS-Lookup für die Mailroute der Domain und eine SMTP-Postfachprüfung. Jakarta Bean Validation liefert Ihnen die erste Schicht kostenlos und einen sauberen Erweiterungspunkt — den ConstraintValidator — für den Rest. Diese Anleitung baut jede Schicht mit lauffähigem Code auf und zeigt genau, wo das Framework aufhört und eine Verifizierungs-API übernimmt.

Die Kurzantwort#

Verwenden Sie @Email aus Jakarta Bean Validation für die Syntax, ein JNDI-MX-Lookup für die Domain und eine Verifizierungs-API für die SMTP-Postfachprüfung — verpackt in einen eigenen @Deliverable-Constraint, der nach dem Bestehen von @Email läuft. Öffnen Sie keine SMTP-Verbindungen aus Ihrer Anwendung heraus, um Postfächer selbst zu prüfen: Ausgehender Port 25 ist auf den meisten Hosts blockiert, und die Antwort hängt von der Reputation der sendenden IP und von Greylisting ab, das Sie nicht neu implementieren wollen. Jede Schicht schließt Adressen günstiger aus als die vorherige; nur die letzte kann eine Adresse einschließen. Es ist dieselbe dreischichtige Form wie in der schlichten Java-Validierungsanleitung, angepasst an den Validierungs-Lifecycle von Spring.

Schicht 1: Formatvalidierung mit @Email und @Valid#

Spring Boot übernimmt Schicht eins über Jakarta Bean Validation. Fügen Sie spring-boot-starter-validation zum Projekt hinzu und annotieren Sie das Feld Ihres Request-DTOs mit jakarta.validation.constraints.Email. Kombinieren Sie es mit @NotBlank, denn Bean-Validation-Constraints behandeln null per Design als gültig — @Email gibt für einen fehlenden Wert true zurück, sodass die Anwesenheit ein separates Anliegen ist:

import jakarta.validation.constraints.Email;
import jakarta.validation.constraints.NotBlank;

public record SignupRequest(
    @NotBlank(message = "Email is required.")
    @Email(message = "Enter a valid email address.")
    String email
) {}

Die @Valid-Annotation am Controller-Argument ist das, was die Validierung auslöst, bevor Ihr Methodenrumpf läuft. Ein fehlschlagender Constraint kurzschließt in eine MethodArgumentNotValidException, die Spring für Sie in einen 400 umwandelt:

import jakarta.validation.Valid;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class SignupController {

    @PostMapping("/signup")
    public ResponseEntity<String> signup(@Valid @RequestBody SignupRequest request) {
        // If we reach here, request.email() is well-formed.
        return ResponseEntity.ok("Welcome, " + request.email());
    }
}

Wissen Sie genau, was Ihnen das einbringt. @Email prüft die Form gegen ein permissives Standardmuster — es löst niemals DNS auf, öffnet niemals einen Socket und hat keine Ahnung, ob example.com existiert. definitely-fake@gmail.com besteht. typo@gmial.com besteht. Beide sind nicht zustellbar, und keine Annotation, die nur den String liest, wird Ihnen das jemals sagen — aus demselben Grund, aus dem Regex-E-Mail-Validierung scheitert: Syntax und Zustellbarkeit sind unterschiedliche Fragen. Die eine ist eine Tatsache über den String; die andere eine Tatsache über das Internet.

Schicht 2: Kann die Domain überhaupt Mail empfangen?#

Eine Domain ohne MX-Einträge kann für niemanden Mail annehmen, sodass ein einziges MX-Lookup tote Domains, falsch geschriebene Firmennamen und erfundene TLDs eliminiert. Da Spring Boot auf der JVM läuft, bekommen Sie das ganz ohne externe Abhängigkeit: Das JDK bringt einen DNS-Provider für JNDI mit, sodass Sie das MX-Attribut über einen DirContext auflösen.

import java.util.Hashtable;
import javax.naming.NamingException;
import javax.naming.directory.Attribute;
import javax.naming.directory.Attributes;
import javax.naming.directory.DirContext;
import javax.naming.directory.InitialDirContext;

public final class MailRoute {

    public static boolean exists(String domain) {
        Hashtable<String, String> env = new Hashtable<>();
        env.put("java.naming.factory.initial", "com.sun.jndi.dns.DnsContextFactory");
        env.put("com.sun.jndi.dns.timeout.initial", "2000"); // don't hang on a slow resolver
        env.put("com.sun.jndi.dns.timeout.retries", "1");
        try {
            DirContext ctx = new InitialDirContext(env);
            try {
                Attributes attrs = ctx.getAttributes(domain, new String[] { "MX" });
                Attribute mx = attrs.get("MX");
                return mx != null && mx.size() > 0;
            } finally {
                ctx.close();
            }
        } catch (NamingException e) {
            // NameNotFoundException → domain doesn't exist; no MX attribute → no mail route
            return false;
        }
    }
}

Sie könnten dies in einen eigenen ConstraintValidator einwickeln und als zweite Annotation stapeln. Aber die SMTP-Schicht braucht dieselbe DNS-Arbeit plus ein Live-Gespräch mit dem Mailhost — statt also einen separaten MX-Validator und dann eine Postfachprüfung zu pflegen, ist es sauberer, einen einzigen API-Aufruf beide Netzwerkschichten übernehmen zu lassen. Die lokale MX-Prüfung bleibt als optionaler Fast-Fail nützlich; der Verifizierungsdienst erledigt den DNS-Schritt so oder so intern.

Schicht 3: Existiert das Postfach tatsächlich?#

Schicht eins und zwei können eine Adresse nur ausschließen. Eine Domain kann perfekte MX-Einträge veröffentlichen und trotzdem kein Postfach an der Adresse haben, die Sie in der Hand halten — noreply-9f2x@gmail.com ist gültige Syntax auf einer aktiven Mailroute, und es ist trotzdem ein Postfach, das nie angelegt wurde. Ein bestimmtes Postfach zu bestätigen bedeutet das SMTP-Zustellgespräch: sich mit dem Mailhost verbinden, RCPT TO absetzen, die Antwort lesen und trennen, bevor irgendetwas gesendet wird. Es steckt mehr dahinter — Catch-all-Domains akzeptieren jede Adresse und hebeln eine naive Prüfung aus —, und wie E-Mail-Verifizierung funktioniert geht die vollständige Pipeline durch.

Sie können SMTP von der JVM aus mit einem rohen Socket skripten, aber Sie sollten es nicht aus Ihrer Anwendung heraus ausführen. Die meisten Cloud-Anbieter blockieren ausgehenden Port 25, die Antwort hängt von der Reputation der IP ab, von der aus Sie sich verbinden, und empfangende Server greylisten und drosseln unbekannte Absender — sodass eine Prüfung, die in einem lokalen Test besteht, in der Produktion still fehlschlägt oder Sie auf eine Blocklist bringt. Dies ist die Schicht, die es sich zu delegieren lohnt.

Springs idiomatischer Anknüpfungspunkt ist ein eigener Constraint. Definieren Sie eine @Deliverable-Annotation und einen Validator, der die Verifizierungs-API aufruft. Beginnen Sie mit der Annotation:

import jakarta.validation.Constraint;
import jakarta.validation.Payload;
import java.lang.annotation.Retention;
import java.lang.annotation.Target;

import static java.lang.annotation.ElementType.FIELD;
import static java.lang.annotation.ElementType.PARAMETER;
import static java.lang.annotation.RetentionPolicy.RUNTIME;

@Target({ FIELD, PARAMETER })
@Retention(RUNTIME)
@Constraint(validatedBy = DeliverableValidator.class)
public @interface Deliverable {
    String message() default "This email address appears to be undeliverable.";
    Class<?>[] groups() default {};
    Class<? extends Payload>[] payload() default {};
}

Weil spring-boot-starter-validation den Hibernate Validator über Springs SpringConstraintValidatorFactory verdrahtet, ist der Validator ein verwalteter Bean — sodass @Value-Injektion und Constructor-Autowiring darin funktionieren. Java 11+ bringt java.net.http.HttpClient mit, sodass Sie keine HTTP-Abhängigkeit brauchen; kombinieren Sie es mit Jackson, das Spring Boot ohnehin schon auf den Classpath legt. Halten Sie den Schlüssel in einer Umgebungsvariable — kodieren Sie ihn niemals hart:

import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import jakarta.validation.ConstraintValidator;
import jakarta.validation.ConstraintValidatorContext;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;

import java.io.IOException;
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.time.Duration;
import java.util.Map;

@Component
public class DeliverableValidator implements ConstraintValidator<Deliverable, String> {

    // Placeholder endpoint — see /developers for the current base URL and shape.
    private static final String ENDPOINT = "https://api.qualisend.com/v1/verify";

    private final HttpClient http = HttpClient.newBuilder()
        .connectTimeout(Duration.ofSeconds(5))
        .build();
    private final ObjectMapper mapper = new ObjectMapper();

    @Value("${qualisend.api-key}") // resolves from QUALISEND_API_KEY, holds YOUR_API_KEY
    private String apiKey;

    @Override
    public boolean isValid(String email, ConstraintValidatorContext context) {
        if (email == null || email.isBlank()) {
            return true; // @NotBlank and @Email own emptiness and format
        }
        try {
            String body = mapper.writeValueAsString(Map.of("email", email));
            HttpRequest request = HttpRequest.newBuilder()
                .uri(URI.create(ENDPOINT))
                .header("Authorization", "Bearer " + apiKey)
                .header("Content-Type", "application/json")
                .timeout(Duration.ofSeconds(10))
                .POST(HttpRequest.BodyPublishers.ofString(body))
                .build();

            HttpResponse<String> response =
                http.send(request, HttpResponse.BodyHandlers.ofString());

            // On our own outage or a rate limit, don't block a real signup.
            if (response.statusCode() >= 400) {
                return true;
            }

            // The verdict lives inside a `result` envelope.
            JsonNode result = mapper.readTree(response.body()).get("result");
            String status = result.get("status").asText(); // deliverable | risky | undeliverable | unknown
            return !"undeliverable".equals(status);         // hard-fail only a confirmed miss
        } catch (IOException e) {
            return true; // transient network failure → treat as unknown, re-verify later
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
            return true;
        }
    }
}

Zeigen Sie die Property in application.properties auf die Umgebungsvariable:

qualisend.api-key=${QUALISEND_API_KEY}

Der Endpunkt oben ist ein Platzhalter — prüfen Sie die API-Referenz für die genaue Basis-URL und Request-Form —, aber die Antwort liest sich als { "result": { ... } }-Umschlag zurück:

{
  "result": {
    "status": "deliverable",
    "score": 95,
    "reason": null,
    "sub_flags": { "role": false, "disposable": false, "free": false, "catch_all": false }
  }
}

status ist einer von deliverable, risky, undeliverable oder unknown. Der Validator oben lässt hart nur undeliverable durchfallen und lässt risky und unknown passieren, sodass Sie nachgelagert entscheiden können, was mit ihnen zu tun ist — nach score gaten oder anhand eines sub_flags-Eintrags wie disposable verzweigen — anstatt eine Grenzfall-Adresse in einen Formularfehler zu verwandeln. Behandeln Sie das obige JSON als die Form, nicht als den Vertrag; die vollständige Feldliste steht in den Entwicklerdokumenten.

Die Zustellbarkeitsprüfung mit Validierungsgruppen nach @Email laufen lassen#

Es gibt einen Haken. Standardmäßig führt Bean Validation jeden Constraint eines Feldes unabhängig von den anderen aus, sodass @Deliverable feuern würde — und ein API-Guthaben verbrauchen würde —, selbst wenn @Email bereits weiß, dass der String Müll ist. Sie wollen eine Reihenfolge: erst das Format, der Netzwerk-Roundtrip nur, wenn es besteht.

Bean Validation drückt Reihenfolge über Gruppen und eine @GroupSequence aus. Definieren Sie ein Marker-Interface für die Netzwerkschicht und legen Sie dann @Deliverable in diese Gruppe, während @NotBlank und @Email in der Standardgruppe bleiben:

public interface NetworkChecks {}

Definieren Sie nun die Standard-Gruppensequenz am DTO selbst neu. Die Klasse zuerst aufzuführen (die für ihre eigenen Standard-Constraints steht) und NetworkChecks als zweites weist den Hibernate Validator an, zuerst das Format zu validieren und die Zustellbarkeitsprüfung nur zu erreichen, wenn dieser Schritt sauber ist:

import jakarta.validation.GroupSequence;
import jakarta.validation.constraints.Email;
import jakarta.validation.constraints.NotBlank;

@GroupSequence({ SignupRequest.class, NetworkChecks.class })
public record SignupRequest(
    @NotBlank(message = "Email is required.")
    @Email(message = "Enter a valid email address.")
    @Deliverable(groups = NetworkChecks.class)
    String email
) {}

Zwei Dinge lassen das funktionieren. Weil die Sequenz die Standard-Gruppe neu definiert, löst das einfache @Valid an Ihrem Controller sie weiterhin aus — kein @Validated(...)-Gruppenargument nötig. Und weil Sequenzschritte kurzschließen, scheitert eine fehlerhafte Adresse am @Email-Schritt, und die API wird nie aufgerufen. Diese Reihenfolge ist der ganze Trick: Die günstige lokale Prüfung schützt die teure Netzwerkprüfung.

Die Schichten zusammensetzen, um eine E-Mail-Adresse in Spring Boot zu validieren#

Das obige DTO setzt bereits alle drei Schichten zusammen; der Controller bleibt das einzeilige @Valid @RequestBody aus Schicht eins. Das Einzige, was noch bleibt, ist, die Fehlerantwort zu formen. Spring gibt für jeden fehlgeschlagenen Constraint eine MethodArgumentNotValidException aus, sodass ein kleines @RestControllerAdvice sie in einen sauberen, feldbezogenen 400 verwandelt:

import org.springframework.http.HttpStatus;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestControllerAdvice;

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

@RestControllerAdvice
public class ValidationErrorHandler {

    @ExceptionHandler(MethodArgumentNotValidException.class)
    @ResponseStatus(HttpStatus.BAD_REQUEST)
    public Map<String, String> onInvalid(MethodArgumentNotValidException ex) {
        Map<String, String> errors = new HashMap<>();
        ex.getBindingResult().getFieldErrors()
            .forEach(e -> errors.put(e.getField(), e.getDefaultMessage()));
        return errors;
    }
}

Die beiden lokalen Schichten kosten nichts und fangen den meisten Müll sofort ab; die API läuft nur bei Adressen, die den Netzwerk-Roundtrip wert sind. Das ist dieselbe Form, die Sie in der Java-Version dieser Anleitung finden, denn die Schichtung — nicht das Framework — ist das, was Validierung funktionieren lässt. Führen Sie diese schnelle synchrone Pipeline bei der Registrierung aus und reservieren Sie schwerere Durchläufe für die Back-Office-Arbeit: Die Anleitung zur serverlosen Registrierung zeigt das Echtzeit-Muster, und wie man eine E-Mail-Liste bereinigt deckt die Batch-Seite ab.

Häufig gestellte Fragen#

Reicht @Email aus, um eine E-Mail-Adresse in Spring Boot zu validieren?#

Für die Syntax ist es das richtige Werkzeug der ersten Schicht — jakarta.validation.constraints.Email ist besser getestet als eine selbst gebaute Regex und lässt sich kostenlos in @Valid und Springs Fehlerbehandlung integrieren. Aber es prüft nur die Form: Es löst niemals DNS auf, kontaktiert keinen Mailserver und behandelt null als gültig, also kombinieren Sie es mit @NotBlank. Ein bestandener Test bedeutet "sieht wie eine E-Mail aus", nicht "wird zugestellt" — fügen Sie ein MX-Lookup und eine SMTP-Postfachprüfung hinzu, bevor Sie der Adresse vertrauen.

Wie schreibe ich in Spring Boot eine eigene E-Mail-Validierungsannotation?#

Erstellen Sie eine Annotation, die mit @Constraint(validatedBy = ...) meta-annotiert ist, und eine Klasse, die ConstraintValidator<IhreAnnotation, String> implementiert. Legen Sie Ihre Logik in isValid ab und geben Sie false zurück, um fehlzuschlagen. Mit spring-boot-starter-validation ist der Validator ein Spring-Bean, sodass Sie Konfiguration und HTTP-Clients hineininjizieren können. Geben Sie für null/leer true zurück, damit @NotBlank und @Email diese Fälle übernehmen, und annotieren Sie das DTO-Feld, um sie anzuwenden.

Wie sorge ich dafür, dass die Zustellbarkeitsprüfung erst nach @Email läuft?#

Verwenden Sie Validierungsgruppen. Legen Sie den teuren @Deliverable-Constraint in eine Marker-Gruppe wie NetworkChecks und fügen Sie dann @GroupSequence({ SignupRequest.class, NetworkChecks.class }) zum DTO hinzu, um dessen Standard-Gruppensequenz neu zu definieren. Da Sequenzschritte kurzschließen, läuft @Email zuerst, und der API-gestützte Constraint feuert nur, wenn das Format bereits gültig ist — und einfaches @Valid löst ihn weiterhin aus, da die Sequenz die Standardgruppe neu definiert.

Sollte ich E-Mails bei der Registrierung oder beim Bereinigen einer Liste validieren?#

Beides, in unterschiedlicher Tiefe. Führen Sie die Format- und Zustellbarkeitsschichten bei der Registrierung synchron aus — sie sind schnell genug, um die Anfrage zu blockieren und sofortiges Feedback zu geben — und reagieren Sie dort auch auf den status der API. Reservieren Sie tiefere, gebündelte Verifizierung für das Bereinigen einer bestehenden Liste, wo Latenz keine Rolle spielt und Gründlichkeit zählt.


Bereit, die Postfachschicht hinzuzufügen? Werfen Sie eine syntaktisch perfekte Adresse in den kostenlosen E-Mail-Checker, um zu sehen, wie ein von @Email genehmigter String als undeliverable zurückkommt, und verdrahten Sie dann dasselbe Urteil in einen ConstraintValidator mit dem /verify-Endpunkt und dem vollständigen result-Umschlag in der API-Referenz.

Your reputation, protected.

Clean your first list in minutes. 100 free credits, no card required.

Get started