Datenströme und Verwaltung von Ressourcen

Wie es unter Java üblich ist werden die einzulesenden Daten der jadice document platform in Form von InputStreams übergeben. Im Lesevorgang entstehen daraus Seitensegmente. Deren zugeordnete PageSegmentSource referenziert den Datenstrom, aus dem das PageSegment entstand. Darüber hinaus können weitere Objekte entstehen, die ebenfalls den originären Datenstrom (oder einen daraus entstandenen Sub-Datenstrom) referenzieren. Attachments sind hierfür ein typisches Beispiel.

Um ein effizientes Speichermanagement und günstiges Laufzeitverhalten zu erreichen, werden die Inhalte von Seitensegmenten typischerweise spät initialisiert. Seiten, die vom Benutzer nie angezeigt werden, müssen unter Umständen nie vollständig geladen werden. Wo dies angebracht ist, werden aufbereitete Informationen im Cache abgelegt. Können sie dort nicht gefunden werden – zum Beispiel weil sie aufgrund von Speicherknappheit automatisch entfernt wurden – müssen sie ebenfalls neu aus dem Dokumentdatenstrom aufbereitet werden. Diese Zusammenhänge führen dazu, dass Datenströme auch nach Abschluss des initialen Lesevorgangs geöffnet bleiben müssen, sodass jederzeit Zugriff darauf genommen werden kann.

Der korrekte Zeitpunkt für das Schließen eines Datenstroms kann durch die jadice document platform nicht automatisch erkannt werden. Es ist Aufgabe der Integration sicherzustellen, dass ein Dokument-Datenstrom erst dann geschlossen wird, wenn keines der aus ihm entstandenen Seitensegmente mehr verwendet wird. Falls weitere Objekte aus dem Lesevorgang entstanden – beispielsweise Attachments – müssen diese ebenfalls in der Entscheidung bedacht werden. Solange sie genutzt werden, muss der Datenstrom offen bleiben.

Die Umsetzung dieser Anforderung kann nur integrationsspezifisch erfolgen und wird je nach Umgebung unterschiedlich ausfallen. Um eine geeignete Lösung zu erarbeiten bieten die folgenden Fragestellungen einen hilfreichen Ausgangspunkt:

  • Welche Daten werden aus dem Dokumentdatenstrom gelesen? In der Hauptsache sind dies Seitensegmente. Es entstehen aber auch Metadaten, in manchen Fällen Attachments oder Outlines, sowie unter Umständen integrationsspezifische Zusatzdaten.

  • Wo werden die Daten verwendet? Seitensegmente können in mehreren Seiten vorkommen. Diese wiederum können Teil mehrerer Dokumente sein. Auch Hintergrundprozesse, wie beispielsweise Druckvorgänge, können die Dokumentdaten verwenden und sind daher relevant für diese Betrachtung.

  • Wie kann erkannt werden, dass die Verwendung abgeschlossen ist? In manchen Fällen – beispielsweise in einer Stapelverarbeitung – gibt es aus dem Programmablauf heraus einen klaren Zeitpunkt dafür. In anderen Fällen müssen Informationen über die Verwendung gesammelt und verwaltet werden. Diese können beispielsweise zur Laufzeit in einer zentralen Datenstruktur gesammelt werden. Alternativ besteht die Möglichkeit, Statusinformationen direkt mit dem Dokument zu transportieren über den Dokumenstatus oder die User Properties.

  • Wer hat die Verantwortung dafür, dass Datenströme geschlossen werden? In vielen Fällen kann die Freigabe von Datenströmen ausgelöst werden, wenn ein Dokument in den Zustand Document.BasicState.CLOSED wechselt. Die Freigabe kann entweder dezentral für jedes Dokument erfolgen, oder von einer zentralen Stelle aus.

Zur effizienten und speicherschonenden Verarbeitung großer Datenmengen versucht die jadice document platform, sofern es das Dokumentformat erlaubt, nur die für die aktuelle Seitendarstellung benötigten Daten zu lesen, zu verarbeiten und dynamisch zu puffern, statt alle Dokumentdaten komplett im Speicher zu halten. Für dieses Vorgehen werden Datenströme benötigt, deren Dateizeiger beliebig positioniert werden kann. In jadice stehen zu diesem Zweck die SeekableInputStreams zur Verfügung. Die Klasse SeekableInputStream ist eine abstrakte Basisklasse, welche die Klasse InputStream um folgende Methoden erweitert:

seek(long)

Setzen der Leseposition im Datenstrom

length()

