We have seen how GraalJS can run Javascript in the JVM. One of the main use cases for embedding Javascript is to make our application ‘extendable,’ where the application can support having the key business logic pieces evolve without changing and releasing the whole application. Let us see how to design an example project applying this technique.
Design approach
The key decision during the design is to identify the boundary of business logic that we want as extendable pieces in our application. This goes hand-in-hand with our domain-driven design and bounded contexts. But, even within the context – what aspects of the context must remain in Java and what can be extended using Javascript? What level of access to the system will our Javascript context need for such implementation?
- For ‘validations’ that will help maintain the consistency of your aggregates?
- Will that need to read additional data from the database?
- Will that need to create or update entities to the database? What’s the transaction boundary?
- Will that need to create objects and invoke methods available in the host (Java) space?
- Will that need to access file system and/or write to it?
- …
Put it behind an interface
Our goal here is to have the application extendable – meaning – we should be able to swap the business logic easily without rebuilding the whole application. One good design pattern that can help us achieve this goal is ‘Ports & Adapters’ (popularly known as Hexagonal architecture).
There are two types of Ports.
- Driver Ports (I prefer to use the term ‘Application Interface’) using which application consumers/clients can use the application.
- Driven Ports (I prefer to use the term ‘Service Provider Interface’) using which the application demarcates boundaries and lets certain aspects be implemented by ‘Driven Adapters’ (SPI Implementations).
We would define a ‘Service Provider Interface (SPI)’ to decouple the business logic from the core application and use Javascript to implement the Adapter.
Backward & forward compatibility
It depends on whether we want the business logic (Javascript) modules tied to a certain version of our core application or it should be possible to “configure/import/deploy” the Javascript modules on different versions of our application, the SPI (Service Provider Interface) has to be designed for flexibility.
When we choose to have Javascript modules tied to a certain version of the application (perhaps, in the same branch in source control) and want to use Embedded Javascript only for delivering change without restarting the application in production, the methods in our SPI can be based on use-cases / features the application version supports.
But, if we want to keep the business logic as separate modules and support additional features or enriched experience based on the support provided by the version of Javascript module configured/imported in the application, the methods in our SPI should be flexible enough on both the input arguments and the return value.
For example:
interface ForDoingX {
float getVersion();
XOutput performX(XInput xInput);
}
Using a pair of Input and Output Value-Object (VO) will help support different versions of Javascript modules, where the VO can evolve to have additional input/output, and the core application can optionally use the getVersion to interpret output from the method.
Security
When we allow Javascript to be imported and executed in our JVM, we should use the right security settings so it can only perform a restricted number of operations that may be needed to provide the services.
Depending on the answers to the questions above (in the Design approach section), the classes Javascript can lookup, and instantiate should be limited. This can be achieved by using GraalJS Context Builder API.
@Override
public TaxRateOutput taxRate(TaxRateInput amount) {
try (Context context = Context
.newBuilder()
.engine(engine)
.allowExperimentalOptions(true)
.allowAllAccess(true)
.allowHostAccess(HostAccess.ALL)
.allowHostClassLookup(className -> {
return TaxRateOutput.class.getName().equals(className);
})
.build()) {
Value scriptContext = context.eval(this.source);
Value taxRateFunction = scriptContext.getMember("taxRate");
if(taxRateFunction != null && taxRateFunction.canExecute()) {
return taxRateFunction.execute(amount).as(TaxRateOutput.class);
}
return null;
}
}
Line 9 is the important piece where the Classes that are allowed for the Javascript is restricted based on use-case flow.
You can refer to the simple Tax Rate Calculation example using GraalJS-based SPI implementation adapter here in the GitHub: https://github.com/kmsquare-in/public-code/tree/main/extendable-java-app
Stay tuned for more.
Leave a Reply
You must be logged in to post a comment.