Java Interoperability with Ballerina
Ballerina 1.1.0 was released in January 2020, and the runtime (jBallerina) is running on top of the JVM. Thus ballerina provides seamless interaction with Java, which allows developers to leverage features of Java through ballerina. This article will explain in detail how you can simply create java objects and invoke java methods through ballerina without much of a hassle.
I will create a simple stack in ballerina with the use of java stack implementation. Here I have assumed the familiarity with basic ballerina syntaxes and semantics. Before we go into the implementation lets get a basic idea of what is supported by ballerina language when it comes to java interoperability.
What is supported?
The bridge that connects ballerina and java worlds is the “external” function. An extern function is a function that doesn’t have a body, but a keyword external
, which says the implementation is coming from an outside world. To indicate which world the implementation is coming from, we can use an annotation. For example, we could import the ballerinax/java
package and use @java:XXX
annotations to say that the implementation is coming from Java.
Using @java:XXX
annotations, we can map ballerina function to Java static methods, Java instance methods, and Java static fields. Mapping Java objects to ballerina objects directly is not supported yet. But that will be supported in future releases.
Implementing a stack
Let’s get started with our stack implementation. First let's create a nice API for our stack, by defining a ballerina object. This Stack
object will act as a wrapper to the java stack instance, so that we can provide a cleaner API.
Create the constructor
Now we want to create an instance of java.util.Stack
and keep it as a field in the object. However, since object-attached functions cannot be written as an external function, we need to create a normal function which is an external function and call it within the Stack object’s constructor (__init()
).
function createJavaStack() returns handle = @java:Constructor {
class: "java/util/Stack"
} external;
Here, to mark that this function maps to the constructor, we use the @java:Constructor{}
annotation. Since the returned value from this function is a Java object, we define the return type as handle
. A handle value represents any foreign value (java value, in this case) within the ballerina world. With this, our stack object looks like below:
Luckily for us, java Stack class only has a single constructor. In a case where we have several overloaded constructors, then we need to specify to which constructor are we mapping this external function. That can be done by specifying the parameters in the annotation as follows:
Here, since the one and only constructor takes no parameters, we simply define the param types as an empty array within the annotation.
Implement methods
Let’s implement the push and pop methods of the stack. Similar to earlier, we need to create top-level external functions for both of the methods.
If we closely look at the push()
method of java/util/Stack
it has a single parameter with a generic type and a generic type return. This is equivalent to a ballerina function with a any
type parameter and a any
type return. Additionally, since this is a java instance method that we are going to call, the first parameter of our ballerina function should be the value on which the function should be invoked. In other words, the first parameter to the ballerina method should be the java stack instance.
Here I have stated the paramType as java/lang/Object
, because internally that’s how java represents the generic types. Similarly, our pop method would look like below:
Now we need to call these interop methods from the ballerina Stack object we defined earlier.
Let’s test our Stack:
This will print the following output:
Mango
Orange
Apple
Alright, but what happens if we pop the stack one more time? The stack is supposed to be empty and there will be a java/util/EmptyStackException
at the java level. But how will it propagate to ballerina? Let's try and see:
Mango
Orange
Apple
error: java.util.EmptyStackException
at testMain:javaStackPop(testMain.bal:32)
$value$Stack:pop(testMain.bal:17)
testMain:main(testMain.bal:47)
As you can see, the java exception has become a panic
at the ballerina level. However, in ballerina, a panic
is an error that is not supposed to handle. But, this empty stack error is a common and a known error for the stack-pop method and should be handled by the user. The “ballerina way” of handling this situation is to have an error return in the function signature. So, anyone who uses the pop() method will be able to deal with the error. Thus we need to trap
this error and return it as follows. Note the trap
keyword and the union return type with the error.
function pop() returns any|error {
return trap javaStackPop(self.javaStack);
}
Of-course this is not the best way to handle the empty stack scenario. We should check if the map is empty and return an error if so, rather than letting it panic and trapping the error. Here I just wanted to show how the java runtime exceptions are propagated to ballerina and how to handle them within the ballerina code.