Integrando o Groovy em aplicativos Java

Integrando o Groovy em aplicativos Java

1. Introdução

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.

Para obter mais informações, leiaIntroduction to Groovy Language ou vá paraofficial documentation.

3. Dependências do Maven

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.groovy
    groovy-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:


    
        bintray
        Groovy Bintray
        https://dl.bintray.com/groovy/maven
        
            
            never
        
        
            false
        
    

Então, na seção do plugin,we tell the Maven compiler which Groovy compiler version it has to use.

Na verdade, o plug-in que usaremos -the Maven compiler plugin - não compila, mas delega o trabalho parathe groovy-eclipse-batch artifact:


    maven-compiler-plugin
    3.8.0
    
        groovy-eclipse-compiler
        ${java.version}
        ${java.version}
    
    
        
            org.codehaus.groovy
            groovy-eclipse-compiler
            3.3.0-01
        
        
            org.codehaus.groovy
            groovy-eclipse-batch
            ${groovy.version}-01
        
    

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:


    org.codehaus.groovy
    groovy-eclipse-compiler
    3.3.0-01
    true

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:


    org.codehaus.gmavenplus
    gmavenplus-plugin
    1.7.0
    
        
            
                execute
                addSources
                addTestSources
                generateStubs
                compile
                generateTestStubs
                compileTests
                removeStubs
                removeTestStubs
            
        
    
    
        
            org.codehaus.groovy
            groovy-all
            
            2.5.6
            runtime
            pom
        
    

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
...

Aqui vemos queGroovy is compiling everything.

5.4. Compilando com o GMavenPlus

O GMavenPlus mostra algumas diferenças:

$ mvn -f gmavenplus-pom.xml clean compile
...
[INFO] --- gmavenplus-plugin:1.7.0:generateStubs (default) @ core-groovy-2 ---
[INFO] Using Groovy 2.5.7 to perform generateStubs.
[INFO] Generated 2 stubs.
[INFO]
...
[INFO] --- maven-compiler-plugin:3.8.1:compile (default-compile) @ core-groovy-2 ---
[INFO] Changes detected - recompiling the module!
[INFO] Compiling 3 source files to XXX\example\TutorialsRepo\core-groovy-2\target\classes
[INFO]
...
[INFO] --- gmavenplus-plugin:1.7.0:compile (default) @ core-groovy-2 ---
[INFO] Using Groovy 2.5.7 to perform compile.
[INFO] Compiled 2 files.
[INFO]
...
[INFO] --- gmavenplus-plugin:1.7.0:removeStubs (default) @ core-groovy-2 ---
[INFO]
...

Percebemos imediatamente que o GMavenPlus segue as etapas adicionais de:

  1. Gerando stubs, um para cada arquivo groovy

  2. Compilando os arquivos Java - stubs e código Java da mesma forma

  3. 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:


    org.apache.maven.plugins
    maven-assembly-plugin
    3.1.0
    
        
        
            jar-with-dependencies
        
        
        
            
                com.example.MyJointCompilationApp
            
        
    
    
        
            make-assembly
            
            package
            
                single
            
        
    

Depois que a compilação estiver concluída, podemos executar nosso código com este comando:

$ java -jar target/core-groovy-2-1.0-SNAPSHOT-jar-with-dependencies.jar com.example.MyJointCompilationApp

6. Carregando código Groovy on the Fly

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:

calcSum:4, CalcScript (com.example)
addWithCompiledClasses:43, MyJointCompilationApp (com.example)
addWithStaticCompiledClasses:95, MyJointCompilationApp (com.example)
main:117, App (com.example)

O que é consistente com o Java. O mesmo acontece quando lançamos o objeto retornado pelo carregador e chamamos seu método.

No entanto, isso é o que a chamadainvokeMethod faz:

calcSum:4, CalcScript (com.example)
invoke0:-1, NativeMethodAccessorImpl (sun.reflect)
invoke:62, NativeMethodAccessorImpl (sun.reflect)
invoke:43, DelegatingMethodAccessorImpl (sun.reflect)
invoke:498, Method (java.lang.reflect)
invoke:101, CachedMethod (org.codehaus.groovy.reflection)
doMethodInvoke:323, MetaMethod (groovy.lang)
invokeMethod:1217, MetaClassImpl (groovy.lang)
invokeMethod:1041, MetaClassImpl (groovy.lang)
invokeMethod:821, MetaClassImpl (groovy.lang)
invokeMethod:44, GroovyObjectSupport (groovy.lang)
invokeMethod:77, Script (groovy.lang)
addWithGroovyShell:52, MyJointCompilationApp (com.example)
addWithDynamicCompiledClasses:99, MyJointCompilationApp (com.example)
main:118, MyJointCompilationApp (com.example)

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.