File: README.ThreadedCallbacks

package info (click to toggle)
hbci4java 3.1.85%2Bdfsg-1
  • links: PTS, VCS
  • area: main
  • in suites: trixie
  • size: 10,508 kB
  • sloc: java: 50,805; xml: 33,578; cpp: 3,264; makefile: 153; sh: 11
file content (327 lines) | stat: -rw-r--r-- 16,411 bytes parent folder | download | duplicates (5)
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.