<jsptutorial />

Newsletter vom 05.07.2007


Hallo,

der letzte Newsletter ist schon etwas her. Umzug, Arbeitsstress, eine neue Anwendung, Sommerurlaub - es gibt letztlich viele Gründe dafür. Außerdem standen in dieser Phase neue Kapitel im Vordergrund. Bevor wir die Neuerungen kurz vorstellen, kommt zunächst ein kurzer Überblick über herausragende News der letzten Zeit im Java-Umfeld:



Unser letzter Newsletter ist zwar schon einige Zeit her, dafür waren wir aber an anderer Stelle fleißig. Die Neuerungen im Tutorial seit dem letzten Newsletter im Überblick:


In diesem Newsletter schließen wir mit dem Beitrag zur Bibliothek Commons-Fileupload unsere Reihe über die Jakarta-Commons-Bibliotheken ab.

Wie immer wünschen wir viel Spaß beim Lesen des Newsletters und beim Entwickeln!

Zum SeitenanfangZum Seitenanfang

Commons FileUpload

Das besondere an HTTP-Uploads

Commons-FileUpload ist eine kleine Bibliothek, die sich wirklich ausschließlich um den Bereich von Datei-Uploads über das World Wide Web kümmert. Dadurch, dass die FileUpload-Bibliothek sich auf einen so eng eingegrenzten Bereich konzentriert, ist ihre Nutzung leicht verständlich. Insofern reichen an dieser Stelle auch ein paar Beispiele und Anmerkungen.
Zunächst einmal ist festzustellen, wie ein FileUpload mittels HTTP vonstatten geht - die Besonderheiten hier sind es, die letztendlich überhaupt eine eigene Bibliothek rechtfertigen. Während bei einem normalen POST-Request die Parameter direkt als Key-Value-Paare im Body des Requests zu finden sind (s. dazu die Screenshots im "Anhang I: HTTP-Grundlagen"), werden Datei-Uploads anders behandelt. Dies betrifft sowohl den HTML-Code, als auch den Transport der Datei und der anderen zusammen mit dieser Datei übermittelten Parameter mittels HTTP als auch die Auswertung in einem Servlet oder in JSPs.
Zunächst betrachten wir die Änderungen im HTML-Code. Hier muss man im <form>-Element der HTML-Seite das Encoding des Formulars wie im folgenden Code-Fragment gezeigt setzen:
<form method="POST" enctype="multipart/form-data" action="/UploadServlet">
   <label for="uploadFile">File</label>
   <input type="file" size="50" name="uploadedFile" class="fileSelector"/>
</form>

Vergisst man im HTML-Code, die Kodierungsart mittels dem Attribute "enctype" zu setzen, so werden nicht etwa die Dateiinhalte, sondern nur der Dateiname übertragen.
Außerdem ist zu beachten, dass Dateien gemäß HTTP-Spezifikation ausschließlich per POST auf den Server geladen werden können. Sofern man ein dediziertes Upload-Servlet verwendet, empfiehlt es sich, die doGet()-Methode wie folgt zu schreiben:
public void doGet(HttpServletRequest request, HttpServletResponse response) 
   throws ServletException, IOException {
   
   response.sendError(HttpServletResponse.SC_METHOD_NOT_ALLOWED, 
         "GET is not allowed for uploads");
}

