Guide to Scripting With the Spring Framework
Scripting is one of the best ways to make your code customizable at runtime. Take a look!
Join the DZone community and get the full member experience.
Join For FreeScripting is one of the most popular ways to make your application adjustable for client needs at runtime. As always, this approach introduces a well-known trade-off between flexibility and manageability. This article shows the different ways you can adopt scripting in your project and how to incorporate a Spring library that provides a convenient scripting infrastructure and other useful features.
Introduction
Scripting (aka a plugin architecture) is the most straightforward way to make your application customizable at runtime. Quite often, scripting comes into your application not by design, but accidentally. Say you have a very unclear part in a functional specification, so not to waste one another day amongst additional business analysis, we decide to create an extension point and call a script that implements a stub. We will clarify how it should work later. There are a lot of well-known pros and cons for using such an approach, like great flexibility to define business logic at runtime and save a massive amount of time on redeployment. This avoids the impossibility to perform comprehensive testing and, hence, unpredictable problems with security, performance issues, and so on. The ways of scripting will be discussed further and might be helpful both for those who already decided to stick with scripting plugins in their Java application or those just now thinking about adding it to their code.
Nothing Special, Just Scripting
With Java’s JSR-233 API, evaluating scripts in Java is a simple task. There is a number of production-class evaluating engines implemented for this API (Nashorn, JRuby, Jython, etc.), so it is not a problem to add some scripting magic to your Java code, as shown here:
Map<String, Object> parameters = createParametersMap();
ScriptEngineManager manager = new ScriptEngineManager();
ScriptEngine scriptEngine = manager.getEngineByName("groovy");
Object result = scriptEngine.eval(script.getScriptAsString("discount.groovy"),
new SimpleBindings(parameters));
Obviously, having such code scattered over all your application is not a great idea when you have more than one script file and one invocation in your codebase, so you may extract this snippet into a separate method that is placed into a utility class. Sometimes, you might even go a bit further. You can create a special class (or set of classes) that group scripted business logic based on a business domain, like the class PricingScriptService
. This will let us wrap calls with evaluateGroovy()
into nice, strongly typed methods, but there is still some boilerplate code. All methods will contain parameter mapping, script text loading logic, and a script evaluation engine invocation similar to this:
public BigDecimal applyCustomerDiscount(Customer customer, BigDecimal orderAmount) {
Map<String, Object> params = new HashMap<>();
params.put("cust", customer);
params.put("amount", orderAmount);
return (BigDecimal)scripting.evalGroovy(getScriptSrc("discount.groovy"), params);
}
This approach brings more transparency in terms of knowing parameter types and returning the value type. And do not forget to add a rule prohibiting “unwrapped” scripting engine calls into your coding standards document!
Scripting On Steroids
Despite the fact that using scripting engines is quite simple, if you have a lot of scripts in your codebase, you may encounter some performance problems. As an example, you can use Groovy templates for reporting and run reports at the same time. Sooner or later, you’ll see that “simple” scripting is becoming a performance bottleneck.
That’s why some frameworks build their own scripting engine over existing APIs, adding some nice features for better performance, execution monitoring, polyglot scripting, etc.
For example, in the CUBA framework, there is a pretty sophisticated Scripting engine that implements features to improve script implementation and execution such as:
Class cache to avoid repetitive script compilation
Ability to write scripts using both Groovy and Java languages
JMX bean for scripting engine management
All of these improve performance and usability, but still, those are low-level APIs for creating parameter maps, fetching script text, etc. Therefore, we still need to group them into high order modules to use scripting efficiently in an application.
And it would be unfair not to mention new experimental GraalVM engine and its polyglot API that allows us to extend Java applications with other languages. So maybe we will see Nashorn retired sooner or later and be able to write on different programming languages in the same source file, but it’s in the future still.
Spring Framework: an Offer That Is Hard to Refuse?
In the Spring framework, we have a built-in scripting support over the JDK’s API. You can find a lot of useful classes in org.springframework.scripting.* packages. There are evaluators, factories, and all the tools you need to build your own scripting support.
Aside from low-level APIs, the Spring framework has an implementation that should simplify dealing with scripts in your application. You can define beans implemented in dynamic languages as described in the documentation.
All you need to do is to implement a class using a dynamic language like Groovy and describe a bean in the configuration XML like this:
<lang:groovy id="messenger" script-source="classpath:Messenger.groovy">
<lang:property name="message" value="I Can Do The Frug" />
</lang:groovy>
After that, you can inject the Messenger bean into your application classes using the XML config. That bean can be “refreshed” automatically in case of underlying script changes, be advised with AOP, etc.
This approach looks good, but you, as a developer, should implement full-fledged classes for your beans if you want to utilize all the power of dynamic language support. Real-life scripts may be pure functions; therefore, you need to add some extra code to your script just to keep it compatible with Spring. Also, nowadays, some developers think of an XML configuration as “outdated” compared to annotations and try to avoid using it, because bean definitions and injections are split between Java code and XML code. Though it is more a matter of taste rather than performance/compatibility/readability, etc., we might take it into account.
Scripting: Challenges and Ideas
So, everything has its price, and when you add scripting to your application, you may meet some challenges:
Manageability — Usually scripts are scattered along the application, so it is quite hard to manage numerous
evaluateGroovy
(or similar) calls.Discoverability — if something goes wrong in a calling script, it's quite hard to find the actual point in the source code. We should be able to find all script invocation points easily in our IDE.
Transparency — writing a scripted extension is not a trivial thing, as there is no information about variables sent to the script and no information about the result it should return. In the end, scripting can be only done by a developer and only looking into the sources.
Test and Updates — deploying (updating) a new script is always dangerous. There is no way to rollback and no tooling to test it before production.
It seems like hiding scripted method calls under regular Java methods may resolve most of these challenges. The preferable way is through injecting “scripted” beans and calling their methods with meaningful names rather than invoking another “eval” method from the utility class. Therefore, our code is becoming self-documented, a developer won’t need to look into file “disc_10_cl.groovy” to figure out parameter names, types, etc.
One more advantage — if all scripts have unique Java methods associated with them, it will be easy to find all extension points in the application using “Find Usages” feature in the IDE as well as to understand what the parameters for this script are and what they return.
This way of scripting makes testing simpler — we’ll be able to not only test these classes “as usual” but also use mocking frameworks as needed.
All of this reminds of the approach mentioned at the beginning of this article — “special” classes for scripted methods. And what if we go one step further and hide all calls to a scripting engine, parameter creation, etc. from a developer?
Scripting Repository Concept
The idea is pretty simple and should be familiar to all developers who worked with the Spring framework. We just create a Java interface and link its methods to the scripts somehow. As an example, the Spring Data JPA uses a similar approach where interface methods are transformed to SQL queries based on a method’s name and then executed by an ORM engine.
What Might We Need to Implement This Concept?
We will likely need a class-level annotation that will help us to detect script repository interfaces and construct a special Spring bean for them.
A method-level annotation will help us to link the method to its scripted implementation.
And it would be nice to have a default implementation for the method that is not a simple stub but a valid part of the business logic. It will work until we implement an algorithm developed by a business analyst. Or we can let him/her try to write the script!
Assume that you need to create a service to calculate a discount based on a user’s profile. And the business analyst says that we can safely assume that a 10 percent discount can be provided for all registered customers by default. We may think about the following code concept for this case:
@ScriptRepository
public interface PricingRepository {
@ScriptMethod
default BigDecimal applyCustomerDiscount(Customer customer,BigDecimal orderAmount) {
return orderAmount.multiply(new BigDecimal("0.9"));
}
}
And when it comes to a proper discounting algorithm implementation, the Groovy script will look like this:
def age = 50
if ((Calendar.YEAR - cust.birthday.year) >= age) {
return amount.multiply(0.75)
} else {
return amount.multiply(0.9)
}
An ultimate goal for all this is to let a developer implement an interface and the discounting algorithm script only. Additionally, they will not fumble around with those “getEngine” and “eval” calls. The scripting solution should do all the magic for you — when the method is invoked, intercept the invocation, find and load the script text, evaluate it, and return the result (or execute default method if the script text is not found). The ideal usage should look similar to this:
@Service
public class CustomerServiceBean implements CustomerService {
@Inject
private PricingRepository pricingRepository;
//Other injected beans here
@Override
public BigDecimal applyCustomerDiscount(Customer cust, BigDecimal orderAmnt) {
if (customer.isRegistered()) {
return pricingRepository.applyCustomerDiscount(cust, orderAmnt);
} else {
return orderAmnt;
}
//Other service methods here
}
The script call is readable and the way it is invoked should be familiar to any Java developer.
Those were the ideas used to create a library for script repository implementation using the Spring framework. The library has facilities for script text load from different sources and evaluation as well as APIs that allow a developer to implement extensions for the library if needed.
How It Works
The library introduces some annotations (as well as XML config for those who prefer it) that initiate dynamic proxies construction for all repository interfaces marked with the @ScriptRepository
annotation during its context initialization. Those proxies are published as singleton beans that implement repository interfaces, meaning that you can inject those proxies into your beans using @Autowired
or @Inject
exactly as shown in the code snippet from the previous section.
The @EnableSpringRepositories
annotation used on one of the application configuration classes to activate script repositories. This approach is similar to other familiar Spring annotations like @EnableJpaRepositories
or @EnableMongoRepositories
. And for this annotation, you need to specify the array of package names that should be scanned similarly to JPA repositories.
@Configuration
@EnableScriptRepositories(basePackages = {"com.example", "com.sample"})
public class CoreConfig {
//More configuration here.
}
As it was shown before, we need to mark every method in script repository with @ScriptMethod
( the library provides @GroovyScript
and @JavaScript
as well) to add metadata to those calls and indicate that these methods are scripted. And the default implementation for the scripted methods is supported, of course.
All components of the solution are displayed in the diagram below. Blue shapes are related to the application code and white ones to the library. Spring beans are marked with the Spring logo.
When an interface’s scripted method is called, it is intercepted by a proxy class, which performs a lookup for two beans — a provider to get implementing script text and an evaluator to get the result. After script evaluation, the result is returned to a calling service.
Both the provider and evaluator may be specified in the @ScriptMethod
annotation properties as well as execution timeout (library provides default values for these properties, though):
@ScriptRepository
public interface PricingRepository {
@ScriptMethod (providerBeanName = "resourceProvider",
evaluatorBeanName = "groovyEvaluator",
timeout = 100)
default BigDecimal applyCustomerDiscount(
@ScriptParam("cust") Customer customer,
@ScriptParam("amount") BigDecimal orderAmount) {
return orderAmount.multiply(new BigDecimal("0.9"));
}
}
You may notice the @ScriptParam
annotation. We need them to provide names for the method’s parameters. Those names should be used in the script since the Java compiler erases actual parameter names on the compilation. You can omit those annotations. In this case, you’ll need to name the script’s parameters “arg0”, “arg1”, etc. which affect code readability.
By default, the library has providers that can read Groovy and JavaScript files from the file system and JSR-233-based evaluators for both script languages. You can create custom providers and evaluators for different script stores and execution engines though. All these facilities are based on Spring framework interfaces ( org.springframework.scripting.ScriptSource
and org.springframework.scripting.ScriptEvaluator
), so you can reuse all your Spring-based classes, e.g. StandardScriptEvaluator
instead of the default one.
Providers (as well as evaluators) are published as Spring beans because the script repository proxy resolves them by name for the sake of flexibility. You can substitute the default executor with a new one without changing application code, replacing one bean in the application context.
Testing and Versioning
Since scripts may be changed easily, we need to ensure that we won’t break the production server when we change a script. The library is compatible with the JUnit test framework. There is nothing special about it. Since you use it in a Spring-based application, you can test your scripts using both unit tests and integration tests as a part of the application before uploading them to production; mocking is also supported.
In addition, you can create a script provider that reads different script text versions from a database or even from Git or another source control system. In this case, it will be easiest to switch to a newer script version or to roll back to the previous version of a script if something goes wrong in production.
Conclusion
The library will help you arrange scripts in your code providing the following:
- By introducing Java interfaces, a developer always has information about script parameters and their types.
- Providers and evaluators help you to get rid of scripting engine calls scattered through your application code.
- We can easily locate all script usages in the application code by using the “Find usages (references)” IDE command or just simple text search by method name.
On top of this, the Spring Boot autoconfiguration is supported, and you also can test your scripts before deploying them to production using familiar unit tests and a mocking technique.
The library has an API for getting scripts' metadata (method names, parameters, etc.) in runtime, you can get wrapped execution results if you want to avoid writing try.catch
blocks to deal with exceptions thrown by scripts. Also, it supports XML configurations if you prefer to store your config in this format.
Lastly, script execution time can be limited with a timeout parameter in an annotation.
Library sources can be found on GitHub.
Opinions expressed by DZone contributors are their own.
Comments