Script

Visão Geral

Scripts (quando bem utilizados e testados) permitem código dinâmico e desacoplado em seu sistema, eles são uma arma poderosa a ser utilizada a seu favor quando o ambiente de seu cliente é instável ou ainda não completamente entendido, tornando desnecessário gerar novas versões para pequenos ajustes. O módulo scripts-api fornece a estrutura básica para não apenas utilizar, mas também adicionar suporte a qualquer linguagem de script desejada. Um script é um pedaço de texto que será lido e interpretado em tempo real pela sua aplicação, e pode ser entendido como uma entidade completamente estruturada que possui seções bem definidas. A interface ScriptDefinition define estas seções, veja abaixo:

  • Nome
  • Identificação
  • Versão
  • Corpo

Todas contêm informações úteis para a execução do script, mas a mais importante é seu corpo, que guarda o que deve ser executado. O suporte a linguagens de script é dado através da implementação de diversas interfaces, a principal é a ScriptExecutor, que é responsável tanto por executar o script quanto por verificar suas dependências e validações. Para enviar um ScriptDefinition a um ScriptExecutor, é necessário utilizar um ScriptDispatcher para preparar um ScriptExecutionContext que será utilizado e e obtermos o resultado. Veja o esquema abaixo:

../../_images/Script_graph.png

Porque utilizar scripts?

Utilizar scripts é uma forma de permitr alterações dinâmicas no sistema, muito útil quando partes do código precisam ser alterados constantemente sem a necessidade de gerar novas versões do sistema.

O componente script do framework Jmine fornece a estrutura básica para utilizar scripts e adicionar suporte a duas linguagens de scripts: Beanshell e Groovy.

Requisitos

O componente de scripts deve fornecer uma interface para execução de scripts embarcados na aplicação, de forma a abstrair do restante da aplicação a linguagem utilizada. Isso é necessário para permitir o intercâmbio de linguagens, favorecendo a manutebilidade do sistema e diminuindo acoplamento.

A integração deve ser feita por linguagens que permitam implementações com as seguintes características:

  1. Expressividade - a linguagem de script deve permitir código altamente expressivo e conciso, de fácil leitura e manutenção.
  2. Performance - a linguagem de script deve permitir a implementação de funcionalidades com alta performance, não gerando lentidão na aplicação mesmo que utilizada de forma intensa.
  3. Modularidade - a linguagem deve permitir a segregação de funcionalidades entre diversos scripts, de forma a diminuir o acoplamento e facilitar a manutenção de funcionalidades complexas.
  4. Testabilidade - deve ser possível testar facilmente um script, seja de forma unitária ou integrada.
  5. Compatibilidade - a linguagem deve ser compatível com a JVM, executando na mesma nativamente e garantindo a portabilidade. Também deve ser compatível com ferramentas de suporte ao desenvolvimento, como editores de código e repositórios.

Escolha de linguagem

Originalmente, em 2007, foi escolhida a linguagem de scripts Beanshell para o módulo, uma das poucas linguagens embarcáveis para a JVM disponíveis na época. Nos últimos anos o panorama de linguagens de script na JVM evoluiu muito, contando com diversas opções, algumas delas bastante maduras. No início de 2014 foi realizado pelo Renato Lundberg um estudo comparativo das linguagens disponíveis para substituir o Beanshell, o que tornou-se necessário devido aos diversos problemas e falta de manutenção da linguagem.

O teste foi feito centrado na performance de execução, por ser um atributo objetivamente mensurável, dado que os demais requisitos são atendidos em maior ou menor grau por boas linguagens. Para a pesquisa foi selecionado um algoritmo matemático simples, o produto escalar de vetores, para ser implementado em todas as linguagens e o tempo de execução medido, com relação ao tempo de execução em Java puro. Também foi executado profiling para identificar o motivo da lentidão em algumas linguagens.

Os resultados foram os seguintes:

  1. Beanshell Compilado - 23 vezes mais lento que Java. Possui problemas de performance por ineficiência no bytecode gerado pelo compilador, que é de difícil manutenção e deve ser substituído, e foi mantido na lista apenas para comparação.
  2. Beanshell Interpretado - 437 vezes mais lento que Java. Possui péssima performance por ser apenas interpretado. Grave problema de manutenção, pois projeto Open Source está inativo há muitos anos.
  3. Groovy Dinâmico - 6 vezes mais lento que Java. Permite código altamente dinâmico, mas com queda de performance.
  4. Groovy Estático - mesmo tempo que Java. Alguns recursos da linguagem não ficam disponíveis no modo estático, mas possui ótima performance.
  5. Javascript - 208 vezes mais lento que Java. Na versão disponível o executor de javascript é interpretado, apresentando performance muito ruim.
  6. Jython - 13 vezes mais lento que Java. Linguagem apresenta problemas de performance ligados ao controle de concorrência de suas estruturas de dados nativas, além do tratamento numérico ineficiente.
  7. JRuby - 14 vezes mais lento que Java. Linguagem sofre com problemas de performance ligados a estruturas de dados e tratamento numérico ineficientes.

