In any incomprehensible situation go scripting

abelyaev's picture

Scripting is one of the most popular ways to make your application adjustable for client needs right at runtime. As always, this approach brings not only good, e.g. there is a well-known trade-off between flexibility and manageability. This article is not one of those which theoretically discuss pros and cons, it practically shows different ways of how to adopt scripting and introduces a Spring library that provides convenient scripting infrastructure and useful features.

Introduction

Scripting (aka plugin architecture) is the most straightforward way to make your application customizable in 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 for additional business analysis we decide to create an extension point and call a script that implements a stub - will clarify how it should work later.

There are a lot of well-known pros and cons of using such approach: e.g. great flexibility to define business logic in runtime and saving a massive amount of time on redeployment versus impossibility to perform comprehensive testing, hence, unpredictable problems with security, performance issues and so on.

The ways of doing scripting discussed further might be helpful both for the ones who already decided to stick with scripting plugins in their Java application or just 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 java code like shown here:

   Map 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 placed to utility class. Sometimes you might go even a bit further: you can create a special class (or set of classes) that group scripted business logic based on a business domain, e.g. class PricingScriptService. This will let us wrap calls to evaluateGroovy() into a nice strongly typed methods, but there is still some boilerplate code, all methods will contain parameter mapping, script text loading logic and script evaluation engine invocation similar to this:

public BigDecimal applyCustomerDiscount(Customer customer, BigDecimal orderAmount) {
  Map 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 return 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 use groovy templates for reporting and run a lot of 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 API, adding some nice features for better performance, execution monitoring, polyglot scripting, etc.

For example, in CUBA framework there is a pretty sophisticated Scripting engine that implements features to improve script implementation and execution such as:

  1. Class cache to avoid repetitive script compilation.
  2. Ability to write scripts using both Groovy and Java languages.
  3. 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: Offer That is Hard to Refuse?

In 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, etc. all the tools you need to build your own scripting support.

Aside from low-level APIs, 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 documentation.

All you need to do is to implement a class using a dynamic language like Groovy and describe a bean in 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 Messenger bean into your application classes using 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. In 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 of “outdated” comparing 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:

  1. Manageability - Usually scripts are scattered along the application, so it is quite hard to manage numerous evaluateGroovy (or similar) calls.
  2. 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.
  3. Transparency - writing a scripted extension is not a trivial thing, as there is no information about variables sent to the script and also there is 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.
  4. 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. Preferable way - inject “scripted” beans and call their methods with meaningful names rather than to invoke just another “eval” method from 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 IDE as well as to understand what are the parameters for this script and what it returns.

This way of doing scripting also makes testing simpler - we’ll be able not only to test these classes “as usual”, but also use mocking frameworks if needed.

All of this reminds of the approach mentioned at the beginning of this article - “special” classes for scripted methods. And what if we will go one step further and hide all the 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 Spring Framework. We just create a java interface and link its methods to scripts somehow. As an example, Spring Data JPA uses a similar approach, where interface methods are transformed to SQL queries based on method’s name and then executed by an ORM engine.

What we may need to implement the concept?

Probably a class level annotation that will help us to detect script repository interfaces and construct a special Spring bean for them.

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 write this 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% 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 proper discounting algorithm implementation the groovy script will be like this:

-------- file discount.groovy --------
def age = 50
if ((Calendar.YEAR - cust.birthday.year) &gt;= age) {
   return amount.multiply(0.75)
}
--------

An ultimate goal for all this - let a developer implement an only interface and the discounting algorithm script only, and do not fumble with all those “getEngine” and “eval” calls. Scripting solution should do all the magic: 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 is familiar to any java developer I guess.

Those were the ideas and they were used to create a library for script repositories implementation using Spring Framework. The library has facilities for script text load from different sources and evaluation as well as some 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 @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 in the previous section.

@EnableScriptRepositories annotation used on one of the application configuration classes activates 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 (library provides @GroovyScript and @JavaScript as well), to add metadata to those calls and indicate that these methods are scripted. And default implementation for scripted methods is supported, of course. All components of the solution are displayed in the diagram below. Blue shapes are related to application code, white ones - to the library. Spring beans are marked with a 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 provider and evaluator may be specified in @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 @ScriptParam annotation - we need them to provide names for method’s parameters. Those names should be used in the script since Java compiler erases actual parameter names on compilation. You can omit those annotations, in this case you’ll need to name script’s parameters “arg0”, “arg1”, etc. which affects code readability.

By default, the library has providers which 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 script repository proxy resolves them by name for the sake of flexibility - you can substitute default executor with a new one without changing application code, but 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 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 easy 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 to arrange scripts in your code providing the following:

  1. By introducing java interfaces, a developer always has an information about script parameters and their types.
  2. Providers and evaluators help you to get rid of scripting engine calls scattered through your application code.
  3. We can easily locate all script usages in the application code by using “Find usages (references)” IDE command or just simple text search by method name.

On top of this Spring Boot autoconfiguration is supported, and you also can test your scripts before deploying them to production using familiar unit tests and 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 configuration if you prefer to store your config in this format.

Also, script execution time can be limited with a timeout parameter in an annotation.

Library sources can be found at https://github.com/cuba-rnd/spring-script-repositories.

Read more

Comments