📜 ⬆️ ⬇️

Java instead of groovy

Suddenly it turns out that the project needs scripts and the question arises: what is better evolution or revolution?
But even an attempt to introduce a brute can fail in a legacy project with a conservative team. And management may find a dozen more reasons not to miss the gruv in the project. Although groovy is much simpler and closer to a programmer who knows java than the same scala.



But even in this case, you can use dynamically compiled scripts in the project. Let's learn how to compile java code dynamically in memory and run it in jvm, use dynamically loadable libraries from maven in it. I would like to write as little code as possible for this and that the process of use is as simple as possible. And I would also not like to hope for the availability of tools.jar in our program.

Warning the indignation of Groovy specialists, I confess that I myself love and use this dynamic programming language and made my modest contribution to Groovy Grape . Without detracting from the merits of Groovy, still try to apply java in the area where the jury on jvm is out of competition - dynamic compilation, interaction with existing java code and dynamic dependency import (what Grape does).
')

About compiling in java. JSR 199


The JSR 199 standard is the java Compiler API, which has been around for a long time. APIs are present in the java packages javax.tools. * . But in order to compile java code from memory to memory and then run it, you need to pretty write code and beat the tambourine . The compiler implementation does not go as part of the JRE and tools.jar is not in the maven repositories.

How to write less with JSR 199


I would like something ready, not to ride every time, and my colleague suggested the Janino project. Janino itself contains its own java subset compiler and is well suited only for evaluation by an expression. There is org.codehaus.janino: commons-compiler-jdk which uses JSR 199, but it depends only heavily on oracle / openjdk tools.jar. After the evening of the work, janino-commons-compiler-ecj (2.3 MB) appeared on file, which includes eclipse java compiler and commons-compiler-jdk modified for it. It is self-sufficient and allows you to compile and download code, even in the JRE. If you add mvn-classloader to it, then in scripts you can do the same magic with dynamic dependencies as in Groovy Grape.

For comparison, the library for the dynamic language mvel2 (989 KB) takes only a couple of times less space, but it does not allow doing such simple things as implementing an interface, defining an internal and instantiating an anonymous class, there is no similarity to the try / catch / finally construction and even debugging scripts on it may seem like hell.

Example


To compile a java script, only the com.github.igor-suhorukov dependency is needed: janino-commons-compiler-ecj: 1.0 and only 3 lines of code:
SimpleClassPathCompiler simpleCompiler = new SimpleClassPathCompiler(dependenciesUrls); simpleCompiler.cook(SCRIPT_NAME+".java", scriptSourceText); Class<?> clazz = simpleCompiler.getClassLoader().loadClass(SCRIPT_NAME); 


Not to be unfounded, there is an example of everything about what I tell on github . To run it, we need a JVM that supports java8, as in the example script there will be a Stream API.

So, let's begin:
 git clone https://github.com/igor-suhorukov/janino-commons-compiler-ecj-example.git cd janino-commons-compiler-ecj-example mvn test 


In order to import the class PhantomJsDowloader from the maven dependency com.github.igor-suhorukov: phantomjs-runner: 1.1 call MavenClassLoader and create a classpath compiler based on this maven artifact:
 List<URL> urlsCollection = MavenClassLoader.usingCentralRepo().getArtifactUrlsCollection("com.github.igor-suhorukov:phantomjs-runner:1.1", null); new SimpleClassPathCompiler(urlsCollection); 


