CodeGym /Kurse /Spring REST & MVC /Architekturgrenzen des Upload-Endpunkts

Architekturgrenzen des Upload-Endpunkts

Spring REST & MVC
Level 26, Lektion 4
Verfügbar

1. Grenzen des Upload-Endpunkts

Nach den Upload-Prüfungen ist das Bild bereits zusammengestellt: Formfehler fängt Spring MVC, @Valid ist für Metadaten zuständig, einfache Dateiprüfungen weisen leere Dateien und nicht unterstützte Typen ab, während 404/409 in den Geschäftsregeln leben. Nun besteht die Hauptgefahr darin, alles in einen Endpunkt zu packen und einen Controller mit 300 Zeilen zu erhalten.

Ein Datei-Upload beginnt fast immer gleich: „Lass uns einfach die Datei nehmen und speichern.“ Und endet fast immer gleich: „Warum haben wir einen Controller mit 300 Zeilen, der Service nimmt MultipartFile entgegen, und jede Änderung zerbricht alles drumherum?“ Das ist keine Magie und kein böses Schicksal – das ist ganz normale Ingenieursrealität: Ein Datei-Szenario wächst schnell mit Prüfungen, Geschäftsregeln, Metadaten und Fehlern.

Bei einem normalen JSON-Endpunkt spürt man den Schmerz meist nicht sofort, weil DTOs schön validiert werden und die Daten klein sind. Bei Dateien fühlt es sich anders an: Sie arbeiten mit Inhalten, Größen, Typen, IOException-Ausnahmen, und dazu kommt die Versuchung, den Endpunkt zu einem „universellen Schweizer Taschenmesser“ zu machen. Genau deshalb ist es beim Upload besonders wichtig, einen einfachen Gedanken im Kopf zu behalten: Der Controller ist ein Übersetzer von HTTP in die Sprache der Anwendung, und der Service ist der Ort, an dem die Operationsregeln leben.

Wenn man die Grenze richtig zieht, wird der Controller kurz (man kann ihn in 30 Sekunden erfassen), der Service kann getestet werden, ohne MVC zu starten, und Ihr Code kann sich ruhig weiterentwickeln. Wenn man die Grenze nicht zieht, erhält man ein „halbtotes“ Wesen: einen Controller, der gleichzeitig Multipart entgegennimmt, validiert, Geschäftskonflikte löst, IDs generiert, in den Speicher schreibt, ProblemDetail formatiert und noch ein bisschen DJ spielt – „Jetzt lege ich Ihnen den Location-Header auf, und dann mische ich ihn mit try/catch“. Klingt lustig … solange es nicht Ihre Codebasis ist.

Deshalb ist es besser, den Upload als separate Operation mit einem eigenen sauberen internen Eingang zu halten; das Ausliefern von Dateien und die Speicherinfrastruktur sind eine andere Aufgabe und haben in derselben Methode nichts zu suchen.

2. Dünner Controller im Upload-Szenario

Der Begriff „dünner Controller“ klingt manchmal wie Ratschläge aus der Fitnesswelt: „Sei schlank, gesund und iss keinen Zucker“. Aber im Backend ist das eine sehr konkrete Technik: Der Controller erledigt ein Minimum an Arbeit, die streng mit der Web-Ebene verbunden ist, und delegiert dann weiter. Nicht weil „es modern ist“, sondern weil man sonst MVC-Spezifika in allen Schichten testen und pflegen müsste.

Der Upload-Endpunkt hat einen idealen Rhythmus, den man sich wie ein kleines Mantra merken sollte (ja, es wird ein Mantra sein, aber ein ingenieurtechnisches): bind → convert → delegate → return. Das heißt, wir haben die Eingangsdaten gebunden (@PathVariable, @RequestPart), sie in ein internes Format umgewandelt (Command/Operationsobjekt), an den Service übergeben und die HTTP-Antwort erstellt.

Von außen bleibt der Upload gleich: Wir erstellen einen Anhang, geben 201 Created, Location und Metadaten der erstellten Ressource zurück. Innen interessiert uns jetzt etwas anderes – wie wir Binding, einfache Dateiprüfungen, Geschäftsregeln und Antwort-Mapping nicht über den Controller verteilen.

