
Olá Habr! Meu nome é Dima, sou desenvolvedor da equipe de "Arquitetura" em hh.ru. Entre outras coisas, facilito as coisas para os colegas. A execução de código na produção é uma tarefa típica. Portanto, quando soube que há problemas com isso, decidi corrigi-los.
Nem sempre as alterações de dados podem ser feitas com simples atualização / inserção - às vezes você precisa usar validação, barramentos de eventos e muito mais. Nesses casos, a solução mais ideal é executar código arbitrário diretamente no aplicativo. Como temos o Java, quando o
JEP-222 apareceu , pensei imediatamente que o JShell poderia tornar o script conveniente novamente. Um milagre não aconteceu e, portanto, você encontrará uma comparação não muito profunda dos mais famosos intérpretes Java (e "quase Java") para a jvm com exemplos. Convido todos os interessados em gato.
Usamos o
BeanShell para executar scripts, e para 2019 é terrível: a última versão de
2016 , a falta de suporte a lambdas e até genéricos - tudo isso nos obriga a escrever código que ninguém escreveu desde o Java
1.4 .
Critérios
Antes de iniciar a comparação, formulamos os requisitos para o mecanismo de script interno. Tendo coçado a cabeça, fiz a seguinte lista:
- suporte à sintaxe java atual;
- a capacidade de passar um contexto externo para o intérprete;
- capacidade de interromper a execução;
- capacidade de redirecionar E / S;
- feedback informativo.
Quanto mais a linguagem em que escrevemos os scripts se assemelha à que estamos desenvolvendo, menos erros - lembram as mãos. Mas quando cometemos erros que foram identificados no estágio de compilação, eles devem permitir que o desenvolvedor os conserte - essas são indicações dos nomes das variáveis ausentes, linhas, trilhas de pilha, etc.
Em seguida, os scripts devem funcionar em um contexto específico, com acesso ao contexto Spring, ao criador de logs que servirá os scripts. Sem essa oportunidade de transferir o contexto, seu recebimento se transforma em uma missão.
Se, no entanto, o erro vazou no tempo de execução, reiniciar a instância inteira para interromper a execução é uma má ideia; portanto, você só precisa interromper a execução do script a qualquer momento.
E a última - todas as mensagens para a saída do sistema durante o trabalho do script só fazem sentido no contexto desse script. Nos logs do sistema, essa conclusão é de pouca utilidade. Portanto, quero poder redirecionar essas mensagens em resposta.
Então vamos
- suporte à sintaxe java atual - sim
- a capacidade de transmitir contexto - não
- capacidade de interromper a execução - sim
- a capacidade de redirecionar E / S - não
- feedback informativo - sim
Devo dizer
imediatamente que o
JEP-222 não tem como objetivo criar um intérprete incorporado - seu objetivo é o
REPL , ou seja, a capacidade de criar rapidamente protótipos de código. Isso está repleto de várias consequências.
Primeiro, a vida não preparou o compilador Java para o fato de que você pode declarar um método fora da classe e usar variáveis que ainda não foram declaradas no corpo do método. Portanto, a própria execução está oculta por trás de uma impressionante camada de abstração.
Em segundo lugar, o REPL pode muito bem ser executado não localmente, mas em algum lugar de uma máquina remota; portanto, a API é feita com esses recursos em mente. Eu acho que esse é o principal motivo pelo qual a API não tem a capacidade de passar um contexto externo para o intérprete e redirecionar a E / S.
Além disso, existem diferentes modos de inicialização -
remoto , quando o shell se conecta à máquina via JDI e
local . Como não há possibilidade de transmitir o contexto programaticamente, mas ainda queremos, a esperança permanece apenas no modo local e podemos usar a
geração de códigoMas, infelizmente, o modo local claramente não foi concebido como o principal -
esse tipo de script chama o
impasse no compilador. Apesar do fato de o mesmo código no modo JDI funcionar sem problemas.
Portanto, tive que abandonar o uso do JShell, embora em geral a API seja estranha, mas compreensível - damos o script à entrada, obtemos o fluxo de eventos, para cada um deles podemos verificar o status, obter erros e informações de cobrança. Erros nos permitem identificar a expressão na qual foi permitido:
UPDATE: ScriptEngine reunido do JShell . Mas para isso eu tive que escrever meu
modo de inicialização- suporte à sintaxe java atual - não
- a capacidade de transmitir contexto - sim
- capacidade de interromper a execução - sim
- a capacidade de redirecionar E / S - sim (mas requer o uso de métodos especiais)
- feedback informativo - sim
O fracasso nos fez prestar atenção ao que estamos usando agora. Após um longo intervalo, o projeto pareceu ganhar vida e, a julgar pelo
roteiro , está avançando com confiança para um lançamento que resolverá todos os nossos problemas - muitos recursos devem funcionar agora.
No momento da redação deste artigo, a casca de feijão realmente suportava genéricos, mas as lambdas ainda não funcionam. Talvez pela liberação da situação a situação mude.
Mas em termos de integração, o mecanismo é bastante amigável - suporte para javax.scripting padrão, erros de tempo de execução são detalhados o suficiente:

