Hallo,
mein Java-Agent verursacht immer wieder nen OutOfMemoryError, wenn er mehrmals hintereinander läuft.
Der Agent wird aus einer Notes-DB gestartet, stellt über JDBC eine Connection zur DB2 her und liest dort Daten aus. In der Notes-DB wird dann ein Dokument erstellt und die Daten dort eingelesen. Das Ganze funktioniert im Grunde, hatte mich aber wohl zu früh gefreut.
Nach ca. 7 Aufrufen des Agenten kommt es zu einem Error in Notes:
NotesError: JVM: Versuch, den Java Agent-Anhang zu öffnen, ist fehlgeschlagen.
In der Java-Konsole steht folgender Error:
java.lang.OutOfMemoryError
Kenne mich mit Java leider gar nicht gut aus und bin schon froh, dass ich den Agenten überhaupt hinbekommen hab.
Hier ist der Code:
import lotus.domino.*;
import java.sql.*;
import java.io.*;
import java.util.*;
public class JavaAgent extends AgentBase {
public void NotesMain() {
try {
Session session = getSession();
AgentContext agentContext = session.getAgentContext();
Database db = agentContext.getCurrentDatabase();
Document doc = agentContext.getDocumentContext();
Document profildok = db.getProfileDocument("Profildok", "");
// Variablen deklarieren
String l_Feld1;
String l_Feld2;
String l_Feld3;
boolean rc;
// Nr. aus Profildokument auslesen
l_Feld1 = profildok.getItemValueString("Nr");
PrintWriter pw = getAgentOutput();
Connection con=null;
Statement stmt=null;
ResultSet rs=null;
String JDBCDriverName="Treibername";
String system="Pfad";
String user = "username";
String pwd = "passwort";
DriverManager.registerDriver((Driver) Class.forName(JDBCDriverName).newInstance());
con = DriverManager.getConnection(system,user,pwd);
stmt = con.createStatement();
// Abfrage
String sql = "SELECT ...;
rs = stmt.executeQuery(sql);
// prüfen ob rs leer ist
rc = rs.next();
if (rc) {
// Felder auslesen
l_Feld1 = rs.getString(1);
l_Feld2 = rs.getString(2);
l_Feld3 = rs.getString(3);
}
else {
l_Feld1 = "";
l_Feld2 = "";
l_Feld3 = "";
};
if (z) {
// neues Dokument anlegen
doc = db.createDocument();
doc.appendItemValue("Form", "Dok");
System.out.println("Dok. angelegt.");
// Felder im Dokument füllen
doc.appendItemValue("NFeld1", l_Feld1);
doc.appendItemValue("NFeld2", l_Feld2);
doc.appendItemValue("NFeld3", l_Feld3);
// Dokument speichern
doc.save(true, true);
}
else {
}
// Close statement and connection
rs.close();
stmt.close();
con.close();
}
catch (Exception e) {
e.printStackTrace();
}
}
}
Bin für jeden Tipp dankbar.
Versuch erstmal Ralfs tipp.
Also:
am Schluss:
con.close();
db.recycle();
}
catch (Exception e) {
e.printStackTrace();
if (con != null) con.close(); // was anderes, aber wo du schon dabeibist.
if (db != null) db.recycle();
}
OBEN:
Deklaration von con und db oberhalb des trys schieben:
Database db = null;
Connection con = null;
try {
Session session = getSession();
AgentContext agentContext = session.getAgentContext();
// auskomentiert Database db = agentContext.getCurrentDatabase();
Document doc = agentContext.getDocumentContext();
Document profildok = db.getProfileDocument("Profildok", "");
[... weiter unten]
PrintWriter pw = getAgentOutput();
// auskommentiert : Connection con=null;
Desweiteren kann das errorhandling verbessert werden, aber dazu später mehr.
Es liegt nicht an Java. Die Integration von Java und Notes ist ein bischen ätzend. Ich programmiere pro Woche durchschnittlich 20 neue SQLConnections low level oder mit wechselnden Frameworks, Tools (Ant, ibatis sqlmaps, springframework-jdbc, hibernate, ejb) und laufe da einfach nicht in eine OutOfMemory. >:(
Keine Sorge, lass mich so schnell nicht vertreiben ;-)
Also danke erstmal für eure Tipps. Das mit dem recycle hab ich eingebaut, hat leider nicht geholfen.
profildok.recycle();
doc.recycle();
db.recycle();
agentContext.recycle();
session.recycle();
Das Deklarieren von Database und Connection hab ich oberhalb des trys platziert. Allerdings bekomme ich zu den beiden If-Clauses am Ende des Codes einen Fehler bei der Kompilierung ???
Fehler:
Exception java.sql.SQLException must be caught, or it must be declared in the throws clause of this mehtod.
public void NotesMain() {
Database db = null;
Connection con = null;
try {
Session session = getSession();
AgentContext agentContext = session.getAgentContext();
db = agentContext.getCurrentDatabase();
Document doc = agentContext.getDocumentContext();
Document profildok = db.getProfileDocument("Profildok", "");
catch (Exception e) {
e.printStackTrace();
if (con != null) con.close();
if (db != null) db.recycle();
}
Hat mich bisher leider nicht weitergebracht.
Fällt euch vielleicht noch was ein?
Yep, die Fehlermeldung ist immer noch die gleiche.
Die Vorgehensweise im Detail:
1. Der User gibt über eine Dialogbox eine Auftragsnr. ein.
2. Die Nr. wird in einem Profildokument gespeichert.
3. Der Java-Agent wird aufgerufen.
- bis hierher alles über LotusScript -
4. Der Agent liest die Nr. aus dem Profildokument aus.
5. Es wird über JDBC eine Verbindung zur DB2 aufgebaut.
6. SQL-Abfrage
7. Im RecordSet wird nach der Nr. gesucht.
8. Anhand der Auftragsnr. werden mehrere Daten aus der DB2 gefischt und in Variablen gespeichert.
9. Ein neues Notes-Document wird erstellt.
10. Die Daten werden an Felder im Notes-Doc übergeben.
11. Das Notes-Doc wird gespeichert.
12. Die JDBC-Verbindung wird gekappt.
13. Objekte werden recycelt und alles auf null gesetzt.
Hier nochmal der Code:
import lotus.domino.*;
import java.sql.*;
import java.io.*;
import java.util.*;
public class JavaAgent extends AgentBase {
public void NotesMain() {
Database db = null;
Connection con = null;
try {
Session session = getSession();
AgentContext agentContext = session.getAgentContext();
db = agentContext.getCurrentDatabase();
Document doc = agentContext.getDocumentContext();
Document profildok = db.getProfileDocument("Profildok", "");
// Variablen deklarieren
String l_nr;
String l_artikel;
String l_stueck;
String l_text;
String l_termin;
boolean rc;
// Nr. aus Profildokument auslesen
l_nr = profildok.getItemValueString("Nr");
System.out.println("lfd.Nr. " + l_lfdnr);
PrintWriter pw = getAgentOutput();
// Connection con=null;
Statement stmt=null;
ResultSet rs=null;
String JDBCDriverName="com.ibm.as400.access.AS400JDBCDriver";
String system="Serverpfad/; libraries=libname";
String user = "username";
String pwd = "password";
DriverManager.registerDriver((Driver) Class.forName(JDBCDriverName).newInstance());
con = DriverManager.getConnection(system,user,pwd);
stmt = con.createStatement();
System.out.println("Connection hergestellt.");
// SQL-Abfrage
String sql = "SELECT feld1, feld2, feld3, feld4 FROM libname.tablename;
rs = stmt.executeQuery(sql);
// prüfen ob rs leer ist
rc = rs.next();
if (rc) {
// Felder auslesen
System.out.println("RS gefüllt");
l_nr = rs.getString(1);
l_artikel = rs.getString(2);
l_stueck = rs.getString(3);
l_termin = rs.getString(4);
l_text = rs.getString(5);
}
else {
// Leerstring
System.out.println("RS leer");
l_nr = "";
l_artikel = "";
l_stueck = "";
l_termin = "";
l_text = "";
};
// Anzahl der Zeichen zählen, wenn < 0 ist Leerstring
int l_zeichen = l_artikel.length();
System.out.println("Zeichen " + l_zeichen);
boolean z = (l_zeichen > 0);
if (z) {
// neues Dokument anlegen
doc = db.createDocument();
doc.appendItemValue("Form", "maskenname");
System.out.println("Dok. angelegt.");
// Felder im Dokument füllen
doc.appendItemValue("Nr", l_nr);
doc.appendItemValue("Artikel", l_artikel);
doc.appendItemValue("Stueck", l_stueck);
doc.appendItemValue("Termin", l_termin);
doc.appendItemValue("Text", l_text);
// Dokument speichern
doc.save(true, true);
}
else {
System.out.println("Kein Dokument angelegt.");
}
// Close statement and connection
rs.close();
stmt.close();
con.close();
// alles bereinigen
profildok.recycle();
doc.recycle();
rs = null;
stmt = null;
con = null;
System.gc();
} //end try
catch (Exception e) {
e.printStackTrace();
if (con != null)
try{
con.close();
} catch (SQLException sqle) {
}
}
}
}
Das die fEhlermeldung kommt verstehe ich nicht, aber es fehlte die Klammerung nach dem if in der catch clause. Mit // NEU gekennzeichnet.
import lotus.domino.*;
import java.sql.*;
import java.io.*;
import java.util.*;
public class JavaAgent extends AgentBase {
public void NotesMain() {
Database db = null;
Connection con = null;
try {
Session session = getSession();
AgentContext agentContext = session.getAgentContext();
db = agentContext.getCurrentDatabase();
Document doc = agentContext.getDocumentContext();
Document profildok = db.getProfileDocument("Profildok", "");
// Variablen deklarieren
String l_nr;
String l_artikel;
String l_stueck;
String l_text;
String l_termin;
boolean rc;
// Nr. aus Profildokument auslesen
l_nr = profildok.getItemValueString("Nr");
System.out.println("lfd.Nr. " + l_lfdnr);
PrintWriter pw = getAgentOutput();
// Connection con=null;
Statement stmt=null;
ResultSet rs=null;
String JDBCDriverName="com.ibm.as400.access.AS400JDBCDriver";
String system="Serverpfad/; libraries=libname";
String user = "username";
String pwd = "password";
DriverManager.registerDriver((Driver) Class.forName(JDBCDriverName).newInstance());
con = DriverManager.getConnection(system,user,pwd);
stmt = con.createStatement();
System.out.println("Connection hergestellt.");
// SQL-Abfrage
String sql = "SELECT feld1, feld2, feld3, feld4 FROM libname.tablename;
rs = stmt.executeQuery(sql);
// prüfen ob rs leer ist
rc = rs.next();
if (rc) {
// Felder auslesen
System.out.println("RS gefüllt");
l_nr = rs.getString(1);
l_artikel = rs.getString(2);
l_stueck = rs.getString(3);
l_termin = rs.getString(4);
l_text = rs.getString(5);
}
else {
// Leerstring
System.out.println("RS leer");
l_nr = "";
l_artikel = "";
l_stueck = "";
l_termin = "";
l_text = "";
};
// Anzahl der Zeichen zählen, wenn < 0 ist Leerstring
int l_zeichen = l_artikel.length();
System.out.println("Zeichen " + l_zeichen);
boolean z = (l_zeichen > 0);
if (z) {
// neues Dokument anlegen
doc = db.createDocument();
doc.appendItemValue("Form", "maskenname");
System.out.println("Dok. angelegt.");
// Felder im Dokument füllen
doc.appendItemValue("Nr", l_nr);
doc.appendItemValue("Artikel", l_artikel);
doc.appendItemValue("Stueck", l_stueck);
doc.appendItemValue("Termin", l_termin);
doc.appendItemValue("Text", l_text);
// Dokument speichern
doc.save(true, true);
}
else {
System.out.println("Kein Dokument angelegt.");
}
// Close statement and connection
rs.close();
stmt.close();
con.close();
// alles bereinigen
profildok.recycle();
doc.recycle();
rs = null;
stmt = null;
con = null;
System.gc();
} //end try
catch (Exception e) {
e.printStackTrace();
if (con != null) { //NEU NEU NEU (die Klammer)
try{
con.close();
} catch (SQLException sqle) {
}
}
}
}
Kanns z.Zt. nicht ausprobieren.
Ja. Der Treiber müsste dann auf jeden Desktop deployed werden.
3-Tier Umgebungen sind mit Notes ein bischen nicht so gut.
Hier zu später Stunde noch das versprochene verbesserte ExceptionHandling:
Über
das hier:
catch(SQLException sqlEx)
{
while(sqlEx != null) {
System.err.println("SQLException information");
System.err.println("Error msg: " + sqlEx.getMessage());
System.err.println("SQLSTATE: " + sqlEx.getSQLState());
System.err.println("Error code: " + sqlEx.getErrorCode());
sqlEx.printStackTrace();
sqlEx=sqlEx.getNextException();
}
}
Falls es Probleme mit dem SQL gibt, erhält man so alle wichtigen Fehlerinformationen. Über SQLState und ErrorCode bekommt man aus der DB2 Doku direkt raus, woran es hakt. Meistens ist bei mir das SQL-Statement doch nicht so richtig (bei mir zumindest)
Btw. bringt System.gc() fast nie etwas.
Ausserdem solltest du PreparedStatement statt Statement verwenden. Das macht den Code oft sicherer und vielleicht können so auch die Ausführungspfade von SQL-Statements auf DB2 gecached werden (die Regeln habe ich immer noch nicht so richtig verstanden). JDBC läuft immer über sogenanntes "dynamisches SQL" und da wird auf DB2 immer zuerst der "Ausführungsplan" errechnet und das nimmt Zeit in Anspruch. Bei einfachen SQL aber im Milisekundenbereich.
Warum diese komische Konstruktion mit connection noch mal in catch schliessen?
Oben öffnest du eine Datenbank-Connection. Das ist eine knappe Ressource. Wenn nun in dem Code des try-Blocks an irgend einer Stelle eine Exception auftritt, wechselt das Programm direkt in den catch-Block und kommt nie wieder in den try zurück. Die Connection muß aber geschlossen werden. So muß man das eben im catch auch noch machen. Und dieses Statement muß wiederum mit try-catch umgeben werden, da connection.close() eine SQLException wirft.
Das sind all diese kleinen JDBC-Basics.
In der realen Java-Welt ist JDBC Programmierung mittlerweile ziemlich selten. Die Leute benutzen irgendwelche Frameworks und Objekt-Relationalen-Mapper wie Hibernate.
Ich hab hier z.B. ein Framework namens IBAtis SQLMaps und da stehen die ganzen SQL-Queries gar nicht mehr im Source-Code sondern werden per xml definiert:
Sieht ungefähr so aus:
Als Beispiel steht ein solcher sql-query in einer xml-Datei (mit den entsprechenden Datentypen die übergeben und zurückgegeben werden:
<select id="getIdByIdUserAndIdStockDescr"
parameterClass="java.util.HashMap" resultClass="java.lang.Long">
Select ID from STockUser where IDUSER=#idUser# AND IDStockDescr=#idStockDescr#
</select>
Wird dann einfach so in einem sogenannten DataAcces Object aufgerufen und ich spar mir den ganzen restlichen Quatsch. Den Rest macht das Framework. Hier lass ich mir nur eine ID als Long zurückgeben. Ganze Objekte geht aber auch. Auch als Collection.
public StockUser getStockUserByIdUserAndIdStockDescr(long idUser, int idStockDescr) throws IllegalArgumentException {
if (idUser < 0) throw new IllegalArgumentException("Parameter idUser cannot be lower than 0");
if (idStockDescr < 0) throw new IllegalArgumentException("Parameter idStockDescr cannot be lower than 0");
Map params = new HashMap();
params.put("idUser", new Long(idUser));
params.put("idStockDescr", new Long(idStockDescr));
return (StockUser)getSqlMapClientTemplate().queryForObject("getStockUserByIdUserAndIdStockDescr", params);
}
Leider kann man mit Notes solche schönen Sachen nur unter ziemlichen Verrenkungen schwer verwenden.
@Teletambi: Dieser Thread war bereits als gelöst gemeldet.
Trotzdem weisst du auf eine sehr wichtige Sache hin.
Ist mir damals entgangen, wobei ich mich in dem Punkt "external Ressourcen schliessen" bisher für paranoid gehalten habe.
Schliessen der Connection im finally (oder meine traditionelle Lösung s.u.) finde ich auch besser, weiss nicht, warum ich damals nicht darauf hingewiesen habe. Das wurde hier am Ende des try und im catch Block geschlossen, was nicht perfekt ist, hier aber offenbar ausreichend.
Danke aber für deinen Hinweis, weil du in gewissen Punkten sicher recht hast.
Jede Exception *einzeln* zu catchen halte ich hingegen für unnötig.
Schliesslich haben Exceptions in Java eine gewisse Vererbungshierarchie, die man sich zu Nutze machen kann.
Wenn du eine gewisse Menge an Funktionalität/per Stunde programmierst, hat man in der Praxis keine Zeit, jede Exception einzeln zu catchen. Statt dessen sollte man sich Gedanken machen, wo spezifisches Exception Handling Sinn macht. Wo nicht, sollte man die Superklassen verwenden (s.u.)
In Java gibt es 2 Arten von Exceptions.
Checked und Unchecked.
Die Checked sind die, wo der Compiler immer anmeckert, dass man diese Exception abfangen muss. Alle checked Exceptions erben von der Klasse java.lang.Exception.
Unchecked Exceptions müssen nach Meinung des Compilers dagegen nicht per catch abgefangen werden. Das sind oft exception, die mit dem Wissen des Compilers eigentlich immer auftreten können. Z.B. NullPointerException. Der Compiler weiss nicht, ob später bei der Ausführung des Programms eine Referenz auf ein Objekt weist oder nicht.
Um sicher zu gehen, dass ein catch-Block für alle Arten von Exceptions zuständig ist, kann man java.lang.Throwable nutzen. Throwable ist die Superklasse von Exception, RuntimeException und Error ist (wobei RuntimeException und Error Superklassen aller unchecked Exceptions sind).
Wenn man z.B. mit externen Ressourcen arbeitet, kann es imho Sinn machen mit Throwable zu arbeiten. Oder man schliesst sie eben im finally.
Über dem Throwable können dann spezifischere Exceptions abgefangen werden.
z.B.:
try {
...
} catch (NotesException e) {
... do stuff ..
}
catch (SQLException e) {
... do stuff ..
}
catch (Throwable t) {
/* würde man hier, in allen darüberliegenden catches und im try die Connections schliessen wäre das safe. Man kann hierfür eine extra Methode vorsehen. So mach ich das immer aus Gewohnheit. */
... do stuff ...
} finally {
/* würde man hier die Connections schliessen und im try nicht wäre das auch safe und weniger Tipparbeit. Vielleicht sollte ich meine Gewohnheit ändern. Wobei ich für DB-Zugriff Frameworks (s.u.) nutze, wo ich kann: Heisst überall ausser in Java in Notes. */
}
In der Tat ist gerade das Exception-Handling und auch Probleme mit stalled Connections (die man in den Griff kriegen kann) Punkte, die oft an der Architektur von JDBC kritisiert worden sind.
Dies ist ein eher unwichtiger Grund, warum die Leute so gerne Frameworks wie Hibernate oder ibatis SQLMaps statt low-level JDBC arbeiten, da diese Frameworks diese und andere Probleme schon gelöst haben.
Ach ja. Und bei OutOfMemoryError extends java.lang.Error wird natürlich weder das catch noch das finally aufgerufen. ;D
Gruß Axel