Damit sich das nicht wie eine Parole anfühlt, sondern wie konkrete Details, ist es hilfreich, die Verantwortungsbereiche in einer Tabelle zu vergleichen:

Zone Wovon sie weiß Wovon sie nicht wissen sollte
Controller (Web-Ebene) HTTP-Pfad/-Teile, @RequestPart, ResponseEntity, Location task not found, Archivierung, Storage-Details
Upload-Validator/-Assembler einfache Dateiprüfungen (isEmpty, Whitelist Content-Type, anwendungsspezifische Maximalgröße), Zusammenbau von AttachmentUploadCommand 404/409, ResponseEntity, Domänenübergänge
Service (Anwendungs-/Domänenschicht) Regeln der Operation „Datei an Aufgabe anhängen“, Existenz der Aufgabe, Archivierung, Speichern des Ergebnisses MultipartFile, MVC-Annotationen, Multipart-Binding
Antwort-Mapper Mapping des internen Metadatenmodells auf externes AttachmentResponse Dateivalidierung und Geschäftsregeln
Fehlerschicht (@ControllerAdvice) Wie eine Ausnahme in ProblemDetail übersetzt wird Upload-Logik als solche

Nun schauen wir uns den Controller so an, wie er in einer anständigen Gesellschaft aussehen sollte (d.h. in einem Projekt, das man weiterführen möchte).

import jakarta.validation.Valid;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;

@RestController
@RequestMapping("/api/v1/tasks/{taskId}/attachments")
public class AttachmentController {

    @PostMapping(
            consumes = MediaType.MULTIPART_FORM_DATA_VALUE,
            produces = MediaType.APPLICATION_JSON_VALUE
    )
    public ResponseEntity
  
