June 10, 2018
This article presents an integration with the desktop framework TorandoFX and the CDI framework Google Guice. In addition to dependency injection, Google Guice provides an implementation of Aspect-Oriented Programming or "AOP". AOP is a technique for enhancing a function without needing to modify the function. AOP works by intercepting the function, applying the enhancement before or after the function is called. In order to enable the intercepting, a framework like Google Guice needs to be involved in the object creating to produce a compound object behind-the-scenes.
This example is based on a ticket I recently worked where I wanted to apply some connection-related error handling to a broad set of classes. There were a lot of classes and since this particular issue was tough to reproduce, I didn't want to modify a lot of files in case I needed to adjust the fix. Also, changing a lot of files would have affected later merges. Since Google Guice was already deployed with this app, getting a special handler attached to many classes added only 3 lines to the Guice Module.
The following video demonstrates the problem of an uncaught exception in a TornadoFX app. The unchecked exceptions IllegalArgumentException and IllegalStateException are generated and handled by a default exception handler. This will be replaced with a custom exception handler hooked in via AOP. Additionally, there is a general confirmation requirement for communicating the successful outcome of an operation to the user. Rather than require the users to add the line of code to each operation, AOP will add the confirmation to classes.
In both segments of the video, clicking on Foo and Bar result in an exception. Clicking on "Ok" results in an information dialog indicating a successful outcome.
The code for UncaughtExceptionApp.kt is found in the source download.
There is another way to specify a default exception handler in TornadoFX; register the handler with Thread.setDefaultUncaughtExceptionHandler()
. That is a handler called for all uncaught exceptions and the example here presents a technique for restricting the handler to a set of classes.
The following UML shows the classes that make up the application. There is an App/View/Controller design typical of an MVC TornadoFX app. Three subsystems make up the business logic and data functions of the application. An AbstractModule configures Guice. There is a handler which is completely decoupled from the subsystems definitions yet linked by Guice.
GlobalHandlerView is the main and only window for the application. It injects the Controller and calls subsystem functions in response to Button presses.
class GlobalHandlerView : View("Global Handler App") {
val controller : GlobalHandlerController by inject()
override val root = hbox {
button("Foo") {
action {
controller.foo()
}
}
button("Bar") {
action {
controller.bar()
}
}
button("Ok" ) {
action {
controller.ok()
}
}
padding = Insets(10.0)
spacing = 4.0
}
}
The Controller class, GlobalHandlerController, delegates to the three subsystems: FooSubsystem, BarSubsystem, and OkSubsystem. The Controller class is also the integration point for Google Guice. Google Guice MUST manage the classes that will be intercepted. That means that Guice will create the objects, performing any dependency injection. If these objects were created as plain Kotlin objects, their functions would not be intercepted.
The three subsystems are also presented below
class GlobalHandlerController : Controller() {
val fooSubsystem : FooSubsystem
val barSubsystem : BarSubsystem
val okSubsystem : OkSubsystem
fun foo() = fooSubsystem.foo()
fun bar() = barSubsystem.bar()
fun ok() = okSubsystem.ok()
init {
val injector = Guice.createInjector(GlobalHandlerModule())
fooSubsystem = injector.getInstance(FooSubsystem::class.java)
barSubsystem = injector.getInstance(BarSubsystem::class.java)
okSubsystem = injector.getInstance(OkSubsystem::class.java)
}
}
open class FooSubsystem {
open fun foo() { throw IllegalStateException("unchecked exception from foo") }
}
open class BarSubsystem {
open fun bar() { throw IllegalArgumentException("unchecked exception from bar")}
}
open class OkSubsystem {
open fun ok() {}
}
In order for the interceptor to work, the classes need to be open. This is so that Guice can provide subclasses behind-the-scenes.
The Guice Module GlobalHandlerModule is a one-liner that binds the interceptor class to a set of target classes. I'm using a package to designate the classes that will receive the interception. There is a rich set of Matchers that can be applied based on class hierarchy and annotations. More than one bindInterceptor() can be used for complex cases spanning Matcher criteria.
class GlobalHandlerModule : AbstractModule() {
override fun configure() {
bindInterceptor(
Matchers.inPackage(Package.getPackage("globalhandlerapp")),
Matchers.any(),
SubsystemResultHandler()
)
}
}
The AOP MethodInterceptor "SubsystemResultHandler" is configured to be called by any method in the three subsystem classes. When invoked, the function is called within a try / catch block. A successful result will return with a confirmation dialog pending. The Platform.runLater() is used in case there is an asynchronous Task added to the subsystems at a later date.
If an exception is thrown, the "SubsystemResultHandler" will catch the exception and present the friendlier dialog. This exception handling code can further filter events by type.
class SubsystemResultHandler : MethodInterceptor {
override fun invoke(invocation: MethodInvocation?): Any {
try {
val retval = invocation!!.proceed() // could be off thread
Platform.runLater({
success("${invocation!!.method.name.capitalize()} Completed!")
})
if( retval == null ) {
return Any()
}
} catch(exc : Exception) {
Platform.runLater({
fatalAlert("There was an error calling ${invocation!!.method.name.capitalize()}")
})
}
return Any()
}
private fun fatalAlert(specificMessage : String) =
alert(Alert.AlertType.ERROR, "System Error", specificMessage)
private fun success(specificMessage : String) =
alert(Alert.AlertType.INFORMATION, "Success", specificMessage)
}
GlobalHandlerApp and the main function are one-liners and are not presented here. See the source code download for a listing.
Not every condition needs an interceptor. Sometimes, you'll need a specific error handling sequence for a business logic call. In those cases, Guice provides a rich set of Matchers that can be used to exclude the classes needing this special treatment. This example showed how to trap a result of a function, however, there are other uses for AOP such as authentication and logging. TornadoFX has its own dependency injection capability, but since Guice is so lightweight (<700k), you can implement interceptors without greatly affecting the footprint.
The source code presented in this article series is a Gradle project found in the zip file below.
To run the demo, create an Application configuration in your IDE that will run the GlobalHandlerApp class. In IntellIJ,
By Carl Walker
President and Principal Consultant of Bekwam, Inc