Skip to main content

So bleiben Sie mit Java-DTOs sicher

Artikel von
feature java dto

11. Oktober 2022

0 Min. Lesezeit

Data Transfer Objects (DTOs) in Java sind Objekte, die Daten zwischen Subsystemen übertragen. Sie sind ein Enterprise-Design-Pattern zur Aggregation von Daten. Ihr Hauptzweck besteht darin, die Anzahl der erforderlichen Systemaufrufe zwischen den Subsystemen zu verringern und so den entstehenden Overhead zu reduzieren.

In diesem Artikel erkläre ich, wie DTOs in modernen Java-Anwendungen eingesetzt werden, welche Vorteile sie Ihrer Anwendung bieten und wie Java-DTOs durch die Vermeidung versehentlicher Datenlecks zu mehr Sicherheit beitragen können.

Was sind POJOs, JavaBeans und Value Objects?

Wie der Name bereits vermuten lässt, ist ein Plain Old Java Object (POJO) ein gewöhnliches Java-Objekt. Es kann sich um eine beliebige Klasse handeln, die keinen besonderen Einschränkungen unterliegt, abgesehen von denen der Java-Sprache. POJOs werden erstellt, um die Wiederverwendbarkeit und Lesbarkeit zu verbessern.

public class CoffeePOJO {

   public String name;
   private List<String> ingredients;

   public CoffeePOJO(String name, List<String> ingredients) {
       this.name = name;
       this.ingredients = ingredients;
   }

   void addIngredient(String ingredient) {
       ingredients.add(ingredient);
   }
}

Eine JavaBean ist ein POJO gemäß dem JavaBean-Standard. Laut diesem Standard sind alle Eigenschaften privat und werden über Getter- und Setter-Methoden aufgerufen. Zusätzlich sollte ein Konstruktor ohne Argumente vorhanden sein, neben einigen weiteren Anforderungen.

Das bedeutet: Alle JavaBeans sind POJOs, aber nicht alle POJOs sind JavaBeans.

public class CoffeeBEAN implements Serializable {

   private String name;
   private List<String> ingredients;

   public CoffeeBEAN() {
   }

   public String getName() {
       return name;
   }

   public void setName(String name) {
       this.name = name;
   }

   public List<String> getIngredients() {
       return ingredients;
   }

   public void setIngredients(List<String> ingredients) {
       this.ingredients = ingredients;
   }
}

Ein Value Object ist ein kleines Objekt, das eine einfache Dateneinheit repräsentiert. Ein Value Object hat jedoch normalerweise keine Identität. Value Objects sind derzeit in Java nicht verfügbar, aber die JDK-Maintainer arbeiten daran, sie als Teil von JEP 401 hinzuzufügen. Bis dahin müssen wir ein POJO erstellen, das diese Aufgabe für uns übernimmt.

Ein Data Transfer Object implementieren

Ein DTO lässt sich als POJO implementieren – oder als JavaBean. Entscheidend ist, dass ein DTO die Zuständigkeiten zwischen Entitäten wie der Präsentationsschicht und dem Domänenmodell trennt.

Sehen wir uns zur Erklärung einen kleinen REST-Service an. Nehmen wir an, wir haben ein Café mit Kaffees und Kunden. Beide sind separate Domänenentitäten im System. Wenn ich den Lieblingskaffee eines Kunden ermitteln möchte, erstelle ich eine API, die die aggregierten Daten im FavoriteCoffeDTO bereitstellt.

Diagramm mit JSON, API, FavoriteCoffeeDTO und Controller in der Präsentationsschicht, verbunden mit Customer und Coffee in der Domänenschicht.

Der Code sieht in etwa so aus:

​​public class Coffee {

   private Long id;
   private String name;
   private List<String> ingredients;
   private String preparation;

}

public class Customer {

   private Long id;
   private String firstName;
   private String lastName;
   private List<Coffee> coffees;

}

public class FavoriteCoffeeDTO {

   private String name;
   private List<String> coffees;

}

