
Das „Warum“ ist schnell geklärt: Hauptsächlich dient dies zur Beschleunigung der Interaktion von Java-Programmen mit der Datenbank durch drastische Reduzierung der Latenz-Zeiten. Je häufiger ein Java Programm Aufrufe an die Datenbank absetzt, egal ob lesend oder schreibend, desto stärker profitiert das Programm davon, daß keinerlei Netzwerk-Stack mehr im Wege steht. Man kann im absoluten Idealfall vom Faktor 10-12 sprechen, um den das Programm dann schneller läuft. Nicht-Datenbank-Operationen laufen mit etwa gewohnter Geschwindigkeit ab.
Der tatsächliche Performance-Gewinn hängt ab von der Mischung aus Datenbank-Operationen und z.B. Berechnungen und Datei-Zugriffen. Von solch kurzen Latenzen profitieren insbesondere langlaufende Operationen wie Batch- und ETL Jobs, zum Beispiel aus Host-Migrationen oder in Java selbstgeschriebene Laderoutinen.

Es gibt eine Fülle von Beispielen dafür, wie einfache Java Programme in die Datenbank geladen und beschleunigt ausgeführt werden. Diese Beispiele nutzen meist nur die JDBC API und verwenden kaum Unterklassen oder zusätzliche Frameworks. Aber lassen sich auch beliebig komplexe Programme in der Datenbank ausführen ?
Die Oracle Datenbank kommt mit einer sehr restriktiven Sandbox daher, bei der Java Programmen nahezu alles verboten wird mit Ausnahme des internen Zugriffs auf Datenbank-Objekte via JDBC. Diese Sandbox und damit verbunden das Setzen notwendiger Java Permissions bilden die hauptsächliche Hürde.
Zweitens prüft die Datenbank beim Laden von Java Code alle Abhängigkeiten der geladenen Java Klassen – sowohl Compile-Time als auch Runtime-Dependecies werden geprüft. Sind sie nicht erfüllt weil Klassen fehlen oder abhängige Klassen als „INVALID“ gelten, ist auch die frisch geladene Klasse „INVALID“ und läßt sich nicht aufrufen. Zum Glück helfen Werkzeuge wie das online Maven-Repository bei der Suche nach nötigen abhängigen Bibliotheken, und einfache SQL Skripte erleichtern das Auflösen von Abhängigkeiten in der Datenbank nach einem Ladeversuch.
Ein gutes, weil gängiges Beispiel für ein komplexeres Java Programm ist eines, das im Umgang mit einer Datenbank typischerweise die Java Persistence API, kurz JPA nutzt. Die JPA Referenz-Implementation des EclipseLink Frameworks bringt ca 10MB komprimierte Java Klassen mit sich, inklusive nahezu aller Abhängigkeiten. Das sind etwa 4500 Java Klassen, die XML Konfigurations-Dateien auslesen, dynamisch Klassen suchen und analysieren (mit Hilfe der Reflections API) und mit anderen komplexen Abhängigkeiten wie Dependecy Injection, Expression Language, Transaktionen, Annotationen und vielem mehr. Genügend komplex und sehr gängig, sollte man meinen, und daher ein idealer Kandidat. In wenigen Schritten ist ein solches Beispiel-Programm geladen, und diese Schritte sind:
1) Laden der JPA Basis Klassen und deren Abhängigkeiten
Innerhalb der Oracle Datenbank gibt es keinen CLASSPATH, mit dem nach Java Klassen gesucht wird. Die Java Klassen werden beinahe wie in einem Dateisystem in einer internen BLOB Tabelle abgelegt. Diese wird durch einen sogenannten „Resolver“ durchsucht. Ein Resolver kann so konfiguriert werden, daß er bestimmte Namespaces auch anderer Datenbank-Benutzer in seine Suche aufzunehmen vermag.
Die Java Klassen werden entweder mittels SQL Kommando geladen – vorausgesetzt, die Java Klassen liegen auf einem Dateisystem des Datenbank Servers – oder mit dem Datenbank-Utility „loadjava“, das man überall sonst betreiben kann. „loadjava“ ist nicht Teil einer Datenbank Client Installation, es gehört zu den Binärdateien einer Datenbank-Server-Installation.
Eine mögliche Syntax, um die JPA Basisklassen zu laden, .jar für .jar, lautet wie folgt:
loadjava -oci8 -resolve -user UN/PW@tnsservice datei.jar
Alle compiletime- und runtime-dependencies sollten geladen werden, die man auf jeden Fall braucht. Für die (aktuelle) EclipseLink 2.7.7 Version werden folgende .jars benötigt:
cdi-api-2.0.SP1.jar javax.el-api-3.0.0.jar javax.inject-1.jar validation-api-2.0.1.Final.jar javax.interceptor-api-1.2.2.jar javax.json-spi-1.1.4.short.jar javax.persistence-2.1.0.jar javax.resource-api-1.7.1.jar org.eclipse.persistence.jpa.jpql-2.7.7.jar org.eclipse.persistence.jpa-2.7.7.jar org.eclipse.persistence.antlr-2.7.7.jar org.eclipse.persistence.asm-2.7.7.jar org.eclipse.persistence.core-2.7.7.jar
Wie kommt man darauf, welche Abhängigkeiten existieren? Trial and error ist sicher eine Möglichkeit um herauszufinden, welche .jars man benötigt. Aber welche Version der .jars sollte man verwenden um unnötige Folgeprobleme zur Laufzeit zu vermeiden?
Das offizielle Maven Repository ist hierbei eine großartige Hilfe. Äußerst umständlich waren die Zeiten, als es so etwas noch nicht gab:
https://mvnrepository.com/artifact/org.eclipse.persistence/org.eclipse.persistence.jpa/2.7.7
Fehlermeldungen beim Laden der Java Klassen über „loadjava“ sind nicht besonders aussagekräftig. Nach einem Ladeversuch können Fehler jeglicher Art, z.B. über noch fehlende Klassen, über die Datenbak-View „USER_ERRORS“ eingesehen werden:
select * from user_errors where text like 'ORA-29521%';
2) Auflösen aller Abhängigkeiten (bzw. nur der meisten)
Nicht alle Abhängigkeiten sind notwendig um ein JPA-basiertes Programm auszuführen. Es wird beim Laden der .jar Dateien definitiv zu Fehlern kommen über nicht auflösbare Klassen.
Für ein gängiges JPA-Programm können wir z.B. auf Komponenten wie OSGI und ANT gerne verzichten und lassen somit etwa 10 Abhängigkeiten offen. Die zugehörigen Klassen werden kaum bis nie genutzt und bleiben damit gerne „INVALID“.
In meiner Umgebung sind und bleiben folgende Java Klassen INVALID: org/eclipse/persistence/javax/persistence/osgi/Activator org/eclipse/persistence/javax/persistence/osgi/OSGiProviderResolver$ForwardingProviderUtil org/eclipse/persistence/javax/persistence/osgi/OSGiProviderResolver org/eclipse/persistence/tools/weaving/jpa/StaticWeaveAntTask javax/resource/spi/ActivationSpec javax/resource/spi/ResourceAdapterAssociation javax/resource/spi/BootstrapContext javax/resource/spi/ResourceAdapter
Trotzdem sollten so viele Java Klassen wie möglich auflösbar sein und den Status „VALID“ erhalten. Es kommt durchaus vor, daß Klassen beim Laden in falscher Reihenfolge geprüft werden und daher erst einmal INVALID sind. Bitte nicht verzagen, denn die wiederholte Ausführung eines kleinen SQL’s bis die Zahl der INVALID Klassen nicht weiter schrumpft ist hier sehr hilfreich und erforderlich:
select 'alter java class "'||object_name||'" resolve;' from user_objects where object_type= 'JAVA CLASS' and status = 'INVALID';
Diese Abfrage erzeugt ein SQL Skript das namentlich alle INVALID Klassen erneut prüft. Das Ergebnis dieser Abfrage wäre zu kopieren und als Skript gegen die Datenbank laufen zu lassen. Dies erfolgt Runde um Runde, bis die Zahl der INVALID Klassen sich nicht mehr verändert, sie sollte sich in etwa auf die Zahl 15 einpendeln.
3) Laden des eigenen Codes
Nachdem die Basis Klassen geladen wurden ist es nun an der Zeit, den selbstgeschriebenen Code (*.class) und dessen Konfiguration (persistence.xml) zu laden. Dies kann wie bereits gehabt geschehen, indem alles in eine .jar Datei verpackt und dann mit loadjava geladen wird.
Alternativ, und das wird auch häufig genutzt um schnell Änderungen in der Konfiguration oder im Code zu patchen, können die Dateien auch einzeln geladen werden. Selbst der Java Quellcode könnte geladen und dabei innerhalb der Datenbank übersetzt werden, aber ein vorhergehender Maven Build oder gar eine grafische IDE ist wesentlich eleganter und aussagefähiger beim Compilieren der Klassen.
In unserem Beispiel laden wir der Einfachheit halber tatsächlich den Quellcode
und lassen ihn in der Datenbank übersetzen. Die Übersetzung erfolgt automatisch beim Laden. Zunächst folgen die Lade-Operationen, danach können Sie den Quellcode einsehen, kopieren und lokal für den Ladevorgang speichern:
loadjava -oci8 -resolve -user UN/PW@tnsservice com/ichag/simplejpa/Workers.java loadjava -oci8 -resolve -user UN/PW@tnsservice com/ichag/simplejpa/StartMe.java loadjava -oci8 -resolve -user UN/PW@tnsservice META-INF/persistence.xml
Ganz besonders wichtig ist hier, daß das loadjava-Tool den korrekten Pfad zu Konfigurations-Dateien und Java Klassen auf den Weg bekommt. Denn diese Pfadangabe werden genau so auch in die Datenbank übernommen.
Der Quellcode der Entity Klasse „Workers“ wäre abzuspeichern im Verzeichnis „com\ichag\simplejpa“ , damit loadjava den Pfad auch korrekt verwendet:
Die „Main“ Klasse, d.h. die steuernde Klasse, die Daten abfrägt und auch neue Daten erzeugen könnte, sollte ebenfalls im Verzeichnis „com\ichag\simplejpa“ abgelegt werden:
Nun folgt die Konfigurationsdatei, die festlegt welche Java Klassen welcher Datenbank-Connection zuzuordnen sind – und wie die Datenbank zu erreichen ist. Das Besondere: innerhalb der Oracle Datenbank ausgeführte Java Programme benötigen keinen herkömmlichen Connect String mit Angabe von Host, Port und Servicenamen. Die vom Treiber bzw. von der Datenbank bereitgestellte „DefaultConnection“ reicht aus. Auch die Datei „persistence.xml“ muß in einem Unterverzeichnis abgelegt sein, dieses muß auf den Namen „META-INF“ lauten. Mit Großbuchstaben.
Um eventuellen Ärger mit copy&paste zu vermeiden sind die drei Dateien auch auf Github verfügbar: https://github.com/ilfur/SimpleJPAinOJVM
Damit der Code auch richtig läuft muß eine Tabelle „Workers“ im gleichen Datenbank-Schema existieren wie der Code – so ist das Beispiel nunmal geschrieben. Das SQL Skript für die Tabelle “Workers”:
4) Erstellen einer Stored Procedure, die das Java Programm aufruft bzw. startet
Per SQL kann nun eine Stored Procedure erzeugt werden, die versucht alle Parameter mit ihren Datentypen von SQL nach Java zu übermitteln und eine sogenannte „static“ Methode im Java Code aufruft. Die standardmäßige „main” Methode ist eine solche und wird hier im Beispiel auch verwendet:
create or replace procedure GetWorkersJPA (dummy IN varchar2) as language java name 'com.ichag.simplejpa.StartMe.main(java.lang.String[])';
5) Java Permissions setzen
So ziemlich alles, was nicht direkt mit der Datenbank zu tun hat, ist erst einmal für Java innerhalb der Datenbank verboten, z.B. Dateizugriffe, Netzwerkzugriffe und vieles mehr. Stößt man bei Ausführung von Java Code auf ein Verbot, wird dies von der Datenbank über einen ORA-Fehler mitgeteilt und sogar ein SQL Statement vorgeschlagen, das das konkrete Verbot aufhebt.
Für unser JPA Programm sind nachfolgende Permissions nötig. Die Kommandos müssen von einem privilegierten Benutzer wie SYS oder SYSTEM ausgeführt werden oder von einem Benutzer mit JAVASYSPRIV Rolle. Einer der Parameter nennt offensichtlich den Benutzer-/Schemanamen, der die Permission erhalten soll – bitte tauschen Sie den „USERNAME“ aus gegen Ihren Datenbank-Benutzer.
call dbms_java.grant_permission( 'USERNAME', 'SYS:java.lang.RuntimePermission', 'getClassLoader', '' ) call dbms_java.grant_permission( 'USERNAME', 'SYS:java.util.PropertyPermission', '*', 'read,write' ); call dbms_java.grant_permission( 'USERNAME', 'SYS:java.lang.RuntimePermission', 'accessDeclaredMembers', '' ); call dbms_java.grant_permission( 'USERNAME', 'SYS:java.lang.reflect.ReflectPermission', 'suppressAccessChecks', '' );
Insbesondere notwendig ist Zugriffserlaubnis für alle benötigten Entity-Klassen,
die im Framework als Stream eingelesen werden. Obacht bitte, der zu tauschende „USERNAME“ taucht hier zweimal auf:
call dbms_java.grant_permission( 'USERNAME', 'SYS:oracle.aurora.rdbms.HandlePermission', 'HandleInputStream.USERNAME:com.ichag.simplejpa.Workers', 'read' )
Bei vielen Java Permissions sind auch Wildcards wie „*“ erlaubt. Alternativ könnte man auch schreiben:
call dbms_java.grant_permission( 'USERNAME', 'SYS:oracle.aurora.rdbms.HandlePermission', 'HandleInputStream.USERNAME:com.ichag.simplejpa.*', 'read' )
6) Aufruf des Programms und (debug)-output
Nun ist es endlich soweit, der Code ist geladen, die Tabelle angelegt, Berechtigungen vergeben, Stored Procedure erzeugt. Nun können wir einen Aufruf wagen und hoffentlich funktioniert auch alles:
set serveroutput on
call dbms_java.set_output(50000);
call getworkersjpa('dummy');
Sowohl die Ergebnis-Meldung “6” als auch alle Stack Traces und Exceptions im Fehlerfall werden direkt im SQL-Tool, z.B. SQL*Developer, ausgegeben.
Zusätzlicher Output landet im .TRC File der Datenbank zur zugehörigen Datenbank-Session. .TRC Inhalte können auch remote via SQL ausgelesen werden, das beschreibt auch schön ein Blog Beitrag meines Kollegen Marcus Schröder:
SELECT timestamp, payload, trace_filename FROM v$diag_trace_file_contents ORDER BY timestamp DESC;
Fertig !
Viel Spaß beim Testen und Ausprobieren,
gerne versuche ich mich auch an Ihrem Code bzw. Ihrer Fehlermeldung !
7) Optional – setzen von System Properties
Falls Sie in die Verlegenheit kommen, Ihren geladenen Code über sogenannte „System Properties“ zu konfigurieren gibt es auch innerhalb der Oracle Datenbank die Möglichkeit dazu.
Diese Einstellung ist (nur) auf Session-Ebene möglich bzw. gültig, also z.B. über einen Login Trigger oder im SQL Skript, das den Ladejob oder das jeweilige Java Programm startet. Ein Beispiel:
select dbms_java.set_property('eclipselink.archive.factory',
'com.oracle.jpa.custom.OJVMArchiveFactoryImpl') from dual;
select dbms_java.get_property('eclipselink.archive.factory') from dual;
Es war zu Testzwecken in meinem Beispiel nötig, das Laden der Persistenz-Archive zu verfolgen und zu sehen, welche Dateien und über welche URLs sie geladen wurden. Dazu kann man bei EclipseLink eine eigene „ArchiveFactoryImpl“ anhängen. Das Feature wird z.B. beim WildFly Applikationsserver genutzt, weil dieser neben Dateiystem- und HTTP-Zugriffen auch ein eigenes internes „vfs“ Dateisystem benutzt.
Die URLs zu den Java-Klassen und -Ressourcen innerhalb einer Oracle Datenbank folgen übrigens dem Schema „jserver: /resource/schema/<Benutzername>/<pfad-zur-ressource>“,
also z.B. „jserver: /resource/schema/JAVA_POC/META-INF/persistence.xml“
8) Optional – setzen von Memory Parametern
Die typischen Java Parameter „-Xmx“ und „-Xms“ zum Setzen des Java Speichers gibt es innerhalb einer Oracle Datenbank nicht in dieser Form. Die Oracle Datenbank übernimmt die Speicher-Zuteilung selbständig. Im Härtefall können Sie den maximalen Speicher für Java-Klassen und -Instanzen als Datenbank-Parameter eigenhändig setzen. Dieser lautet „JAVA_POOL_SIZE“ und steht normalerweise auf „0“, d.h. automatische Zuordnung.
Es gibt auf github ein schönes Beispiel-Programm als Java Stored Procedure, das die aktuelle Java-Speicherbelegung einer Datenbank-Session darstellen kann:
https://github.com/oracle/oracle-db-examples/blob/master/java/ojvm/memtest.sql
9) Optional – Debugging von Java Stored Procedures
Auch das ist natürlich möglich! Mit einem herkömmlichen Debug-Tool für Java kann eine Debug-Session zur Datenbank aufgebaut werden. Dazu ist innerhalb der Datenbank per SQL die Debug session zu starten, die aktiv auf einen wählbaren Port eines lauschenden Debuggers zugreift.
Bevor man das tun darf, sind Benutzer-Berechtigungen zu erteilen und eine Access-Control-List zu befüllen, die den Zugriff der Datenbank auf den Debug-Rechner erlaubt.
Eine schöne Beschreibung hierzu bietet unsere Dokumentation:
https://docs.oracle.com/en/database/oracle/oracle-database/18/jjdev/debugging-Java-stored-procedures.htm