Länge der Daten in Bytes. Falls die Länge nicht bekannt ist, wird der Wert -1 zurückgegeben.

getStreamPosition()

Aktuelle Leseposition innerhalb des Datenstroms

Die Verwendung von SeekableInputStreams stellt sich für Integratoren einfach dar. Für bestimmte Typen von Datenströmen stellt jadice verschiedene Arten von SeekableInputStreams zur Verfügung, die den eigentlichen Datenstrom umhüllen. Abhängig vom Anwendungsfall und dem Typ des Eingangsdatenstroms sollte die passende Art von SeekableInputStream gewählt werden.

Die gebräuchlichsten SeekableInputStreams werden in den folgenden Abschnitten kurz erläutert.

RandomAccessFileInputStream stellt einen SeekableInputStream für lokal (als Datei) verfügbare Bilddaten dar.

Die Klasse bietet einen Konstruktor mit einem File-Objekt, das auf die Bilddatei zeigt, zur Instantiierung an. Diese Datei wird mit einem bestimmten FileInputStream geöffnet, der für die folgenden Lesevorgänge gehalten wird. Während der Verarbeitung in jadice bleibt der FileInputStream zu der Bilddatei offen und ein Lesezeiger wird innerhalb des Streams positioniert. Zu beachten ist hierbei, dass die Quelldatei, während das Dokument im Viewer geöffnet ist, nicht verändert werden darf. Dies führt, zum Beispiel unter Windows, gegebenenfalls auch zur Sperrung des Zugriffes auf die betreffende Datei.

Zur Verarbeitung der Bilddaten in jadice wird eine temporäre Datei erzeugt, in der bereits gelesene Daten abgelegt werden. Nach Gebrauch, spätestens jedoch beim nächsten Start der jadice document platform, werden die nicht mehr benötigten temporären Dateien gelöscht. Falls durch den Integrator keine abweichende Konfiguration vorgenommen wird, werden die temporären Dateien im durch das System vorgegebenen Temporär-Verzeichnis abgelegt.

Zur Instantiierung bietet der FileCacheInputStream einen Konstruktor, der einen gegebenen InputStream aufnimmt und eine Bearbeitungsdatei im gesetzten Temporär-Verzeichnis anlegt. Ein weiterer Konstruktor bietet die Möglichkeit neben dem InputStream ein Temporär-Verzeichnis und ein Flag, ob die erzeugte Datei nach Gebrauch zu löschen ist, anzugeben.

Bilddaten werden bei diesem Stream-Typ im Hauptspeicher gehalten. Ähnlich dem FileCacheInputStream bietet diese Klasse einen Konstruktor, der einen gegebenen InputStream aufnimmt und für Lesevorgänge hält. Ein zweiter Konstruktor erlaubt zusätzlich eine Angabe der Blockgröße, in der Daten im Hauptspeicher gepuffert werden sollen. Diese Art der internen Datenverwaltung wird in Security-Relevanten Kontexten bevorzugt.

Ein Vorteil dieser Datenhaltung ist ein extrem schneller Zugriff auf die Dokumentrohdaten. Nachteilig ist ein erhöhter Speicherbedarf durch die Datenhaltung im Hauptspeicher.

BufferManager und Datenzwischenspeicherung

Ein Dokument, das der Viewer anzeigen soll, erhält er zum Beispiel als PDF- oder TIFF-Strom. Der Viewer muss an verschiedene Stellen im Datenstrom springen können, um die Daten einer bestimmten Seite des Dokuments anzeigen zu können, andernfalls müsste man den gesamten Datenstrom im Hauptspeicher halten.

Die Datenströme werden typischerweise von einem Server – beispielsweise aus einem Langzeitarchiv – geladen. Da der Server viele Clients versorgt, ist es wünschenswert die Daten möglichst schnell vollständig abzuholen um dann den InputStream zu schließen.

Beide Gründe sprechen dafür, einen Zwischenspeicher für Datenströme zu schaffen, die aktuell angezeigt werden. Im vorhergehenden Abschnitt wurden verschiedene Klassen genannt, die diesem Zweck dienen. Darüberhinaus bietet jadice Komponenten, die eine einefache Umsetzung der Datenzwischenspeicherung ermöglichen. Sie erfüllen die beiden genannten Anforderungen: Es entstehen Datenströme in denen gesprungen werden kann und der Original-Datenstrom kann geschlossen werden, sobald er vollständig im Puffer liegt.

Die Klasse IOUtils bietet den Einsprungspunkt für die Arbeit mit gepufferten Daten.

Buffering in temporäre Dateien