Damit wird jeder Versuch, eine Datei per GET hochzuladen, mit einem angemessenen Fehler beantwortet. Unabhängig von der Antwort des Servers funktioniert GET analog zu einem inkorrekt gesetzten Attribut "enctype". D.h. anstelle der Dateien werden nur die Dateinamen übertragen.
Was aber bedeutet es, das Attribut enctype auf multipart/form-data zu setzen? Die Auswirkung davon kann am Besten anhand eines konkreten Requests betrachtet werden. Wir nutzen dazu wie im JSP-Tutorial üblich das Tool "VisualProxy", das dazu dient, die Kommunikation zwischen Browser und Server zu verdeutlichen. Schaut man sich bspw. den folgenden Screenshot an, so sieht man, dass der Request-Body in mehrere deutlich getrennte Teile aufgesplittet ist. Zu jedem Form-Element gibt es einen eigenen "Part", daher auch der Name "multipart" für das Attribut "enctype".
image_newsletter4_upload_parts.png
In den gezeigten Daten erkennt man deutlich die einzelnen Bereiche mit eigenen Headern wieder. Als erstes wurde ein Textfeld übertragen - die Hinweise auf den Content-Type und den Dateinamen fehlen entsprechend. Bei den beiden folgenden Teilen der POST-Daten handelt es sich offensichtlich um Dateien. Es gibt den Header "filename", in dem der Dateiname steht, und einen Header "Content-Type", der Informationen zum Dateitypen enthält. Deutlich erkennt man zudem den Unterschied zwischen binär übertragenen Daten und Daten aus Textdateien.
Dieser Transport-Mechanismus unterscheidet sich deutlich von dem normalen Mechanismus, wie er im JSP-Tutorial im "Anhang I: HTTP-Grundlagen" beschrieben ist. Das hat entsprechende Auswirkungen auf das Auslesen von Request-Parametern. Nutzt man normalerweise dazu die Methode getParameter(String) des Request-Objekts (s. dazu das Kapitel "Implizite Objekte"), so funktioniert das nun nicht mehr - man bekommt als Rückgabewert null zurück, unabhängig davon, ob es ein entsprechend benanntes Formularelement gibterz oder nicht. Auf die übliche Weise kommt man bei per multipart/form-data übertragenen Fomularen weder an den Inhalt der hochgeladenen Datei noch an die sonstigen Request-Parameter heran. Genau an dieser Stelle setzt nun die Upload-Bibliothek des Jakarta Commons-Projektes an.

Dependencies

Commons FileUpload ist eine kleine, sehr spezialisierte Bibliothek. Entsprechend ist Commons FileUpload nur von der Bibliothek Commons-IO abhängig.

Nutzung von Commons FileUpload

Ähnlich Commons HttpClient gibt es auch bei FileUpload zwei zentrale Klassen/Interfaces: org.apache.commons.fileupload.FileItem und org.apache.commons.fileupload.FileUpload. Wir zeigen zunächst ein kleines Beispiel für die Nutzung:

package org.jsptutorial.examples.servlets;

import java.io.IOException;
import java.util.*;
import javax.servlet.*;
import javax.servlet.http.*;
import org.apache.commons.fileupload.*;
import org.apache.commons.fileupload.disk.*;
import org.apache.commons.fileupload.servlet.*;


public class SimpleFileUploadServlet extends HttpServlet {
   
   protected void doPost(HttpServletRequest request, HttpServletResponse response) 
   throws ServletException, IOException {
   
      // step 1: create DiskFileItemFactory
      DiskFileItemFactory factory = new DiskFileItemFactory();
      
      // step2: create ServletFileUpload and pass factory to constructor
      ServletFileUpload upload = new ServletFileUpload(factory);
      try {
         // optional but highly recommendable step: 
         // verify if we really have an upload
         RequestContext ctx = new ServletRequestContext(request);
         if (ServletFileUpload.isMultipartContent(ctx)) {
            // step3: parse request
            List<DiskFileItem> list = upload.parseRequest(request);
            // step 4: do s.th. meaningful with each part 
            for (DiskFileItem fileItem : list) {
               String formField = fileItem.getFieldName();
               String fileName = fileItem.getName();
               // ...
            }
         }
      }
      catch(FileUploadException e) {
         response.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, 
                 "fileupload failed; reason: " + e.toString());
      }
      // some response for the client
      // ...
   }
   
}

