25. November 2013

Remote Debugging für Java Stored Procedures

Remote Debugging Java Stored Procedures
Auf diesem Blog habe ich ja schon etwas öfter mit Java in der Datenbank gearbeitet. Auf der DOAG2013 erreichte mich nun nochmal die Frage, ob (und wenn ja, wie) man Java Stored Procedures (die in der Datenbank laufen) mit einem Java-Werkzeug remote debuggen kann. In der Oracle-Dokumentation (Java Developers' Guide) steht drin, dass es geht, aber nicht, wie. Im Internet gibt es das eine oder andere Blog-Posting zum Thema, meist sind diese aber schon etwas älter und passen nicht mehr zu den heute verfügbaren Werkzeugen. Daher gibt es heute eine Schritt-für-Schritt-Anleitung zum Remote-Debugging einer Java Stored Procedure.

Voraussetzungen für das Remote-Debugging

Zunächst braucht es den Java Code, der in der Datenbank als Java Stored Procedure laufen soll und in dem wir nun Breakpoints setzen und ein wenig debuggen wollen.
public class DebugTest {
  public static String cnt(int iTimes) {
    String s = "";
    for (int i=0;i<iTimes;i++) {
      s = s + ".";
    }
    return s;
  }
}
Außerdem braucht es ein Werkzeug, mit dem man überhaupt debuggen kann. Mit dem SQL Developer geht es leider nicht - denn der kann zwar PL/SQL, aber kein Java debuggen. Wir müssen also den JDeveloper hernehmen - der lässt sich, wie immer, aus dem OTN herunterladen. Für dieses Beispiel könnt Ihr die kleinere Java Edition nehmen; mit 146MB ist der Download wesentlich schlanker als die 1,8G der vollständigen Edition. Und zum Debuggen einer Java Stored Procedure reicht das aus.

Bevor es weitergeht, eine wichtige Voraussetzung: Es muss eine Netzwerkverbindung zwischen der Datenbank und dem installierten JDeveloper möglich sein. Mehr noch: Für das Remote Debugging wird die Datenbank eine Netzwerkverbindung zum JDeveloper hin öffnen - der JDeveloper (auf dem PC des Entwicklers) ist also dann der "Server", der von der Datenbank kontaktiert wird. Das muss vom Netzwerk her möglich sein und darf nicht durch Firewalls blockiert werden - am besten überprüft Ihr das vorher (ping, putty, etc.).

JDeveloper Projekt einrichten

Nach dem Herunterladen, Auspacken und Starten des JDeveloper erzeugt Ihr eine Application, darin ein Projekt und darin eine neue Java-Klasse mit obigem Code.
Nun müsst Ihr das Debugging für euer Projekt einschalten. Dazu öffnet Ihr im Baum links oben das Kontextmenü und navigiert zu den Project Properties.
Dort navigiert Ihr zuerst zum Abschnitt Compiler. Setzt dort das Häkchen bei Full Debug Info und setzt die JDK Version Compatibility auf die Version, die in der Datenbank aktiv ist. Für eine 10.2-Datenbank muss das auf 1.4 gesetzt werden, für eine 11g-Datenbank auf 1.5 und für 12c muss es 1.6 sein.
Danach geht es zum Bereich Run/Debug. Ihr seht dort eine Konfiguration namens Default - die wollen wir ändern, klickt dazu den Button Edit auf der rechten Seite.
Es öffnet sich ein weiteres Fenster. Im Bereich der Launch Settings aktiviert Ihr mit einem Häkchen das Remote Debugging ...
... und dann navigiert Ihr zum Abschnitt Debugger -> Remote. Dort wird bei Protokoll Listen for JPDA ausgewählt. Wenn Ihr möchtet, könnt Ihr noch einen TCP/IP Port vergeben, es kann aber auch bei der voreingestellten 4000 bleiben. Danach speichert Ihr mit Klick auf OK eure Änderungen ab - solange, bis alle Property-Fenster weg sind.

Java Stored Procedure kompilieren und in die Datenbank laden

Als nächstes kompiliert Ihr den Quellcode eures Java Programms. Ihr könnt das entweder manuell (auf der Kommandozeile) oder mit dem JDeveloper machen. Wenn Ihr auch mit dem JDeveloper kompiliert, wird eine Java-Klassendatei für die eingestellte Java-Version (hier: 1.5 für Oracle11g) mitsamt Debug-Einstellungen generiert und im JDeveloper-Projektverzeichnis abgelegt.
Alternativ könnt Ihr manuell (auf der Kommandozeile) kompilieren.
$ javac -target 1.5 -g DebugTest.java
$ ls
DebugTest.java DebugTest.class
Die entstandene .class-Datei muss nun mit Hilfe des Oracle-Werkzeugs loadjava in die Datenbank geladen werden. loadjava ist (ebenso wie SQL*Plus) Teil einer Oracle-Datenbankinstallation.
Es gibt drei Wege, Java-Code in die Datenbank zu laden: Wenn Ihr Java in der Datenbank remote debuggen möchtet, müsst Ihr die Kompilate, also die .class-Dateien, mit loadjava in die Datenbank laden. Wenn Ihr dagegen Java-Quellcode ladet oder CREATE JAVA SOURCE verwendet, kompiliert Ihr in der Datenbank selbst. Dort wird aber keine Debug-Information dazukompiliert, was dazu führt, dass das Remote-Debugging nicht geht. Also: loadjava verwenden und den Bytecode laden.
$ loadjava -u scott/tiger -o -r -v DebugTest.class
arguments: '-u' 'scott/***' '-o' '-r' '-v' 'DebugTest.class'
identical: DebugTest
skipping : class DebugTest
Classes Loaded: 0
Resources Loaded: 0
Sources Loaded: 0
Published Interfaces: 0
Classes generated: 0
Classes skipped: 1
Synonyms Created: 0
Errors: 0
$
Nun ist der Java-Code in die Datenbank geladen. Damit er aufgerufen werden kann, braucht es noch etwas PL/SQL-Code:
create or replace function debugtest (p_cnt in number) return varchar2
is language java name 'DebugTest.cnt(int) return java.lang.String';
/

alter function debugtest compile debug
/
Die Prozedur generiert einen aus Punkten zusammengesetzten String - der Parameter legt die Anzahl der Punkte und damit die Länge fest. Ein kurzer Test der Prozedur empfiehlt sich ...
SQL> select debugtest(50) from dual;

DEBUGTEST(50)
--------------------------------------------------------------------------------
..................................................

1 Zeile wurde ausgewählt.
Außerdem braucht das Datenbankschema, in dem sich die Prozeduren befinden, noch das zum Remote-Debugging nötige Systemprivileg DEBUG CONNECT SESSION.
SQL> grant debug connect session to scott;

Benutzerzugriff (Grant) wurde erteilt.

Debugging-Sitzung im JDeveloper starten

Im JDeveloper setzt Ihr nun einen Breakpoint (sinnvollerweise innerhalb der Schleife); dann öffnet Ihr, im Baum auf der linken Seite, das Kontextmenü zu eurer Java-Klasse. Wählt Debug aus.
Dann öffnet sich ein kleines Fenster, in dem Ihr den TCP/IP-Port des Remote-Debuggers nochmals ändern könnt. Klickt auf OK. Es passiert dann nichts weiter, als dass links oben ein Fenster Processes geöffnet wird, in dem die Meldung erscheint, dass der Remote-Debugger des JDeveloper nun auf Verbindungen wartet. Lasst den JDeveloper nun offen ...
Der JDeveloper agiert nun also tatsächlich als "Server". Der Debugging-Vorgang muss nun aus der Datenbank heraus gestartet werden - denn dort soll das Debugging ja stattfinden. Daher ist es so wichtig, dass die Datenbank vom Netzwerk her in der Lage ist, eine Verbindung zum JDeveloper hin zu öffnen. Und um das möglich zu machen, brauchen wir als nächstes die IP-Adresse des Rechners, auf dem der JDeveloper läuft; typischerweise ist das ein Arbeitsplatz-PC.
D:\>ipconfig

Windows IP Configuration

Ethernet adapter Local Area Connection:

   Connection-specific DNS Suffix  . : mycompany.com
   IPv4 Address. . . . . . . . . . . : 10.1.1.100
   Subnet Mask . . . . . . . . . . . : 255.255.255.0
   Default Gateway . . . . . . . . . : 10.1.1.1

Debugging aus der Datenbank heraus durchführen

Nun geht es wieder zur Datenbank. Öffnet eine SQL Session als Eigentümer der Java Stored Procedure. Als erstes muss sich die Datenbank mit dem JDeveloper verbinden. Die IP-Adresse habt Ihr mit ipconfig herausgefunden - den Port im JDeveloper eingestellt (oder bei 4000 belassen).
SQL> execute dbms_debug_jdwp.connect_tcp('10.1.1.100', 4000); 

PL/SQL-Prozedur erfolgreich abgeschlossen.
Nun sollte sich im JDeveloper etwas getan haben; im Fenster Processes solltet Ihr nun sehen, dass sich eine Datenbank mit dem Remote-Debugger verbunden hat.
Und jetzt geht's los: Startet eure Java Stored Procedure.
SQL> select debugtest(50) from dual;
Im Gegensatz zum Normalfall bleibt der Prozeduraufruf "hängen"; im JDeveloper könnt Ihr nun aber sehen, dass der Remote Debugger aktiv ist und die Ausführung beim Breakpoint angehalten hat. Und die immer beim Debuggen könnt Ihr nun im Einzelschrittmodus durch das Programm gehen, die Inhalte der Variablen verfolgen und sogar ändern.
Wenn das Debugging abgeschlossen ist, endet der Prozeduraufruf; und das Ergebnis wird an die SQL bzw. PL/SQL Engine zurückgegeben. Man sieht deutlich, dass die im Debugger veränderte Variable sich tatsächlich auf das Prozedurergebnis ausgewirkt hat - so wie es sein soll.
SQL> select debugtest(50) from dual;

DEBUGTEST(50)
--------------------------------------------------------------------------------
>>>>..................................................

1 Zeile wurde ausgewählt.
Fehlt zum Abschluss noch die Abmeldung der Datenbank vom Debugger im JDeveloper ...
SQL> execute dbms_debug_jdwp.disconnect;
Obwohl der Java an einem etwas exotischen Ort abläuft (in der Oracle-Datenbank), und aus einer SQL-Abfrage heraus gestartet wird, kann man ihn dennoch mit einem Standardwerkzeug remote debuggen - und man kann während des Debuggings vom JDeveloper aus sogar Java-Variablen in der Oracle-Datenbank ändern. Das finde ich schon recht interessant. Das Protokoll JPDA für das Remote-Debugging ist übrigens nicht auf den JDeveloper beschränkt - es ist ein Standardprotokoll, welches auch von anderen Werkzeugen beherrscht wird. Mit Eclipse sollte die Vorgehensweise also analog möglich sein.
I mentioned Java Stored Procedures in several blog postings in the past. During the DOAG2013 conference I got (again) the question, whether it is possible to use a Java IDE like JDeveloper to remote debug Java Stored Procedures in the database. And if that is possible: How to do this? The Oracle documentation (the Java Developers Guide) tells us, that remote debugging does work, but it does not describe what to do. There are also some blog postings showing the process with old JDeveloper versions - I got no one to work. So I decided to create a current step-by-step guide for remote debugging a Java Stored Procedure.

Prerequisites

First, we need some Java code to debug. It's a rather simple example, but it can be loaded into the database, we can set breakpoints and therefore we can perform some debugging.
public class DebugTest {
  public static String cnt(int iTimes) {
    String s = "";
    for (int i=0;i<iTimes;i++) {
      s = s + ".";
    }
    return s;
  }
}
Furthermore, we need a tool, which allows us to remote debug Java Code in the database. SQL Developer will not help, since it can only remote debug PL/SQL, but not Java in database. So we go for Oracle's JDeveloper, which can be downloaded from OTN. You only need the small Java Edition - it's download footprint is 146M, which is much more convenient than the "full" edition with 1.8G. And for debugging the Java Stored Procedure, this Java Edition is sufficient.

The next prerequisite is very important: The database must be able to open a network connection to JDeveloper (which is typically installed on the developer's PC). The network connection will be initiated by the database - JDeveloper will be the "listener" - this setup is rather uncommon, so you might check whether there are really no network firewalls making this impossible. It's a good idea to test this in advance (ping, ssh, putty, etc.)

Create and configure a JDeveloper Project

After downloading, unpacking and starting JDeveloper, create a new Application, within this a new Project and within that a new java class containing the above code.
Now it's about to enable debugging: Open the context menu in the navigation tree on the left and navigate to Project Properties.
First, go to the Compiler Section, check the Full Debug Info and set the JDK Version Compatibility to the Java Version in the database you are using. For a 10.2 database, this must be 1.4, for 11.1 and 11.2 set to 1.5 and for 12.1, use 1.6.
Afterwards, proceed to Run/Debug. There is one Run Configuration named Default. We'll change this now, so click the Edit button.
Another Window will open. Within the Launch Settings, activate Remote Debugging ...
... then go to the Debugger -> Remote section. Choose Listen for JPDA within the Protocol select list. If you like, change the default TCP/IP port. After doing this, save your changes by clicking OK, until all windows have been closed.

Compile and load the Java Stored Procedure into the database

Now compile the java source code. You can do this using JDeveloper or manually using the command line. After compiling with JDeveloper, the Java Binary Code is being placed into the project directory; you'll need it for the next step.
As said: Compiling on the command line is the alternative. Note to add the -target and -g switches.
$ javac -target 1.5 -g DebugTest.java
$ ls
DebugTest.java DebugTest.class
The resulting .class file must now be loaded into the database. We use the loadjava utility for this. As e.g. SQL*Plus, loadjava is part of an Oracle database installation.
There are three ways to load java into the database: For remote debugging, you must load the .class file into the database with loadjava. Loading Java sources (either with loadjava oder with SQL "CREATE JAVA SOURCE") will not work, since the Compiler inside the Oracle Database will not add debugger-specific information.
$ loadjava -u scott/tiger -o -r -v DebugTest.class
arguments: '-u' 'scott/***' '-o' '-r' '-v' 'DebugTest.class'
identical: DebugTest
skipping : class DebugTest
Classes Loaded: 0
Resources Loaded: 0
Sources Loaded: 0
Published Interfaces: 0
Classes generated: 0
Classes skipped: 1
Synonyms Created: 0
Errors: 0
$
Now, as we have loaded the Java code into the database, we need some PL/SQL in order to actually execute the Java Stored Procedure.
create or replace function debugtest (p_cnt in number) return varchar2
is language java name 'DebugTest.cnt(int) return java.lang.String';
/

alter function debugtest compile debug
/
This procedure will generate a string by concatenating point characters. The parameter determines the amount of points. For the records, a short test ...
SQL> select debugtest(50) from dual;

DEBUGTEST(50)
--------------------------------------------------------------------------------
..................................................

1 row selected.
And finally, the database user needs the system privilege DEBUG CONNECT SESSION. With this privilege, the session is able to perform the communication with the remote debugger inside JDeveloper.
SQL> grant debug connect session to scott;

Grant succeeded.

Open the debugging session in JDeveloper

Back to JDeveloper: Set a breakpoint in your code. After that, go to your java code within the navigation tree on the left and open the context menu with a right click. Choose Debug.
Now, another window will open in which you can change the TCP/IP port for the remote debugger (if you like). Click on OK. Now JDeveloper starts a listener process for the debugger - which you can see in the Processes window. Leave the JDeveloper window open ...
As you can see, JDeveloper is now the "server". It's now on the database to start the debugging process. And that is the reason, why it is so important that there is a clear network between the database and the PC JDeveloper is running on. Before going back to the database, we need the IP address of the JDeveloper PC - on windows, ipconfig will help (use ifconfig on linux).
D:\>ipconfig

Windows IP Configuration

Ethernet adapter Local Area Connection:

   Connection-specific DNS Suffix  . : mycompany.com
   IPv4 Address. . . . . . . . . . . : 10.1.1.100
   Subnet Mask . . . . . . . . . . . : 255.255.255.0
   Default Gateway . . . . . . . . . : 10.1.1.1

Perform Remote Debugging

Now we come back to the database. Open a session as the owner of the Java Stored Procedure (say: with SQL*Plus). First we need the database to connect to JDeveloper - we have determined the IP address and we have set the TCP/IP port within JDeveloper.
SQL> execute dbms_debug_jdwp.connect_tcp('10.1.1.100', 4000); 

PL/SQL procedure completed successfully.
You should see something within JDeveloper - the Processes window should indicate, that the database actually has connected to the debugger.
And then: Let's get started: Run your Java Stored Procedure.
SQL> select debugtest(50) from dual;
You should notice that your call seems to "hang". But in JDeveloper the debugger is active and has stopped that your breakpoint. And as always, you can step through the java program, monitor and even change the variable contents.
After completing the walk through the java code, the Java Stored Procedure finishes and returns a result to the SQL or PL/SQL engine. And as we'd expect it: the variable content we have changed within the debugger, really influenced the functions' result.
SQL> select debugtest(50) from dual;

DEBUGTEST(50)
--------------------------------------------------------------------------------
>>>>..................................................

1 row selected.
Finally, disconnect the database from JDeveloper.
SQL> execute dbms_debug_jdwp.disconnect;
Although this Java Stored Procedure runs in an exotic environment (inside the Oracle Database) and is being invoked with a SQL Query, we can use a standard java IDE in order to perform remote debugging. As with every other Java debugger, we can step through the progran, monitor and even change the variables. And since the JPDA protocol is not restricted to JDeveloper, a similar setup should be possible with other Java Development tools.

Kommentare:

tkleiber hat gesagt…

Hallo!

Ich habe das mit dem JDeveloper 12c getestet, aber es funktioniert nicht!

Die Debugsession wird initialisiert, aber der Code hält nicht in meinem Java-Breakpoint.

Viele Grüße
Torsten

Carsten Czarski hat gesagt…

Hallo Torsten,

das sollte schon funktionieren. Wichtig ist, dass die Java-Klassen außerhalb der Datenbank "mit debug" kompiliert und die entstehenden CLASS bzw. JAR Dateien dann in die Datenbank geladen werden. Das Laden von Java-Quellen in die Datenbank funktioniert in der Tat nicht ...

Hilft dies weiter?

Beste Grüße
Carsten

tkleiber hat gesagt…

Hallo!

Also ich habs pro Version 12.1.2 und 11.1.1.5 genau einmal ans laufen bekommen. 12.1.2 steigt dann mit unexpected errors aus. 11.1.1.5 läuft bei keinem weiteren Aufruf mehr in den Breakpoint. Datenbank ist 11.2.0.3.

Viele Grüße

Carsten Czarski hat gesagt…

Hallo Torsten,

hmmm ... meine Version war auch die 12.1.2 - damit lief es ganz hervorragend. Müsste man nun mal im Detail nachsehen, woran es liegt ...

Beste Grüße

-Carsten

Carsten Czarski hat gesagt…

Hallo Torsten,

Gibt es Error Stacks, die man sich mal ansehen könnte ...?

Beste Grüße

Carsten

Torsten Kleiber hat gesagt…

Hallo!

In 12c scheint wohl irgendwie das Audit Framework einen Fehler zu verursachen:

org.netbeans.ProxyClassLoader:Feb 04, 2014 7:04:30 AM org.netbeans.ProxyClassLoader stripInitialSlash
WARNING: Should not use initial / in calls to ClassLoader.getResource(s): /META-INF/extension.xml

Performing action (284) Debug [ from ProjectNavigatorWindow ] [ for ( DebugTest.java, Project.jpr, PocDebugDbJavaStoredProcedure.jws ) ]
Performing action (216) Clear [ from MessagePage ] [ for ( , Project.jpr, PocDebugDbJavaStoredProcedure.jws ) ]
Performing action (511) Run in SQL*Plus... [ from ProjectNavigatorWindow ] [ for ( call.sql, Project.jpr, PocDebugDbJavaStoredProcedure.jws ) ]
Uncaught exception
java.lang.NullPointerException
o.ji.audit.core.DefaultTransformer.hasTransforms(DefaultTransformer.java:328)
o.i.status.editor.ViolationList.(ViolationList.java:99)
o.i.status.editor.StatusEditorPlugin.setViolations(StatusEditorPlugin.java:420)
o.i.status.editor.StatusEditorPlugin.addIssueListener(StatusEditorPlugin.java:302)
o.i.status.editor.StatusEditorPlugin.access$800(StatusEditorPlugin.java:111)
o.i.status.editor.StatusEditorPlugin$5.run(StatusEditorPlugin.java:1067)
j.a.event.InvocationEvent.dispatch(InvocationEvent.java:251)
j.a.EventQueue.dispatchEventImpl(EventQueue.java:727)
j.a.EventQueue.access$200(EventQueue.java:103)
j.a.EventQueue$3.run(EventQueue.java:688)
j.a.EventQueue$3.run(EventQueue.java:686)
j.security.AccessController.doPrivileged(Native Method)
j.security.ProtectionDomain$1.doIntersectionPrivilege(ProtectionDomain.java:76)
j.a.EventQueue.dispatchEvent(EventQueue.java:697)
o.javatools.internal.ui.EventQueueWrapper._dispatchEvent(EventQueueWrapper.java:169)
o.javatools.internal.ui.EventQueueWrapper.dispatchEvent(EventQueueWrapper.java:151)
j.a.EventDispatchThread.pumpOneEventForFilters(EventDispatchThread.java:242)
j.a.EventDispatchThread.pumpEventsForFilter(EventDispatchThread.java:161)
j.a.EventDispatchThread.pumpEventsForHierarchy(EventDispatchThread.java:150)
j.a.EventDispatchThread.pumpEvents(EventDispatchThread.java:146)
j.a.EventDispatchThread.pumpEvents(EventDispatchThread.java:138)
j.a.EventDispatchThread.run(EventDispatchThread.java:91)

Ausschalten der Audits führt allerdings auch zu Fehlern.

Viele Grüße
Torsten

Torsten Kleiber hat gesagt…

Hallo!

In 11.1.1.5 sehe ich keine offensichtlichen Error Stacks.

Viele Grüße
Torsten

Beliebte Postings