    upload( @PathVariable String taskId, // bind: taskId aus URL holen @RequestPart("file") MultipartFile file, // bind: Datei aus multipart holen @Valid @RequestPart("metadata") AttachmentUploadMetadataRequest metadata // bind + validation: Metadaten als separaten Teil ) { // bind -> convert -> delegate -> return } }
  

Die Signatur zeigt bereits den externen Vertrag; 201 Created, Location und die Antwort-Metadaten werden nach der Delegation an den Service hinzugefügt. Keine Repositorien, keine try/catch für Geschäftsfehler, keine System.out.println(file.getBytes()) (das ist ein separates Verbrechen gegen Logs und gesunden Menschenverstand).

3. MultipartFile nur in der Web-Ebene

Bei MultipartFile gibt es eine typische Falle: Es ist so praktisch, dass man es einfach weiterreichen möchte, „damit der Service selbst klarkommt“. Aber dieser Komfort ist wie billiger Zucker: Er gibt schnell Energie, aber später ist es irgendwie traurig.

MultipartFile ist ein MVC/Web-Typ. Es kommt aus der Mechanik des Multipart-Bindings, ist an die Servlet-Welt gebunden und daran, wie Spring Requests parst. Wenn Sie es in den Service durchschleusen, machen Sie den Service abhängig vom Web-Stack. Später wollen Sie den Service separat testen – und plötzlich müssen Sie irgendwo ein MockMultipartFile besorgen, ein Stück MVC-Kontext hochfahren oder die Datei manuell so bauen, wie Spring sie baut. Das ist nicht unmöglich, aber es ist ein unnötiger Preis.

Es gibt noch einen weiteren, eher philosophischen Grund. Die Service-Schicht in unserem Projekt ist der „Ort der Operationsregeln“. Sie sollte mit Operationsdaten arbeiten: taskId, contentType, size, content usw. Sie muss nicht wissen, dass diese Daten aus einem Multipart stammen. Heute kommen sie aus Multipart, morgen könnten sie aus einem anderen Transport kommen. Wir bauen in diesem Kurs keinen zweiten Transport auf, aber die Disziplin ist trotzdem nützlich: Sie macht den Code robust und erklärbar.

Eine gute Analogie: Der Controller ist der Kellner, der die Bestellung in menschlicher Sprache aufnimmt, und der Service ist die Küche, wo die Gerichte zubereitet werden. Der Kellner bringt in die Küche „Bestellung: Pizza 30 cm, ohne Pilze“, nicht den Kunden selbst mit den Worten „Der Koch soll selbst nachfragen, was er wollte“. Der Kunde hat in der Küche nichts zu suchen – dort ist es heiß, scharf und beängstigend.

Deshalb ist es logisch, einfache Dateiprüfungen wie leere Datei, nicht unterstützten Medientyp und anwendungsspezifische Maximalgröße in der Nähe der Eingangsgrenze zu halten, solange wir noch MultipartFile haben. Der Service sollte dann bereits normale Betriebsdaten erhalten.

So sieht eine schlechte Grenze aus – der Service akzeptiert MultipartFile:

import org.springframework.web.multipart.MultipartFile;

public interface AttachmentService {
    // Schlecht: Die Service-Schicht wird von Spring MVC abhängig (Web-Typ wurde in die Anwendung geschleust)
    void upload(String taskId, MultipartFile file, String description);
}

Auf den ersten Blick – bequem. Auf den zweiten – Sie unterschreiben, mit einem Web-Typ in der Tiefe der Anwendung zu leben.

Und so sieht eine gesunde Grenze aus – der Service akzeptiert einen Command:

public interface AttachmentService {
    // Gut: Der Service erhält reine Betriebsdaten, keine Transportdetails (Multipart/MVC)
    AttachmentMetadata upload(AttachmentUploadCommand command);
}

Jetzt ist es dem Service egal, woher der Inhalt genau kam. Er hat einfach „Betriebsdaten“. Und das ist ein sehr gutes Fundament für ein ruhiges Leben.

4. Command und Mapping des Uploads

AttachmentUploadCommand als Eingang der Operation

Wenn wir „Command“ sagen, bedeutet das nicht „Command Pattern aus dem GoF-Buch, schnell UML rausnehmen und leiden anfangen“. Hier ist es eine einfache und nützliche Idee: Wir erstellen ein normales Java-Objekt, das den internen Eingang der Operation beschreibt. Es ist kein öffentliches Request-DTO, denn ein Request-DTO ist ein API-Vertrag, während ein Command ein internes „Datenpaket“ ist, das sich bequem weiterreichen lässt.

Im Upload-Szenario ist ein Command aus zwei Gründen nützlich. Erstens schleppt es keine MVC-Typen in den Service. Zweitens hilft es, nicht zu verwechseln, „was der Client senden kann“ und „was der Server selbst berechnet“. Beispielsweise gibt der Client nicht size und contentType vor – der Server liest sie aus der Datei und stellt sie als Tatsache fest. Und das lässt sich bequem im Command-Objekt ausdrücken: Ein Teil der Daten stammt aus den Metadaten, ein Teil wird aus der Datei berechnet.

Und noch eine nützliche Grenze: leere Datei, Whitelist Content-Type und anwendungsspezifische Maximalgröße sollte man vor file.getBytes() aussieben. Danach kann der Assembler ruhig den Command aus dem technisch geeigneten Eingang zusammenbauen.

Eine minimale Version des Commands könnte so aussehen:

public record AttachmentUploadCommand(
        String taskId,            // Aufgaben-ID: stammt aus URL, ist aber für die Domänenoperation einfach ein Eingabeparameter
        String originalFileName,   // Originaldateiname: lesen aus Multipart
        String contentType,        // contentType: lesen aus Multipart, vertrauen dem Client nicht direkt über Metadaten
        long size,                // Größe: Server erfasst tatsächliche Größe der erhaltenen Datei
        byte[] content,           // Inhalt: im Übungsprojekt vollständig einlesen
        String description         // Beschreibung: stammt aus Metadaten
) {
}

Hier gibt es einen wichtigen Punkt: Wir akzeptieren nicht vom Client uploadedAt, id, storageKey, „echter Pfad auf der Festplatte“ und andere Dinge, die der Client nicht verwalten soll. Der Command spiegelt die „interne Wahrheit der Operation“ wider, nicht „alles, was vom Client kam“.

Wenn Sie dem Command weitere Felder hinzufügen möchten (z. B. uploadedAt), halten Sie inne und fragen Sie sich: Soll dieses Feld vom Client oder vom Server gesetzt werden? Wenn vom Server, dann erscheint es erst nach der Ausführung der Operation in der Domänenmodell/Metadaten. Im Command muss es nicht sein.

Mapping in einen separaten Mapper

Damit der Controller dünn bleibt, sollte er den internen Command nicht manuell direkt innerhalb der Methode „zusammenbauen“. Nicht weil „eine zusätzliche Klasse schön ist“, sondern weil der Zusammenbau des Commands meist allmählich wächst: Behandlung von null beim Dateinamen, Normalisierung von contentType, Lesen von Bytes, Übersetzung von IOException in eine verständliche Ausnahme. Wenn man das alles im Controller behält, wird der Controller sehr schnell „dick“.

Daher ein guter Kompromiss: Den Zusammenbau des Commands in einen Mapper/Assembler im Paket api.mapper auslagern. Der Controller liest sich dann wie ein Skript: „entgegengenommen → zusammengestellt → delegiert → zurückgegeben“. Der Mapper enthält den „technischen“ Teil der Umwandlung von Multipart-Daten in ein internes Objekt.

Beispiel einer einfachen Mapper-Klasse:

import org.springframework.stereotype.Component;
import org.springframework.web.multipart.MultipartFile;

import java.io.IOException;
import java.io.UncheckedIOException;

@Component
public class AttachmentUploadMapper {

    public AttachmentUploadCommand toCommand(
            String taskId,
            MultipartFile file,
            AttachmentUploadMetadataRequest metadata
    ) {
        try {
            // convert: wir gehen davon aus, dass einfache Dateiprüfungen bereits bestanden sind;
            // hier wandeln wir Web-Daten in das interne Operationsobjekt um
            return new AttachmentUploadCommand(
                    taskId,
                    file.getOriginalFilename(), // technisches Detail: Name aus Multipart-Headern
                    file.getContentType(),      // technisches Detail: contentType aus Multipart
                    file.getSize(),             // Tatsache: Größe dessen, was tatsächlich ankam
                    file.getBytes(),            // im Übungsprojekt vollständig in den Speicher einlesen
                    metadata.description()      // Geschäftsteil, den der Client tatsächlich angibt
            );
        } catch (IOException e) {
            // wichtig: IOException ist ein „Transport“-/technischer Fehler, es ist praktisch, ihn als Runtime nach oben zu werfen
            throw new UncheckedIOException("Dateiinhalt konnte nicht gelesen werden", e);
        }
    }
}

Ja, hier haben wir uns für byte[] entschieden und lesen die Datei komplett ein. In der echten Produktion könnte man streaming verwenden, aber im Rahmen unseres Übungsmodells ist das ein vernünftiger Kompromiss: Wir lernen den Vertrag und die Schichtengrenzen kennen, bauen keinen Dateiserver für Terabyte.

Beachten Sie, dass der Mapper kein ResponseEntity zurückgibt, nicht über 201 nachdenkt, nicht entscheidet, ob die Aufgabe archiviert ist. Er macht genau eines: Web-Daten in interne Betriebsdaten umwandeln.

Wo genau die einfachen Dateiprüfungen leben – ein separater Validator oder ein Assembler daneben – ist zweitrangig. Die Grenze ist wichtig: Sie müssen vor der schweren Dateiverarbeitung stattfinden, und der Service erhält bereits reine Betriebsdaten.

Jetzt kann der Controller kompakt und verständlich gemacht werden:

import jakarta.validation.Valid;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;

import java.net.URI;

@RestController
@RequestMapping("/api/v1/tasks/{taskId}/attachments")
public class AttachmentController {

    private final AttachmentUploadMapper uploadMapper;
    private final AttachmentMapper attachmentMapper;
    private final AttachmentService attachmentService;

    public AttachmentController(
            AttachmentUploadMapper uploadMapper,
            AttachmentMapper attachmentMapper,
            AttachmentService attachmentService
    ) {
        this.uploadMapper = uploadMapper;
        this.attachmentMapper = attachmentMapper;
        this.attachmentService = attachmentService;
    }

    @PostMapping(
            consumes = MediaType.MULTIPART_FORM_DATA_VALUE,
            produces = MediaType.APPLICATION_JSON_VALUE
    )
    public ResponseEntity
  
    upload( @PathVariable String taskId, // bind @RequestPart("file") MultipartFile file, // bind @Valid @RequestPart("metadata") AttachmentUploadMetadataRequest metadata // bind + validate ) { // einfache Dateiprüfungen sollten vor dem Lesen der Bytes erfolgen; danach wird der interne Befehl zusammengestellt AttachmentUploadCommand command = uploadMapper.toCommand(taskId, file, metadata); // delegate: Domänenregeln und Operation – im Service AttachmentMetadata created = attachmentService.upload(command); // return: externer Vertrag bleibt 201 Created + Location + Metadaten AttachmentResponse response = attachmentMapper.toResponse(created); URI location = URI.create("/api/v1/tasks/" + taskId + "/attachments/" + created.id()); return ResponseEntity.created(location).body(response); } }
  

Und hier sieht man unseren Rhythmus bind → convert → delegate → return. Der Controller macht das, was er kann: Web-Binding und HTTP-Antwort. Alles andere geht in andere Teile des Systems.

5. Service-Schicht: Operation und Regeln

Zu diesem Zeitpunkt erhält der Service bereits einen technisch geeigneten Eingang: Multipart-Form wurde von MVC geparst, Metadaten wurden validiert, einfache Dateiprüfungen haben offensichtlichen Müll aussortiert. Jetzt beginnen die fachlichen Regeln.

Unter AttachmentMetadata reicht es hier, das interne Metadatenmodell des Anhangs zu verstehen, ohne Bindung an HTTP.

Der Service ist in diesem Szenario der Hauptverantwortliche: Er kennt die fachlichen Regeln. Er sollte nicht in Multipart-Teilen „herumstochern“, denn das ist Transport. Aber er muss wissen, ob eine Datei an eine Aufgabe angehängt werden kann, ob die Aufgabe existiert, ob sie nicht archiviert ist und welche Fehler bei Regelverstößen zurückgegeben werden.

Wichtig ist, dass der Service keine HTTP-Antwort erstellt. Er weiß nicht, was 201 Created ist (das muss er nicht wissen). Er weiß, dass die Operation „Anhang hochladen“ entweder erfolgreich ist und Metadaten des erstellten Anhangs zurückgibt oder mit einer sinnvollen Ausnahme scheitert.

Das Service-Interface kann maximal einfach gehalten werden:

public interface AttachmentService {
    // Service-Vertrag: „Führe die Upload-Operation aus und gib die Metadaten des erstellten Anhangs zurück“
    AttachmentMetadata upload(AttachmentUploadCommand command);
}

Die Implementierung enthält dann die Regeln. Beispiel eines stark vereinfachten Logikausschnitts (ohne Storage-Details, weil uns jetzt die Grenzen wichtig sind):

import java.time.Instant;
import java.util.UUID;

public class DefaultAttachmentService implements AttachmentService {

    private final TaskRepository taskRepository;

    public DefaultAttachmentService(TaskRepository taskRepository) {
        this.taskRepository = taskRepository;
    }

    @Override
    public AttachmentMetadata upload(AttachmentUploadCommand command) {
        // Domänenprüfung #1: Aufgabe muss existieren
        Task task = taskRepository.findById(command.taskId())
                .orElseThrow(() -> new TaskNotFoundException(command.taskId()));

        // Domänenprüfung #2: Es können keine Dateien an eine archivierte Aufgabe angehängt werden
        if (task.status() == TaskStatus.ARCHIVED) {
            throw new FileUploadNotAllowedException("Dateien können nicht an eine archivierte Aufgabe angehängt werden");
        }

        // Speicherung von Inhalt/Storage hier ausgelassen: uns ist die Schichtgrenze wichtig
        return new AttachmentMetadata(
                UUID.randomUUID().toString(),
                command.taskId(),
                command.originalFileName(),
                command.contentType(),
                command.size(),
                command.description(),
                Instant.now()
        );
    }
}

Hier ist nicht wichtig, dass wir die Datei „nicht gespeichert“ haben (das ist im Übungspunkt vorläufig akzeptabel), sondern dass der Service:

1) reine Betriebsdaten entgegennimmt,
2) Domänenregeln prüft,
3) das Operationsergebnis (Metadaten) zurückgibt,
4) Domänenausnahmen wirft, die später von unserer globalen Fehlerschicht in ProblemDetail übersetzt werden.

