Neste tutorial, exploraremos as técnicas mais recentes para integrar Groovy em um aplicativo Java.
2. Algumas palavras sobre Groovy
A linguagem de programação Groovy é um poderosooptionally-typed and dynamic language. É apoiado pela Apache Software Foundation e pela comunidade Groovy, com contribuições de mais de 200 desenvolvedores.
Ele pode ser usado para criar um aplicativo inteiro, criar um módulo ou uma biblioteca adicional interagindo com nosso código Java ou executar scripts avaliados e compilados em tempo real.
No momento da redação deste artigo, a versão estável mais recente é a 2.5.7, enquanto o Groovy 2.6 e 3.0 (ambos iniciados no outono de 17) ainda estão no estágio alfa.
Semelhante ao Spring Boot,we just need to include the groovy-all pom to add all the dependencies podemos precisar, sem nos preocupar com suas versões:
org.codehaus.groovygroovy-all${groovy.version}pom
4. Compilação Conjunta
Antes de entrar nos detalhes de como configurar o Maven, precisamos entender com o que estamos lidando.
Our code will contain both Java and Groovy files. O Groovy não terá nenhum problema para encontrar as classes Java, mas e se quisermos que o Java encontre as classes e métodos do Groovy?
Aí vem a compilação conjunta para o resgate!
Joint compilation is a process designed to compile both Java and Groovy arquivos no mesmo projeto, em um único comando Maven.
Com a compilação conjunta, o compilador Groovy irá:
analisar os arquivos de origem
dependendo da implementação, crie stubs compatíveis com o compilador Java
invoque o compilador Java para compilar os stubs junto com fontes Java - desta forma as classes Java podem encontrar dependências do Groovy
compilar as fontes Groovy - agora nossas fontes Groovy podem encontrar suas dependências Java
Dependendo do plug-in que o implementou, talvez seja necessário separar os arquivos em pastas específicas ou informar ao compilador onde encontrá-los.
Without joint compilation, the Java source files would be compiled as if they were Groovy sources. Às vezes, isso pode funcionar, pois a maior parte da sintaxe do Java 1.7 é compatível com Groovy, mas a semântica seria diferente.
5. Plugins do Maven Compiler
There are a few compiler plugins available that support joint compilation, cada um com seus pontos fortes e fracos.
Os dois mais usados com o Maven são Groovy-Eclipse Maven e GMaven +.
5.1. O plugin Groovy-Eclipse Maven
OGroovy-Eclipse Maven pluginsimplifies the joint compilation by avoiding stubs generation, ainda uma etapa obrigatória para outros compiladores como GMaven+, mas apresenta algumas peculiaridades de configuração.
Para habilitar a recuperação dos artefatos mais recentes do compilador, precisamos adicionar o repositório Maven Bintray:
A versão de dependênciagroovy-all deve corresponder à versão do compilador.
Finalmente, precisamos configurar nosso autodiscovery de origem: por padrão, o compilador olharia para pastas comosrc/main/javaesrc/main/groovy,, masif our java folder is empty, the compiler won’t look for our groovy sources.
O mesmo mecanismo é válido para nossos testes.
Para forçar a descoberta do arquivo, podemos adicionar qualquer arquivo emsrc/main/javaesrc/test/java, ou simplesmente adicionar ogroovy-eclipse-compiler plugin:
A seção<extension> é obrigatória para permitir que o plug-in adicione a fase de construção extra e os objetivos, contendo as duas pastas de origem do Groovy.
5.2. O GMavenPlus Plugin
OGMavenPlus plugin pode ter um nome semelhante ao antigo plugin GMaven, mas em vez de criar um mero patch, o autor fez um esforço parasimplify and decouple the compiler from a specific Groovy version.
Para fazer isso, o plug-in se separa das diretrizes padrão para plug-ins de compilador.
O compilador GMavenPlusadds support for features that were still not present in other compilers at the time, comoinvokedynamic, o console de shell interativo e Android.
Por outro lado, apresenta algumas complicações:
émodifies Maven’s source directories para conter as fontes Java e Groovy, mas não os stubs Java
érequires us to manage stubs se não os excluirmos com os objetivos adequados
Para configurar nosso projeto, precisamos adicionar ogmavenplus-plugin:
Para permitir o teste deste plugin, criamos um segundo arquivo pom chamadogmavenplus-pom.xml em a amostra.
5.3. Compilando com o plug-in Eclipse-Maven
Agora que tudo está configurado, podemos finalmente construir nossas classes.
No exemplo que fornecemos, criamos um aplicativo Java simples na pasta de origemsrc/main/javae alguns scripts Groovy emsrc/main/groovy, onde podemos criar classes e scripts Groovy.
Vamos construir tudo com o plugin Eclipse-Maven:
$ mvn clean compile
...
[INFO] --- maven-compiler-plugin:3.8.0:compile (default-compile) @ core-groovy-2 ---
[INFO] Changes detected - recompiling the module!
[INFO] Using Groovy-Eclipse compiler to compile both Java and Groovy files
...
Percebemos imediatamente que o GMavenPlus segue as etapas adicionais de:
Gerando stubs, um para cada arquivo groovy
Compilando os arquivos Java - stubs e código Java da mesma forma
Compilando os arquivos Groovy
Ao gerar stubs, o GMavenPlus herda uma fraqueza que causou muitas dores de cabeça aos desenvolvedores nos últimos anos, ao trabalhar com a compilação conjunta.
No cenário ideal, tudo funcionaria bem, mas introduzindo mais etapas, também temos mais pontos de falha: por exemplo,the build may fail before being able to clean up the stubs.
Se isso acontecer, os stubs antigos restantes podem confundir nosso IDE, que mostraria erros de compilação onde sabemos que tudo deve estar correto.
Somente uma construção limpa evitaria uma longa e dolorosa caça às bruxas.
5.5. Dependências de empacotamento no arquivo jar
Arun the program as a jar from the command line, adicionamosthe maven-assembly-plugin, que incluirá todas as dependências do Groovy em um "jar de gordura" nomeado com o postfix definido na propriedadedescriptorRef:
A compilação do Maven nos permite incluir arquivos Groovy em nosso projeto e referenciar suas classes e métodos a partir de Java.
Porém, isso não é suficiente se quisermos mudar a lógica em tempo de execução: a compilação é executada fora do estágio de tempo de execução, portantowe still have to restart our application in order to see our changes.
Para tirar proveito do poder dinâmico (e dos riscos) do Groovy, precisamos explorar as técnicas disponíveis para carregar nossos arquivos quando nosso aplicativo já estiver em execução.
6.1. GroovyClassLoader
Para conseguir isso, precisamos deGroovyClassLoader,, que pode analisar o código-fonte em texto ou formato de arquivo e gerar os objetos de classe resultantes.
When the source is a file, the compilation result is also cached, para evitar sobrecarga quando solicitamos ao carregador várias instâncias da mesma classe.
Script vindo diretamente de aString object, instead, won’t be cached, portanto, chamar o mesmo script várias vezes ainda pode causar vazamentos de memória.
GroovyClassLoader é a base sobre a qual outros sistemas de integração são desenvolvidos.
A implementação é relativamente simples:
private final GroovyClassLoader loader;
private Double addWithGroovyClassLoader(int x, int y)
throws IllegalAccessException, InstantiationException, IOException {
Class calcClass = loader.parseClass(
new File("src/main/groovy/com/example/", "CalcMath.groovy"));
GroovyObject calc = (GroovyObject) calcClass.newInstance();
return (Double) calc.invokeMethod("calcSum", new Object[] { x, y });
}
public MyJointCompilationApp() {
loader = new GroovyClassLoader(this.getClass().getClassLoader());
// ...
}
6.2. GroovyShell
O métodoparse() do Shell Script Loader aceita fontes em texto ou formato de arquivo egenerates an instance of the Script class.
Esta instância herda o métodorun() deScript, que executa todo o arquivo de cima para baixo e retorna o resultado fornecido pela última linha executada.
Se quisermos, também podemos estenderScript em nosso código e substituir a implementação padrão para chamar diretamente nossa lógica interna.
A implementação para chamarScript.run() tem a seguinte aparência:
private Double addWithGroovyShellRun(int x, int y) throws IOException {
Script script = shell.parse(new File("src/main/groovy/com/example/", "CalcScript.groovy"));
return (Double) script.run();
}
public MyJointCompilationApp() {
// ...
shell = new GroovyShell(loader, new Binding());
// ...
}
Observe querun() não aceita parâmetros, então precisaríamos adicionar ao nosso arquivo algumas variáveis globais para inicializá-los por meio do objetoBinding.
Como este objeto é passado na inicialização deGroovyShell, as variáveis são compartilhadas com todas as instânciasScript.
Se preferirmos um controle mais granular, podemos usarinvokeMethod(), que pode acessar nossos próprios métodos por meio de reflexão e passar argumentos diretamente.
Vejamos esta implementação:
private final GroovyShell shell;
private Double addWithGroovyShell(int x, int y) throws IOException {
Script script = shell.parse(new File("src/main/groovy/com/example/", "CalcScript.groovy"));
return (Double) script.invokeMethod("calcSum", new Object[] { x, y });
}
public MyJointCompilationApp() {
// ...
shell = new GroovyShell(loader, new Binding());
// ...
}
Nos bastidores,GroovyShell depende deGroovyClassLoader para compilar e armazenar em cache as classes resultantes, portanto, as mesmas regras explicadas anteriormente se aplicam da mesma maneira.
6.3. GroovyScriptEngine
A classeGroovyScriptEngine é particularmente para aqueles aplicativos querely on the reloading of a script and its dependencies.
Embora tenhamos esses recursos adicionais, a implementação possui apenas algumas pequenas diferenças:
private final GroovyScriptEngine engine;
private void addWithGroovyScriptEngine(int x, int y) throws IllegalAccessException,
InstantiationException, ResourceException, ScriptException {
Class calcClass = engine.loadScriptByName("CalcMath.groovy");
GroovyObject calc = calcClass.newInstance();
Object result = calc.invokeMethod("calcSum", new Object[] { x, y });
LOG.info("Result of CalcMath.calcSum() method is {}", result);
}
public MyJointCompilationApp() {
...
URL url = null;
try {
url = new File("src/main/groovy/com/example/").toURI().toURL();
} catch (MalformedURLException e) {
LOG.error("Exception while creating url", e);
}
engine = new GroovyScriptEngine(new URL[] {url}, this.getClass().getClassLoader());
engineFromFactory = new GroovyScriptEngineFactory().getScriptEngine();
}
Desta vez, temos que configurar as raízes de origem e nos referimos ao script apenas com seu nome, que é um pouco mais limpo.
Olhando dentro do métodoloadScriptByName, podemos ver imediatamente a verificaçãoisSourceNewer onde o mecanismo verifica se a fonte atualmente no cache ainda é válida.
Cada vez que nosso arquivo muda,GroovyScriptEngine irá recarregar automaticamente aquele arquivo particular e todas as classes que dependem dele.
Embora seja um recurso útil e poderoso, pode causar um efeito colateral muito perigoso:reloading many times a huge number of files will result in CPU overhead without warning.
Se isso acontecer, podemos precisar implementar nosso próprio mecanismo de cache para lidar com esse problema.
6.4. GroovyScriptEngineFactory (JSR-223)
JSR-223 fornece umstandard API for calling scripting frameworks desde Java 6.
A implementação é semelhante, embora voltemos ao carregamento por caminhos de arquivo completo:
private final ScriptEngine engineFromFactory;
private void addWithEngineFactory(int x, int y) throws IllegalAccessException,
InstantiationException, javax.script.ScriptException, FileNotFoundException {
Class calcClas = (Class) engineFromFactory.eval(
new FileReader(new File("src/main/groovy/com/example/", "CalcMath.groovy")));
GroovyObject calc = (GroovyObject) calcClas.newInstance();
Object result = calc.invokeMethod("calcSum", new Object[] { x, y });
LOG.info("Result of CalcMath.calcSum() method is {}", result);
}
public MyJointCompilationApp() {
// ...
engineFromFactory = new GroovyScriptEngineFactory().getScriptEngine();
}
É ótimo se estivermos integrando nosso aplicativo com várias linguagens de script, masits feature set is more restricted. Por exemplo,it doesn’t support class reloading. Dessa forma, se estivermos apenas integrando ao Groovy, talvez seja melhor seguir as abordagens anteriores.
7. Armadilhas da Compilação Dinâmica
Usando qualquer um dos métodos acima, poderíamos criar um aplicativo quereads scripts or classes from a specific folder outside our jar file.
Isso nos dariaflexibility to add new features while the system is running (a menos que exijamos um novo código na parte Java), obtendo assim algum tipo de desenvolvimento de Entrega Contínua.
Mas tome cuidado com essa faca de dois gumes: agora precisamos nos proteger com muito cuidado defailures that could happen both at compile time and runtime, garantindo de fato que nosso código falhe com segurança.
8. Armadilhas da Execução de Groovy em um Projeto Java
8.1. atuação
Todos sabemos que quando um sistema precisa ser muito eficiente, existem algumas regras de ouro a serem seguidas.
Dois que podem pesar mais em nosso projeto são:
evitar reflexão
minimizar o número de instruções de bytecode
A reflexão, em particular, é uma operação cara devido ao processo de verificação da classe, dos campos, dos métodos, dos parâmetros do método e assim por diante.
Se analisarmos as chamadas de método de Java para Groovy, por exemplo, ao executar o exemploaddWithCompiledClasses, a pilha de operação entre.calcSume a primeira linha do método Groovy real será semelhante a:
Nesse caso, podemos avaliar o que realmente está por trás do poder do Groovy: oMetaClass.
AMetaClass defines the behavior of any given Groovy or Java class, so Groovy looks into it whenever there’s a dynamic operation to execute para encontrar o método ou campo alvo. Uma vez encontrado, o fluxo de reflexão padrão o executa.
Duas regras de ouro quebradas com um método de invocação!
Se precisarmos trabalhar com centenas de arquivos Groovy dinâmicos,how we call our methods will then make a huge performance difference em nosso sistema.
8.2. Método ou propriedade não encontrada
Como mencionado anteriormente, se quisermosdeploy new versions of Groovy files no ciclo de vida de um CD, precisamostreat them like they were an API separados de nosso sistema central.
Isso significa colocarmultiple fail-safe checks and code design restrictions no lugar para que nosso desenvolvedor recém-ingressado não destrua o sistema de produção com um impulso errado.
Exemplos de cada um são: ter um pipeline de IC e usar a reprovação de método em vez de exclusão.
O que acontece se não o fizermos? Recebemos exceções terríveis devido a métodos ausentes e contagens e tipos de argumentos errados.
E se achamos que a compilação nos salvaria, vamos olhar o métodocalcSum2() de nossos scripts Groovy:
// this method will fail in runtime
def calcSum2(x, y) {
// DANGER! The variable "log" may be undefined
log.info "Executing $x + $y"
// DANGER! This method doesn't exist!
calcSum3()
// DANGER! The logged variable "z" is undefined!
log.info("Logging an undefined variable: $z")
}
Olhando por todo o arquivo, vemos imediatamente dois problemas: o métodocalcSum3()e a variávelz não estão definidos em lugar nenhum.
Mesmo assim, o script é compilado com êxito, sem um único aviso, estaticamente no Maven e dinamicamente no GroovyClassLoader.
Ele falhará apenas quando tentarmos invocá-lo.
A compilação estática do Maven mostrará um erro apenas se nosso código Java se referir diretamente acalcSum3(), depois de lançar oGroovyObject como fazemos no métodoaddWithCompiledClasses(), mas ainda é ineficaz se usarmos reflexão em vez disso .
9. Conclusão
Neste artigo, exploramos como podemos integrar o Groovy em nosso aplicativo Java, analisando diferentes métodos de integração e alguns dos problemas que podemos encontrar com linguagens mistas.
Como de costume, o código-fonte usado nos exemplos pode ser encontrado emGitHub.