Skip to main content

Spring Boot Scripting Language Runner 🛫

·6 mins

🛫 Spring Boot Scripting Language Runner #

Welcome, fellow code explorer! 🚀 In this post, I’m diving into a fun experiment: running scripting languages (JavaScript, Groovy, and Python) directly from Spring Boot. Why? Because sometimes config files just aren’t enough—you want real code as config!


🧩 Code as Config #

Modern software often needs configs that can be changed on the fly to tweak behavior. Enter “Config as Code” (CaC):

  • Store config in version control (like Git)
  • Track and audit changes just like source code
  • Make your app more dynamic and flexible

Example: Spring Boot application.yml

server:
  port: 8080

spring:
  datasource:
    url: jdbc:mysql://localhost:3306/mydb
    username: root
    password: secret
  jpa:
    hibernate:
      ddl-auto: update
    show-sql: true

myapp:
  feature:
    enable: true
  service:
    timeout: 5000

But what if you need your config to act like code? Maybe you want:

  • Reusability
  • On-the-fly changes
  • Arithmetic, string manipulation, selection, repetition

🧙 Scripting Language as Config #

For this experiment, I picked three scripting languages: JavaScript, Groovy, and Python. No deep reason—just wanted to see how each stacks up!


🧪 Testing Methodology #

I used Spring Boot as the host, with three endpoints for each language. Each runner does simple tasks: selection, string manipulation, repetition, and JSON work. Here’s a peek at the controller:

RunnerController.java

@Controller
public class RunnerController {

    private final GroovyRunner groovyRunner;

    private final JavascriptRunner javascriptRunner;

    private final PythonRunner pythonRunner;


    public RunnerController(GroovyRunner groovyRunner, 
                            JavascriptRunner javascriptRunner, 
                            PythonRunner pythonRunner) {
        this.groovyRunner = groovyRunner;
        this.javascriptRunner = javascriptRunner;
        this.pythonRunner = pythonRunner;
    }

    @PostMapping(value = "/groovy")
    public ResponseEntity<JSONObject> groovy(@RequestBody JSONObject request) {
        JSONObject response = groovyRunner.run(request);
        System.out.println(response.toJSONString());
        return ResponseEntity.status(200).body(response);
    }

    @PostMapping(value = "/javascript")
    public ResponseEntity<JSONObject> javascript(@RequestBody JSONObject request) {
        JSONObject response = javascriptRunner.run(request);
        System.out.println(response.toJSONString());
        return ResponseEntity.status(200).body(response);
    }

    @PostMapping(value = "/python")
    public ResponseEntity<JSONObject> python(@RequestBody JSONObject request) {
        JSONObject response = pythonRunner.run(request);
        System.out.println(response.toJSONString());
        return ResponseEntity.status(200).body(response);
    }

}

Each of the runner will perform simple task which consists of selection, string manipulation, repetition, and JSON manipulation

🖥️ My Test Rig #

For the curious, here’s my setup:

ComponentSpecification
OSWindows 11 Home Single Language
ProcessorIntel(R) Core(TM) i7-9750H CPU @ 2.60GHz (12 CPUs)
Memory16 GB DDR4-2666 SDRAM (2 x 8 GB)
Storage512 GB PCIe® NVMe™ M.2 SSD
Spring Bootv3.2.5
Java21.0.1

Javascript #

To run JavaScript from Spring Boot, I used GraalVM and the following dependencies:

pom.xml

<!-- Graalvm -->
<dependency>
    <groupId>org.graalvm.polyglot</groupId>
    <artifactId>polyglot</artifactId>
    <version>24.0.0</version>
</dependency>

<dependency>
    <groupId>org.graalvm.polyglot</groupId>
    <artifactId>js</artifactId>
    <version>24.0.0</version>
    <type>pom</type>
</dependency>

<dependency>
    <groupId>org.graalvm.polyglot</groupId>
    <artifactId>tools</artifactId>
    <version>24.0.0</version>
    <type>pom</type>
</dependency>

In my case, the scripting language is available in the code as script.js. For other needs, this maybe stored in seperate database or other storage.

JavascriptRunner.java

@Service
public class JavascriptRunner implements Runner {

    private final FileService fileService;

    public JavascriptRunner(FileService fileService) {
        this.fileService = fileService;
    }

    public JSONObject run(JSONObject input) {
        long startTime = System.currentTimeMillis();

        String content = fileService.getContentFromFile("/runner/javascript/script.js");
        JSONObject resultJson = new JSONObject();
        // Initialize context
        try (Context context = Context.create("js")) {
            // Set the binding
            context.getBindings("js").putMember("message", input.toJSONString());

            Value result = context.eval("js", content);
            resultJson = JSON.parseObject(result.asString());
            return resultJson;
        } finally {
            // endTime set when the result is parsed and returned
            long endTime = System.currentTimeMillis();
            resultJson.put("timecost", (endTime - startTime));
        }
    }
}

The timecost is measured internally when the method run is called and stopped when the Runner is able to complete the task. This timecost then appended to the result that will be used as data source for the plot.

script.js

function main(input) {
    let obj = JSON.parse(input);

    // if logic
    if (obj.age < 18) {
        obj.drink = false;
    } else {
        obj.drink = true;
    }

    // string manipulation
    obj.name = capitalizeFirstLetterOfEachWord(obj.name);

    // JSON manipulation
    let fullAddress = obj.addressDetail.street + " - House No " + obj.addressDetail.houseNo + " - Postal Code " + obj.addressDetail.postalCode;
    obj.additionalInfo = {
        fullAddress,
        job: obj.job
    }
    delete obj.job;
    delete obj.addressDetail;

    // putting null value
    obj.null = null;
    return JSON.stringify(obj);
}