Wenn Sie in den vorherigen Modulen bereits @ControllerAdvice und Fehler-Mapping erstellt haben, fügt sich dieser Service natürlich in das System ein. Beispielsweise wird TaskNotFoundException in 404 umgesetzt, FileUploadNotAllowedException in 409. Der Service wird dabei nicht zu einem „Mini-Controller“ und spielt nicht HTTP.

Der HTTP-Teil (Location, AttachmentResponse, 201) bleibt dabei außen, im Controller. Der Service gibt das Operationsergebnis zurück oder wirft eine Ausnahme.

6. Anti-Patterns des Upload-Endpunkts

Bei Dateien gibt es fast immer die Versuchung: „Da der Client sowieso Multipart sendet, lassen Sie uns doch gleich in dieser Anfrage den Status ändern, einen Kommentar hinzufügen, ein Tag setzen und die Aufgabe umbenennen.“ Klingt nach „Optimierung“ und „weniger Anfragen“, aber in der Praxis verwandelt das die API in einen schlecht verwalteten Brei. Sie vermischen mehrere Operationen mit unterschiedlichen Regeln, Status, Fehlern und Semantik. Sie können dem Client nicht mehr vernünftig erklären, was Erfolg bedeutet, was Fehler bedeuten und welche Nebenwirkungen eingetreten sind.