In der obigen Implementierung habe ich die Domänenschicht von der Präsentationsschicht getrennt. So kann mein Controller jetzt die beiden Domänenentitäten dem DTO zuordnen.

In diesem Beispiel habe ich die Felder als privat definiert. Um auf sie zuzugreifen, muss ich daher Getter- und Setter-Methoden erstellen. Häufig orientiert man sich bei DTOs ganz oder teilweise am JavaBean-Standard. Das ist natürlich nicht zwingend erforderlich – Sie können die Methode wählen, die Ihren Anforderungen entspricht. Weitere Möglichkeiten sind, alle Felder öffentlich zu machen und direkt darauf zuzugreifen (wie im folgenden Beispiel) oder das Objekt mit einem Konstruktor für alle Argumente und einigen Getter-Methoden unveränderlich zu gestalten.

public class FavoriteCoffeeDTO {

   public String name;
   public List<String> coffees;

}

String name = favCoffeeDTO.name;

Wenn Sie auf eine neuere Java-Version aktualisiert haben, können Sie für Ihre DTOs auch Java Records verwenden. Java Records sind einfache, unveränderliche Klassen, die automatisch einen Konstruktor für alle Argumente, Zugriffsmethoden, toString() und hashCode() bereitstellen, ohne dass diese explizit definiert werden müssen. Dadurch wird Ihr Code kompakter und besser lesbar. Beachten Sie, dass Records nicht der JavaBean-Spezifikation entsprechen, da ihre Zugriffsmethoden kein get- oder set-Präfix haben.

public record FavoriteCoffeeDTO(String name, List<String> coffees) {}

String name = favoriteCoffeeDTO.name();

Was zeichnet ein gutes DTO aus?

Ein DTO dient dazu, Daten zwischen Prozessen zu übertragen. Ein gutes DTO enthält daher nur die Informationen, die für den jeweiligen Teil des Systems erforderlich sind. In unserem API-Beispiel müssen wir lediglich den Namen des Kunden und seine bevorzugte Kaffeebestellung zurückgeben. Weitere Angaben, etwa Geschäftslogik, sind nicht erforderlich. Grundsätzlich sollten DTOs so einfach, klein und übersichtlich wie möglich sein.

Nach der Initialisierung sollte sich der Zustand eines DTO außerdem nicht mehr ändern. Daher eignet sich eine unveränderliche Datenstruktur sehr gut für ein DTO. Da ein DTO nur Daten enthält, die nicht verändert werden sollten, ist ein Java Record ideal – insbesondere, weil JSON-Serialisierungsbibliotheken wie Jackson Java Records unterstützen.

Sicherheitsaspekte bei DTOs

Wie bereits erwähnt, entkoppeln wir mit diesem DTO-Pattern das Domänenmodell von der Präsentationsschicht. Einfache DTOs, die nur die für das jeweilige Subsystem oder die API erforderlichen Daten und keine Geschäftslogik enthalten, können auch Ihre Sicherheit verbessern.

Bei vielen Proofs of Concept werden sämtliche Domäneneinträge ausgegeben. Dadurch können unnötige Daten außerhalb des Systems verfügbar werden und potenziell Datenlecks entstehen.

Angenommen, unsere API verfügt über eine Funktion, mit der alle Kunden gefunden werden, verwendet aber kein DTO:

@GetMapping("/customers")
public List<Customer> getAllCustomers(){
   return repository.findAll();       
}

Wenn unser Kundenobjekt dem aus dem vorherigen Beispiel entspricht, geben wir bereits zu viele Informationen preis. Benötigen wir wirklich die id oder die Liste der Lieblingscoffees? Noch problematischer wird es, wenn wir dem Kunden weitere Informationen wie eine Privatadresse hinzufügen.

public class Customer {
   private Long id;
   private String firstName;
   private String lastName;
   private List<Coffee> coffees;
   Private String homeAddress;

}