function capitalizeFirstLetterOfEachWord(str) {
  return str.split(' ').map(word => {
    return word.charAt(0).toUpperCase() + word.slice(1).toLowerCase();
  }).join(' ');
}

main(message);

Groovy #

For Groovy, here’s the dependency:

pom.xml

<!-- Groovy -->
<dependency>
    <groupId>org.codehaus.groovy</groupId>
    <artifactId>groovy-all</artifactId>
    <version>3.0.17</version>
</dependency>

The Runner looks like this

GroovyRunner.java

@Service
public class GroovyRunner implements Runner {

    private final FileService fileService;

    public GroovyRunner (FileService fileService) {
        this.fileService = fileService;
    }

    public JSONObject run(JSONObject input) {
        long startTime = System.currentTimeMillis();
        String content = fileService.getContentFromFile("/runner/groovy/script.groovy");

        // Initialize the GroovyShell and set the binding
        Binding binding = new Binding();
        binding.setVariable("message", input.toJSONString());
        GroovyShell shell = new GroovyShell(binding);

        // Execute the script
        Object result = shell.evaluate(content);
        JSONObject resultJson = JSON.parseObject(result.toString());

        // endTime set where JSON is parsed
        long endTime = System.currentTimeMillis();

        resultJson.put("timecost", (endTime - startTime));
        return resultJson;
    }


}

script.groovy

import groovy.json.JsonSlurper
import groovy.json.JsonOutput

def main(String input) {
    def jsonSlurper = new JsonSlurper()
    def obj = jsonSlurper.parseText(input)

    // if logic
    if (obj['age'] < 18) {
        obj['drink'] = false
    } else {
        obj['drink'] = true
    }

    // string manipulation
    obj['name'] = capitalizeFirstLetterOfEachWord(obj.name)

    // JSON manipulation
    def fullAddress = "${obj['addressDetail']['street']} - House No ${obj['addressDetail']['houseNo']} - Postal Code ${obj['addressDetail']['postalCode']}"
    obj['additionalInfo'] = [
            fullAddress: fullAddress,
            job: obj['job']
    ]
    obj.remove('job')
    obj.remove('addressDetail')

    // putting null value
    obj['null'] = null

    return JsonOutput.toJson(obj)
}

def capitalizeFirstLetterOfEachWord(String str) {
    return str.split(' ').collect { word ->
        word[0].toUpperCase() + word[1..-1].toLowerCase()
    }.join(' ')
}

return main(message)

Python #

For Python, I used GraalVM’s python-community dependency:

pom.xml

<dependency>
    <groupId>org.graalvm.polyglot</groupId>
    <artifactId>python-community</artifactId>
    <version>24.0.0</version>
    <type>pom</type>
</dependency>

The Runner looks similar with Javascript version with a little bit of adjustment

PythonRunner.java

@Service
public class PythonRunner implements Runner {

    private final FileService fileService;

    public PythonRunner(FileService fileService) {
        this.fileService = fileService;
    }

    @Override
    public JSONObject run(JSONObject input) {
        long startTime = System.currentTimeMillis();

        String content = fileService.getContentFromFile("/runner/python/script.py");
        JSONObject resultJson = new JSONObject();
        try (Context context = Context.create("python")) {
            // Set the binding
            context.getBindings("python").putMember("message", input.toJSONString());

            Value result = context.eval("python", content);
            resultJson = JSON.parseObject(result.asString());
            return resultJson;
        } finally {
            // endTime set when the result is parsed and returned
            long endTime = System.currentTimeMillis();
            resultJson.put("timecost", (endTime - startTime));
        }
    }
}

script.py

import json

def capitalize_first_letter_of_each_word(s):
    return ' '.join(word.capitalize() for word in s.split(' '))

def main(input):
    obj = json.loads(input)

    # if logic
    if obj['age'] < 18:
        obj['drink'] = False
    else:
        obj['drink'] = True

    # string manipulation
    obj['name'] = capitalize_first_letter_of_each_word(obj['name'])

    # JSON manipulation
    full_address = f"{obj['addressDetail']['street']} - House No {obj['addressDetail']['houseNo']} - Postal Code {obj['addressDetail']['postalCode']}"
    obj['additionalInfo'] = {
        'fullAddress': full_address,
        'job': obj['job']
    }
    del obj['job']
    del obj['addressDetail']

    # putting null value
    obj['null'] = None
    return json.dumps(obj)

main(message)

📊 Testing Result #

After running the tests, here’s what I found:

  • 🐍 Python was the slowest on the first request (7156ms), but improved over time.
  • 🚀 Groovy was the fastest on the first run (722ms) and stayed quick.
  • JavaScript and Groovy were neck-and-neck for repeated requests (30-40ms).

For the next 50 requests:

  • JavaScript was the fastest (~10ms)
  • 🚀 Groovy followed (~20ms)
  • 🐍 Python stayed above 100ms

🏁 Conclusions #

Here’s what I learned:

  1. Performance:
    • JavaScript is the speed king for repeated requests.
    • Groovy is a solid, reliable choice.
    • Python lags behind, but still works if you need it.
  2. Use Case:
    • Pick your scripting language based on your app’s needs. If you want speed and flexibility, JavaScript or Groovy are great picks.

Using scripting languages for config can make your app super flexible, but always keep performance in mind.

Full code is here: spring-boot-script-runner 🛠️