Exportando Dados para Arquivo Texto com Groovy

Outro dia desses um amigo precisava fazer a exportação de dados de venda de um supermercado para posterior importação em outro sistema.

Ele já havia tentado o processo utilizando algumas ferramentas, mas sem sucesso. A exportação compreendia aproximadamentes 4 milhões de registros de venda sendo que os arquivos deveriam ser gerados dia a dia, separados em uma hierarquia de pastas estruturadas num formato tipo /ano/mes.

A solução foi escrever um simples Script em Groovy, que concluiu todo o processo em menos de cinco minutos. Para execução do script foi necessário apenas a instalação do Groovy, baixar o Driver do banco de dados e executar o comando:

$ groovy -cp "jaybird-full-2.1.6.jar" MegaExport.groovy

Isto mostra a simplicidade e o poder da linguagem Groovy e como ela pode ser empregada para soluções de problemas corriqueiros. Abaixo o script em questão:

import java.sql.DriverManager;
 
import groovy.sql.Sql
 
//Parâmetros de configuração
def SERVIDOR = "127.0.0.1"
def BANCO_DE_DADOS = "/path/to/database.fdb"
def USUARIO = "SYSDBA"
def SENHA = "masterkey"
def DIRETORIO_DESTINO = "/path/to/files"
def DRIVER = "org.firebirdsql.jdbc.FBDriver"
def STRING_CONEXAO = "jdbc:firebirdsql://${SERVIDOR}/${BANCO_DE_DADOS}"
 
//Comando SQL de seleção dos períodos com vendas
def COMANDO_SQL_PERIODO = """\
SELECT EXTRACT(year from sai_d_ve) AS ano
    , EXTRACT(month from sai_d_ve) AS mes 
FROM sai001 
WHERE (not sai_d_ve is null) 
GROUP BY 1, 2 
ORDER by 1, 2
"""
 
//Comando SQL de seleção das vendas
def COMANDO_SQL_VENDAS = """\
SELECT EXTRACT(day from sao.sai_d_ve) AS dia
    , sao.cupom AS cupom
    , sao.pro_barr AS ean
    , CAST(coalesce(sao.sai_quan,0) * 1000 AS bigint) AS quantidade
    , CAST(coalesce(sao.sai_quan,0) * coalesce(sao.sai_valo,0) * 100 AS bigint) AS subtotal
    , CAST(coalesce(sao.valor_desconto,0) * 100 AS bigint) AS desconto
    , CAST(coalesce(sao.acrescimo,0) * 100 AS bigint) AS acrescimo
    , CASE sao.item_excluido
        WHEN 'S' then 'C'
        ELSE 'V'
    END AS situacao
    , sao.descricao as descricao
    , CASE sao.id
        WHEN 'FF' then 'F   '
        WHEN 'II' then 'I   '
        WHEN 'NN' then 'N   '
        WHEN '01' then '0700'
        WHEN '02' then '1700'
        WHEN '03' then '1700'
        WHEN '04' then '2500'
        WHEN '05' then '0400'
        WHEN '06' then '0300'
        WHEN '07' then '2700'
        WHEN '08' then '1200'
        ELSE '    '
    END AS tributacao
FROM sai001 sao
WHERE (EXTRACT(year from sao.sai_d_ve) = ?) AND (EXTRACT(month from sao.sai_d_ve) = ?)
ORDER BY sao.sai_d_ve
"""
 
println "Carregando o driver de conexão (${DRIVER})";
Class.forName(DRIVER);
 