Wenn wir die Daten in unserem Endpoint nicht filtern, entsteht plötzlich eine Datenschutzverletzung, denn die Weitergabe des vollständigen Namens und der Privatadresse gilt in vielen Ländern als Verstoß gegen den Datenschutz. Durch die Entkopplung der API vom Datenmodell mithilfe eines DTOs wäre das verhindert worden, da der Mapper-Controller nur die erforderlichen Felder im DTO befüllt hätte. Selbst wenn wir unser Domänenmodell später erweitern, verhindert ein DTO, dass wir versehentlich personenbezogene Daten preisgeben.

public class CustomerDTO {
   private String firstName;
   private String lastName;
}

Empfehlungen

DTOs in Java zur Entkopplung von Subsystemen zu verwenden, ist grundsätzlich eine gute Idee. Aus Engineering-Sicht verringert dies die Anzahl der Roundtrips zwischen den verschiedenen Schichten. Auch aus Sicherheitsgründen hilft es, versehentliche Datenlecks zu verhindern. Ich empfehle, DTOs spezifisch zu gestalten und ihre Wiederverwendung einzuschränken. Wenn Sie Ihre DTOs für verschiedene Funktionen wiederverwenden, sollten Sie genau wissen, wo sie eingesetzt werden, bevor Sie Änderungen daran vornehmen.

Halten Sie Ihre DTOs grundsätzlich knapp, möglichst frei von Geschäftslogik und beschränken Sie sie auf die Daten, die für bestimmte Funktionen erforderlich sind. Außerdem halte ich Unveränderlichkeit für eine natürliche Eigenschaft von DTOs. Java Records, die mit Java 16 vollständig eingeführt wurden, eignen sich daher hervorragend zur Implementierung von DTOs in Java.

In den folgenden Ressourcen erfahren Sie mehr über Java-Sicherheit:

Testen Sie auch unser kostenloses Online-Tool zur Prüfung von Java-Code. Damit sehen Sie, wie die Snyk-Code-Engine Ihren Code auf Sicherheits- und Qualitätsprobleme analysiert.

FAQ

Was ist ein DTO?

DTO steht für Data Transfer Object. Es ist ein Objekt, das Daten zwischen verschiedenen Prozessen in einer Anwendung überträgt. Ein DTO ist speziell darauf ausgelegt, durch die Aggregation von Daten die Anzahl der Methodenaufrufe zwischen Client und Server zu verringern. Außerdem verhindert es eine enge Kopplung zwischen Entitäten wie dem Domänenmodell und der Präsentationsschicht einer Anwendung.

Was ist ein DTO in Java?

Ein Data Transfer Object in Java ist in der Regel ein POJO (Plain Old Java Object) ohne Geschäftslogik. Es hat üblicherweise eine flache Datenstruktur, die speziell auf den Zweck der Transaktion zwischen den Methoden zugeschnitten ist, für die es verwendet wird. DTOs in Java können der JavaBean-Konvention mit privaten Feldern und Getter- und Setter-Methoden folgen, müssen es aber nicht.

Ist ein DTO dasselbe wie ein POJO?

Kurz gesagt: Ein DTO ist ein POJO, aber nicht jedes POJO ist ein DTO. Ein Data Transfer Object ist ein spezielles POJO zur Übertragung von Daten zwischen Prozessen.

Ist die Verwendung von DTOs eine gute Praxis?

Ja. Gut implementierte DTOs verringern die enge Kopplung und die Anzahl der Methodenaufrufe – das verbessert die Sicherheit und reduziert Datenlecks.

Was ist der Unterschied zwischen einem DAO und einem DTO?

DAO steht für Data Access Object und bezeichnet ein Pattern zur Entkopplung der Geschäftslogik von der Persistenzschicht. DAOs werden häufig für CRUD-Operationen wie Aktualisieren, Löschen und Speichern verwendet. Ein DTO dient ausschließlich dazu, Daten zwischen Subsystemen zu übertragen, und sollte keine Geschäftslogik enthalten.

Starten Sie mit Capture the Flag

Erfahren Sie in unserem virtuellen On-Demand-Workshop für Einsteiger, wie Sie Capture-the-Flag-Herausforderungen lösen.