Wenn kein BufferManager verwendet wird, wird die Klasse RandomAccessFileInputStream eingesetzt. Dann wird im Verzeichnis für Temporäre Dateien pro Stream eine Datei angelegt. Der Nachteil dieser Vorgehensweise ist, dass viele Dateien erzeugt werden und im Ordner liegen. Diese müssen danach aufgeräumt werden und vertrauliche Daten, die eigentlich nur angezeigt werden sollten, liegen unverschlüsselt im Dateisystem. Für einfachere Anwendungsfälle handelt es sich aber um eine funktionale und leicht umsetzbare Lösung.

Buffering mittels BufferManager:

Mit dem BufferManager werden gelesene Daten im Hauptspeicher und einer temporären Datei gepuffert. Wenn der gepufferte Stream geschlossen wird, werden die enstprechenden Bereiche zum Überschreiben freigegeben. Dies geschieht, wenn es keine Referenzen mehr auf den gepufferten Datenstrom gibt beziehungszweise zum Zeitpunkt der Garbage Collection.

Das Einlesen der Dokumente erfolgt mithilfe der Methode IOUtils.wrap(). Voraussetzung ist, dass der BufferManager mithilfe der Klasse BufferManagerConfigurer korrekt konfiguriert wurde. Ein Beispiel befindet sich unter Beispiel 7.9, „Puffern von Stream-Inhalten mittels BufferManager“

Das Verhalten bei der Pufferung sieht folgendermaßen aus:

  • Falls es einen freien Bereich im Hauptspeicherpuffer gibt, wird dieser verwendet.

  • Falls der Hauptspeicherbereich keinen freien Platz besitzt, wird der am wenigsten kürzlich verwendete Puffer in die Pufferdatei geschoben.

  • Falls weder in Hauptspeicher noch in der Pufferdatei ein freier Bereich existiert, wird die Pufferdatei vergrößert.

  • Die Pufferdatei wird nicht verkleinert.

  • Freigegebene Bereiche in der Datei werden wieder beschrieben.

  • Beim Neustart der Anwendung wird eine bestehende Pufferdatei gelöscht und eine neue Pufferdatei mit der Größe 0 angelegt.

Es erfolgt ein hierarchisches Speichermanagement. Was kürzlich gebraucht wurde, liegt im Hauptspeicher, wenn der Hauptspeicher voll ist, werden die Daten in eine einzige Datei (für alle gepufferten Inhalte) ausgelagert. Es kann mehrere Buffer geben. Bei einem Buffer handelt es sich um einen physikalischen Datenzwischenspeicher, der genutzt wird, während Daten von einem Ort an einen anderen übertragen werden. Die Größe der Buffer ist einstellbar, 64kB ist der Standard. Die Auslagerung der Daten erfolgt entweder über normales FileIO, oder über MemoryMapping. Beim MemoryMapping wird jedem Abschnitt in der Datei ein bestimmter Buffer zugeordnet. Dadurch wird, wenn in einen bestimmten Buffer geschrieben wird, die Datei an der jeweiligen Stelle aktualisiert. Das Lesen des Buffers erfolgt direkt aus der Datei. Vorteil des MemoryMapping ist, dass es eine Kopier-Operation weniger erfordert.

Beim MemoryMapping wird nur eine einzige Datei pro jadice-Prozess beziehungsweise JVM angelegt. Die Datei wird bei einem ordentlichen Herunterfahren gelöscht. Falls beim Shutdown die Datei noch von Prozessen genutzt wird, wird sie (abhängig vom Betriebssystem) gegebenenfalls zu diesem Zeitpunkt nicht gelöscht. Dann wird die Datei beim nächsten Start gelöscht. Es gibt ein Implementierungslimit unter Windows.[28]

Der BufferManager stellt die folgenden Konfigurationsmöglichkeiten zur Verfügung:

  • Die Größe des Puffers im Hauptspeicher

  • Anzahl der Puffer

  • Wahl der Pufferdatei

  • Speicherstrategie in der Pufferdatei

  • Lesbarkeit beziehungsweise Verschlüsselung der Pufferdatei

Man kann den BufferManager programmatisch konfigurieren: „Datenströme und Verwaltung von Ressourcen“



[28] Durch die Beschränkungen der J2SE Plattform ist es auf Windowssystemen nicht möglich, die Pufferdatei während des Herunterfahrens zu löschen. Siehe

Datei nicht gelöscht

beziehungsweise

Fehlendes Unmapping verhindert Dateibearbeitung

[jadice document platform Version 5.5.12.1: Dokumentation für Entwickler. Veröffentlicht: 2021-08-17]