Ein sehr anschauliches Anti-Beispiel – „zu schlauer Endpunkt“:

import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;

@RestController
public class BadAttachmentController {

    @PostMapping(path = "/api/v1/tasks/{taskId}/attachments")
    public ResponseEntity<?> upload(
            @PathVariable String taskId,
            @RequestPart(value = "file", required = false) MultipartFile file, // Warnsignal: Die Datei „kann fehlen“
            @RequestPart(value = "comment", required = false) String comment // Warnsignal: In den Upload wurde plötzlich eine andere Operation eingebaut
    ) {
        // Ein solcher Endpunkt ist schwer zu dokumentieren: Was gilt als Erfolg, wenn die Datei fehlt? Und wenn der Kommentar vorhanden ist?
        return ResponseEntity.ok().build();
    }
}

Hier sieht man das Problem bereits ohne Details. required = false für die Datei ist fast immer ein Zeichen dafür, dass der Endpunkt zu viel versucht. Plus wir haben einen comment im Upload. Das ist eine andere Unterressource (Kommentare), ein anderer Vertrag, ein anderer Validierungssatz und eine andere Antwortbedeutung. Einen solchen Endpunkt kann man nicht richtig dokumentieren und testen, weil „was er tut“ davon abhängt, welche Teile gesendet wurden.