Next, I provide the text of the main java program that loads dependencies from the repository, compiles the script and executes it.
janino-commons-compiler-ecj-example / src / test / java / org.codehaus.commons.compiler.jdk / SimpleClassPathCompilerTest.java
 package org.codehaus.commons.compiler.jdk; import com.github.igorsuhorukov.codehaus.plexus.util.IOUtil; import com.github.igorsuhorukov.smreed.dropship.MavenClassLoader; import org.junit.Test; import java.lang.reflect.Method; import java.net.URL; import java.util.List; public class SimpleClassPathCompilerTest { @Test public void testClassloader() throws Exception { final String SCRIPT_NAME = "MyScript"; List<URL> urlsCollection = MavenClassLoader.usingCentralRepo().getArtifactUrlsCollection("com.github.igor-suhorukov:phantomjs-runner:1.1", null); SimpleClassPathCompiler simpleCompiler = new SimpleClassPathCompiler(urlsCollection); simpleCompiler.setCompilerOptions("-8"); simpleCompiler.setDebuggingInformation(true,true,true); String src = IOUtil.toString(getClass().getResourceAsStream(String.format("/%s.java", SCRIPT_NAME))); simpleCompiler.cook(SCRIPT_NAME+".java", src); Class<?> clazz = simpleCompiler.getClassLoader().loadClass(SCRIPT_NAME); Method main = clazz.getMethod("main", String[].class); main.invoke(null, (Object) null); } public static void runIt(){ System.out.println("DONE!"); } } 


And this is the script itself, which uses the external library from maven and also calls the runIt method of the class that compiled it.
janino-commons-compiler-ecj-example / src / test / resources / MyScript.java
 import com.github.igorsuhorukov.phantomjs.PhantomJsDowloader; import com.github.igorsuhorukov.smreed.dropship.MavenClassLoader; import org.codehaus.commons.compiler.jdk.SimpleClassPathCompilerTest; import java.util.Arrays; import java.util.List; import java.util.stream.Collectors; public class MyScript{ public static void main(String[] args) throws Exception{ class Wrapper{ private String value; public Wrapper(String value) { this.value = value; } public String getValue() { return value; } } SimpleClassPathCompilerTest.runIt(); List<String> res = Arrays.asList(new Wrapper("do"), new Wrapper("something"), new Wrapper("wrong")).stream(). map(Wrapper::getValue).collect(Collectors.toList()); System.out.println(String.join(" ",res)); System.out.println("Classes from project classpath. For example "+MavenClassLoader.class.getName()); System.out.println(PhantomJsDowloader.getPhantomJsPath()); } } 


For example operation, the following dependencies are needed.

pom.xml
 <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd"> <modelVersion>4.0.0</modelVersion> <groupId>com.github.igor-suhorukov</groupId> <artifactId>janino-commons-compiler-ecj-example</artifactId> <packaging>jar</packaging> <version>1.0-SNAPSHOT</version> <properties> <maven.compiler.source>1.8</maven.compiler.source> <maven.compiler.target>1.8</maven.compiler.target> </properties> <dependencies> <dependency> <groupId>com.github.igor-suhorukov</groupId> <artifactId>janino-commons-compiler-ecj</artifactId> <version>1.0</version> <exclusions><exclusion><groupId>*</groupId><artifactId>*</artifactId></exclusion></exclusions> </dependency> <dependency> <groupId>com.github.igor-suhorukov</groupId> <artifactId>mvn-classloader</artifactId> <version>1.3</version> </dependency> <dependency> <groupId>junit</groupId> <artifactId>junit</artifactId> <version>4.12</version> <scope>test</scope> </dependency> </dependencies> </project> 


Java script debug


Debugging the script, as usual debugging java program. We set breakpoints and do not forget to include debugging information when compiling the script:
 simpleCompiler.setDebuggingInformation(true,true,true); 



findings


We learned how to compile java code from a java program by adding just a few lines. We can also include dependencies from the maven repositories in this script and debug the code in the IDE.

The approach from the article can replace groovy for scripts in the project, if the requirements do not allow using anything other than java or colleagues are hostile to the groove and nothing can be done with it. You can argue about AST / metaprogramming, that groovy is ahead and you will be right, in java this is not all simple. About working with the AST java program I told in the article " Parsing Java programs using a java program. ". We will try to solve the situation with metro programming in the following publications.

Despite the fact that the article describes the approach on “pure” java and political motives could influence the choice of this approach in the project, I think Java is better with Groovy than “Java instead of Groovy”.

Source: https://habr.com/ru/post/273683/


All Articles