Wie in der Beschreibung des Aufbaus von Upload-Requests im vorigen Abschnitt gesehen, enthalten Multipart-Anfragen ihrem Namen gemäß mehrere Teile, die jeweils einen Parameter eines Formulars behandeln. Der Aufbau entspricht dem Mail-Format für Mails mit Attachments, dem sogenannten MIME-Format. Die Bereiche sind, wie im obigen Screenshot erkennbar, durch den im Header "Content-Type" angegebenen Begrenzer ("boundary") voneinander getrennt und enthalten eigene Header-Angaben, die über die normalen Request-Header hinaus ergänzende Informationen zu den jeweiligen Teilen enthalten (bei Uploads bspw. den Namen des Formular-Elements, den Dateinamen und das Datenformat).
Die einzelnen Teile eines Multipart-Requests sind für Commons FileUpload repräsentiert durch Objekte vom Typ org.apache.commons.fileupload.FileItem. Auch normale Formular-Elemente, die mit einem Upload mitgeschickt wurden, werden als FileItems repräsentiert; der Name ist somit etwas unglücklich gewählt.
Erzeugt werden FileItems mithilfe einer geeigneten FileItemFactory. Hierbei handelt es sich ebenfalls um ein Interface aus dem Package org.apache.commons.fileupload. Diese Factory braucht das FileUpload-Objekt bevor es den Request analysiert, entweder durch Übergabe an den Konstruktor oder durch Aufruf der Methode setFileItemFactory().
Die eigentlich interessante Arbeit geschieht in den Unterklassen von org.apache.commons.fileupload.FileUploadBase: Für Servlet-basierte Uploads ist dies org.apache.commons.fileupload.servlet.ServletFileUpload, für Portlet-basierte Uploads org.apache.commons.fileupload.portlet.PortletFileUpload. Die wichtigste Methode ist schon im obigen Beispiel zu sehen. Es ist die Methode parseRequest, der je nach Umgebung ein javax.servlet.http.HttpServletRequest oder ein javax.portlet.ActionRequest mitgegeben wird. Der Rückgabewert dieser Methode ist eine Liste von FileItem-Objekten. Bei den jeweiligen FileItems kann man sich nun über die Methode get() das Ergebnis in Form eines Byte-Arrays, mit der Methode getString() als String oder mit der Methode getInputStream() als InputStream abholen.
Die Bibliothek bringt selber jeweils nur eine Implementierung der FileItem- und FileItemFactory-Interfaces mit: DiskFileItem und DiskFileItemFactory aus dem Package org.apache.commons.fileupload.disk. Auch hier entspricht der Name nicht ganz dem, was man erwarten würde. Im Gegensatz zum Namen werden nämlich nicht alle DiskFileItems direkt auf der Platte gespeichert, sondern zunächst nur solche, die eine gegebene Größe überschreiten. Die Default-Größe, ab der Elemente auf der Platte zwischengespeichert werden, beträgt 10 Kilobyte, kann allerdings durch Aufruf der Methode setSizeThreshold(int) der DiskFileItemFactory geändert werden. Ebenso kann über die DiskFileItemFactory der Ordner für die temporären Dateien vergeben werden. Wird dieser Wert nicht gesetzt, wird die System-Property für temporäre Dateien verwendet ("java.io.tmpdir").1
Die meisten Methoden in den FileUpload-Klassen sind protected, so dass neben parseRequest nur zwei weitere Methoden für die Entwicklerin von Interesse sind. Mit der Methode isMultiPartContent(RequestContext)kann und sollte geprüft werden, ob überhaupt eine Upload-Anfrage vorliegt. Prüft man dies nicht ab und das Servlet wurde mit einem normalen Formular aufgerufen, bekommt man eine InvalidContentTypeException (eine innere Klasse von FileUpload; s. dazu den Abschnitt zu Exceptions weiter unten).
Die andere öffentliche Methode aus FileUploadBase sollte man möglichst immer nutzen. Mit setSizeMax(long) gibt man die Anzahl der maximal zulässigen Bytes pro Upload an. Man kann auch den Wert -1 für unbeschränkte Uploads angeben, was auch der Standard-Wert ist. Aber davon kann ich aus Performancegründen nur abraten (s. nächsten Abschnitt).

Performance-Warnung

FileUploads blockieren Threads des ServletContainers für eine lange Zeit. Dabei ist die Anzahl der maximal zulässigen gleichzeitig bearbeitbaren Threads begrenzt. Belässt man die hochladbare Datenmenge bei dem Standardwert, d.h. man begrenzt die Datenmenge gar nicht, so können schon relativ wenige unerwartet große Upload-Streams den Server lahm legen. Einen allgemeinen Richtwert für eine zulässige Upload-Größe kann man nicht angeben. Dieser ist von den zur Verfügung stehenden Ressourcen und vor allem von der geplanten Anwendung abhängig. Fest steht aber: Man sollte eine klar definierte Grenze haben.
Die Threads des Threadpools eines Servlet-Containers stehen ohne besondere Vorkehrungen allen Requests gleichermaßen zur Verfügung, unabhängig davon, ob es sich um Uploads oder Standard-Requests handelt. Da Uploads zeitintensiv sind und immer einen der Threads für verhältnismäßig lange Zeit blockieren, stehen dem Rest der Anwendung entsprechend weniger Threads zur Verfügung. Hier müssen ggf. die Einstellungen des Threadpools (bei Tomcat ist dies im Connector-Element in der Datei "server.xml" konfiguriert; bei GlassFish im "request-processing"-Element in der Datei "domain.xml") angepasst werden. Eine automatische Überwachung des Threadpools bietet sich ebenfalls an - in der Startphase einer neuen Anwendung ist eine Überwachung des Pools zur Feinjustierung der Konfiguration ohnehin unerlässlich.
Als zusätzlichen Punkt sollte man - ähnlich dem bei Commons HttpClient Gesagten - auch bei Commons FileUpload beachten, dass man außer bei Kleinst-Uploads das Ergebnis nie mit get() als Byte-Array auslesen sollte. Stattdessen empfiehlt es sich, die Daten immer in Form eines InputStreams, den man sich mit getInputStream() holen kann, weiter zu verarbeiten. Auf diese Weise wird ein mehrfaches, unnötiges Umkopieren der Daten vermieden.