println "Selecionando as datas para exportação" ;
def sqlPeriodo = Sql.newInstance(STRING_CONEXAO, USUARIO, SENHA, DRIVER);
try {
    sqlPeriodo.eachRow(COMANDO_SQL_PERIODO) { periodo ->
        println "Exportando período ${periodo.ANO}/${periodo.MES}";
 
        int dia = 0;
        int mes = periodo.MES;
        int ano = periodo.ANO;
 
        println " > Criando diretório ${DIRETORIO_DESTINO}/${ano.toString().padLeft(4, '0')}/${mes.toString().padLeft(2, '0')}";
        def diretorio = new File("${DIRETORIO_DESTINO}/${ano.toString().padLeft(4, '0')}/${mes.toString().padLeft(2, '0')}");
        diretorio.mkdirs();
 
        println " > Exportando os dados das vendas";
        def writer = null;
        def arquivo = null;
        def sqlVendas = Sql.newInstance(STRING_CONEXAO, USUARIO, SENHA, DRIVER);
        try {
            sqlVendas.eachRow(COMANDO_SQL_VENDAS, [ano, mes]) { venda ->
                if (dia == 0 || dia != venda.DIA) {
                    dia = venda.DIA;					
                    def nomeArquivo = "IT_${dia.toString().padLeft(2,'0') + mes.toString().padLeft(2, '0')}.txt";
 
                    println " > Criando arquivo ${diretorio.absolutePath}/${nomeArquivo}";
                    arquivo = new File("${diretorio.absolutePath}/${nomeArquivo}");
                    arquivo.delete();
                    arquivo.createNewFile();
 
                    if (writer) {
                        writer.flush();
                        writer.close();
                    }
                    //a escrita deve ser feita em buffer por questões de desempenho
                    writer = arquivo.newWriter(); 
                }
                writer.write ano.toString().padLeft(4, "0");
                writer.write mes.toString().padLeft(2, "0");
                writer.write venda.DIA.toString().padLeft(2, "0");
                writer.write "000";
                writer.write venda.CUPOM.toString().padLeft(6, "0");
                writer.write "00000";
                writer.write venda.EAN.toString().padLeft(14, "0");
                writer.write "0000000";
                writer.write venda.QUANTIDADE.toString().padLeft(9, "0");
                writer.write venda.SUBTOTAL.toString().padLeft(11, "0");
                writer.write venda.DESCONTO.toString().padLeft(7, "0");
                writer.write venda.ACRESCIMO.toString().padLeft(7, "0");
                writer.write venda.SITUACAO.toString().padLeft(1, " ");
                writer.write venda.DESCRICAO.toString().padRight(30, " ");
                writer.write venda.TRIBUTACAO.toString().padRight(4, " ");
                writer.newLine();
            }
        } finally {
            sqlVendas.close();
            if (writer) {
                writer.flush();
                writer.close();
            }
        }    
    }
} finally {
    sqlPeriodo.close();
}
 
println "Exportação finalizada.";

Como Testar Sua Aplicação Grails

O framework Grails oferece uma API rica e simples para execução de testes, para várias fases do processo de produção do aplicativo. Estas fases são inicialmente separadas em:

  • unit
  • integration
  • functional
  • other

Novas fases ou tipos de testes podem ser adicionados a estas através do uso de plugins, sendo que as fases functional e other necessitam de plugins específicos antes de serem utilizadas.

Testes de Unidade

Os testes de unidade têm por finalidade garantir o funcionamento de porções mínimas do código implementado, como um método ou a verificação do estado de um objeto após uma determinada operação.

Para se testar comportamentos dinâmicamente criados pelo framework, nesta fase, é necessário fazer a injeção manual destes nas classes ou fazer o mocking de objetos para se conseguir testar por exemplo as constraints de uma classe de domínio ou os controllers.

Teste de integração

A fase de testes de integração difere da fase de teste de unidade pelo fato de o Grails preparar todo o ambiente de execução dos testes conforme este seria no ambiente de execução do aplicativo.

Nesta fase todos os métodos dinamicamente injetados pelo framework estarão disponíveis para uso, sem necessidade de fazer mocking ou injeção manual dos mesmos.

Durante os testes de integração você tem acesso ao um banco de dados configurado no arquivo conf/DataSource.groovy para o ambiente de testes. Durante os testes o Grails e encarrega de criar a estrutura do banco de dados, iniciar uma transação e no final desfazer (rollback) tudo no final do processo transparentemente.