Ein weiteres Anti-Pattern – Web-Typen in die Service-Schicht durchschleusen:

import org.springframework.web.multipart.MultipartFile;

public class BadAttachmentService {
    public void upload(String taskId, MultipartFile file) {
        // Schlecht: Der Service kennt plötzlich Multipart und Spring MVC
        // Ergebnis: Das Testen und Wiederverwenden des Service ohne Web-Kontext wird schmerzhafter
    }
}

Formal funktioniert es. Architektonisch haben Sie die Domänenoperation an einen einzigen Transportmechanismus „geklebt“, und jetzt wird jede Änderung der Web-Ebene den Service mitziehen. Und das Testen des Service wird zu einer Übung „Baue ein MultipartFile aus der Luft“.

Es gibt noch einen subtilen, aber sehr häufigen Fehler: Das Metadaten-DTO beginnt, serververwaltete Felder anzuhäufen. Wenn ein Entwickler sieht, dass er contentType und size benötigt, denkt er manchmal: „Oh! Ich füge sie in AttachmentUploadMetadataRequest ein.“ Und hier muss man sich sagen: „Stop“. Der Client sollte nicht size senden – das ist einfach eine Lüge in schöner Verpackung. Der Server sollte die Größe selbst ermitteln und nur dem vertrauen, was er tatsächlich erhalten hat.