Dentre as linguagens testadas a linguagem Groovy foi a que apresentou melhores resultados. A linguagem é altamente expressiva, e possui boa performance mesmo no modo dinâmico, permitindo a implementação de trechos críticos em modo estático com excelente desempenho. A linguagem é Open Source, com uma ativa comunidade em torno do ecossistema, com desenvolvimento ativo, utilizado e apoiado por grandes empresas.

Com base nesse estudo, foi escolhida a linguagem Groovy como linguagem padrão para implementação de scripts em aplicações baseadas no Jmine.

Persistência

O script-persistence pode ser entendido como uma implementação da API de scripts, e fornece uma implementação da ScriptDefinition persistível, através da entidade Script. Um script possui algumas seções importantes além do seu próprio código.

../../_images/Script_persistence.png

Esta camada permite:

Tudo isto é extraído do ‘cabeçalho’ do script, o corpo, independentemente da linguagem, é armazenado como texto.

Informações Básicas

As seguintes informações são básicas para todos os scripts:

  • Mnemônico
  • Database ID
  • Descrição

Além disto, outros comentários podem ser adicionados, apenas com o objetivo de documentação, eles serão ignorados quando lidos:

/**
 * Mnemônico: LOGGED_USERS_MONITOR
 * Database ID: 1234
 * Descrição: Monitor de usuários logados
 */

// Corpo do script aqui

Propósito

Propósitos definem para quê e como o script será usado. Para criar um propósito você precisa além de cadastrar o postscript, também criar um script com o mesmo nome. Um propósito bem definido encapsula completamente o conteúdo dos scripts:

  • fornecendo uma API básica para lidar com sua entrada/saída
  • serve como único ponto de alteração caso seja necessário alterar as informações trocadas entre script e seu contexto de execução
/**
 * Mnemônico: LOGGED_USERS_MONITOR
 * Finalidade: SYSTEM_MONITOR
 * Database ID: 1234
 * Descrição: Monitor de usuários logados
 */

// Corpo do script aqui

Injeção de Dependência

Para utilizar a injeção de dependência é simples, basta informar o nome do bean e o nome da varável que receberá o bean. No exemplo abaixo, o script receberá o bean usersLoggedCounter a variável counter:

/**
 * Mnemônico: LOGGED_USERS_MONITOR
 * Finalidade: SYSTEM_MONITOR
 * Database ID: 1234
 * Descrição: Monitor de usuários logados
 * Inject bean: usersLoggedCounter into: counter
 */

// Corpo do script aqui

APIs

APIs podem ser entendidas como ‘bibliotecas’ para scripts. Uma API é um script que possui um conjunto de funções comuns passíveis de reutilização. Para importar uma API basta importar o seu Mnemônico.

/**
 * Mnemônico: LOGGED_USERS_MONITOR
 * Finalidade: SYSTEM_MONITOR
 * Database ID: 1234
 * Import: USERS_API, MAPS_API, TEC_API
 * Descrição: Monitor de usuários logados
 * Inject bean: usersLoggedCounter into: counter
 */

// Corpo do script aqui

Linguagens

No momento, temos apenas algumas linguagens suportadas pela estrutura de script, mas nada impede o suporte a outras linguagens. Beanshell, velocity e Groovy são exemplos do que já está disponível nos pacotes script-beanshell, script-velocity e script-groovy, além disto, também há o script-beanshell-compiler que implementa uma versão do próprio beanshell com algumas facilidades embutidas. Para implementar o suporte a uma linguagem específica, você precisa basicamente de:

  • HeaderExtractor
  • ScriptLanguageIdentifier
  • AbstractScriptDriver
  • ScriptExecutor
  • Testes
  • Classe utilitária para fornecer suporte à linguagem a um TransientScriptRepository

Testando Scripts

É possível testar unitariamente seus scripts usando o TransientScriptRepository, esta classe cria (ou permite que o desenvolvedor crie) mocks para tudo que está relacionado com o script (contexto de linguagem, dependências, injeção de dependência etc...), diminuindo consideravelmente o custo de execução e fazendo com que o desenvolvedor possa focar apenas no que seu script faz e não em montar todo o ambiente necessário para isto. Seus principais métodos são:

  • loadScript: carrega um script
  • addExecutor: adiciona suporte a um executor de scripts
  • addLoader: adiciona suporte a um loader de scripts
  • addLanguage: adiciona suporte a uma linguagem de script
  • addToBeanFactory: permite inserir beans a serem usados pelo mock de beanFactory do repositório

As classes utilitárias BeanshellSupport e VelocitySupport recebem um repositório e realizam chamadas para adicionar tudo o que for necessário para que ele possa lidar com as respectivas linguagens.