Testes funcionais

Os testes funcionais têm por objetivo enviar requisições HTTP para a aplicação e verificar o comportamento resultante. O Grails não provê suporte nativo para a execução de testes funcionais. Alguns plugins estão disponíveis atualmente para isto:

Executando os Testes

Por definição as classes de teste devem estender a classe GrailsUnitTestCase. Seu nome deve ter o sufixo Tests e seus métodos o prefixo test, conforme a especificação do JUnit 3, que é a base de execução dos testes de unidade e integração do Grails.

As classes de teste de unidade se encontram dentro do diretório test/unit e as de teste de integração no diretório test/integration.

//Um exemplo de classe de teste do Grails
package exemplo
 
import grails.test.*
 
class PessoaTests extends GrailsUnitTestCase {
 
    protected void setUp() {
        super.setUp()
    }
 
    protected void tearDown() {
        super.tearDown()
    }
 
    void testAlgumaCoisa() {
        assertTrue false
    }
}

As classes de teste de unidade e integração pode ser criadas respectivamentes pelos comandos:

$ grails create-unit-test MeuTesteDeUnidade
$ grails create-integration-test MeuTesteDeIntegracao

O comando test-app do Grails oferece algumas opções bem interessantes para acelerar e automatizar o processo de testes da aplicação.

Em sua forma mais comum, sem parâmetros, todos os testes encontrados serão executados:

$ grails test-app

Alguns argumentos podem ser adicionados ao comando para agilizar o processo de teste, como por exemplo específicar uma fase de testes:

$ grails test-app unit:
$ grails test-app integration:

Além disso você pode combinar mais parâmetros para testar uma classe isoladamente…

$ grails test-app unit: exemplo.Pessoa
$ grails test-app integration: exemplo.PessoaController

ou testar um método específico…

$ grails test-app unit: exemplo.Pessoa.testAlgumaCoisa

Metacaracteres

Você pode utilizar metacaracteres para testar todas as classes de um pacote…

$ grails test-app unit: exemplo.*

todas as classes de um pacote e seus subpacotes…

$ grails test-app integration: exemplo.**.*

classes terminadas com uma determinada sentença…

$ grails test-app *Controller

ou combinar tudo isto…

$ grails test-app exemplo.**.* *Controller

Visualizando os resultados

O resultado da execução dos testes é salvo na pasta target/test-reports em formato plain text, HTML e XML.

Conclusão

Este artigo é apenas uma breve introdução a API e execução de de testes do Grails. Não deixe de consultar a última versão da documentação do framework.

Groovy e Grails em 60 Segundos

Este vídeo é um pouco antigo, mas é muito divertido. Ta aí pra quem ainda não viu.

Meu Ambiente de Trabalho em 7 Itens

Não sei quando começou este meme entre desenvolvedores, mas está bem legal e como meu camarada @vixlima me convidou a entrar na brincadeira, vamos lá.

1- Ambiente

Meu ambiente de trabalho precisa ser bem iluminado com luz natural e bem fresco, porque não consigo raciocinar corretamente quando estou com calor. Deixo sempre uma caneca de água gelada na mesa e gosto de ter algumas guloseimas sempre a disposição. Não uso fones de ouvido e não acho muito educado usá-los enquanto se está trabalhando com outras pessoas. Uma música ambiente agradável tocando baixinho sempre é bem vinda.

Gosto da minha gatinha cutucando o teclado ou tirando um cochilo atrás do monitor enquanto trabalho. Perder o foco um pouquinho e se distrair ajuda a resolver problemas e ela colabora muito com isso.

2- Sistema Operacional

Na empresa é Windows e pronto. Não tem como usar outro. Não tenho nada contra o Windows e inclusive acho que a Microsoft até que surpreendeu com o Windows Vista/7. Mas se posso optar quero o melhor e esse é o MacOS.