Eigene FileItem- und FileItemFactory- Implementierungen

Im Abschnitt "Nutzung von Commons FileUpload" weiter oben wurde erwähnt, dass es von den Interfaces FileItemFactory und FileItem jeweils nur eine Implementierung gibt, die zusammen das Speichern der FileItems auf die Festplatte steuern, sofern die einzelnen Multipart-Teile eine gewisse Größe überschreiten.
Das mag nicht immer wünschenswert sein. Denkbar wäre, dass man die eintreffenden Uploads gleich in eine Datenbank speichern oder über das Netz weiterreichen will. In solchen Fällen macht das temporäre Speichern keinen Sinn. Als eine Lösung könnte man sich Implementierungen schreiben, die die Daten nur im Speicher halten. Serhat Cinar hat in seinem Blog eine entsprechende Implementierung von InMemoryFileItem und InMemoryFileItemFactory mit vollständigem Quellcode angegeben. Hierbei ist natürlich der Speicherverbrauch deutlich größer als bei der DiskFileItem-Implementierung und eine umsichtige Handhabung dringend angeraten.
Ein anderes Beispiel ist eine Umsetzung, die ich vor kurzem vorgenommen habe, bei der eine FileItem-Implementierung mit Listener-Unterstützung zum Tragen kam, um Ajax-basierte Fortschrittsbalken beim Fileupload anzeigen zu können.

Exceptions

Bei einem FileUpload können etliche Fehlerfälle auftreten, für die die Bibliothek Commons FileUpload geeignete Exceptions mitbringt. Die im Folgenden aufgelisteten Exception-Klassen liegen alle im Package org.apache.commons.fileupload.

ExceptionUrsache
FileUploadExceptionBasisklasse für die folgenden drei Exceptions, die aber in speziellen Fällen auch direkt geworfen wird. Kündigt allgemein Fehler bei der Requestbearbeitung an.
FileUploadBase.InvalidContentTypeExceptionWird beim Parsen geworfen, wenn kein Multipart-Request vorliegt.
FileUploadBase.SizeLimitExceededExceptionDiese Exception teilt mit, dass die Größe des Contents das vorgegebene Maximum übersteigt.
FileUploadBase.UnknownSizeExceptionIst bei einem Upload der Header "Content-Length" nicht gesetzt, wird diese Exception geworfen.
MultipartStream.IllegalBoundaryExceptionWeicht die Länge eines Boundary-Strings von dem definierten Boundary-String ab, ist dieser falsch geformt und es wird diese Exception geworfen. Ist abgeleitet von java.io.IOException.
MultipartStream.MalformedStreamExceptionException, die anzeigt, dass der Stream fehlerhaft geformt ist oder dass beim Lesen des Streams Fehler aufgetreten sind. Ist ebenfalls von java.io.IOException abgeleitet.


Zum SeitenanfangZum Seitenanfang

Anmerkungen:

1) Beim Tomcat ist das temporäre Verzeichnis das temp-Verzeichnis unterhalb des Tomcat-Basis-Verzeichnisses. Beim Start des Tomcats werden die von Tomcat genutzten Verzeichnisvariablen ausgegeben: Das Basis-Verzeichnis, das temporäre Verzeichnis und das Tomcat-Home-Verzeichnis. Letzteres entspricht dem Installationsverzeichnis. Die von Tomcat erwarteten Umgebungsvariablen sind in der Start-Datei catalina.sh bzw. catalina.bat erklärt. (zurück)


www.jsptutorial.org
© 2005, 2006, 2007