This is an automated email from the ASF dual-hosted git repository.
daniellansun pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/groovy.git
The following commit(s) were added to refs/heads/master by this push:
new 5d09bb508f GROOVY-12026: Graduate JavaShell from incubating to stable
5d09bb508f is described below
commit 5d09bb508f916c6c2cc6a0d239d2c947d6191cff
Author: Paul King <[email protected]>
AuthorDate: Fri May 22 10:13:20 2026 +1000
GROOVY-12026: Graduate JavaShell from incubating to stable
---
.../java/org/apache/groovy/util/JavaShell.java | 7 +-
src/spec/doc/core-testing-guide.adoc | 41 ++++++++++++
src/spec/doc/guide-integrating.adoc | 55 ++++++++++++++++
src/spec/test/IntegrationTest.groovy | 55 ++++++++++++++++
.../test/testingguide/JavaShellExampleTests.groovy | 75 ++++++++++++++++++++++
.../main/groovy/groovy/console/ui/Console.groovy | 2 +-
.../src/spec/doc/groovy-console.adoc | 17 +++++
7 files changed, 250 insertions(+), 2 deletions(-)
diff --git a/src/main/java/org/apache/groovy/util/JavaShell.java
b/src/main/java/org/apache/groovy/util/JavaShell.java
index 00892147f8..5155cac6dd 100644
--- a/src/main/java/org/apache/groovy/util/JavaShell.java
+++ b/src/main/java/org/apache/groovy/util/JavaShell.java
@@ -81,7 +81,6 @@ import java.util.concurrent.ConcurrentHashMap;
* additionally written to disk by {@code compileAllTo}). See the {@code
javac} documentation
* for the complete list of supported flags.
*/
-@Incubating
public class JavaShell {
private static final String MAIN_METHOD_NAME = "main";
private static final URL[] EMPTY_URL_ARRAY = new URL[0];
@@ -194,6 +193,10 @@ public class JavaShell {
private void doCompile(String className, String src, Iterable<String>
options) throws IOException {
JavaCompiler compiler = ToolProvider.getSystemJavaCompiler();
+ if (compiler == null) {
+ throw new IllegalStateException("JavaShell requires a JDK at
runtime; "
+ + "ToolProvider.getSystemJavaCompiler() returned null
(running on a JRE?).");
+ }
try (BytesJavaFileManager bjfm = new
BytesJavaFileManager(compiler.getStandardFileManager(null, locale, charset))) {
StringBuilderWriter out = new StringBuilderWriter();
JavaCompiler.CompilationTask task =
@@ -240,6 +243,7 @@ public class JavaShell {
* @throws JavaShellCompilationException if the source fails to compile
* @since 6.0.0
*/
+ @Incubating
public Map<String, Path> compileAllTo(String className, Iterable<String>
options,
String src, Path outputDir)
throws IOException {
@@ -269,6 +273,7 @@ public class JavaShell {
* @throws JavaShellCompilationException if the source fails to compile
* @since 6.0.0
*/
+ @Incubating
public Map<String, Path> compileAllTo(String className, String src, Path
outputDir)
throws IOException {
return compileAllTo(className, Collections.emptyList(), src,
outputDir);
diff --git a/src/spec/doc/core-testing-guide.adoc
b/src/spec/doc/core-testing-guide.adoc
index 1d3f128755..685a57b72b 100644
--- a/src/spec/doc/core-testing-guide.adoc
+++ b/src/spec/doc/core-testing-guide.adoc
@@ -21,6 +21,10 @@
= Testing Guide
+ifndef::guide-integrating[]
+:guide-integrating: guide-integrating.adoc
+endif::[]
+
== Introduction
The Groovy programming language comes with great support for writing tests. In
addition to the language
@@ -355,6 +359,43 @@ cobertura {
Several output formats can be chosen for Cobertura coverage reports and test
code coverage reports can be added to
continuous integration build tasks.
+==== JavaShell
+
+The `org.apache.groovy.util.JavaShell` class (covered in detail in the
+<<{guide-integrating}#integ-javashell,integration guide>>) is also useful in
tests when you need
+real Java classes alongside your Groovy code. It avoids two recurring
frictions:
+
+* you can declare Java fixture classes _inside_ the test that uses them,
instead of adding
+ `src/test/java/...` files that exist only to support one Groovy test;
+* you can capture genuine `javac`-emitted bytecode for use cases such as
comparing what the
+ Groovy compiler produces against what Java produces for an equivalent source
— useful when
+ asserting joint-compilation behaviour, annotation-processing output, or
`@CompileStatic`
+ codegen equivalence.
+
+A short fixture example:
+
+[source,groovy]
+----
+include::../test/testingguide/JavaShellExampleTests.groovy[tags=javashell_test_fixture,indent=0]
+----
+<1> compile a Java POJO in-memory; no `.java` file is added to the test source
tree
+<2> attach `JavaShell`'s class loader to a `GroovyShell` so the test's Groovy
code can resolve
+ the freshly-compiled class
+<3> exercise the fixture from Groovy
+
+For a bytecode-equivalence assertion, `compileAllTo` writes the generated
`.class` files to a
+temporary directory from which the bytes can be read and fed into a
bytecode-diff tool of choice
+(ASM, `javap` output, etc.). The corresponding Groovy-side bytecode can be
obtained through the
+`GroovyClassLoader` / `CompilationUnit` paths used elsewhere in Groovy's own
test suite.
+
+[source,groovy]
+----
+include::../test/testingguide/JavaShellExampleTests.groovy[tags=javashell_bytecode_capture,indent=0]
+----
+<1> compile and write the Java class to a temp directory
+<2> read the class bytes back from disk
+<3> compare against equivalent Groovy bytes obtained however best suits your
test
+
== Testing with JUnit
Groovy simplifies JUnit testing in the following ways:
diff --git a/src/spec/doc/guide-integrating.adoc
b/src/spec/doc/guide-integrating.adoc
index 4266c26c68..eedc3074f4 100644
--- a/src/spec/doc/guide-integrating.adoc
+++ b/src/spec/doc/guide-integrating.adoc
@@ -314,6 +314,61 @@ Hello, dependency 2!
Hello, dependency 2!
----
+[[integ-javashell]]
+=== JavaShell
+
+While `GroovyShell` and `GroovyClassLoader` compile Groovy source,
`org.apache.groovy.util.JavaShell`
+compiles _Java_ source in-memory using the platform
`javax.tools.JavaCompiler`. It is useful when a
+Groovy or polyglot application needs to compile and load Java classes at
runtime: code generation,
+plug-in loaders, scripting-style Java endpoints, or mixed-language testing
setups.
+
+NOTE: `JavaShell` requires a JDK at runtime, not just a JRE —
`ToolProvider.getSystemJavaCompiler()`
+returns `null` on a JRE.
+
+The simplest usage compiles a single source string and returns the loaded
`Class`:
+
+[source,groovy]
+----
+include::../test/IntegrationTest.groovy[tags=javashell_compile,indent=0]
+----
+<1> create a new `JavaShell` (an optional `ClassLoader` argument sets the
parent loader)
+<2> compile the source; `className` is the fully-qualified binary name and
must match the
+ `package` declaration — see the `JavaShell` javadoc for the full naming
rules
+<3> invoke the compiled method reflectively
+
+`compileAll` returns _every_ class produced from the source unit — useful when
the source declares
+auxiliary or nested classes:
+
+[source,groovy]
+----
+include::../test/IntegrationTest.groovy[tags=javashell_compile_all,indent=0]
+----
+<1> compile a primary public class together with its package-private helper
+<2> the returned map is keyed by binary class name
+
+To additionally persist the generated class files to disk (for tools that
consume `.class` files
+or to seed a classpath outside the JVM), use `compileAllTo`. It accepts a
`Path` output directory
+and writes each class to its conventional package subdirectory:
+
+[source,groovy]
+----
+include::../test/IntegrationTest.groovy[tags=javashell_compile_all_to,indent=0]
+----
+<1> compile and write to disk in one call; the second argument passes `javac`
options (here
+ `-parameters` to retain formal parameter names)
+<2> packaged classes land under `<outputDir>/<package as
path>/<ClassName>.class`; default-package
+ classes land directly under `outputDir`
+
+The compiled classes remain available through `js.getClassLoader()` after
`compileAllTo` returns,
+so the same instance covers both "compile to memory" and "compile to memory
and disk".
+
+When the source fails to compile, `JavaShell` throws
`JavaShellCompilationException` carrying the
+`javac` diagnostics. See the `JavaShell` javadoc for the full set of overloads
(with/without an
+`Iterable<String>` options argument) and the expected `className`/`options`
formats.
+
+NOTE: `compileAllTo` and its no-options overload are marked `@Incubating` —
their signatures may
+evolve in a subsequent 6.x release based on usage feedback.
+
=== CompilationUnit
Ultimately, it is possible to perform more operations during compilation by
relying directly on the
diff --git a/src/spec/test/IntegrationTest.groovy
b/src/spec/test/IntegrationTest.groovy
index ca2e71c9c9..0d08cf5944 100644
--- a/src/spec/test/IntegrationTest.groovy
+++ b/src/spec/test/IntegrationTest.groovy
@@ -254,6 +254,61 @@ try {
} finally {
tmpDir.deleteDir()
}
+'''
+ }
+
+ @Test
+ void testJavaShell() {
+ assertScript '''// tag::javashell_compile[]
+import org.apache.groovy.util.JavaShell
+
+def js = new JavaShell() //
<1>
+def src = """
+ package demo;
+ public class Foo {
+ public static String greet(String who) { return "hello, " + who; }
+ }
+"""
+Class<?> foo = js.compile('demo.Foo', src) //
<2>
+assert foo.getDeclaredMethod('greet', String).invoke(null, 'world') == 'hello,
world' // <3>
+// end::javashell_compile[]
+'''
+
+ assertScript '''// tag::javashell_compile_all[]
+import org.apache.groovy.util.JavaShell
+
+def js = new JavaShell()
+def src = """
+ package demo;
+ public class Box { public static int hidden() { return Helper.value(); } }
+ class Helper { static int value() { return 42; } }
+"""
+Map<String, Class<?>> classes = js.compileAll('demo.Box', src) //
<1>
+assert classes.keySet() == ['demo.Box', 'demo.Helper'] as Set //
<2>
+assert classes['demo.Box'].getDeclaredMethod('hidden').invoke(null) == 42
+// end::javashell_compile_all[]
+'''
+
+ assertScript '''// tag::javashell_compile_all_to[]
+import org.apache.groovy.util.JavaShell
+import java.nio.file.Files
+
+def out = Files.createTempDirectory('javashell-')
+try {
+ def js = new JavaShell()
+ def src = """
+ package demo;
+ public class Deep { public static String tag(String label) { return
label; } }
+ """
+ def written = js.compileAllTo('demo.Deep', ['-parameters'], src, out) //
<1>
+ def classFile = out.resolve('demo/Deep.class') //
<2>
+ assert written['demo.Deep'] == classFile && Files.exists(classFile)
+ // classes also remain available through js.classLoader, same as compileAll
+ assert js.classLoader.loadClass('demo.Deep').getDeclaredMethod('tag',
String).invoke(null, 'x') == 'x'
+} finally {
+ out.toFile().deleteDir()
+}
+// end::javashell_compile_all_to[]
'''
}
}
diff --git a/src/spec/test/testingguide/JavaShellExampleTests.groovy
b/src/spec/test/testingguide/JavaShellExampleTests.groovy
new file mode 100644
index 0000000000..87333a526c
--- /dev/null
+++ b/src/spec/test/testingguide/JavaShellExampleTests.groovy
@@ -0,0 +1,75 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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
+ *
+ * http://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 testingguide
+
+import org.junit.jupiter.api.Test
+
+import static groovy.test.GroovyAssert.assertScript
+
+final class JavaShellExampleTests {
+
+ @Test
+ void testJavaFixture() {
+ assertScript '''// tag::javashell_test_fixture[]
+import org.apache.groovy.util.JavaShell
+
+def js = new JavaShell()
+js.compile('fixtures.PojoFixture', """
+ package fixtures;
+ public class PojoFixture {
+ private final String name;
+ public PojoFixture(String name) { this.name = name; }
+ public String getName() { return name; }
+ }
+""")
// <1>
+
+// share JavaShell\'s class loader so Groovy code under test sees the fixture
+def shell = new GroovyShell(js.classLoader)
// <2>
+shell.evaluate """
+ def bean = fixtures.PojoFixture.newInstance('Groovy')
+ assert bean.name == 'Groovy'
+"""
// <3>
+// end::javashell_test_fixture[]
+'''
+ }
+
+ @Test
+ void testBytecodeCapture() {
+ assertScript '''// tag::javashell_bytecode_capture[]
+import org.apache.groovy.util.JavaShell
+import java.nio.file.Files
+
+def out = Files.createTempDirectory('bc-')
+try {
+ new JavaShell().compileAllTo('bc.Calc', """
+ package bc;
+ public class Calc { public static int add(int a, int b) { return a +
b; } }
+ """, out)
// <1>
+ byte[] javaBytes = Files.readAllBytes(out.resolve('bc/Calc.class'))
// <2>
+ // Feed javaBytes (and bytes for the equivalent Groovy source) into ASM,
+ // javap, or your bytecode-diff tool of choice for an equivalence
assertion. // <3>
+ assert javaBytes.length > 0
+ assert javaBytes[0..3] == [(byte)0xCA, (byte)0xFE, (byte)0xBA, (byte)0xBE]
+} finally {
+ out.toFile().deleteDir()
+}
+// end::javashell_bytecode_capture[]
+'''
+ }
+}
diff --git
a/subprojects/groovy-console/src/main/groovy/groovy/console/ui/Console.groovy
b/subprojects/groovy-console/src/main/groovy/groovy/console/ui/Console.groovy
index cce1b481ed..4958354781 100644
---
a/subprojects/groovy-console/src/main/groovy/groovy/console/ui/Console.groovy
+++
b/subprojects/groovy-console/src/main/groovy/groovy/console/ui/Console.groovy
@@ -1628,7 +1628,7 @@ class Console implements CaretListener,
HyperlinkListener, ComponentListener, Fo
Optional<String> optionalPrimaryClassName =
findPrimaryClassName(src)
if (optionalPrimaryClassName.isPresent()) {
def js = new
JavaShell(Thread.currentThread().contextClassLoader)
- js.run(optionalPrimaryClassName.get(), src)
+ js.run(optionalPrimaryClassName.get(), src,
Console.this.scriptArgsArray)
} else {
System.err.println('Initial parsing successful but no public
class found. Compile/run will not proceed.')
}
diff --git a/subprojects/groovy-console/src/spec/doc/groovy-console.adoc
b/subprojects/groovy-console/src/spec/doc/groovy-console.adoc
index 1ec3267a98..e8b19811b8 100644
--- a/subprojects/groovy-console/src/spec/doc/groovy-console.adoc
+++ b/subprojects/groovy-console/src/spec/doc/groovy-console.adoc
@@ -83,6 +83,23 @@ executed.
* You can turn the System.out capture on and off by selecting `Capture
System.out` from the `Actions` menu
+[[GroovyConsole-RunningAsJava]]
+=== Running and compiling as Java
+
+The console can also treat the editor contents as Java source rather than
Groovy:
+
+* `Script > Run as Java` (shortcut `Alt+R`) compiles the input as Java and
invokes its `main`
+ method, with any program arguments taken from `Script > Set Script
Arguments`.
+* `Script > Run Selection as Java` does the same for just the highlighted text.
+* `Script > Compile as Java` compiles the input without running it — useful as
a quick syntax
+ check.
+
+The source must declare a single top-level public class whose name matches a
fully-qualified
+binary name in any `package` declaration; the console parses the source to
locate that class
+and reports if none is found. Compilation and execution go through the
+`org.apache.groovy.util.JavaShell` API, so the same `javac` diagnostics
surface in the output
+area.
+
[[GroovyConsole-EditingFiles]]
=== Editing Files