O MacOS está a anos luz a frente de qualquer outro SO em termos de produtividade, facilidade de uso e estabilidade. Os aplicativos para o MacOS conseguem ser tão bons quanto o sistema, são bem acabados e objetivos. Não sou fanboy da Apple, mas tenho que admitir que o MacOS sempre reinou neste aspecto. Pra mim o tempo é um artigo precioso e com o MacOS eu economizo muito tempo, simplesmente porque as coisas aqui simplesmente funcionam e tudo é fácil e rápido.

Já usei Linux no Desktop (Debian) por muito tempo e até gosto do Ubuntu, mas acho que o Linux só brilha mesmo quando está num servidor. Meu foco é desenvolver software e não quero mais ter que esquentar a cabeça com drivers, configs obscuras, incompatibilidades, etc.

Além do Mac tenho um PC com dual boot Windows/Ubuntu que utilizo para testes. Não utilizo maquinas virtuais.

3- Editor de texto e IDE

Na empresa a regra é: “time que está ganhando não se mexe”. Absurdamente ainda utilizamos o Delphi 6 e nosso software é completamente acoplado a esta plataforma, a ponto de ser complicado até mesmo mudar para uma versão mais nova. No Windows costumo fazer tudo no Notepad++, que é um excelente editor de textos.

Quando estou num terminal *nix meu editor é o Vim, já tentei o Emacs e fiquei meio perdido. Me limito a usar o Vim basicamente enquanto estou usando o terminal, manipular arquivos de configuração e dar manutenção em scripts.

HTML, JavaScript, PHP, postar no blog e tudo mais eu uso uso o TextMate.

Quando estou programando em Java e Groovy/Grails uso o Intellij Idea. Usava o Eclipse, mas comecei a achar que ele estava me atrapalhando mais do que ajudando e então decidi experimentar o Idea 9.

O Intellij Idea é uma IDE leve e completa que me facilita muito o processo de visualização, organição e refatoração de projetos maiores. Na versão community consigo fazer absolutamente tudo, mesmo as coisas que teoricamente esta versão não da suporte, como programar em Groovy/Grails.

4- Controle de versão

Na empresa nosso sistema de bugtracking foi desenvolvido internamente e é muito bom (meus 2 cents!) por ser perfeitamente adaptado aos nossos processos. Não podemos manipular diretamente o repositório, quem faz isso é o sistema de bugtracking e por traz dos panos ele usa o velho CVS.

Nos meu próprios projetos usava o Subversion, mas hoje em dia nada se compara ao Git/GitHub. Só uso ele e não vejo motivos para se escolher outro.

5- Browsers

Eu sou um usuário promíscuo quando se fala de browsers. Já usei todos e estou sempre trocando. Considero o Firefox uma extensão da IDE (Selenium, Firebug, etc) e não uso ele para navegar. Já usei o Opera por muito tempo, principalmente quando ainda usava Linux, mas hoje minha preferência é o Safari. Ainda não consegui gostar do Chrome.

6- Adobe Dreamweaver e Fireworks

Uso muito o Fireworks e o Dreamweaver para prototipar páginas. São ferramentas fantásticas para webdesigners, mas eu as uso basicamente porque acho entediante e anti-produtivo escrever HTML. Com estas ferramentas consigo feedback visual instantâneo do que estou fazendo.

7- Produtividade

Não vivo sem o Google Apps. GMail, Google Agenda e Google Reader são basicamente o que uso na Internet. Tirando alguns sites como o Facebook, YouTube e os fóruns que frequento, leio tudo dentro do Google Reader.

No desktop não vivo sem o iWork, iCal, DropBox e Echofon. Conheci esses dias o Things e comecei a gostar. Comunicação instantânea é Skype e não tem outro. Só mantenho o MSN instalado porque todo mundo que conheço só usa ele.

Mantenho na minha pasta Sites a documentação de todos os frameworks e linguagens que trabalho. É mais rápido e prático assim. Mantenho muitos livros técnicos na minha estante (virtual ou física) que precisam estar sempre a mão para quando eu não lembro de algo porque não gosto de pertubar meus colegas com perguntas enquanto estão trabalhando.

