Spring Boot Scripting Language Runner 🛫
🛫 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:
| Component | Specification |
|---|---|
OS | Windows 11 Home Single Language |
Processor | Intel(R) Core(TM) i7-9750H CPU @ 2.60GHz (12 CPUs) |
Memory | 16 GB DDR4-2666 SDRAM (2 x 8 GB) |
Storage | 512 GB PCIe® NVMe™ M.2 SSD |
Spring Boot | v3.2.5 |
Java | 21.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:
- Performance:
JavaScriptis the speed king for repeated requests.Groovyis a solid, reliable choice.Pythonlags behind, but still works if you need it.
- 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 🛠️