GH-9507: Migrate Python support to GraalVM Polyglot
Fixes: #9507 Issue link: https://github.com/spring-projects/spring-integration/issues/9507 * Deprecate `PythonScriptExecutor` in favor of `PolyglotScriptExecutor` with a `python` as language * Add handling for `PolyglotWrapper` return type of the script evaluation * Rework `DeriveLanguageFromExtensionTests.testParseLanguage()` to the `@ParameterizedTest`
This commit is contained in:
@@ -87,7 +87,6 @@ ext {
|
||||
jsonpathVersion = '2.9.0'
|
||||
junit4Version = '4.13.2'
|
||||
junitJupiterVersion = '5.11.0'
|
||||
jythonVersion = '2.7.4'
|
||||
kotlinCoroutinesVersion = '1.8.1'
|
||||
kryoVersion = '5.6.0'
|
||||
lettuceVersion = '6.4.0.RELEASE'
|
||||
@@ -866,17 +865,18 @@ project('spring-integration-scripting') {
|
||||
optionalApi 'org.jetbrains.kotlin:kotlin-scripting-jsr223'
|
||||
provided "org.graalvm.sdk:graal-sdk:$graalvmVersion"
|
||||
provided "org.graalvm.polyglot:js:$graalvmVersion"
|
||||
provided "org.graalvm.polyglot:python:$graalvmVersion"
|
||||
|
||||
testImplementation "org.jruby:jruby-complete:$jrubyVersion"
|
||||
testImplementation 'org.apache.groovy:groovy-jsr223'
|
||||
testImplementation "org.python:jython-standalone:$jythonVersion"
|
||||
}
|
||||
|
||||
tasks.withType(JavaForkOptions) {
|
||||
jvmArgs '--add-opens', 'java.base/sun.nio.ch=ALL-UNNAMED',
|
||||
'--add-opens', 'java.base/java.io=ALL-UNNAMED',
|
||||
'--add-opens', 'java.base/java.lang=ALL-UNNAMED',
|
||||
'--add-opens', 'java.base/java.util=ALL-UNNAMED'
|
||||
'--add-opens', 'java.base/java.util=ALL-UNNAMED',
|
||||
'-Dpolyglot.engine.WarnInterpreterOnly=false'
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -21,7 +21,6 @@ import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.function.Function;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import org.springframework.beans.factory.BeanFactory;
|
||||
import org.springframework.beans.factory.SmartInitializingSingleton;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2022 the original author or authors.
|
||||
* Copyright 2022-2024 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -16,6 +16,8 @@
|
||||
|
||||
package org.springframework.integration.scripting;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import org.graalvm.polyglot.Context;
|
||||
@@ -36,7 +38,7 @@ public class PolyglotScriptExecutor implements ScriptExecutor {
|
||||
|
||||
private final String language;
|
||||
|
||||
private Context.Builder contextBuilder;
|
||||
private final Context.Builder contextBuilder;
|
||||
|
||||
/**
|
||||
* Construct an executor based on the provided language id.
|
||||
@@ -67,11 +69,28 @@ public class PolyglotScriptExecutor implements ScriptExecutor {
|
||||
Value bindings = context.getBindings(this.language);
|
||||
variables.forEach(bindings::putMember);
|
||||
}
|
||||
return context.eval(this.language, scriptSource.getScriptAsString()).as(Object.class);
|
||||
String scriptAsString = scriptSource.getScriptAsString();
|
||||
Object result = context.eval(this.language, scriptAsString).as(Object.class);
|
||||
// We have to copy all the expected PolyglotWrapper instances before context is closed.
|
||||
if (result instanceof Map<?, ?> map) {
|
||||
String returnVariable = parseReturnVariable(scriptAsString);
|
||||
result = map.get(returnVariable);
|
||||
}
|
||||
if (result instanceof List<?> list) {
|
||||
result = new ArrayList<>(list);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
catch (Exception ex) {
|
||||
throw new ScriptingException(ex.getMessage(), ex);
|
||||
}
|
||||
}
|
||||
|
||||
private static String parseReturnVariable(String script) {
|
||||
String[] lines = script.trim().split("\n");
|
||||
String lastLine = lines[lines.length - 1];
|
||||
String[] tokens = lastLine.split("=");
|
||||
return tokens[0].trim();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -32,9 +32,14 @@ import javax.script.ScriptEngine;
|
||||
*
|
||||
* @author David Turanski
|
||||
* @author Gary Russell
|
||||
* @author Artem Bilan
|
||||
*
|
||||
* @since 2.1
|
||||
*
|
||||
* @deprecated in favor of {@link org.springframework.integration.scripting.PolyglotScriptExecutor}
|
||||
* with a {@code python} language argument.
|
||||
*/
|
||||
@Deprecated(forRemoval = true, since = "6.4")
|
||||
public class PythonScriptExecutor extends AbstractScriptExecutor {
|
||||
|
||||
public PythonScriptExecutor() {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2023 the original author or authors.
|
||||
* Copyright 2002-2024 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -35,7 +35,7 @@ public final class ScriptExecutorFactory {
|
||||
|
||||
public static ScriptExecutor getScriptExecutor(String language) {
|
||||
if (language.equalsIgnoreCase("python") || language.equalsIgnoreCase("jython")) {
|
||||
return new PythonScriptExecutor();
|
||||
return new PolyglotScriptExecutor("python");
|
||||
}
|
||||
else if (language.equalsIgnoreCase("ruby") || language.equalsIgnoreCase("jruby")) {
|
||||
return new RubyScriptExecutor();
|
||||
@@ -56,11 +56,16 @@ public final class ScriptExecutorFactory {
|
||||
int index = scriptLocation.lastIndexOf('.') + 1;
|
||||
Assert.state(index > 0, () -> "Unable to determine language for script '" + scriptLocation + "'");
|
||||
String extension = scriptLocation.substring(index);
|
||||
if (extension.equals("kts")) {
|
||||
return "kotlin";
|
||||
}
|
||||
else if (extension.equals("js")) {
|
||||
return "js";
|
||||
switch (extension) {
|
||||
case "kts" -> {
|
||||
return "kotlin";
|
||||
}
|
||||
case "js" -> {
|
||||
return "js";
|
||||
}
|
||||
case "py" -> {
|
||||
return "python";
|
||||
}
|
||||
}
|
||||
ScriptEngineManager engineManager = new ScriptEngineManager();
|
||||
ScriptEngine engine = engineManager.getEngineByExtension(extension);
|
||||
|
||||
@@ -5,8 +5,9 @@
|
||||
xsi:schemaLocation="http://www.springframework.org/schema/beans https://www.springframework.org/schema/beans/spring-beans.xsd
|
||||
http://www.springframework.org/schema/integration/scripting https://www.springframework.org/schema/integration/scripting/spring-integration-scripting.xsd">
|
||||
|
||||
<int-script:script location="foo.rb"/>
|
||||
<int-script:script location="foo.groovy"/>
|
||||
<int-script:script location="foo.py"/>
|
||||
<int-script:script location="foo.kts"/>
|
||||
<int-script:script location="script.rb"/>
|
||||
<int-script:script location="script.groovy"/>
|
||||
<int-script:script location="script.py"/>
|
||||
<int-script:script location="script.kts"/>
|
||||
<int-script:script location="script.js"/>
|
||||
</beans>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2023 the original author or authors.
|
||||
* Copyright 2002-2024 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -16,14 +16,20 @@
|
||||
|
||||
package org.springframework.integration.scripting.jsr223;
|
||||
|
||||
import java.util.Map;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.params.ParameterizedTest;
|
||||
import org.junit.jupiter.params.aggregator.ArgumentsAccessor;
|
||||
import org.junit.jupiter.params.provider.Arguments;
|
||||
import org.junit.jupiter.params.provider.MethodSource;
|
||||
|
||||
import org.springframework.beans.factory.BeanDefinitionStoreException;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.context.ApplicationContext;
|
||||
import org.springframework.context.support.ClassPathXmlApplicationContext;
|
||||
import org.springframework.integration.scripting.PolyglotScriptExecutor;
|
||||
import org.springframework.integration.scripting.ScriptExecutor;
|
||||
import org.springframework.integration.test.util.TestUtils;
|
||||
import org.springframework.test.context.junit.jupiter.SpringJUnitConfig;
|
||||
|
||||
@@ -41,30 +47,26 @@ public class DeriveLanguageFromExtensionTests {
|
||||
@Autowired
|
||||
private ApplicationContext ctx;
|
||||
|
||||
@Test
|
||||
public void testParseLanguage() {
|
||||
String[] langs = {"ruby", "Groovy", "python", "kotlin"};
|
||||
Class<?>[] executors = {
|
||||
RubyScriptExecutor.class,
|
||||
DefaultScriptExecutor.class,
|
||||
PythonScriptExecutor.class,
|
||||
DefaultScriptExecutor.class
|
||||
};
|
||||
@ParameterizedTest
|
||||
@MethodSource("languageExecutorSource")
|
||||
public void testParseLanguage(String language, Class<?> executorClass, ArgumentsAccessor argumentsAccessor) {
|
||||
assertThat(this.ctx.getBeansOfType(ScriptExecutingMessageProcessor.class)).hasSize(5);
|
||||
|
||||
Map<String, ScriptExecutingMessageProcessor> scriptProcessors =
|
||||
this.ctx.getBeansOfType(ScriptExecutingMessageProcessor.class);
|
||||
assertThat(scriptProcessors.size()).isEqualTo(4);
|
||||
var processor =
|
||||
ctx.getBean(
|
||||
"org.springframework.integration.scripting.jsr223.ScriptExecutingMessageProcessor#" +
|
||||
(argumentsAccessor.getInvocationIndex() - 1),
|
||||
ScriptExecutingMessageProcessor.class);
|
||||
|
||||
for (int i = 0; i < 4; i++) {
|
||||
ScriptExecutingMessageProcessor processor = ctx.getBean(
|
||||
"org.springframework.integration.scripting.jsr223.ScriptExecutingMessageProcessor#" + i,
|
||||
ScriptExecutingMessageProcessor.class);
|
||||
|
||||
AbstractScriptExecutor executor =
|
||||
TestUtils.getPropertyValue(processor, "scriptExecutor", AbstractScriptExecutor.class);
|
||||
assertThat(executor.getScriptEngine().getFactory().getLanguageName()).isEqualTo(langs[i]);
|
||||
assertThat(executor.getClass()).isEqualTo(executors[i]);
|
||||
ScriptExecutor executor = TestUtils.getPropertyValue(processor, "scriptExecutor", ScriptExecutor.class);
|
||||
if (executor instanceof PolyglotScriptExecutor) {
|
||||
assertThat(TestUtils.getPropertyValue(executor, "language")).isEqualTo(language);
|
||||
}
|
||||
else {
|
||||
AbstractScriptExecutor abstractScriptExecutor = (AbstractScriptExecutor) executor;
|
||||
assertThat(abstractScriptExecutor.getScriptEngine().getFactory().getLanguageName()).isEqualTo(language);
|
||||
}
|
||||
assertThat(executor.getClass()).isEqualTo(executorClass);
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -85,4 +87,13 @@ public class DeriveLanguageFromExtensionTests {
|
||||
.withStackTraceContaining("Unable to determine language for script 'foo'");
|
||||
}
|
||||
|
||||
private static Stream<Arguments> languageExecutorSource() {
|
||||
return Stream.of(
|
||||
Arguments.of("ruby", RubyScriptExecutor.class),
|
||||
Arguments.of("Groovy", DefaultScriptExecutor.class),
|
||||
Arguments.of("python", PolyglotScriptExecutor.class),
|
||||
Arguments.of("kotlin", DefaultScriptExecutor.class),
|
||||
Arguments.of("js", PolyglotScriptExecutor.class));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -22,9 +22,9 @@ import java.util.Map;
|
||||
import org.assertj.core.api.InstanceOfAssertFactories;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.python.core.PyTuple;
|
||||
|
||||
import org.springframework.core.io.ClassPathResource;
|
||||
import org.springframework.integration.scripting.PolyglotScriptExecutor;
|
||||
import org.springframework.integration.scripting.ScriptExecutor;
|
||||
import org.springframework.scripting.ScriptSource;
|
||||
import org.springframework.scripting.support.ResourceScriptSource;
|
||||
@@ -44,7 +44,7 @@ public class PythonScriptExecutorTests {
|
||||
|
||||
@BeforeEach
|
||||
public void init() {
|
||||
this.executor = new PythonScriptExecutor();
|
||||
this.executor = new PolyglotScriptExecutor("python");
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -76,11 +76,8 @@ public class PythonScriptExecutorTests {
|
||||
new ClassPathResource("/org/springframework/integration/scripting/jsr223/test3.py"));
|
||||
Object obj = this.executor.executeScript(source);
|
||||
assertThat(obj)
|
||||
.isNotNull()
|
||||
.isInstanceOf(PyTuple.class)
|
||||
.asInstanceOf(InstanceOfAssertFactories.LIST)
|
||||
.element(0)
|
||||
.isEqualTo(1);
|
||||
.containsOnly(1, 2, 3);
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -92,11 +89,8 @@ public class PythonScriptExecutorTests {
|
||||
variables.put("foo", "bar");
|
||||
Object obj = this.executor.executeScript(source, variables);
|
||||
assertThat(obj)
|
||||
.isNotNull()
|
||||
.isInstanceOf(PyTuple.class)
|
||||
.asInstanceOf(InstanceOfAssertFactories.LIST)
|
||||
.element(0)
|
||||
.isEqualTo(1);
|
||||
.containsOnly(1, 2, 3);
|
||||
}
|
||||
|
||||
@Test
|
||||
|
||||
@@ -1,54 +0,0 @@
|
||||
/*
|
||||
* Copyright 2002-2024 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.integration.scripting.jsr223;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
import org.junit.Test;
|
||||
|
||||
import org.springframework.core.io.ClassPathResource;
|
||||
import org.springframework.scripting.ScriptSource;
|
||||
import org.springframework.scripting.support.ResourceScriptSource;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
/**
|
||||
* @author David Turanski
|
||||
*
|
||||
*/
|
||||
public class PythonVariableParserTests {
|
||||
|
||||
@Test
|
||||
public void testBasic() throws IOException {
|
||||
String var = PythonScriptExecutor.PythonVariableParser.parseReturnVariable("x=2");
|
||||
assertThat(var).isEqualTo("x");
|
||||
|
||||
var = PythonScriptExecutor.PythonVariableParser.parseReturnVariable("\n\n\nx = 2\n\n\n");
|
||||
assertThat(var).isEqualTo("x");
|
||||
|
||||
var = PythonScriptExecutor.PythonVariableParser.parseReturnVariable("\n\n\nx\n\n\n");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void test2() throws IOException {
|
||||
ScriptSource source =
|
||||
new ResourceScriptSource(new ClassPathResource("/org/springframework/integration/scripting/jsr223/test2.py"));
|
||||
String var = PythonScriptExecutor.PythonVariableParser.parseReturnVariable(source.getScriptAsString());
|
||||
assertThat(var).isEqualTo("bar");
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,5 +0,0 @@
|
||||
def foo(y):
|
||||
x=y
|
||||
return x
|
||||
|
||||
bar = foo(7)
|
||||
@@ -28,7 +28,7 @@ compile "org.springframework.integration:spring-integration-scripting:{project-v
|
||||
----
|
||||
======
|
||||
|
||||
In addition, you need to add a script engine implementation, e.g. JRuby, Jython.
|
||||
In addition, you need to add a script engine implementation, e.g. JRuby.
|
||||
|
||||
Starting with version 5.2, Spring Integration provides a Kotlin Jsr223 support.
|
||||
You need to add this dependency into your project to make it working:
|
||||
@@ -293,9 +293,14 @@ They are mutually exclusive.
|
||||
[[graalmv-polyglot]]
|
||||
=== GraalVM Polyglot
|
||||
|
||||
Starting with version 6.0, the framework provides a `PolyglotScriptExecutor` which is based the https://www.graalvm.org/22.2/reference-manual/embed-languages[GraalVM Polyglot API].
|
||||
Starting with version 6.0, the framework provides a `PolyglotScriptExecutor` which is based the https://www.graalvm.org/latest/reference-manual/languages/[GraalVM Polyglot API].
|
||||
The JSR223 engine implementation for JavaScript, removed from Java by itself, has been replaced by using this new script executor.
|
||||
See more information about enabling JavaScript support in GraalVM and what https://www.graalvm.org/22.2/reference-manual/js[configuration options] can be propagated via script variables.
|
||||
See more information about enabling JavaScript support in GraalVM and what https://www.graalvm.org/latest/reference-manual/js/[configuration options] can be propagated via script variables.
|
||||
|
||||
Starting with version 6.4, the Python scripts support has been migrated to GraalVM Polyglot as well.
|
||||
Now these scripts can be written in Python 3.x and can use third-party libraries.
|
||||
See https://www.graalvm.org/latest/reference-manual/python/[GraalPy] documentation for more information.
|
||||
|
||||
By default, the framework sets `allowAllAccess` to `true` on the shared Polyglot `Context` which enables this interaction with host JVM:
|
||||
|
||||
* The creation and use of new threads.
|
||||
@@ -307,6 +312,4 @@ By default, the framework sets `allowAllAccess` to `true` on the shared Polyglot
|
||||
* The creation and use of new sub-processes.
|
||||
* The access to process environment variables.
|
||||
|
||||
This can be customized via overloaded `PolyglotScriptExecutor` constructor which accepts a `org.graalvm.polyglot.Context.Builder`.
|
||||
|
||||
To enable this JavaScript support, GraalVM with the `js` component installed has to be used or, when using a regular JVM, the `org.graalvm.sdk:graal-sdk` and `org.graalvm.js:js` dependencies must be included.
|
||||
This can be customized via overloaded `PolyglotScriptExecutor` constructor which accepts a `org.graalvm.polyglot.Context.Builder`.
|
||||
@@ -84,3 +84,10 @@ See xref:mqtt.adoc[MQTT Support] for more information.
|
||||
|
||||
The `ZipTransformer` now exposes a `fileNameGenerator` property to customize a target zip file (and optional zip entry) name generation.
|
||||
See xref:zip.adoc[Zip Support] for more information.
|
||||
|
||||
|
||||
[[x6.4-scripting-changes]]
|
||||
=== Scripting Changes
|
||||
|
||||
The Python scripts evaluation is now migrated to the GraalVM Polyglot.
|
||||
See xref:scripting.adoc[Scripting Support] for more information.
|
||||
|
||||
Reference in New Issue
Block a user