Metaprogramação em Groovy

Metaprogramação em Groovy

1. Visão geral

Groovy é uma linguagem JVM dinâmica e poderosa que possui vários recursos comoclosuresetraits.

Neste tutorial, vamos explorar o conceito de metaprogramação no Groovy.

2. O que é metaprogramação?

Metaprogramação é uma técnica de programação para escrever um programa para modificar a si próprio ou outro programa usando metadados.

No Groovy, é possível realizar metaprogramação em tempo de execução e tempo de compilação. Daqui para frente, exploraremos alguns recursos notáveis ​​de ambas as técnicas.

3. Metaprogramação em tempo de execução

A metaprogramação em tempo de execução nos permite alterar as propriedades e métodos existentes de uma classe. Além disso, podemos anexar novas propriedades e métodos; tudo em tempo de execução.

O Groovy fornece alguns métodos e propriedades que ajudam a alterar o comportamento de uma classe em tempo de execução.

3.1. propertyMissing

Quando tentamos acessar uma propriedade indefinida de uma classe Groovy, ele lança umMissingPropertyException.. Para evitar a exceção, Groovy fornece o métodopropertyMissing.

Primeiro, vamos escrever uma classeEmployee com algumas propriedades:

class Employee {
    String firstName
    String lastName
    int age
}

Em segundo lugar, criaremos um objetoEmployee e tentaremos exibir uma propriedade indefinidaaddress.. Consequentemente, ele lançará oMissingPropertyException: __

Employee emp = new Employee(firstName: "Norman", lastName: "Lewis")
println emp.address
groovy.lang.MissingPropertyException: No such property:
address for class: com.example.metaprogramming.Employee

Groovy provides the propertyMissing method to catch the missing property request. Portanto, podemos evitar umMissingPropertyException em tempo de execução.

Para capturar a chamada do método getter de uma propriedade ausente, vamos defini-la com um único argumento para o nome da propriedade:

def propertyMissing(String propertyName) {
    "property '$propertyName' is not available"
}
assert emp.address == "property 'address' is not available"

Além disso, o mesmo método pode ter o segundo argumento como o valor da propriedade, para capturar a chamada de método setter de uma propriedade ausente:

def propertyMissing(String propertyName, propertyValue) {
    println "cannot set $propertyValue - property '$propertyName' is not available"
}

3.2. methodMissing

O métodomethodMissing é semelhante apropertyMissing. No entanto,methodMissing intercepta uma chamada para qualquer método ausente, evitando assim oMissingMethodException.

Vamos tentar chamar o métodogetFullName em um objetoEmployee. ComogetFullName está faltando, a execução lançaráMissingMethodException em tempo de execução:

try {
    emp.getFullName()
} catch (MissingMethodException e) {
    println "method is not defined"
}

Portanto, em vez de envolver uma chamada de método emtry-catch, podemos definirmethodMissing:

def methodMissing(String methodName, def methodArgs) {
    "method '$methodName' is not defined"
}
assert emp.getFullName() == "method 'getFullName' is not defined"

3.3. ExpandoMetaClass

Groovy provides a metaClass property in all its classes. A propriedademetaClass se refere a uma instância deExpandoMetaClass.

A classeExpandoMetaClass fornece várias maneiras de transformar uma classe existente em tempo de execução. Por exemplo, podemos adicionar propriedades, métodos ou construtores.

Primeiro, vamos adicionar a propriedadeaddress ausente à classeEmployee usando a propriedademetaClass:

Employee.metaClass.address = ""
Employee emp = new Employee(firstName: "Norman", lastName: "Lewis", address: "US")
assert emp.address == "US"

Indo adiante, vamos adicionar o métodogetFullName ausente ao objeto de classeEmployee no tempo de execução:

emp.metaClass.getFullName = {
    "$lastName, $firstName"
}
assert emp.getFullName() == "Lewis, Norman"

Da mesma forma, podemos adicionar um construtor à classeEmployee em tempo de execução:

Employee.metaClass.constructor = { String firstName ->
    new Employee(firstName: firstName)
}
Employee norman = new Employee("Norman")
assert norman.firstName == "Norman"
assert norman.lastName == null

Da mesma forma, podemos adicionar métodosstatic usandometaClass.static.

A propriedademetaClass não é útil apenas para modificar classes definidas pelo usuário, mas também classes Java existentes em tempo de execução.

Por exemplo, vamos adicionar um métodocapitalize à classeString:

String.metaClass.capitalize = { String str ->
    str.substring(0, 1).toUpperCase() + str.substring(1);
}
assert "norman".capitalize() == "Norman"

3.4. Extensões

Uma extensão pode adicionar um método a uma classe em tempo de execução e torná-lo acessível globalmente.

Os métodos definidos em uma extensão devem ser sempre estáticos, com o objeto de classeself como o primeiro argumento.

Por exemplo, vamos escrever uma classeBasicExtension para adicionar um métodogetYearOfBirth à classeEmployee:

class BasicExtensions {
    static int getYearOfBirth(Employee self) {
        return (new Date().getYear() + 1900) - self.age;
    }
}

Para habilitarBasicExtensions, precisaremos adicionar o arquivo de configuração no diretórioMETA-INF/services do nosso projeto.

Então, vamos adicionar o arquivoorg.codehaus.groovy.runtime.ExtensionModule com a seguinte configuração:

moduleName=core-groovy-2
moduleVersion=1.0-SNAPSHOT
extensionClasses=com.example.metaprogramming.extension.BasicExtensions

Vamos verificar o métodogetYearOfBirth adicionado na classeEmployee:

Employee emp = new Employee(age: 28)
assert emp.getYearOfBirth() == 1991

Da mesma forma, para adicionar métodosstatic em uma classe, precisaremos definir uma classe de extensão separada.

Por exemplo, vamos adicionar um métodostaticgetDefaultObj à nossa classeEmployee definindo a classeStaticEmployeeExtension:

class StaticEmployeeExtension {
    static Employee getDefaultObj(Employee self) {
        return new Employee(firstName: "firstName", lastName: "lastName", age: 20)
    }
}

Em seguida, habilitamosStaticEmployeeExtension adicionando a seguinte configuração ao arquivoExtensionModule:

staticExtensionClasses=com.example.metaprogramming.extension.StaticEmployeeExtension

Agora, tudo o que precisamos é testar nosso métodostaticgetDefaultObj na classeEmployee:

assert Employee.getDefaultObj().firstName == "firstName"
assert Employee.getDefaultObj().lastName == "lastName"
assert Employee.getDefaultObj().age == 20

Da mesma forma,using extensions, we can add a method to pre-compiled Java classes comoInteger eLong:

public static void printCounter(Integer self) {
    while (self > 0) {
        println self
        self--
    }
    return self
}
assert 5.printCounter() == 0
public static Long square(Long self) {
    return self*self
}
assert 40l.square() == 1600l

4. Metaprogramação em tempo de compilação

Usando anotações específicas, podemos alterar sem esforço a estrutura da classe em tempo de compilação. Em outras palavras,we can use annotations to modify the abstract syntax tree of the class at the compilation.

Vamos discutir algumas das anotações que são bastante úteis no Groovy para reduzir o código clichê. Muitos deles estão disponíveis no pacotegroovy.transform.

Se analisarmos cuidadosamente, perceberemos que algumas anotações fornecem recursos semelhantes aoProject Lombok do Java.

4.1. @ToString

A anotação@ToString adiciona uma implementação padrão do métodotoString a uma classe em tempo de compilação. Tudo o que precisamos é adicionar a anotação à classe.

Por exemplo, vamos adicionar a anotação@ToString à nossa classeEmployee:

@ToString
class Employee {
    long id
    String firstName
    String lastName
    int age
}

Agora, vamos criar um objeto da classeEmployee e verificar a string retornada pelo métodotoString:

Employee employee = new Employee()
employee.id = 1
employee.firstName = "norman"
employee.lastName = "lewis"
employee.age = 28