Infolgedessen klingt die gute architektonische Formel für einen Upload-Endpunkt langweilig, funktioniert aber perfekt: ein Endpunkt → eine Operation → minimaler Vertrag → klare Schichtengrenzen. Langweilig ist gut. Wie Ingenieure sagen: „Wenn Code langweilig ist, ist er vorhersagbar.“ Und Vorhersagbarkeit ist Liebe.

7. Typische Fehler beim Upload-Endpunkt

Fehler 1: Der Controller macht alles auf einmal, und dann „irgendwann verschieben wir es in den Service“.
Das ist der klassische Weg in technische Schulden. Ein Upload-Endpunkt wird fast garantiert mit Prüfungen und Nuancen überwuchert, und wenn Sie nicht sofort den Rhythmus bind → convert → delegate → return einhalten, werden Sie in einer Woche nicht mehr richtig verstehen, was die Methode tut. Es ist billiger, den Zusammenbau des Commands sofort in einen Mapper auszulagern und den Service unabhängig von MVC zu halten.

Fehler 2: MultipartFile reist in den Service „der Einfachheit halber“.
Die Bequemlichkeit ist kurzfristig. Danach wird der Service an Spring MVC gebunden, Tests werden komplizierter, und die Grenze der Web-Ebene verschwimmt. In einem Übungsprojekt ist das besonders gefährlich: Sie werden anfangen, Web-Typen immer tiefer zu schleusen, bis Sie „web everywhere“ haben.

Fehler 3: Das Metadaten-DTO verwandelt sich in einen Mülleimer für Felder, einschließlich serververwalteter Werte.
Wenn der Client beginnt, size, contentType, uploadedAt und schon gar id zu senden, bauen Sie sich selbst ein Problem auf. Diese Felder werden entweder ignoriert (wozu dann überhaupt?) oder beginnen, das Verhalten zu beeinflussen (dann verwaltet der Client Dinge, die er nicht verwalten sollte). Die Regel ist einfach: Ein Request-DTO enthält nur das, was der Client tatsächlich vertraglich vorgibt.

Fehler 4: Der Endpunkt wird „zu schlau“ und beginnt, mehrere verschiedene Operationen gleichzeitig auszuführen.
Noch ein Kommentar, Status, Tags, „Archivierung“ im Upload hinzugefügt – und schon hat man kein REST API mehr, sondern eine RPC-Kombination. Es entstehen versteckte Nebenwirkungen, unklare Status und Fehler, und das Schlimmste – das ist sehr schwer zu dokumentieren und zu testen. Halten Sie eine Operation pro Endpunkt, besonders in einem Übungsprojekt.

Fehler 5: Der Service erstellt HTTP-Antworten und kennt ResponseEntity.
Das sieht aus wie „nun, wir geben ja den Status zurück“, aber tatsächlich ist es eine Vermischung der Schichten. Der Service sollte das Operationsergebnis zurückgeben oder eine Ausnahme werfen. Das Erstellen der HTTP-Antwort ist die Verantwortung des Controllers und des globalen Fehlerbehandlers. Wenn der Service ResponseEntity kennt, können Sie ihn nicht mehr als reine Geschäftsoperation ohne MVC-Kontext wiederverwenden und testen.

1
Aufgabe
Spring REST & MVC, Level 26, Lektion 4
Gesperrt
Dünner Controller und `AttachmentUploadCommand`
Dünner Controller und `AttachmentUploadCommand`
1
Aufgabe
Spring REST & MVC, Level 26, Lektion 4
Gesperrt
Separater Mapper für Avatar-Upload
Separater Mapper für Avatar-Upload
1
Umfrage/Quiz
Multipart-Upload, Level 26, Lektion 4
Nicht verfügbar
Multipart-Upload
Dateien und JSON gemeinsam
Kommentare
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION