Let’s use the existing CountLinks.class file to create a GraalVM native image from it. We need two JSON files containing reflection configuration for GraalVM. The first one can be found here, and it contains a configuration of all dynamically generated runtime methods for Groovy 2.5.8. The second one contains only Groovy script class we created.

You can also generate dgm.json file on your own using the following Groovy script.

I would recommend using native-image-agent as explained in the previous blog post. It makes generating reflections JSON as easy as running a simple Java application.

Listing 2. src/countlinks.json [ { "name": "CountLinks", "allDeclaredConstructors": true, "allPublicConstructors": true, "allDeclaredMethods": true, "allPublicMethods": true } ]

Listing 3. Creating native image $ native-image -Dgroovy.grape.enable=false \ --no-server \ --allow-incomplete-classpath \ --no-fallback \ --report-unsupported-elements-at-runtime \ --initialize-at-build-time \ --initialize-at-run-time=org.codehaus.groovy.control.XStreamUtils,groovy.grape.GrapeIvy \ -H:ConfigurationFileDirectories=out/conf/ \ --enable-url-protocols=http,https \ -cp ".:$HOME/.m2/repository/org/codehaus/groovy/groovy/2.5.8/groovy-2.5.8.jar:$HOME/.groovy/grapes/org.jsoup/jsoup/jars/jsoup-1.11.3.jar" \ CountLinks [countlinks:305] classlist: 2,110.17 ms [countlinks:305] (cap): 998.28 ms [countlinks:305] setup: 2,746.31 ms [countlinks:305] (typeflow): 47,883.31 ms [countlinks:305] (objects): 107,634.87 ms [countlinks:305] (features): 1,475.31 ms [countlinks:305] analysis: 158,631.80 ms [countlinks:305] universe: 1,639.31 ms [countlinks:305] (parse): 5,070.39 ms [countlinks:305] (inline): 4,234.00 ms [countlinks:305] (compile): 34,543.96 ms [countlinks:305] compile: 46,402.57 ms [countlinks:305] image: 10,556.78 ms [countlinks:305] write: 1,365.01 ms [countlinks:305] [total]: 223,632.13 ms

The native image generation succeeds. Let’s run it.

$ ./countlinks https://e.printstacktrace.blog Exception in thread "main" groovy.lang.MissingMethodException: No signature of method: static org.codehaus.groovy.runtime.InvokerHelper.runScript() is applicable for argument types: (Class, [Ljava.lang.String;) values: [class CountLinks, [https://e.printstacktrace.blog]] at groovy.lang.MetaClassImpl.invokeStaticMissingMethod(MetaClassImpl.java:1528) at groovy.lang.MetaClassImpl.invokeStaticMethod(MetaClassImpl.java:1514) at org.codehaus.groovy.runtime.callsite.StaticMetaClassSite.call(StaticMetaClassSite.java:52) at org.codehaus.groovy.runtime.callsite.CallSiteArray.defaultCall(CallSiteArray.java:47) at org.codehaus.groovy.runtime.callsite.AbstractCallSite.call(AbstractCallSite.java:116) at org.codehaus.groovy.runtime.callsite.AbstractCallSite.call(AbstractCallSite.java:136) at CountLinks.main(CountLinks.groovy)

No luck. GraalVM throws this exception because at the current stage of the development it is not possible to invoke any Groovy script class that is not statically compiled. Let’s fix it. We use compiler configuration script file named compiler.groovy . It adds static compilation and type checking.

Listing 4. src/compiler.groovy withConfig(configuration) { ast(groovy.transform.CompileStatic) ast(groovy.transform.TypeChecked) }

Let’s recompile the code using compiler configuration script.

$ groovyc --configscript=compiler.groovy CountLinks.groovy org.codehaus.groovy.control.MultipleCompilationErrorsException: startup failed: CountLinks.groovy: 7: [Static type checking] - The variable [args] is undeclared. @ line 7, column 20. final String url = args[0] ^ 1 error

Bad luck. The error thrown by the static type checking says that there is no args variable available. We need to modify our initial script to make args variable available.

Listing 5. src/CountLinks.groovy #!groovy import org.jsoup.Jsoup import org.jsoup.nodes.Document @Grab(group='org.jsoup', module='jsoup', version='1.11.3') final String[] args = getProperty("args") as String[] final String url = args[0] final Document doc = Jsoup.connect(url).get() final int links = doc.select("a").size() println "Website ${url} contains ${links} links."

Before we create a native image, let’s run this statically compiled Groovy script as a Java program to see if it makes any difference comparing to the previous example. It is not a bulletproof benchmark, but it looks like the new bytecode executes in around 830 milliseconds.

$ time java -Dgroovy.grape.enable=false -cp ".:$HOME/.m2/repository/org/codehaus/groovy/groovy/2.5.8/groovy-2.5.8.jar:$HOME/.groovy/grapes/org.jsoup/jsoup/jars/jsoup-1.11.3.jar" CountLinks https://e.printstacktrace.blog Website https://e.printstacktrace.blog contains 95 links. java -Dgroovy.grape.enable=false -cp CountLinks 2,59s user 0,13s system 330% cpu 0,823 total

Let’s recreate the native image.

$ native-image -Dgroovy.grape.enable=false \ --no-server \ --allow-incomplete-classpath \ --no-fallback \ --report-unsupported-elements-at-runtime \ --initialize-at-build-time \ --initialize-at-run-time=org.codehaus.groovy.control.XStreamUtils,groovy.grape.GrapeIvy \ -H:ConfigurationFileDirectories=out/conf/ \ --enable-url-protocols=http,https \ -cp ".:$HOME/.m2/repository/org/codehaus/groovy/groovy/2.5.8/groovy-2.5.8.jar:$HOME/.groovy/grapes/org.jsoup/jsoup/jars/jsoup-1.11.3.jar" \ CountLinks [countlinks:17259] classlist: 1,989.96 ms [countlinks:17259] (cap): 989.83 ms [countlinks:17259] setup: 2,380.31 ms [countlinks:17259] (typeflow): 42,717.13 ms [countlinks:17259] (objects): 105,959.35 ms [countlinks:17259] (features): 1,133.75 ms [countlinks:17259] analysis: 151,461.35 ms [countlinks:17259] universe: 1,489.67 ms [countlinks:17259] (parse): 4,564.73 ms [countlinks:17259] (inline): 4,501.88 ms [countlinks:17259] (compile): 33,623.14 ms [countlinks:17259] compile: 45,452.90 ms [countlinks:17259] image: 9,294.79 ms [countlinks:17259] write: 743.83 ms [countlinks:17259] [total]: 212,978.90 ms

And let’s run it.

$ time ./countlinks https://e.printstacktrace.blog WARNING: The sunec native library, required by the SunEC provider, could not be loaded. This library is usually shipped as part of the JDK and can be found under <JAVA_HOME>/jre/lib/<platform>/libsunec.so. It is loaded at run time via System.loadLibrary("sunec"), the first time services from SunEC are accessed. To use this provider's services the java.library.path system property needs to be set accordingly to point to a location that contains libsunec.so. Note that if java.library.path is not set it defaults to the current working directory. Exception in thread "main" org.codehaus.groovy.runtime.InvokerInvocationException: java.lang.UnsatisfiedLinkError: sun.security.ec.ECDSASignature.verifySignedDigest([B[B[B[B)Z [symbol: Java_sun_security_ec_ECDSASignature_verifySignedDigest or Java_sun_security_ec_ECDSASignature_verifySignedDigest___3B_3B_3B_3B] at org.codehaus.groovy.reflection.CachedMethod.invoke(CachedMethod.java:111) at groovy.lang.MetaMethod.doMethodInvoke(MetaMethod.java:326) at groovy.lang.MetaClassImpl.invokeMethod(MetaClassImpl.java:1235) at groovy.lang.MetaClassImpl.invokeMethod(MetaClassImpl.java:1041) at org.codehaus.groovy.runtime.InvokerHelper.invokePogoMethod(InvokerHelper.java:1018) at org.codehaus.groovy.runtime.InvokerHelper.invokeMethod(InvokerHelper.java:1001) at org.codehaus.groovy.runtime.InvokerHelper.runScript(InvokerHelper.java:423) at CountLinks.main(CountLinks.groovy) Caused by: java.lang.UnsatisfiedLinkError: sun.security.ec.ECDSASignature.verifySignedDigest([B[B[B[B)Z [symbol: Java_sun_security_ec_ECDSASignature_verifySignedDigest or Java_sun_security_ec_ECDSASignature_verifySignedDigest___3B_3B_3B_3B] at com.oracle.svm.jni.access.JNINativeLinkage.getOrFindEntryPoint(JNINativeLinkage.java:145) at com.oracle.svm.jni.JNIGeneratedMethodSupport.nativeCallAddress(JNIGeneratedMethodSupport.java:54)

Another error. We already used to it, right? :) This time the error we see is entirely expected. GraalVM does not support HTTPS protocol by default , that is why we had to add --enable-url-protocols=https . However, the image we have built does not include required native library. It tries to load it, but it uses the current working directory, and it fails. The solution is simple - we need to add -Djava.library.path in the command line, and we are good to go.

$ time ./countlinks -Djava.library.path=$JAVA_HOME/jre/lib/amd64 https://e.printstacktrace.blog Website https://e.printstacktrace.blog contains 95 links. ./countlinks -Djava.library.path=$JAVA_HOME/jre/lib/amd64 0,02s user 0,01s system 18% cpu 0,196 total