assert employee.toString() == "com.example.metaprogramming.Employee(1, norman, lewis, 28)"

Também podemos declarar parâmetros comoexcludes,includes,includePackageeignoreNulls com@ToString para modificar a string de saída.

Por exemplo, vamos excluiridepackage da string do objeto Employee:

@ToString(includePackage=false, excludes=['id'])
assert employee.toString() == "Employee(norman, lewis, 28)"

4.2. @TupleConstructor

Use@TupleConstructor no Groovy para adicionar um construtor parametrizado na classe. Esta anotação cria um construtor com um parâmetro para cada propriedade.

Por exemplo, vamos adicionar@TupleConstructor à classeEmployee:

@TupleConstructor
class Employee {
    long id
    String firstName
    String lastName
    int age
}

Agora, podemos criar o objetoEmployee passando parâmetros na ordem das propriedades definidas na classe.

Employee norman = new Employee(1, "norman", "lewis", 28)
assert norman.toString() == "Employee(norman, lewis, 28)"

Se não fornecermos valores para as propriedades ao criar objetos, o Groovy irá considerar os valores padrão:

Employee snape = new Employee(2, "snape")
assert snape.toString() == "Employee(snape, null, 0)"

Semelhante a@ToString, podemos declarar parâmetros comoexcludes,includeseincludeSuperProperties com@TupleConstructor para alterar o comportamento de seu construtor associado conforme necessário.

4.3. @EqualsAndHashCode

Podemos usar@EqualsAndHashCode para gerar a implementação padrão dos métodosequalsehashCode em tempo de compilação.

Vamos verificar o comportamento de@EqualsAndHashCode adicionando-o à classeEmployee:

Employee normanCopy = new Employee(1, "norman", "lewis", 28)

assert norman == normanCopy
assert norman.hashCode() == normanCopy.hashCode()

4.4. @Canonical

@Canonical is a combination of @ToString, @TupleConstructor, and @EqualsAndHashCode annotations.

Apenas adicionando-o, podemos facilmente incluir todos os três em uma classe Groovy. Além disso, podemos declarar@Canonical com qualquer um dos parâmetros específicos de todas as três anotações.

4.5. @AutoClone

Uma maneira rápida e confiável de implementar a interfaceCloneable é adicionando a anotação@AutoClone.

Vamos verificar o métodoclone depois de adicionar@AutoClone à classeEmployee:

try {
    Employee norman = new Employee(1, "norman", "lewis", 28)
    def normanCopy = norman.clone()
    assert norman == normanCopy
} catch (CloneNotSupportedException e) {
    e.printStackTrace()
}

4.6. Suporte de registro com@Log, @Commons, @Log4j, @Log4j2, e@Slf4j

Para adicionar suporte de registro a qualquer classe Groovy, tudo o que precisamos é adicionar anotações disponíveis no pacotegroovy.util.logging.

Vamos habilitar o registro fornecido pelo JDK adicionando a anotação@Log à classeEmployee. Depois, vamos adicionar o métodologEmp:

def logEmp() {
    log.info "Employee: $lastName, $firstName is of $age years age"
}

Chamar o métodologEmp em um objetoEmployee mostrará os logs no console:

Employee employee = new Employee(1, "Norman", "Lewis", 28)
employee.logEmp()
INFO: Employee: Lewis, Norman is of 28 years age

Da mesma forma, a anotação@Commons está disponível para adicionar suporte de registro do Apache Commons. @Log4j está disponível para suporte de registro do Apache Log4j 1.x e@Log4j2 paraApache Log4j 2.x. Finalmente, use@Slf4j para adicionar o suporteSimple Logging Facade for Java.

5. Conclusão

Neste tutorial, exploramos o conceito de metaprogramação no Groovy.

Ao longo do caminho, vimos alguns recursos de metaprogramação notáveis ​​para tempo de execução e tempo de compilação.

Ao mesmo tempo, exploramos anotações úteis adicionais disponíveis no Groovy para um código mais limpo e dinâmico.

Como de costume, as implementações de código para este artigo estão disponíveisover on GitHub.