No entanto, o uso de fluxos sem lambda é um inferno que queima no inferno. Pode até ser mais fácil escrever em outro idioma. Então, decidi dar uma olhada no segmento "near-java". E o primeiro candidato para o papel de intérprete de script aqui, é claro
- suporte à sintaxe java atual - não
- a capacidade de transmitir contexto - não
- capacidade de interromper a execução - sim
- a capacidade de redirecionar E / S - não
- feedback informativo - sim
O código Java, com muita sorte, será um código kotlin válido. Mas não consegui lançar nada pelo menos de alguma forma adequado no java no kotlin, mas, no entanto, vamos tentar.
O Kotlin já
anunciou o suporte a javax.scripting há alguns anos.
O primeiro problema com o qual temos de lidar é o inferno da dependência.
O Kotlin-compiler inclui as classes org.jdom que começaram a combater o org.jdom no aplicativo e o agrupa ... Então, temos o kotlin-compiler-embeddable, onde todas essas classes são colocadas em pacotes personalizados.
No entanto, após a configuração, verifica-se que a transferência do contexto externo não funciona. E agora esse é um problema sério, até sua solução não faz sentido ir mais fundo. Se você sabe qual é o problema e como corrigi-lo - escreva nos comentários.
Os erros também são bastante detalhados:

- suporte à sintaxe java atual - não, mas existem análogos
- a capacidade de transmitir contexto - sim
- capacidade de interromper a execução - sim
- a capacidade de redirecionar E / S - sim
- feedback informativo - sim
O Grooves, além de oferecer suporte ao javax.scripting, fornece sua própria API, mais avançada para integração de intérpretes. Por exemplo, é possível passar uma transformação AST, que permite adicionar uma
interrupção condicional após cada expressão . A coisa é tão poderosa que já é
assustadora .
Além disso, o código Java (e especialmente a casca de feijão) pode ser um código groove bastante válido.
A operação de integração e teste foi bem-sucedida, com exceção da inicialização da planilha e da sintaxe lambda (elas precisam ser colocadas entre chaves), os scripts existentes do binshell funcionaram sem problemas. Os erros são mais do que detalhados:

Talvez hoje seja o único intérprete que, por um lado, permite escrever código para a amostra em 2019 e, por outro lado, atende a todos os requisitos que é razoável apresentar ao intérprete.
Que conclusões podemos tirar?
Primeiro e mais importante: o REPL não é um mecanismo de script. Pode parecer um passo da linha de comando para a integração no aplicativo, mas, após um exame mais detalhado, essas ferramentas têm tarefas diferentes, às vezes se contradizendo.
A segunda - hoje não existe uma única ferramenta para executar scripts em Java; portanto, se você precisar de uma ferramenta, esteja preparado para aprender uma nova sintaxe.