Danke Jackson. Danke, dass es dich gibt!
Man kann CSV-Dateien lesen oder man kann sie lesen lassen
Ersteres war lange der Weg, den ich gegangen bin wenn es darauf ankam. Klar wurden Werkzeuge wie apache-commons-csv herangezogen. Aber alles in allem hat es sich jedes mal aufs neue sehr - na wie soll ich sagen - altmodisch und mit angezogener Handbremse angezogen angefühlt.
Zufällig war ich neulich auf der Projektseite von Jackson unterwegs und da sind mir ein paar ziemlich coole Databindings über den Weg gelaufen. Neben richtig coolen Dingen wie Protobuf-data-binding findet man auch YAML oder CSV bindings. Letztere sind einfach Dinge, die (anm. meiner Selbst) einfach jedem Entwickler immer wieder über den Weg laufen. Und daher habe ich mir gedacht, probiere ich es mal aus und schreibe ein paar Zeilen dazu.
Setup
Falls ihr gerne Sourcecode unter den Fingern habt, findet ihr ein lauffähiges Projekt auf meinem Github Account. Ansonsten benötigt ihr zum starten erstmal nichts anderes als ein Java-Projekt über das ihr mit irgendeinem Dependency-Management (bei mir wie immer Maven) die folgende Dependency eintragt.
<dependency>
<groupId>com.fasterxml.jackson.dataformat</groupId>
<artifactId>jackson-dataformat-csv</artifactId>
<version>2.7.0</version>
</dependency>
Diese Dependency holt sich transitiv noch ein paar weitere Jackson-Dependencies mit ins Projekt.
Diese sind bei o.g. Version: jackson-core
, jackson-databind
, jackson-annotation
.
Die Mission
Ich stelle hier zunächst einmal das Szenario vor, welches es zu lösen gilt. Daraufhin schauen wir uns zum Einen die Lösung mit Apache-Commons-CSV und zum anderen mit Jackson-Dataformat-CSV an.
Nehmen wir mal an, wir hätten folgende CSV-Datei:
username,password,email
famousUser,secret,famousUser@acme.com
anotherUser,withAnotherSecret,andAnIncredibleEmailAddy@acme.com
Diese Liste soll eingelesen und in einer Liste aus Pojos der Klasse User abgelegt werden. Die Pojo Klasse folgt dabei folgendem Aufbau:
public class User {
private String username;
private String password;
private String email;
// ... getter and setter.
}
Wie ich bisher CSV desierialisiert habe
Bis zu dem Moment, an dem mir das CSV-Databinding von Jackson über den Weg gelaufen ist, habe ich die Aufgabe der (De-)Serialisierung immer über Apache-Commons-CSV erledigt.
Ein typischer Code sähe dabei wie folgt aus:
public static List<User> importUsers(URL url) throws IOException {
Reader in = new FileReader(url.getFile());
Iterable<CSVRecord> records = CSVFormat.DEFAULT.parse(in);
return StreamSupport.stream(records.spliterator(), false)
.skip(1) // Skip the header line.
.map(record -> { // Transform each entry.
String username = record.get(0);
String password = record.get(1);
String email = record.get(2);
return new User(username, password, email);
})
.collect(Collectors.toList()); // Collect the users to a list.
}
Zugegeben… Es geht da durchaus auch hässlicher, aber es bietet sich dennoch einiges an Handarbeit - beispielsweise das händische Mapping der Attribute. Und wenn wir an der Stelle kein Java 8 verwenden würden, sähe die Lösung noch weit weniger komprimiert aus.
Wie ich ab jetzt CSV deserialisieren werde
Ab jetzt wird mein Code anders aussehen. Um das o.g. Problem zu lösen können wir folgendes Schreiben:
public static List<User> importUsers(URL url) throws IOException {
CsvMapper mapper = new CsvMapper();
CsvSchema schema = CsvSchema.emptySchema().withHeader();
return mapper.readerFor(User.class).with(schema).readValues(url).readAll();
}
Dabei wird nicht nur der Header - also die erste Zeile der CSV genutzt um ein Object-Mapping herzustellen, nein; wir bekommen gleich ein wunderhübsches Fluent-Pattern an die Hand welches uns unglaublich viele Konfigurationsmöglichkeiten bietet. Und am Ende sparen wir uns sogar noch Code bei einer - wie ich finde - unglaublich guten Lesbarkeit.
Fazit
Endlich fühlt sich das in die Jahre gekommene Datenformat CSV nicht mehr so in die Jahre gekommen an. Ich bin persönlich davon überzeugt, dass ich dieses Feature an vielen Stellen nutzen werde und ab jetzt regelmäßig einen Blick auf die aktuelle Entwicklung von Jackson sowie den angebotenen Databindings werfen werde.