⬆️ ⬇️

Java REPL you do not ScriptEngine





Hello, Habr! My name is Dima, I am a developer in the “Architecture” team at hh.ru. Among other things, I make things easier for colleagues. Running code in production is a typical task. So when I heard that there are problems with this, I decided to fix them.



Not always data changes can be made simple UPDATE / INSERT - sometimes you need to use validation, event buses, and more. In such cases, the most optimal solution is to execute arbitrary code directly in the application. We have Java, so when JEP-222 appeared , I immediately thought that JShell might be able to make scripting convenient again. A miracle did not happen, and therefore under the cut you will find a not very deep comparison of the most famous Java interpreters (and "near-Java") for jvm with examples. I invite everyone interested under cat.



To run the scripts, we use BeanShell , and for 2019 it is terrible: the last release from 2016 , the lack of support for lambdas and even generics - all this forces us to write code that no one has written since Java 1.4 .

')

Criteria





Before starting the comparison, we formulate the requirements for the built-in scripting engine. Having scratched my head, I made up the following list:



  1. support for current java syntax;
  2. the ability to pass an external context to the interpreter;
  3. ability to interrupt execution;
  4. ability to redirect I / O;
  5. informative feedback.


The more the language in which we write scripts resembles the one we are developing, the fewer errors - hands remember. But when we make mistakes that were identified at the compilation stage, they should allow the developer to fix them - these are indications of the names of missing variables, lines, stacktracks etc.

Next, the scripts should work in a specific context, with access to the Spring context, to the logger that will serve the scripts. Without such an opportunity to transfer the context, its receipt turns into a quest.

If the error nevertheless leaked into runtime, then restarting the entire instance to stop execution is a bad idea, so you need to be able to simply interrupt the execution of the script at any time.

And the last - any messages to the system output during the script work only make sense in the context of this script. In system logs, such a conclusion is of little use. Therefore, I want to be able to redirect these messages in response.



So let's go



Jshell







I must say right away that JEP-222 does not aim to create an embedded interpreter - its purpose is REPL , that is, the ability to quickly prototype code. This is fraught with a number of consequences.



First, life did not prepare the Java compiler for the fact that it is possible to declare a method outside the class, and in the body of the method to use variables that have not yet been declared. Therefore, execution itself is hidden behind an impressive layer of abstraction.



Secondly, REPL may well be executed not locally, but somewhere on a remote machine, so the API is made with such features in mind. I think this is the main reason why the API does not have the ability to pass an external context to the interpreter and redirect I / O.

In addition, there are different startup modes - remote when the shell connects to the machine via JDI, and local . Since there is no possibility to convey the context programmatically, but we still really want to, the hope remains only in the local mode and that we can use code generation

But, unfortunately, the local mode was clearly not conceived as the main one - this kind of script calls the deadlock on the compiler. Despite the fact that the same code in JDI mode works without problems.



Thus, I had to abandon the use of JShell, although in general the API is strange, but understandable - we give the script to the input, we get the stream of events, for each of them we can check the status, get errors and billing information. Errors allow us to identify the expression in which it was allowed:







Beanshell







Failure made us pay attention to what we are using now. After a long break, the project seemed to come to life, and judging by the roadmap , it is confidently moving towards a release that will solve all our problems - a lot of features should work now.



At the time of this writing, beanshell did indeed support generics, but lambdas still don't work. Perhaps by the release of the situation the situation will change.

But in terms of integration, the engine is quite friendly - support for standard javax.scripting, runtime errors are verbose enough:







However, using lambda-free streams is hell that burns in hell. It might even be easier to write in another language. So I decided to take a closer look at the "near-java" segment. And the first candidate for the role of a script interpreter here, of course



Kotlin





Java code, with much luck, will be valid kotlin code. But I didn’t succeed in launching anything at least somehow adequate on java in kotlin, but nevertheless let's try.

Kotlin has announced javax.scripting support for a couple of years now .



The first problem we have to deal with is dependency-hell.

Kotlin-compiler includes org.jdom classes that started to fight with org.jdom in the application and wrap it ... So, we have kotlin-compiler-embeddable, where all these classes are put into custom packages.



However, after configuration, it turns out that the transfer of the external context does not work. And now this is a serious problem, until its solution it makes no sense to dig deeper. If you know what the problem is there and how to fix it - write in the comments.



Errors are also quite verbose:







Groovy







Grooves, in addition to supporting javax.scripting, provides its own, more advanced API for interpreter integration. For example, it is possible to pass an AST transformation, which allows you to add a conditional interrupt after each expression . The thing is so powerful that it’s already scary .



Moreover, Java (and especially beanshell) code can be quite valid groove code.

Integration and test operation was successful, with the exception of sheet initialization and lambda syntax (they have to be wrapped in curly braces), the existing binshell scripts worked without problems. Errors are more than verbose:







Perhaps today it is the only interpreter that, on the one hand, allows you to write code for the sample in 2019, and on the other hand, it meets all the requirements that it is reasonable to present to the interpreter.



What conclusions can we draw?



First and foremost: REPL is not a scripting engine. It may seem that from the command line to integration into the application one step, but upon closer examination it turns out that these tools have different tasks, sometimes contradicting each other.



The second - today there is not a single tool for executing scripts in Java, so if you need such a tool, be prepared to learn a new syntax.

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



All Articles