|
|||||||||||||||||||||||||||||||
HOME | COURSES | TALKS | ARTICLES | GENERICS | LAMBDAS | IOSTREAMS | ABOUT | CONTACT | | | | |||||||||||||||||||||||||||||||
|
Java 7 - JSR 203 - NIO2
|
||||||||||||||||||||||||||||||
Mit diesem Artikel geht die Reihe über Neuerungen in Java 7 weiter. Diesmal wollen wir uns die I/O Erweiterungen (NIO2), die mit Java 7 gekommen sind, genauer ansehen. Bisher gibt es im JDK zwei Top-Level-Packages mit I/O Funktionalität. Dies ist zum einen das Package java.io , das es schon seit dem JDK 1.0 gibt. Mit Java 1.4 ist dann das Package java.nio neu dazugekommen. Die darin enthalte Funktionalität nennt sich NIO (= new i/o). Mit Java 7 ist nun wieder neue I/O Funktionalität in den JDK gekommen. Diese Erweiterung nennt sich NIO2 und findet auch unter dem Top-Level-Package java.nio ihren Platz. Die zwei großen NIO2 Themen sind:
Neues File System APIAusgangssituationWer bisher in Java auf das File System zugreifen wollte, kam um die Benutzung der Klasse java.io.File nicht herum. Die Klasse gibt es seit Java 1.0 im JDK. Leider hat sich gezeigt, dass sie einige Schwächen hat. Kritik gibt es dabei im Wesentlichen an zwei Punkten: Zum einen ist das API Design an einigen Stellen suboptimal. Zum anderen ist der API Umfang zum Teil nicht ausreichend.Ein Beispiel für die Designschwächen des API ist die Instanzmethode delete() , mit der man eine Datei / ein Directory von einem Java Programm aus löschen kann: public boolean delete() Der Returnwert gibt an, ob die Datei / das Directory, mit dem das Objekt assoziiert ist, wirklich gelöscht werden konnte oder nicht. Das Problem ist nun, dass im Fehlerfall (Returnwert = false ) unklar ist, warum das Löschen nicht funktioniert hat, denn weitere Indikatoren über die Fehlerursache gibt es nicht: die Methode wirft keine Exception. Das heißt, man kann in einer solchen Situation weder vom Programm aus auf den Fehler reagieren noch an den Benutzer eine sinnvolle Fehlermeldung ausgeben. delete() ist nicht die einzige Methode, die dieses Problem hat; für mkdir() , mkdirs() , renameTo() , setLastModified() , setReadOnly() gilt im Prinzip das gleiche.
Die Kritik am unzureichenden Umfang des bestehenden APIs lässt
sich am besten am API für den Zugriff auf Dateiattribute festmachen.
Die Klasse
File
stellt ein paar Methoden dafür zur Verfügung.
Zum Beispiel lässt sich mit
lastModified()
,
setLastModified()
der Zeitpunkt der letzten Modifikation lesen bzw. schreiben. Ein
API, mit dem man aber auf alle Attribute einer Datei zugreifen kann, gibt
es nicht. Ein weiterer Nachteil ist, dass das existierende API auch
noch relativ ineffizient ist, wenn es um den Zugriff auf mehre Attribute
einer Datei geht. Jedes Attribut muss mit jeweils einem Methodenaufruf
bearbeitet werden.
Bulk
-Zugriffe, die mehrere oder alle Attribute
mit einem Methodenaufruf lesen oder schreiben, gibt es nicht.
java.nio.file.Path und java.nio.file.FilesDas neue File System API findet sich unter im Package java.nio.file . Zwei wichtige Abstraktionen sind das Interface java.io.file.Path und die Klasse java.io.file.Files . Zusammen ersetzen sie die komplette Funktionalität der Klasse java.io.File , ohne dass die Klasse File in Zukunft deprecated wird.Schauen wir uns also die beiden neuen Abstraktionen mal genauer an, mit besonderem Schwerpunkt auf den oben diskutierten Kritikpunkten an File . Path repräsentiert ein Element im Filesystem, das über einen hierarchischen Pfad identifiziert wird, also eine Datei oder ein Directory in Windows, UNIX, Linux, usw. . Files ist eine Klasse mit statischen Methoden, die Operationen zur Verfügung stellen, um Dateien und Directories zu bearbeiten: copy() , move() , delete() , ... . Das bedeutet, die Funktionalität die bisher von java.io.File und seinen Instanzmethoden zur Verfügung gestellt wurde, findet man nun in java.nio.files.Files' statischen Methoden. Die Dateien bzw. Directories, die bearbeitet werden sollen, werden als Objekte vom Typ Path an Files ' statischen Methoden übergeben. Schauen wir uns die delete() Methode von Files an. Sie hat folgende Signatur: public static void delete(Path path) throws IOException; Der Parameter path spezifiziert die Datei / das Directory das gelöscht werden soll. Im Vergleich zur bisherigen delete() Methode von File hat die neue Methode in Files keinen expliziten Returnwert mehr. Dafür wirft sie nun eine IOException . Diese Exception ist zum einen die Indikation dafür, dass ein Fehler aufgetreten ist. Andererseits spezifiziert sie auch die Ursache des Fehlers. Man sieht also, an dieser Stelle ist das API Design verbessert worden: es ist jetzt möglich, den konkreten Fehler im Programm auszuwerten und auf ihn zu reagieren. Wir wollen uns hier jetzt nicht jede Methode von Files ansehen. Sie lassen sich eigentlich alle ganz gut mit Hilfe der Javadoc verstehen. Schauen wir uns deshalb nur noch an, wie der Zugriff auf Dateiattribute im neuen File System API gelöst ist. Files stellt dafür die Zugriffmethoden getAttribute() und setAttribute() zur Verfügung. Das Attribut, auf das hierbei zugegriffen werden soll, wird durch einen speziellen String spezifiziert, der als Parameter der jeweiligen Methode übergeben wird. Zum Beispiel ist "basic:lastModifiedTime" der String, der den Zeitpunkt der letzten Modifikation spezifiziert. Die Strings selbst sind zu Gruppen in so genannten Attribute Views organisiert: BasicFileAttributeView , DosFileAttributeView , PosixFileAttributeView im Package java.nio.file.attribute . Unser Beispielstring "basic:lastModifiedTime" ist aus BasicFileAttributeView . Wir benutzen ihn nun, um den Zeitpunkt der letzten Modifikation der mit myFile vom Typ Path assoziierten Datei zu lesen: FileTime ft = (FileTime) Files.getAttribute(myfile, "basic:lastModifiedTime"); Genau genommen könnten wir in diesem Beispiel auch "lastModifiedTime" statt "basic:lastModifiedTime" verwenden, das Strings ohne View-Prefix als Attribute aus dem BasicFileAttributeView interpretiert werden. Schauen wir uns nun an, wie das Lesen bzw. Schreiben mehrerer Attribute einer Datei (also Bulk-Zugriffe) funktionieren. Schreibende Bulk-Zugriffe gibt es nicht. Aber für das Lesen von mehreren Attributen bietet Path die Methode readAttributes() an. Hier muss man alle Attribute, die man lesen will, in einem String Parameter spezifizieren. Das heißt, der Aufruf: Map<String, ?> result = Files.getAttribute(myfile, "basic:size,lastModifiedTime"); ließt nun zwei BasicFileAttribute s, nämlich die Größe und den Zeitpunkt der letzten Modifikation der Datei. Der Returnwert ist eine Map , die den Namen des Attributs auf seinen gelesenen Wert abbildet. Wie oben bereits erwähnt kann man den View-Prefix im Fall von Attributen aus dem BasicFileAttributeView weglassen. Der Aufruf: Map<String, ?> result = Files.getAttribute(myfile, "*"); liefert die Werte für alle Attribute der BasicFileAttributeView in der result Map. Reichen die vorgegeben Views und ihre Attribute nicht aus, weil man auf exotische Attribute (z.B. MP3-Id-Tags von MP3-Audiodateien) zugreifen möchte, so kann man durch Implementieren des Interfaces UserDefinedFileAttributeView einen neuen Attribute-View mit eigenen Attributen definieren. Natürlich muss man dabei den Zugriff auf die neuen Attribute komplett selbst implementieren. Dafür kann der Zugriff auf die Attribute dann aber über Files Methoden erfolgen. Wenn man nun sieht, was man alles mit Files machen kann, das früher mit File nur schlecht oder gar nicht möglich war, kommt vielleicht der Wunsch auf, in bereits bestehendem Code bei zukünftigen Erweiterungen Files statt File zu verwenden. Dazu muss man glücklicherweise nicht den existierenden Code, der auf Basis von File implementiert ist, unter Verwendung von Files und Path reimplementieren. Vielmehr hat File in Java 7 eine neue Methode toPath() , die es erlaubt, das File Objekt in ein Path Objekt zu konvertieren und so ist es problemlos möglich die neue Funktionalität von Files zu nutzen. So kann man dann den bisherigen Aufruf boolean isDelteted = myFile.delete(); einfach zu
try { Files.delete(myFile.toPath()); }
ändern, wenn man auf den Fehler beim Löschen einer Datei reagieren
will.
Weitere File System API NeuerungenFiles und Path sind aber nicht die einzigen Neuerungen im NIO 2 File System API.Neu sind auch die Klassen FileSystem , FileSystems , FileStore (alle aus dem Package java.nio.file ), die das API für den Zugriff auf bestimmte Elemente des Dateisystems bilden.
aufrufen, um das Dateisystem zu bekommen, auf dem das Programm aktuell ausgeführt wird.
public Iterable<FileStore> getFileStores();Neben dem Zugriffs-API bringt die NIO2 noch zwei wichtige Neuerungen beim File System API: Watch Service und System Provider Interface . Wenn man vor Java 7 einen View auf ein Directory am Bildschirm anzeigen wollte, der automatisch die Änderungen (z.B. Löschen von Dateien im Directory) anzeigt, so musste man im Anwendungsprogramm das Directory periodisch pollen. Typischerweise machte man das, indem man auf dem java.io.File Objekt des Directorys zeitgesteuert immer wieder eine der list() oder listFile() Methoden aufrief.
Mit NIO 2 gibt es nun den
java.nio.file.WatchService
.
Dieser liefert, wenn entsprechend konfiguriert, die Änderungen als
Events, die man aktiv mit
poll()
oder
take()
abholen
muss. Dabei haben beide Methoden die gleiche Semantik wie bei der
Queue:
poll()
liefert
null
zurück, wenn kein Event
da ist,
take()
blockiert und wartet, bis ein Event da ist.
Nachdem man all die tollen Features des neuen File System API gesehen
hat, kann man auf die Idee kommen, dass man einen eher exotischen Dateienspeicher
(z.B. ein WORM Archiv) in Java über das neue File System API ansprechen
möchte. Dazu muss man das System Provider Interface implementieren.
Asynchrone I/O ErweiterungenSynchrone und asynchrone I/O in Java
Synchrone I/O kann aber zu einem Problem werden, zum Beispiel bei Servern, die mit einer hohen Anzahl von Clients (zigtausende!) parallel kommuniziert. Hier brauch man dann nämlich für jede Clientverbindung einen Thread, der auf den Input vom Client wartet. Auf einigen Systemen kann man so an die maximale Anzahl Threads stoßen, die in der JVM verfügbar sind. Aber selbst, wenn das nicht der Fall ist, ist dies keine besonders effiziente Architektur, da man eine extrem hohe Anzahl von Threads benötigt und entsprechend Systemressourcen sowohl in der JVM als auch im Betriebssystem bindet. Kommt noch dazu, dass die meisten dieser Threads dann nichts anderes machen, als im Idle-Zustand auf Input zu warten. Das ist im Wesentlichen die Motivation, die dazu geführt hat, dass mit Java 1.4 asynchrone I/O im Rahmen der NIO eingeführt wurde. Bei der asynchronen I/O wartet die Leseoperation nun nicht mehr, bis Input-Daten vorhanden sind, sondern kehrt, wenn keine da sind, sofort und ohne Daten zurück. Dass die Leseoperation hier nicht blockiert, ist zwar wichtig, aber nicht das einzige Element, dass asynchrone I/O ausmacht. Zusätzlich braucht man einen Notifikationsmechanismus, der darüber informiert, dass Input-Daten vorhanden sind und sich die Leseoperation überhaupt lohnt. Für den Notifikationsmechanismus hat man sich bei der Java 1.4 Entwicklung vom select() Systemcall inspirieren lassen, der zuerst in 4.2 BSD implementiert wurde und später auch in ähnlicher Form in anderen Betriebssystemen Eingang gefunden hat. Konkret sind mit Java 1.4 folgende Klassen für die asynchrone I/O dazu gekommen:
Selector selector = SelectorProvider.provider().openSelector();
// 1
while(i.hasNext())
{
// 8
if( sk.isReadable( ) ) {
//11
Zuerst muss man sich ein Selector Objekt erzeugen (Zeile 1). Im Aufruf seiner select() Methode wartet der Selector-Thread dann darauf, dass sich auf einem der bei ihm registrierten SocketChannel s etwas tut (Zeile 4). Werden Daten auf einem oder mehreren SocketChannel s empfangen, für die man sich registriert hat, so kommt die select() Methode zurück. Nun muss man über alle SelectionKey s iterieren (Zeile 8), denn jeder Key repräsentiert die Notifikation dafür, dass sich auf dem korrespondierenden SocketChannel etwas getan hat. Mit dem Aufruf von isReadable() (Zeile 11) wird geprüft, ob Daten auf dem Channel angekommen sind. Wenn ja, werden diese Daten in einen ByteBuffer eingelesen. Mit dem Aufruf von handleClientDataAsynchronously() (Zeile 15) werden die Daten an einen anderen Thread zur Weiterverarbeitung übergeben. Das hier ein Threadwechsel stattfindet, ist enorm wichtig, denn der Selector-Thread, der den select() bedient, darf nicht mit anderen Aufgaben aufgehalten werden. Er muss vielmehr über die verbleibenden SelectionKey s iterieren und, wenn er damit fertig ist, wieder in den select() Aufruf zurückkehren. Denn es kann sein, dass während der Iteration an weiteren SocketChannel s schon wieder Daten angekommen sind. Dann muss der Selector-Thread diese wieder lesen und ausliefern.
So sieht das Programmiermodell mit der asynchronen I/O aus Java 1.4
also aus. Wenn man genau hinschaut, fragt man sich, warum man den
ganzen Code (der in der Praxis noch komplizierter ist, weil man sich nicht
nur für Input interessiert) immer wieder neu in der Anwendung implementieren
muss. Schön wäre es doch, wenn der redundante Code im JDK
wäre und man die Input-Daten in einem asynchronen Callback ans Anwendungsprogramm
ausgeliefert bekäme.
Neuerungen der Asynchronen I/O in Java
AsynchronousSocketChannel asc = AsynchronousSocketChannel.open(); // 1 … // connect final ByteBuffer buf = ByteBuffer.allocate(BYTE_BUFFER_SIZE);
asc.read(buf, null, new CompletionHandler<Integer,
Object> () { // 4
Nach Erzeugen eines AsynchronousSocketChannel (Zeile 1) besteht der Code im Wesentlichen nur noch aus dem Aufruf der read() Methode des AsynchronousSocketChannel s (Zeile 4). Der erste Parameter des Methodenaufrufs ist der ByteBuffer , in den die Daten dann asynchron eingelesen werden sollen. Der dritte und letzte Parameter ist der Callback vom Typ CompletionHander , den wir im Beispiel als Anonymous Inner Class implementieren. Der read() Aufruf kehrt natürlich sofort zurück, da es sich um asynchrone I/O handelt. Die asynchrone Input-Operation selbst wird vom JDK gemacht, wenn Inputdaten vorhanden sind. Das Anwendungsprogramm bekommt die Notifikation, das die Daten eingelesen wurden und in den ByteBuffer geschrieben worden sind, durch Aufruf der Callback Methode completed() . completed() wird von einem Thread aus dem JDK heraus aufgerufen. Um in der completed() Methode auf den ByteBuffer zugreifen und die eingelesenen Daten verarbeiten zu können, ist die ByteBuffer Variable buf im Kontext der äußeren Funktion final deklariert. Die Methode failed() aus dem Callback, wird aufgerufen, wenn ein Fehler bei der asynchronen Einleseoperation auftritt, zum Beispiel, wenn die Verbindung abgebrochen worden ist. Der erste Parameter von failed() gibt Auskunft über die Details des Fehlers. Bleibt bei der Diskussion des Beispielcodes noch der zweite Parameter der read() Methode. Er ist in unserem Beispiel null . Der Parameter dient dazu, Daten vom Aufrufkontext an die Callback Methoden durchzuschleusen. Das heißt, das Objekt, was man bei der read() Methode als zweiten Parameter mitgibt, kommt als zweiter Parameter bei failed() bzw. completed() wieder an. Man kann sich vorstellen, dass er verwendet wird, um einen Kontext durchzuschleusen, der den Zustand der Kommunikation mit dem Client repräsentiert. Der Typ des Parameters ist beliebig; alle Methoden, die ihn bekommen, sind generisch. Der AsynchronousSocketChannel bietet weitere überladene read() Methoden an. Zum Beispiel gibt es eine Variante mit zusätzlichem Timeout, die es erlaubt, das Warten auf den asynchronen Input zeitgesteuert abzubrechen. Grundsätzlich sind aber alle read() Varianten ähnlich zu benutzen, wie im oben gezeigten Beispiel. So wie wir unseren Beispielcode oben implementiert haben, wird der Callback von irgendeinem Thread eines Thread Pools innerhalb des JDKs ausgeführt. Bei der Java 1.4 Lösung mit dem Selector hatte man Kontrolle über den Thread Pool und seine Threads. Man kann zum Beispiel die Anzahl der Threads, den Namen der Threads, usw. festlegen. Möchte man diese Form der Kontrolle auch beim AsynchronousSocketChannel haben, so gibt es eine überladene Variante der statischen open() Factory Methode des AsynchronousSocketChannel s (Zeile 1). Diese Methode hat einen Parameter vom Typ AsynchronousChannelGroup . Die AsynchronousChannelGroup ist nichts anderes als ein selbst definierter Thread Pool, der beim open() Aufruf an einen oder, was wohl in der Praxis häufiger vorkommt, mehrere AsynchronousSocketChannel s gebunden wird. Noch ein Hinweis zur Implementierung des AsynchronousSocketChannel : Diese muss nicht unbedingt, wie man aus dem einleitenden Motivationsteil vielleicht herauslesen mag, auf einer select() bzw. Selector Lösung basieren. Wenn das Betriebssystem von sich aus eine Callback-Schnittstelle zur Verfügung stellt, wird diese direkt von der Implementierung des AsynchronousSocketChannel genutzt. Neben der Code-Redundanz ist ein weiterer Kritikpunkt an der Java 1.4 Lösung für asynchrone I/O, dass nur Stream-orientierte Socket-Kommunikation unterstützt wird. Datagram-orientierte Socket-Kommunikation und Datei I/O konnten bisher nicht asynchron abgewickelt werden. Dieser Misstand ist mit der NIO 2 in Java 7 nun auch behoben worden:
ZusammenfassungDie beiden wichtigen Neuerungen der NIO2 in Java 7 sind ein neues File System API und die Erweiterungen für die asynchrone I/O. Aber auch die kleinen Neuerungen (zum Beispiel TLS 1.2) können unter Umständen glücklich machen, wenn man darauf gewartet hat.Literaturverweise
Die gesamte Serie über Java 7:
|
|||||||||||||||||||||||||||||||
© Copyright 1995-2013 by Angelika Langer. All Rights Reserved. URL: < http://www.AngelikaLanger.com/Articles/EffectiveJava/60.Java7.NIO2/60.Java7.NIO2.html> last update: 24 Jan 2013 |