1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327
|
Prinzipielle Funktionsweise des neuen Threaded-Callback-Mechanismus'
====================================================================
Der "normale" Callback-Mechanismus funktioniert wiefolgt:
---------------------------------------------------------
hbci.execute()
... (HBCI-Dialoge werden begonnen)
callback.callback(passport,reason,msg,datatype,retData)
... (Antwortdaten für Callback in retData einstellen)
... (HBCI-Dialoge werden fortgesetzt)
callback.callback(passport,reason,msg,datatype,retData)
... (Antwortdaten für Callback in retData einstellen)
... (HBCI-Dialoge werden fortgesetzt)
usw.
Job-Result auswerten
---------------------------------------------------------
Die Methode hbci.execute() terminiert also erst, wenn alle HBCI-Dialoge
ausgeführt wurden. Tritt während der Ausführung von hbci.execute() ein Callback
auf, führt das zu einem asynchronen Aufruf der Methode callback() des
registrierten Callback-Objektes. "Asynchron" deshalb, weil aus Sicht der
Anwendung ja immer noch hbci.execute() läuft und die Methode callback()
der via HBCIUtils.init() registrierten Callback-Klasse nicht von der Anwendung
selbst, sondern vom HBCI-Kernel aufgerufen wird.
Sobald die callback()-Methode terminiert und die benötigten Daten zurückgegeben
hat, läuft hbci.execute() weiter. Das ganze geschieht aus Sicht des Aufrufers
von hbci.execute() völlig transparent, so dass der Aufrufer von hbci.execute()
keine Möglichkeit hat, bei bestimmten Callbacks beispielsweise die Methode
hbci.execute() terminieren zu lassen, bestimmte Aktionen auszuführen und den
HBCI-Dialog später an der Stelle der "Unterbrechung" fortzusetzen.
In einigen Anwendungsfällen kann diese Asynchronität unerwünscht sein, z.B.
dann, wenn Informationen aus dem aktuellen Kontext zwar an der Stelle bekannt
sind, wo hbci.execute() aufgerufen wurde, nicht aber in der callback()-Methode.
Außerdem ist dieses asynchrone Verhalten nicht sehr praktisch, wenn es notwendig
ist, dass bei einem auftretenden Callback der HBCI-Dialog "gehalten" wird, die
Methode hbci.execute() aber vorerst beendet wird, so dass die Anwendung wieder
die Kontrolle über den "Programmfluss" erhält.
Eine entsprechende Lösung wurde in verschiedenen Projekten bereits mit Hilfe
von mehreren Threads auf Anwendungsseite geschaffen. Die richtige Verwaltung
der Threads, deren Synchronisation und die korrekte Fehler-Behandlung innerhalb
einer Anwendung ist allerdings sehr fehleranfällig. Die notwendige Verwaltung
und Synchronisation der Threads führt vor allem dazu, dass die Anwendung selbst
sehr schnell unübersichtlich wird.
Aus diesem Grund wurde ein entsprechender Mechanismus direkt in HBCI4Java
integriert. Dieser neue Mechanismus ("threaded callbacks") ermöglicht es,
dass hbci.execute() beim Auftreten von bestimmten Callbacks terminiert, der
dazugehörige HBCI-Dialog aber erhalten bleibt. Nach der (synchronen) Behandlung
des "Callbacks" (der durch die Terminierung von hbci.execute() angezeigt wird)
kann die Anwendung den HBCI-Dialog wieder aufnehmen.
Das ganze soll am Beispiel eines Servlets demonstriert werden. Das
Servlet soll Requests von einem Client entgegennehmen. Ein solcher Request
enthält beispielsweise Daten für einen Überweisungsauftrag, welcher durch das
Servlet via HBCI-PIN/TAN an die Bank übermittelt werden soll. Während der
Ausführung des HBCI-Dialoges verlangt der HBCI-Server eine TAN vom Anwender.
Da das iTAN-Verfahren eingesetzt wird, kann die TAN nicht initial zusammen
mit den Daten für die Überweisung übermittelt werden, sondern der Client muss
tatsächlich während des HBCI-Dialoges die richtige TAN zu einer Challenge
liefern. Diese Challenge wird erst im Laufe des HBCI-Dialoges vom Server an
den Client gesandt.
Aus Sicht des Clients sieht der Ablauf also wiefolgt aus: Der Client sendet
zunächst die Daten für den Überweisungsauftrag und wartet auf eine Antwort
vom Servlet. Diese Antwort kann nun eine Status-Information über den Erfolg /
Nicht-Erfolg des Überweisungsauftrages sein, oder aber die Frage nach
zusätzlichen Daten (in diesem Fall die Frage nach einer TAN).
Falls keine TAN benötigt wird (z.B. weil das Servlet gar nicht HBCI-PIN/TAN,
sondern das RDH-Verfahren verwendet), erhält der Client als Response also die
Job-Status-Informationen. Falls aber z.B. das iTAN-Verfahren verwendet wird
und der HBCI-Server eine TAN für den Überweisungsauftrag anfordert, wird die
Response noch nicht aus den Job-Status-Informationen bestehen, sondern die
Frage nach einer konkreten TAN (via Challenge vom Server).
Im nächsten Schritt sendet der Client also die TAN in einem neuen Request
an das Servlet. Das Servlet verwendet diese TAN, um den Überweisungsauftrag
zu autorisieren. Als Response erwartet der Client nun wieder entweder die
Status-Informationen über den Erfolg / Nicht-Erfolg des Auftrages oder die
Nachfrage nach weiteren benötigten Daten.
In diesem Fall wird die nächste Response vom Servlet also aus den Job-Status-
Informationen bestehen.
Aus Sicht des Servlets (welches HBCI4Java verwendet), sieht das ganze dann so
aus: Zunächst erhält das Servlet einen Request vom Client, welcher Daten für
einen auszuführenden Überweisungsauftrag enthält. Das Servlet starten einen
entsprechenden HBCI-Dialog und versucht, den Überweisungsauftrag bei der Bank
einzureichen. Während der Ausführung des HBCI-Dialoges (via hbci.execute())
tritt nun ein Callback auf, der anzeigt, dass vom HBCI-Kernel eine TAN benötigt
wird, mit der der Überweisungsauftrag autorisiert wird. Diese TAN kennt das
Servlet aber noch nicht. Das Servlet muss also das laufende hbci.execute()
terminieren und als Response nicht die Job-Status-Informationen an den Client
senden, sondern die Challenge für die Frage nach einer TAN.
Der darauffolgende neue Request vom Client, der nur die TAN enthält, muss das
Servlet das "unterbrochene" hbci.execute() wieder aufnehmen, indem die gerade
übermittelte TAN als Antwort auf den ursprünglichen Callback an den HBCI-
Kernel übergeben wird, damit dieser die Ausführung des HBCI-Dialoges fortsetzen
kann.
Sobald der HBCI-Dialog dann tatsächlich beendet ist, muss das Servlet die Job-
Status-Informationen als Antwort auf diesen zweiten Request zurückgeben.
Wie man sieht, wäre es mit dem derzeitigen asynchronen Callback-Mechanimus
relativ aufwändig, dieses Szenario umzusetzen (vor allem, wenn man eine gene-
rische Umsetzung realisieren will, die auch nicht-TAN-pflichtige GVs berück-
sichtigt oder gar die Möglichkeit, dass während eines HBCI-Dialoges mehrere TANs
benötigt werden).
Unter Verwendung des neuen threaded-callback-Mechanismus' würde eine
entsprechende Servlet-Umsetzung in etwa so aussehen (nur Pseudo-Code, ohne
Fehlerbehandlung):
------------------------------------------------------------------------------
// erster Request (Daten für den Überweisungsauftrag) kommt herein
// HBCI-Engine initialisieren, dabei das "normale" Callback-Objekt in einem
// HBCICallbackThreaded-Objekt kapseln
HBCIUtils.init(null,null, new HBCICallbackThreaded(myCallback))
// Passport und HBCIHandler initialisieren, Job erzeugen
...
// HBCI-Dialog mit hbci.executeThreaded() anstatt hbci.execute() starten
status=hbci.executeThreaded()
// die Methode hbci.executeThreaded() terminiert, sobald entweder alle HBCI-
// Dialoge ausgeführt wurden oder sobald ein Callback auftritt, der synchron
// behandelt werden soll (z.B. die Frage nach einer TAN)
if (status.isCallback()) {
// die Ausführung des HBCI-Dialoges ist noch nicht beendet, sondern
// es muss ein synchroner Callback beantwortet werden.
// Objekt "hbci" unter einer zufälligen ID merken
id = random();
globalHBCITable.put(id, hbci);
// RESPONSE an den Client mit den Callback-Infos (z.B. "brauche eine TAN")
// und der zufälligen ID generieren
response = "id="+id + "TANChallenge="+status.getChallengeForTan();
} else {
// status.isFinished()==true
// RESPONSE an den Client mit den Job-Ergebnis-Daten generieren
response = status.getJobResultData();
}
// RESPONSE an den Client senden
...
------------------------------------------------------------------------------
// nächster REQUEST vom Client kommt herein
// wenn es sich um die Antwort auf ein Callback handelt, muss darin die
// ID und die Antwort für den Callback drin stehen
// hbci-Objekt mit Hilfe der ID restaurieren
id = request.exttract("id");
hbci = globalHBCITable.get(id);
// übermittelte TAN an den kernel übergeben und dialog fortsetzen
tan = request.extract("tan")
status=hbci.continueThreaded(tan)
// an dieser stelle weiter wie oben nach "hbci.executeThreaded()"
...
------------------------------------------------------------------------------
Anstelle von hbci.execute() verwendet die Anwendung nun hbci.executeThreaded().
Außerdem muss das "normale" Callback-Objekt für die Behandlung von Callbacks
in einem HBCICallbackThreaded()-Objekt gekapselt werden.
Damit der neue Mechanismus überhaupt aktiv wird, muss das "normale" Callback-
Objekt die Methode "useThreadedCallback()" überschreiben und für die Callbacks,
die jetzt synchron behandelt werden sollen, "true" zurückgeben.
Tritt nun während der Ausführung von hbci.executeThreaded() ein Callback auf,
für den useThreadedCallback() "true" zurückgibt, terminiert
hbci.executeThreaded() (im Gegensatz zum "normalen" Callback-Mechanismus, bei
dem hbci.execute() weiterläuft und der Callback nur im Callback-Objekt
aufläuft).
Der Rückgabewert von hbci.executeThreaded() zeigt an, ob die Methode terminiert
ist, weil die komplette Ausführung der HBCI-Dialoge abgeschlossen ist, oder weil
es sich um einen Callback handelt, der laut useThreadedCallback() synchron
behandelt werden soll.
Falls es sich um einen Callback handelte, kann die HBCI-Anwendung den HBCI-
Dialog fortsetzen, indem sie die benötigten Callback-Daten mit der Methode
hbci.continueThreaded() an den HBCI-Kernel übergibt. Das dabei verwendete
Objekt "hbci" muss das selbe(!) sein wie das, mit dem ursprünglich
hbci.executeThreaded() ausgeführt wurde (evtl. muss das "hbci"-Objekt also
in einer Session gesichert werden). Mit hbci.continueThreaded() wird der HBCI-
Dialog, der durch den synchronen Callback unterbrochen wurde, fortgesetzt.
Der Rückgabewert von hbci.continueThreaded() zeigt nun wiederum an, ob die
HBCI-Dialoge nun tatsächlich beendet sind oder ob ein weiterer Callback statt-
gefunden hat, der synchron behandelt werden muss.
Um eventuelle TAN-Abfragen innerhalb der Initialisierung des hbci-Objektes
(z.B. bei Abfrage der UPD-Daten) ähnlich zu behandeln, kann das hbci-Objekt
durch einen dritten Kontruktor-Parameter lazyInit zunächst ohne UPD-Abfrage
initialisiert werden, und anschließend durch initThreaded() analog wie bei
executeThreaded() gekapselt werden.
Die entsprechende Funktionsweise im Pseudo-Code, ohne Fehlerbehandlung:
------------------------------------------------------------------------------
handle = new HBCIHandler(HBCIVersion.HBCI_300.getId(), passport, true);
HBCIExecThreadedStatus status = handle.initThreaded();
if (status.isCallback()) {
// die Ausführung des HBCI-Dialoges ist noch nicht beendet, sondern
// es muss ein synchroner Callback beantwortet werden.
//
// siehe oben
} else {
// status.isFinished()==true
//
// handle normal kann verwenden werden.
}
------------------------------------------------------------------------------
// zur Behandlung des Callbacks
//
// übermittelte TAN an den kernel übergeben und dialog fortsetzen
tan = request.extract("tan")
hbci.continueThreaded(tan)
// an dieser stelle weiter wie oben nach "hbci.executeThreaded()"
------------------------------------------------------------------------------
Theoretisch könnte man diesen Mechanismus generell aktivieren, so dass weder
die Kapselung des normalen Callback-Objekts in HBCICallbackThreaded() noch die
Verwendung von hbci.executeThreaded() notwendig wäre, so dass einzig und allein
die Methode callback.useThreadedCallback() darüber entscheiden würde, ob ein
Callback synchron oder asynchron behandelt werden soll. Da aber der threaded-
callback-Mechanimus im Gegensatz zum "normalen" Callback-Mechanismus mit
mehreren Threads arbeitet, müssten dann in jedem Fall mehrere Threads für
die Abarbeitung von HBCI-Dialogen erzeugt und synchronisiert werden. Aus
Performance- und Kompatibilitätsgründen wurde deshalb darauf verzichtet, diesen
Mechanismus generell zu aktivieren. Statt dessen funktioniert dieser Mechanismus
nur dann, wenn tatsächlich HBCICallbackThreaded() und hbci.executeThreaded()
verwendet werden. In diesem Fall funktionieren die "normalen" Callbacks
natürlich auch weiterhin - eine absolut generische Applikation würde also immer
hbci.executeThreaded() verwenden. Eine Applikation, bei der der threaded-
callback-Mechanismus niemals benötigt wird, würde weiterhin das normale
hbci.execute() verwenden.
(interne Funktionsweise:)
Innerhalb von HBCI4Java wird bei hbci.executeThreaded() ein neuer Thread
erzeugt, innerhalb dessen die eigentlichen HBCI-Dialoge via hbci.execute()
geführt werden. Der main-Thread selbst versetzt sich zunächst in einen wait-
Zustand, bis er vom HBCI-Thread aufgrund irgendeines Ereignisses wieder auf-
geweckt wird. Aus Sicht der Anwendung ist die Methode hbci.executeThreaded()
also blockiert, weil sie gerade auf eine Nachricht vom HBCI-Thread wartet,
welcher hbci.execute() ausführt.
Tritt während der Ausführung von hbci.execute() im HBCI-Thread ein Callback auf,
schlägt dieser Callback zunächst bei einer Instanz von HBCICallbackThreaded auf
(wg. des modifizierten HBCIUtils.init()).
Dort wird geprüft, ob dieser Callback synchron oder asynchron behandelt
werden soll (anhand der Methode useThreadedCallback() des "normalen" Callback-
Objektes). Soll der Callback "normal" behandelt werden, wird wie gewohnt die
callback()-Methode des "normalen" Callback-Objektes aufgerufen (asynchrone
Callback-Behandlung, weil sich der main-Thread immer noch im wait()-Zustand
befindet). An dieser Stelle kann es evtl. zu Problemen mit existierenden
Anwendungen kommen, weil der Aufruf von callback() in einem anderen Thread
(nämlich dem von hbci.executeThreaded() erzeugten HBCI-Thread) erfolgt als der
ursprüngliche Aufruf von hbci.executeThreaded(). Falls eine Anwendung also
Thread-lokale Variablen verwendet, müssten für diesen Fall entsprechende
Anpassungen vorgenommen werden.
Soll der Callback allerdings synchron behandelt werden, so übergibt der HBCI-
Thread die Callback-Informationen (die Argumente der callback()-Methode) an
den main-Thread und sorgt dafür, dass der main-Thread wieder "aufwacht".
Der HBCI-Thread selbst versetzt sich in einen wait-Zustand und wartet darauf,
dass die Callback-Antwortdaten vom main-Thread bereitgestellt werden (das ganze
passiert also in der callback()-Methode des HBCICallbackThreaded-Objektes).
Innerhalb des main-Thread wird nun also hbci.executeThreaded() beendet (während
der HBCI-Thread mit hbci.execute() noch läuft bzw. sich gerade in einem wait-
Zustand befindet). Anhand der Rückgabedaten von hbci.executeThreaded() muss
die HBCI-Anwendung nun entsprechend reagieren.
Setzt die HBCI-Anwendung den HBCI-Dialog mit hbci.continueThreaded() fort,
übergibt der main-Thread (von dem aus hbci.continueThreaded() aufgerufen
wurde) die Callback-Antwort-Daten an den HBCI-Thread, weckt ihn wieder auf und
versetzt sich selbst wieder in einen wait-Zustand. Somit kann der HBCI-Thread
nun die Callback-Antwort an den HBCI-Kernel übergeben (der HBCI-Thread befindet
sich ja gerade in der callback()-Methode von HBCICallbackThreaded und wartet auf
die Daten, die im retData-Objekt an den Kernel zurückgegeben werden sollen).
Sobald der HBCI-Thread tatsächlich beendet ist (oder wieder ein synchroner
Callback auftritt), wird wieder der main-Thread aktiviert. Anhand der
Rückgabedaten, die der main-Thread vom HBCI-Thread erhält und an die Anwendung
zurückliefert, kann die Anwendung entscheiden, ob ein weiteres
hbci.continueThreaded() notwendig ist oder ob der HBCI-Dialog regulär beendet
ist.
|