Dando continuidade à brincadeira

Agora convido meus amigos @vitorhug e @eveblood a dar continuidade a brincadeira.

Utilizando Codecs em Testes Unitários no Grails

Com a função loadCodec da classe GrailsUnitTestCase você pode utilizar facilmente seus codecs dentro de um teste unitário, disponibilizando assim os métodos encodeAs e decode em todos os objetos durante sua execução. Lembrando que os codecs padrões da plataforma ficam no pacote org.codehaus.groovy.grails.plugins.codecs. Veja no exemplo:

import grails.test.*
import org.codehaus.groovy.grails.plugins.codecs.HTMLCodec
 
class SomeTests extends GrailsUnitTestCase {
    protected void setUp() {
        super.setUp()
        loadCodec(HTMLCodec)
    }
 
    protected void tearDown() {
        super.tearDown()
    }
 
    void testSomething() {
        assertEquals "&lt;p&gt;Hello World&lt;/p&gt;", "<p>Hello World</p>".encodeAsHTML()
    }
}

Groovy e Performance: Os Tipos Numéricos

Uma das maiores críticas sobre o Groovy tem sido a sua “baixa performance”, principalmente ao se executar operações numéricas. O objetivo da linguagem se cumpre pela simpicidade e ganho de produtividade, não pelo desempenho, mas nem por isso podemos virar as costas para a otimização do código ou virar as costas para o Java.

O que precisa ser observado é que a linguagem possui algumas particularidades que precisam ser conhecidas e algumas fraquezas quanto a invocação e tipagem dinâmicas.

Um coisa básica, que muitos programadores que iniciam na linguagem desconhecem Groovy não possui os tipos primitivos do Java:

int i = 0
assert i instanceof java.lang.Integer

Apesar de a variável ter sido declarada como int o que obtemos na verdade é um java.lang.Integer. A palavra reservada para o tipo primitivo em java funciona como um atalho para a classe wrapper deste e isto é válido para todos os tipos primitivos do Java (byte, short, int, long, float, double, char e boolean).

Outro detalhe muito importante é que ao se trabalhar com números no Groovy, este utiliza sempre BigDecimal quando não se especifíca estaticamente o tipo da variável, ou não é explicitado o tipo do valor sendo atribuido a variável, quando este é um decimal.

def a = 1.1
def b = 1.1d
double c = 1.1
 
assert a instanceof java.math.BigDecimal
assert b instanceof java.lang.Double
assert c instanceof java.lang.Double

Não é necessário dizer como BigDecimal é lento para cálculos, mesmo no Java um código que efetue muitas operações seguidas com BigDecimal poderá resultar numa baixa performance dependendo da situação.

Por outro lado isto é um facilitador quando se trabalha com números decimais, como por exemplo valores monetários

Fique sempre atento a estes detalhes quando estiver programando em Groovy, principalmente porque exeções do seguinte tipo poderão ocorrer ao misturar código Groovy com Java:

groovy.lang.MissingMethodException: No signature of method: [I.getAt() is applicable for argument types: (java.math.BigDecimal) values: [499999.5]

Entrevista com Graeme Rocher e Guillaume Laforge Sobre o Futuro do Groovy e do Grails

Saiu hoje no InfoQ uma entrevista com o Graeme Rocher e o Guillaume Laforge sobre o futuro da linguagem do Groovy e do Grails.

Para quem não sabe, Graeme Rocher é líder e co-fundador do Grails e Guillaume Laforge é quem está a frente do Groovy na SpringSource. Laforge também é líder da JSR-241, que tem como objetivo padronizar a linguagem Groovy na plataforma Java.

link

Utilizando classes de domínio JPA no Grails

Utilizando o Metaobject Protocol (MOP) fornecido pelo Groovy e as convenções do GORM, é possível utilizar classes anotadas com JPA do modelo EJB3 como classes de domínio dinâmicas do framework. Além disso o Grails é capaz de injetar finders dinâmicos, constraints e fornecer as funcionalidades que estamos acostumados num classe de domínio padrão do Grails.

O mais interessante nisto tudo é que estas classes tornam-se objetos totalmente dinâmicos, sem qualquer necessidade de acessarmos seu código fonte, bastando apenas seu bytecode. Isto é feito de maneira bem simples e transparente seguindo a filosofia “Convention Over Configuration” do framework, como veremos a seguir.

Preparando um Projeto de Exemplo

Para iniciar crie uma aplicação de exemplo com o nome bookstore:

$ grails create-app bookstore
$ cd bookstore

Faça o download do arquivo bookstore.jar neste link e coloque-o na pasta lib da aplicação recém criada.

Neste arquivo, no pacote ex.library.entity, estão contidas duas classes anotadas: Author e Book.

Estas classes possuem um relacionamento do tipo many-to-many que demonstra que um autor pode possuir muitos livros, bem como um livro pode ser de autoria de vários autores. Ambas são exemplos muito simples com finalidade meramente didática.

O hibernate.cfg.xml

Além do script DataSource.groovy, o Grails nos possibilita utilizar a forma tradicional de mapeamento de classes do Hibernate para definição do domínio da aplicação. Com isso você pode trabalhar exclusivamente com classes que seguem o padrão EJB3 ou mesmo misturando as coisas. Para que funcione o arquivo hibernate.cfg.xml deve obrigatoriamente ser salvo na pasta /grails-app/conf/hibernate.

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE hibernate-configuration PUBLIC
		"-//Hibernate/Hibernate Configuration DTD 3.0//EN"
		"http://hibernate.sourceforge.net/hibernate-configuration-3.0.dtd">
<hibernate-configuration>
    <session-factory>
        <mapping class="ex.library.entity.Author" />
        <mapping class="ex.library.entity.Book" />
    </session-factory>
</hibernate-configuration>

Para quem não conhece o Hibernate, este arquivo diz ao framework basicamente quais classes serão mapeadas para o banco de dados. Numa configuração normal do Hibernate teriamos mais informações neste arquivo, como o driver, dialeto do banco de dados, usuário, senha, etc. Entretanto todas estas informações são supridas pelo arquivo /grails-app/conf/DataSource.groovy não se fazendo necessário estas configurações aqui.

Com esta configuração as classes Author e Book passam a ser reconhecidas pelo framework como classes de domínio. Note que nem mesmo temos acesso ao código fonte destas, que estão empacotadas no arquivo bookstore.jar que depositamos na pasta lib da aplicação.

O uso do script DataSource.groovy em detrimento ao hibernate.cfg.xml para configuração da camada de persistência é preferível, já que a configuração pelo script nos possibilita trabalhar com separação de configuração por ambientes além de não se tratar de um arquivo estático, o que nos dá muita flexibilidade na hora da configuração.

Scaffolding

Se com esta configuração o Grails considera as classes mapeadas como classes de domínio, então não há segredo algum para iniciarmos o scaffolding da aplicação.

$ grails generate-all ex.library.entity.Author
$ grails generate-all ex.library.entity.Book

Note a seguinte mensagem durante a saída do comando:

Domain class not found in Grails-app/domain, trying hibernate mapped classes...

Quando o Grails não consegue encontrar a classe referida no comando dentro da pasta /Grails-app/domain ele tenta encontrá-la no arquivo hibernate.cfg.xml. Note que isto vale também para os comando create-controller, create-view ou qualquer outro comando em que o argumento seja uma classe de domínio.

Com o scaffolding criado, é hora de verificar se tudo correu bem:

$ grails run-app

Constraints

Além do mapeamento, é possível adicionar constraints para estas classes de domínio, seguindo o padrão estabelecido pelo GORM. Para isto basta seguir a seguinte convenção:

  • As constraints deverão estar contidas num script Groovy que contenha uma closure constraints;
  • Este script deve estar localizado na pasta /src/java do projeto, no mesmo pacote que a classe mapeada alvo da constraint;
  • O script deve ser salvo com o mesmo nome da classe mapeada, acrescentando o sufixo Constraints a este nome.

Seguindo esta convenção, vamos acrescentar dois scripts Grails na pasta /src/java da aplicação criada, certificando-se de que estejam no mesmo pacote das classes anotadas.

/*
 * Constraints for ex.library.Author
 * File: src/java/ex/library/entity/AuthorConstraints.groovy
 */
package ex.library.entity
 
constraints = {
    name blank:false  
    birthPlace blank:false
}

Neste script definimos duas constraints incrivelmente simples. As propriedades name e birthPlace de Author não podem estar em branco.

/*
 * Constraints for ex.library.Book
 * File: src/java/ex/library/entity/BookConstraints.groovy
 */
package ex.library.entity
 
constraints = {
    title blank:false
    publishedYear min:0, max:Calendar.instance.get(Calendar.YEAR)
    isbn blank:false, matches:"[0-9]{3}-[0-9]-[0-9]{5}-[0-9]{3}-[0-9]"
}

Neste segundo script definimos que um livro não pode ter um título em branco, que o ano de publicação deve ser no mínimo zero e no máximo o ano corrente. Além disso é forçada uma formatação para o campo ISBN por uma expressão regular.

Você pode verificar as constraints em funcionamento executando a aplicação e tentando inserir valores inválidos.

Neste exemplo demonstrei como Grails é capaz de mapear e injetar comportamento de maneira bem simples em classes de domínio externas. Com isso há um benefício claro de desacoplamento do domínio com o resto do framework e ainda assim podendo-se tirar proveito da camada de infra-estrutura oferecida pelo framework. Um grande benefício para quem emprega Domain Driven Design no projeto da aplicação e precisa de agilidade na hora de lidar com a camada de infra-estrutura

Groovy: Os métodos equals() e is()

A linguagem Groovy introduz uma série de novos métodos e modifica de maneira sutíl o comportamento de alguns operadores da linguagem Java.

Dentre eles, um que causa certa confusão a princípio é o operador == que passa a operar sobre o método equals() de uma classe. Na linguagem Java este operador apenas compara se duas variáveis distintas referem-se a mesma instância do objeto e não tem nenhuma relação direta com a implementação do método equals().

A comparação de instâncias no Groovy deve ser feita usando-se o método is() que se comporta exatamente como o operador == da linguagem Java. Por exemplo:

class Person {
    String name;
    Person(String name) {
        this.name = name
    }
    boolean equals(Object other) {
        if (other instanceof Person) {
            Person otherPerson = (Person) other
            return this.name == otherPerson.name
        } else
            return false
    }
}
 
def a = new Person("Jim")
def b = new Person("Jim")
assert a == b //== chama o método equals
assert !a.is(b) //são instâncias diferentes

Note que propositalmente não fiz a verificação inicial no método equals() se o parâmetro other se refere a mesma instância do objeto em que o método está sendo chamado.

Se for introduzido no método equals() algo como:

boolean equals(Object other) {
    if (this == other) //comapara as duas instâncias
        return true
    //...

A chamada a:

assert a == b

Lançará uma java.lang.StackOverflowError, porque o método equals() da classe Person entra em recursividade infinita com esta implementação. Um erro muito comum de quem está começando com Groovy

Isto ocorre porque para o Groovy utilizar o operador == ou chamar o método equals() significa a mesma coisa. A implementação correta de equals() para a classe Person neste caso é:

class Person {
    String name;
    Person(String name) {
        this.name = name
    }
    boolean equals(Object other) {
        if (this.is(other)) //comapara as duas instâncias
            return true
        else if (other instanceof Person) {
            Person otherPerson = (Person) other
            return this.name == otherPerson.name 
        } else
            return false
    }
}