Embedding Typescript in Java
Getting our feet wet with Polyglot Programming by tackling one of Java’s annoying shortcomings…
Almost everyone who has worked with JSON in Java knows it’s not the easiest thing to do, especially when compared to the built-in support present in languages like Javascript, PHP and others.
While for simple serialization and deserialization you can rely on Jackson, Mapstruct, JSONPath and the likes, when you need to map very large data structures from and to many different expected payload formats, it gets extremely laborious and complex to maintain.
While we tried to abstract the differences away to have a more consistent mapping layer, this in itself was taking away too much time, while also itself contributing to adding more complexity to the system. This added complexity meant that you’d need to learn our abstraction setup and also learn at least about Jackson, Mapstruct and JSONPath to properly create or adapt a mapping! This wasn’t scalable in the long term.
So, why is all of this needed in Java in the first place? Why is working with JSON much more straightforward in Javascript than Java?
Two things in particular: No language support and easy syntax for a generic data type and no type inference or dynamic typing like Javascript’s. While strong, static typing is a boon in general, when you’re simply mapping data it can become cumbersome. Yes, you can use Objects, builders and Maps of Maps, but this becomes unreadable pretty fast, even for simplest examples, like below:
Hopefully, we can agree that the Javascript version is much easier to read and expand from than Java’s.
In Java, in order to avoid using these Maps of Maps, you’d end up creating a type to at least have a workable solution, but when you don’t have a common abstraction to work with, as was our case, you end up with dozens of different classes for only a specific case.
A language within a language
Hence, we went looking for alternatives that reach out of the Java language bounds.
While we were already using JSLT to do some mappings for other particular cases, this had some drawbacks. You need to learn a new language with an unfamiliar syntax, very niche and not widely spread usage, not easy to extend with new functionality, sub-optimal modularity, and almost nonexistent IDE support (no auto-complete, no hinting). For what’s it’s worth, it does its job well, but we wanted something easier to extend and better IDE support.
The Holy GraalJS
Finally, we arrive to the main topic of this article, GraalJS.
As defined in their repository, GraalJS is
A high performance implementation of the JavaScript programming language. Built on the GraalVM by Oracle Labs.
Do not let the “Built on the GraalVM” part confuse you, though. We can use this on an ordinary JVM, like OpenJDK’s HotSpot. You need to use the GraalVM if you want to run Node.js apps, though, but that’s not the focus of this article.
GraalJS is now the de facto replacement for Rhino and Nashorn, and it allows interoperability between Java and JS. Meaning you can invoke JS code from Java (for example, call a JS function from Java) and vice-versa (for example, use Java classes in JS).
This is essentially what we wanted: allow us to easily map data, take advantage of the more flexible typing system and JS’s strengths, while also still being able to reuse our domain objects which guarantee some invariants and formats. This, coupled to the fact that we can easily extend either via the Javascript or Java codebase, made a lot of sense to us.
The barebones example
So, how do we achieve this in practice?
Let’s see the example below (you can check the final result in the repository):
So, of course, we first need to import GraalJS into the project.
Then, the rest of the example contains already a lot of useful information:
- First, on the Java side, we create a context that only allows the Javascript language (yes you can also do this with Python, R…).
- We setup some policies. You should change this if you do not trust the JSs being executed (for example, they are fetched at runtime from some third party).
- We load the script from our resources and make the engine evaluate it, which will make the
map
function available.
a. On the Javascript side, on evaluation, it’ll see that we’re importing aJava.type
we defined. This will allow us to create an object of that type and use it later on the Java side.
b. Notice that we have actx
parameter on themap
function. Thisctx
is provided by Java at runtime. - We call
map
with the providedctx
(notice the interoperability) and we coerce the Java type from it (from theJava.type
we defined earlier). - Finally, we print the result, which yields
MappingResult{result={myNameIs:"Mauro",myValueIs:"value"}}
.
Getting serious
Now that we’ve proven that this setup works, we want to improve it, namely in the following areas:
IDE support and typing
As you noticed, we’re using a Java class we created in the Javascript mapping. The IDE doesn’t know what this type is within the JS script, though. What’s more, even if we created a class within Javascript, Javascript’s type system is not as rich as Java’s, so there would always be some sort of impedance.
For these reasons, we started investigating how to integrate Typescript, whose type system is basically a super set of Java’s, even supporting Union Types, but also how to automate the export of Typescript type declarations from our Java code.
Thankfully, we’ve found java2typescript. This library exports most basic Java types to Typescript type definitions, while also allowing you to define what other types you want to export. Do not expect a perfect workflow, though. For example, the library is not smart enough to infer whether a method or parameter is nullable or not.
Nevertheless, this was still a big improvement compared to the status quo. Now we had a strategy:
- Export Java types to Typescript definitions using java2typescript to allow IDE auto-completion and hints of Java’s and our custom types.
- Create our mapping scripts with Typescript.
- Transpile everything to Javascript.
Modularity
As a project grows in size and complexity, the need to break your application in multiple parts and reuse those parts in multiple places increases. Although GraalJS supports ES6’s modules and NodeJS’s (when using GraalVM), it requires a custom implementation to load module files from within a fat Jar.
There are some drawbacks, though.
Unfortunately, source maps support was limited at the time of writing. While debugging support with source maps is possible (we’ll talk about debugging later), errors will point to the bundled JS instead. So, test your stuff, and additionally, if your pipeline does not guarantee reproducible builds and artifacts, consider making the bundle source controlled or otherwise have it easily accessible in case these errors happen.
Let’s do this
With all that in mind, let’s actually implement it.
To have a picture of what I’ll be talking about, here’s the structure and files we’ll walk you through (you can also check the repository):
First, we created the js
sources folder with all our Typescript and node stuff.
As promised, we’re now using Typescript. Notice that now we’re importing the Java types. These come from js/common/java-types/j2ts
that were generated by java2ts-processor
processor. We’ll later explain how this works.
Also, now we also have IDE hinting!
Notice that there’s separate imports for the MappingResult
return and Construct
types. This is because java2ts-processor
generates type definitions and their constructors separately. One in a type definitions file (interface only), another in actual implementation type, using Java.type
as seen earlier, which allows us to actually create Java objects within Javascript.
You can take a look at what those look like in the repository here and here.
For transpiling and bundling we’re using esbuild with the following configuration:
This configuration will transpile Typescript to Javascript and bundle everything and export it to the ../resources/js/index.js
file, which is then evaluated the by GraalJS!
It’s important to notice bundling this for browser and globally namespacing our functions to the Mapping
global name. This will generate an Immediately Invoked Function Expression (IIFE) that will provide our function into a global variable named Mapping
. This prevents Tree shaking so that we’re still able to access our functions.
Additionally, we’re also enabling keepNames
so we can consistently reference the exported functions within our bundle.
Check index.js
to have a better picture of what’s being generated.
esbuild automatically detects file types and processes them accordingly. So it’ll process Typescript with the Typescript version available in our environment.
On the Java side, we’re now importing and java2ts-processor
dependency and using the maven-processor-plugin
to trigger it. Check it out in the pom.xml. Now, running mvn clean compile
will generate the Typescript type definitions from Java.
In order to tell java2ts-processor
what classes to export, we create and define the following in package-info.java
:
We annotate the root package with the Java2TS
annotation and we add some types for it to export.
When the flag export
is set to true, it will not only export the type definition (interface) but also the constructors, as explained above.
The final point on the Java side, our Main class that will trigger this example:
First, we’re now depending on the bundled index.js
(not index.ts
) which contains all our mapping code, but also the lorem ipsum dependency we added to the mapped object.
Second, we’re now getting the Mapping
member before getting the map
function, as a result of now having an IIFE, as explained above.
We’re not going to go in depth here, since this heavily depends on your workflow and objectives, but the transpilation and bundling steps could also be made part of your maven and CI pipelines, with, for example, the frontend-maven-plugin.
Thread safety, Spring integration, Debugging and Performance, it’s a doozy!
Great! We’ve adopted Typescript, automated Java type exports, improved interoperability and now have IDE hinting!
What’s next, you ask? Why, of course, thread safety, Spring support and logging!
Thread safety
So, you thought Context
could be shared among threads and execute our function safely? Ah!
Unfortunately that’s not the case, but it’s not too hard to make our code thread safe. Read up on https://www.graalvm.org/22.0/reference-manual/js/Multithreading/.
So how do we make our code thread safe? Instead of using a single Context
, in all threads, create a Context
for each thread! Not that fast though. Because by doing this you’ll also trigger multiple evaluations of the same scripts. This may not be ideal if you cannot “warm” all Context
s beforehand, or if you’re creating multiple Context
s at runtime, for example, a long running web app where threads get created and destroyed indefinitely. So how do we solve this? How do we avoid evaluating the same script multiple times and share caches between Context
s?
By default, Context
creates its own Engine
internally. By creating a shared Engine
object, the Context
s will, eventually, use the cached version that Engine
provides them.
This is all explained in the comments of the example below:
Spring integration
So, how do we integrate this with Spring? Especially, how do we guarantee that each thread is fed a different Context
?
For this example, we’ll be creating a classic web project that greets a person, given their name. You can check in the repository.
Since most web servers Spring uses, uses a thread pool model, we’ll need to use what we’ve learned about multi-threading and prepare our bean creation accordingly. Explaining all details in depth would justify its own article, so I’ll keep it to the point and skip some details.
In the end, we have the following Spring configuration and accompanying Javascript Greeting service:
In order to have Spring create a different instance for each web client thread, we had to use some specialized factories.
ThreadLocalTargetSource threadLocalJsContext
, when called, will check if the current thread already contains an instance of our GreetingService
which contains our Context
instance. If it exists, it will return it, if not, it will create one.
ProxyFactoryBean proxiedThreadedJsContext
is a proxy that enables AOP processing for beans. In our case, it will enable us to delegate bean creation to ThreadLocalTargetSource threadLocalJsContext
.
The result is that each thread in the web client thread pool will have its own GreetingService
and Context
instance.
Another point to take notice is that we’re using SLF4JBridgeHandler
to forward Graal’s logs to SLF4J.
Debugging
In order to aid during development and debugging, we’d also like to be able to use a debugger.
The Graal project has the chromeinspector
tool which allows debugging from within the Chromium debugger. IntelliJ also supports the protocol. chromeinspector
even supports source maps, which we’re now enabling to debug Typescript directly instead of the bundled Javascript.
So, first we’ll enable inline source maps in our esbuild.config.js
with sourcemap: 'inline'
and transpile and bundle our script again. You’ll now be able to see the source map in our bundle: /# sourceMappingURL=data:application/json;base64,...
Then, on the Java side, we need to add org.graalvm.tools:chromeinspector
and some new Engine
configurations that enable a debugging session:
We’re going to start our debugger on port 4444 and will NOT stop on the first operation by setting the Suspend option to false.
When starting the Spring Application, you’ll now see a new message:
Debugger listening on ws://127.0.0.1:4444/… For help, see: https://www.graalvm.org/tools/chrome-debugger E.g. in Chrome open: devtools://devtools/bundled/js_app.html?ws=127.0.0.1:4444/…
IntelliJ allows you to connect to the websocket and use their debugger. If you’re familiar with IntelliJ’s debugger, there’s nothing too different.
Improving performance with the GraalVM compiler
As referenced in their documentation, running GraalJS on the JDK will not be as optimized as if we were using GraalVM and its compiler.
However, we can add the GraalVM compiler via JEP 243: Java-Level JVM Compiler Interface so that the JVM uses it to JIT compile our JS code. An example is provided on their repository.
I’ve also included one in this article’s repository. Its usage is as follows:
- Use the
graal-compiler
maven profile to package the project. It will also download and copy the Graal compiler and related dependencies:mvn clean package -Pgraal-compiler
- Run the app with the JVMCI and related references:
java -XX:+UnlockExperimentalVMOptions -XX:+EnableJVMCI -XX:+UseJVMCICompiler --module-path=compiler/ --upgrade-module-path=compiler/compiler.jar:compiler/compiler-management.jar -jar spring-0.0.1-SNAPSHOT.jar
In order to make this more manageable, you should probably containerize both your JAR and related Graal dependencies and make the above your startup command.
Summary
In this article we got our feet wet with polyglot programming by adding Javascript support to an otherwise ordinary Java application.
We took advantage of Javascript’s native support of JSON to streamline our particular case of mappings between third parties, as a middleware.
We also took a step further by adding Typescript support to our flow and automating Java types export in order to enable IDE hints.
Finally, we handled some caveats and made our setup more production ready by handling debugging, multi-threading, Spring integration and performance improvements.
Hopefully this article gets your creativity running and opens some paths for you, for example, where polyglot programming could solve some of the issues you face when Java as a language is a limitation, or when you want to build an extensible, scriptable system…