
Os scripts são uma das maneiras mais comuns de tornar um aplicativo mais flexível, com a capacidade de corrigir algo em qualquer lugar. Obviamente, essa abordagem também apresenta desvantagens: você deve sempre lembrar o equilíbrio entre flexibilidade e capacidade de gerenciamento. Porém, neste artigo, não discutiremos “de maneira geral” sobre os prós e os contras do uso de scripts, consideraremos maneiras práticas de implementar essa abordagem e também apresentaremos uma biblioteca que fornece uma infraestrutura conveniente para adicionar scripts a aplicativos escritos no Spring Framework.
Algumas palavras introdutórias
Quando você deseja adicionar a capacidade de alterar a lógica de negócios em um aplicativo sem recompilação e implantação subsequente, os scripts são uma das maneiras que vem à mente em primeiro lugar. Freqüentemente, os scripts aparecem não porque foram planejados, mas porque aconteceram. Por exemplo, na especificação, há uma parte da lógica que não está completamente clara no momento, mas para não passar mais alguns dias (e às vezes mais) para análise, você pode criar um ponto de extensão e chamar um script - um esboço. E então, é claro, esse script será reescrito quando os requisitos ficarem claros.
O método não é novo, e suas vantagens e desvantagens são bem conhecidas: flexibilidade - você pode alterar a lógica em um aplicativo em execução e economizar tempo em uma reinstalação, mas, por outro lado, os scripts são mais difíceis de testar, daí os possíveis problemas de segurança, desempenho etc.
Essas técnicas, que serão discutidas mais adiante, podem ser úteis tanto para desenvolvedores que já usam scripts em seus aplicativos quanto para aqueles que estão apenas pensando sobre isso.
Nada pessoal, apenas scripts
Com o JSR-233, o script em Java se tornou muito simples. Existem mecanismos de script suficientes baseados nessa API (Nashorn, JRuby, Jython e um pouco mais), portanto, adicionar um pouco de magia ao script não é um problema:
Map<String, Object> parameters = createParametersMap(); ScriptEngineManager manager = new ScriptEngineManager(); ScriptEngine scriptEngine = manager.getEngineByName("groovy"); Object result = scriptEngine.eval(script.getScriptAsString("discount.groovy"), new SimpleBindings(parameters));
Obviamente, se esse código estiver espalhado por todo o aplicativo, ele se tornará algo incompreensível. E, é claro, se você tiver mais de uma chamada de script em seu aplicativo, precisará criar uma classe separada para trabalhar com eles. Às vezes, você pode ir ainda mais longe e criar classes especiais que envolvem chamadas de
evaluateGroovy()
em métodos Java de tipo regular. Esses métodos terão um código de utilitário bastante uniforme, como no exemplo:
public BigDecimal applyCustomerDiscount(Customer customer, BigDecimal orderAmount) { Map<String, Object> params = new HashMap<>(); params.put("cust", customer); params.put("amount", orderAmount); return (BigDecimal)scripting.evalGroovy(getScriptSrc("discount.groovy"), params); }
Essa abordagem aumenta bastante a transparência ao chamar scripts a partir do código do aplicativo - você pode ver imediatamente quais parâmetros o script aceita, que tipo são e o que é retornado. O principal é não esquecer de adicionar aos padrões de gravação de código uma proibição de chamar scripts que não sejam de métodos digitados!
Desenvolvemos scripts
Apesar de os scripts serem simples, se você tiver muitos deles e usá-los intensivamente, há uma chance real de encontrar problemas de desempenho. Por exemplo, se você usar vários modelos de groovy para gerar relatórios e executá-los ao mesmo tempo, mais cedo ou mais tarde isso se tornará um dos gargalos no desempenho do aplicativo.
Portanto, muitas estruturas fazem vários complementos sobre a API padrão para melhorar a velocidade do trabalho, o armazenamento em cache, o monitoramento da execução, usando diferentes linguagens de script em um aplicativo, etc.
Por exemplo, um mecanismo de
script bastante engenhoso foi criado no CUBA que suporta recursos adicionais, como:
- Capacidade de escrever scripts em Java e Groovy
- Cache de classe para não recompilar scripts
- Compartimento JMX para controlar o mecanismo
Tudo isso, é claro, melhora o desempenho e a usabilidade, mas ainda assim o mecanismo de baixo nível permanece baixo e você ainda precisa ler o texto do script, passar parâmetros e chamar a API para executar o script. Portanto, você ainda precisa fazer algum tipo de wrapper em cada projeto para tornar o desenvolvimento ainda mais eficiente.
E seria injusto não mencionar o GraalVM - um mecanismo experimental que pode executar programas em diferentes idiomas (JVM e não JVM) e permite inserir
módulos nessas linguagens nos
aplicativos Java . Espero que Nashorn entre na história mais cedo ou mais tarde, e tenhamos a oportunidade de escrever partes do código em diferentes idiomas em uma fonte. Mas isso é apenas um sonho.
Spring Framework: uma oferta difícil de recusar?
O Spring possui suporte à execução de script integrado sobre a API JDK. No pacote
org.springframework.scripting.*
Você pode encontrar muitas classes úteis - tudo para que você possa usar convenientemente a API de baixo nível para scripts em seu aplicativo.
Além disso, há um nível mais alto de suporte, descrito em detalhes na
documentação . Em resumo - você precisa criar uma classe em uma linguagem de script (por exemplo, Groovy) e publicá-la como um bean por meio de uma descrição XML:
<lang:groovy id="messenger" script-source="classpath:Messenger.groovy"> <lang:property name="message" value="I Can Do The Frug" /> </lang:groovy>
Depois que um bean é publicado, ele pode ser adicionado às suas classes usando IoC. O Spring fornece atualização automática do script ao alterar o texto no arquivo, você pode pendurar aspectos em métodos etc.
Parece bom, mas você precisa criar classes "reais" para publicá-las; não é possível escrever uma função regular em um script. Além disso, os scripts podem ser armazenados apenas no sistema de arquivos, para usar o banco de dados que você precisa escalar no Spring. Sim, e muitos consideram a configuração XML obsoleta, especialmente se o aplicativo já tiver tudo nas anotações. É claro que isso é saboroso, mas geralmente é preciso contar com isso.
Scripts: Dificuldades e Idéias
Portanto, cada solução tem seu próprio preço e, se falarmos sobre scripts em aplicativos Java, ao introduzir essa tecnologia, podemos encontrar algumas dificuldades:
- Gerenciabilidade. Freqüentemente, as chamadas de script estão espalhadas por todo o aplicativo e, com alterações no código, é bastante difícil rastrear as chamadas dos scripts necessários.
- Capacidade de encontrar pontos de discagem. Se algo der errado em um script específico, a localização de todos os seus
evaluateGroovy()
discagem será um problema, a menos que você aplique uma pesquisa por nome de arquivo ou por chamadas de método, como evaluateGroovy()
- Transparência Escrever um script não é uma tarefa fácil por si só, e ainda mais difícil é para quem chama esse script. Você precisa se lembrar de como os parâmetros de entrada são chamados, que tipo de dados eles têm e qual é o resultado da execução. Ou observe o código-fonte do script sempre.
- Testando e atualizando - nem sempre é possível testar o script no ambiente do código do aplicativo e, após carregá-lo no servidor de "batalha", você precisa, de alguma forma, reverter tudo rapidamente se algo der errado.
Parece que o agrupamento de chamadas de script nos métodos Java ajudará a resolver a maioria dos problemas acima. É muito bom que essas classes possam ser publicadas no contêiner de IoC e chamar métodos com nomes normais e significativos em seus serviços, em vez de chamar
eval(“disc_10_cl.groovy”)
de alguma classe de utilitário. Outra vantagem é que o código se auto-documenta, o desenvolvedor não precisa se preocupar com o tipo de algoritmo oculto por trás do nome do arquivo.
Além disso, se cada script for associado a apenas um método, você poderá encontrar rapidamente todos os pontos de discagem no aplicativo usando o menu “Find Usages” do IDE e entender o local do script em cada algoritmo de lógica de negócios específico.
O teste é simplificado - ele se transforma em teste de classe "normal", usando estruturas familiares, zombarias e muito mais.
Todas as opções acima estão de acordo com a idéia mencionada no início do artigo - classes “especiais” para métodos implementados por scripts. Mas e se você der mais um passo e ocultar todo o código de serviço do mesmo tipo para chamar os mecanismos de script do desenvolvedor, para que ele nem pense nisso (bem, quase)?
Repositórios de scripts - conceito
A idéia é bastante simples e deve ser familiar para aqueles que pelo menos uma vez trabalharam com o Spring, especialmente com o Spring JPA. O que você precisa é criar uma interface Java e chamar o script ao chamar seus métodos. Na JPA, a propósito, uma abordagem idêntica é usada - a chamada para CrudRepository é interceptada, com base no nome e nos parâmetros do método, é criada uma solicitação, que é então executada pelo mecanismo de banco de dados.
O que é necessário para implementar o conceito?
Primeiro, uma anotação em nível de classe para que você possa encontrar a interface - o repositório e fazer uma lixeira com base nela.
Além disso, as anotações sobre os métodos dessa interface provavelmente serão úteis para armazenar os metadados necessários para chamar o método. Por exemplo - onde obter o texto do script e qual mecanismo usar.
Uma adição útil será a capacidade de usar métodos com implementação na interface (também conhecida como padrão) - esse código funcionará até que o analista de negócios exiba uma versão mais completa do algoritmo e o desenvolvedor crie um script com base em
esta informação. Ou deixe o analista escrever o script e o desenvolvedor simplesmente o copia no servidor. Existem muitas opções :-)
Portanto, suponha que, para uma loja online, você precise fazer um serviço para calcular descontos com base no perfil do usuário. Ainda não está claro como fazer isso, mas o analista de negócios jura que todos os usuários registrados têm direito a um desconto de 10%; ele descobrirá o restante do cliente dentro de uma semana. O serviço é necessário amanhã - depois da temporada. Como seria o código para este caso?
@ScriptRepository public interface PricingRepository { @ScriptMethod default BigDecimal applyCustomerDiscount(Customer customer, BigDecimal orderAmount) { return orderAmount.multiply(new BigDecimal("0.9")); } }
E então o próprio algoritmo, escrito, por exemplo, em groovy, chegará a tempo, aí os descontos serão ligeiramente diferentes:
def age = 50 if ((Calendar.YEAR - customer.birthday.year) >= age) { return orderAmount.multiply(0.75) } else { return orderAmount.multiply(0.9) }
O objetivo de tudo isso é dar ao desenvolvedor a capacidade de escrever apenas o código da interface e o código do script, e não mexer com todas essas chamadas para
getEngine
,
eval
e outras. A biblioteca para trabalhar com scripts deve fazer toda a mágica - interceptar a invocação do método de interface, obter o texto do script, substituir os valores dos parâmetros, obter o mecanismo de script desejado, executar o script (ou chamar o método padrão se não houver texto de script) e retornar o valor. Idealmente, além do código que já foi escrito, o programa deve ter algo como isto:
@Service public class CustomerServiceBean implements CustomerService { @Inject private PricingRepository pricingRepository;
O desafio é legível, compreensível e, para fazê-lo, não é necessário ter nenhuma habilidade especial.
Essas foram as idéias com base nas quais uma pequena biblioteca para trabalhar com scripts foi feita. Destina-se a aplicativos Spring, essa estrutura foi usada para criar a biblioteca. Ele fornece uma API extensível para carregar scripts de várias fontes e executá-los, o que oculta o trabalho de rotina com os mecanismos de script.
Como isso funciona
Para todas as interfaces marcadas com
@ScriptRepository
, os objetos proxy são criados durante a inicialização do contexto Spring usando o método
newProxyInstance
da classe
Proxy
. Esses proxies são publicados no contexto do Spring como beans singleton, para que você possa declarar um campo de classe com um tipo de interface e colocar a anotação
@Autowired
ou
@Inject
nele. Exatamente como planejado.
A varredura e o processamento de interfaces de script são ativados usando a anotação
@EnableSriptRepositories
, assim como no Spring, JPA ou repositórios para MongoDB são ativados (
@EnableJpaRepositories
e
@EnableMongoRepositories
respectivamente). Como parâmetros de anotação, você precisa especificar uma matriz com os nomes dos pacotes que deseja verificar.
@Configuration @EnableScriptRepositories(basePackages = {"com.example", "com.sample"}) public class CoreConfig {
Os métodos devem ser anotados com
@ScriptMethod
(também existem
@GroovyScript
e
@JavaScript
, com a especialização correspondente) para adicionar metadados para chamar o script. Obviamente, os métodos padrão nas interfaces são suportados.
A estrutura geral da biblioteca é mostrada no diagrama. Componentes destacados em azul que precisam ser desenvolvidos, brancos - que já estão na biblioteca. O ícone do Spring marca os componentes que estão disponíveis no contexto do Spring.
Quando o método de interface é chamado (na verdade, o objeto proxy), o manipulador de chamadas é iniciado, que no contexto do aplicativo procura dois beans: o provedor, que procurará o texto do script, e o executor, que, de fato, o texto encontrado será executado. Em seguida, o manipulador retorna o resultado ao método de chamada.
Os nomes do
@ScriptMethod
provedor e do executor são especificados na anotação
@ScriptMethod
, onde você também pode definir um limite de tempo na execução do método. Abaixo está um exemplo de código de uso da biblioteca:
@ScriptRepository public interface PricingRepository { @ScriptMethod (providerBeanName = "resourceProvider", evaluatorBeanName = "groovyEvaluator", timeout = 100) default BigDecimal applyCustomerDiscount( @ScriptParam("cust") Customer customer, @ScriptParam("amount") BigDecimal orderAmount) { return orderAmount.multiply(new BigDecimal("0.9")); } }
Você pode observar
@ScriptParam
anotações
@ScriptParam
- elas são necessárias para indicar os nomes dos parâmetros ao transmiti-las para o script, pois o compilador Java apaga os nomes originais das fontes (existem maneiras de fazer isso não fazer isso, mas é melhor não confiar nele). Você pode omitir os nomes dos parâmetros, mas, neste caso, você precisará usar “arg0”, “arg1” no script, o que não melhora muito a legibilidade.
Por padrão, a biblioteca possui provedores para ler arquivos .groovy e .js do disco e dos executores correspondentes, que são wrappers da API JSR-233 padrão. Você pode criar seus próprios beans para diferentes fontes de script e para diferentes mecanismos. Para isso, é necessário implementar as interfaces correspondentes:
ScriptProvider
e
SpringEvaluator
. A primeira interface usa
org.springframework.scripting.ScriptSource
e a segunda é
org.springframework.scripting.ScriptEvaluator
. A API do Spring foi usada para que classes prontas pudessem ser usadas se já estiverem no aplicativo.
O fornecedor e o artista são pesquisados pelo nome para obter maior flexibilidade - você pode substituir os beans padrão da biblioteca em seu aplicativo nomeando seus componentes com os mesmos nomes.
Teste e controle de versão
Como os scripts mudam com frequência e facilidade, você precisa ter uma maneira de garantir que as alterações não quebrem nada. A biblioteca é compatível com o JUnit, o repositório pode ser simplesmente testado como uma classe regular como parte de um teste de unidade ou integração. As bibliotecas simuladas também são suportadas. Nos testes da biblioteca, você pode encontrar um exemplo de como fazer simulação no método de repositório de scripts.
Se a versão for necessária, você poderá criar um provedor que leia diferentes versões de scripts no sistema de arquivos, no banco de dados ou no Git, por exemplo. Portanto, será fácil organizar uma reversão para a versão anterior do script em caso de problemas no servidor principal.
Total
A biblioteca apresentada ajudará a organizar scripts no aplicativo Spring:
- O desenvolvedor sempre terá informações sobre quais parâmetros os scripts precisam e o que é retornado. E se os métodos de interface tiverem um nome significativo, o que o script fará.
- Provedores e executores ajudarão a manter o código para receber scripts e interagir com o mecanismo de script em um único local, e essas chamadas não serão espalhadas pelo código do aplicativo.
- Todas as chamadas de script podem ser facilmente encontradas usando Localizar usos.
Autoconfiguração Spring Boot, teste de unidade, simulação são suportados. Você pode obter dados sobre os métodos de "script" e seus parâmetros por meio da API. E você também pode agrupar o resultado da execução com um objeto ScriptResult especial, no qual haverá um resultado ou uma instância de exceção se você não quiser se preocupar com try ... catch ao chamar scripts. A configuração XML é suportada se for necessária por um motivo ou outro. E finalmente - você pode especificar um tempo limite para o método de script, se necessário.
As fontes da biblioteca estão aqui.