Groovyでのメタプログラミング

  • link:/category/programming/ [プログラミング]

  • Groovy

1. 概要

link:/groovy-language[Groovy]は、https://www.baeldung.com/groovy-closures [closures]やhttps:// wwwなどの多数の機能を備えた動的かつ強力なJVM言語です。 .baeldung.com / groovy-traits [traits]。
このチュートリアルでは、Groovyでのメタプログラミングの概念を探ります。

2. メタプログラミングとは何ですか?

メタプログラミングは、メタデータを使用して自分自身または別のプログラムを変更するプログラムを作成するプログラミング手法です。
Groovyでは、実行時とコンパイル時の両方でメタプログラミングを実行できます。 今後は、両方の手法の注目すべき機能をいくつか検討していきます。

3. ランタイムメタプログラミング

ランタイムメタプログラミングにより、クラスの既存のプロパティとメソッドを変更できます。 また、新しいプロパティとメソッドを添付できます。すべて実行時に。
Groovyは、実行時にクラスの動作を変更するのに役立ついくつかのメソッドとプロパティを提供します。

3.1. propertyMissing

Groovyクラスの未定義のプロパティにアクセスしようとすると、_MissingPropertyException._がスローされます。例外を回避するために、Groovyは_propertyMissing_メソッドを提供します。
最初に、いくつかのプロパティを持つ_Employee_クラスを書きましょう:
class Employee {
    String firstName
    String lastName
    int age
}
次に、_Employee_オブジェクトを作成し、未定義のプロパティ_address._を表示しようとします。その結果、_MissingPropertyException _:_ + _がスローされます。
Employee emp = new Employee(firstName: "Norman", lastName: "Lewis")
println emp.address
groovy.lang.MissingPropertyException: No such property:
address for class: com.baeldung.metaprogramming.Employee
  • Groovyは、欠落しているプロパティリクエストをキャッチする_propertyMissing_メソッドを提供します。*したがって、実行時に_MissingPropertyException_を回避できます。

    欠落しているプロパティのgetterメソッド呼び出しをキャッチするために、プロパティ名の単一の引数で定義します。
def propertyMissing(String propertyName) {
    "property '$propertyName' is not available"
}
assert emp.address == "property 'address' is not available"
また、同じメソッドがプロパティの値として2番目の引数を持ち、欠落しているプロパティのセッターメソッド呼び出しをキャッチできます。
def propertyMissing(String propertyName, propertyValue) {
    println "cannot set $propertyValue - property '$propertyName' is not available"
}

3.2. methodMissing

_methodMissing_メソッドは、_propertyMissing_に似ています。 ただし、_methodMissing_は、欠落しているメソッドの呼び出しをインターセプトし、それによって_MissingMethodException_を回避します。
_Employee_オブジェクトで_getFullName_メソッドを呼び出してみましょう。 _getFullName_がないため、実行時に実行時に_MissingMethodException_がスローされます。
try {
    emp.getFullName()
} catch (MissingMethodException e) {
    println "method is not defined"
}
そのため、_try-catch_でメソッド呼び出しをラップする代わりに、_methodMissing_を定義できます。
def methodMissing(String methodName, def methodArgs) {
    "method '$methodName' is not defined"
}
assert emp.getFullName() == "method 'getFullName' is not defined"

3.3. _ ExpandoMetaClass _

  • Groovyは、すべてのクラスで_metaClass_プロパティを提供します。 ** _ metaClass_プロパティは、_ExpandoMetaClass_のインスタンスを参照します。

    http://docs.groovy-lang.org/latest/html/api/groovy/lang/ExpandoMetaClass.html[_ExpandoMetaClass_]クラスは、実行時に既存のクラスを変換する多くの方法を提供します。 たとえば、プロパティ、メソッド、またはコンストラクターを追加できます。
    まず、_metaClass_プロパティを使用して、欠落している_address_プロパティを_Employee_クラスに追加します。
Employee.metaClass.address = ""
Employee emp = new Employee(firstName: "Norman", lastName: "Lewis", address: "US")
assert emp.address == "US"
さらに、不足している_getFullName_メソッドを実行時に_Employee_クラスオブジェクトに追加しましょう。
emp.metaClass.getFullName = {
    "$lastName, $firstName"
}
assert emp.getFullName() == "Lewis, Norman"
同様に、実行時に_Employee_クラスにコンストラクターを追加できます。
Employee.metaClass.constructor = { String firstName ->
    new Employee(firstName: firstName)
}
Employee norman = new Employee("Norman")
assert norman.firstName == "Norman"
assert norman.lastName == null
同様に、_metaClass.static._を使用して_static_メソッドを追加できます。
  • _metaClass_プロパティは、ユーザー定義クラスを変更するのに便利なだけでなく、実行時に既存のJavaクラスも変更します。*

    たとえば、_capitalize_メソッドを_String_クラスに追加しましょう。
String.metaClass.capitalize = { String str ->
    str.substring(0, 1).toUpperCase() + str.substring(1);
}
assert "norman".capitalize() == "Norman"

3.4. 拡張機能

*拡張機能は、実行時にクラスにメソッドを追加し、グローバルにアクセスできるようにします。*
拡張機能で定義されるメソッドは、_self_クラスオブジェクトを最初の引数として、常に静的である必要があります。
たとえば、_Basic_Extension_クラスを記述して、_getYearOfBirth_メソッドを_Employee_クラスに追加します。
class BasicExtensions {
    static int getYearOfBirth(Employee self) {
        return (new Date().getYear() + 1900) - self.age;
    }
}
__BasicExtension__sを有効にするには、プロジェクトの_META-INF / services_ディレクトリに構成ファイルを追加する必要があります。
したがって、次の構成で_org.codehaus.groovy.runtime.ExtensionModule_ファイルを追加しましょう。
moduleName=core-groovy-2
moduleVersion=1.0-SNAPSHOT
extensionClasses=com.baeldung.metaprogramming.extension.BasicExtensions
_Employee_クラスに追加された_getYearOfBirth_メソッドを確認しましょう。
Employee emp = new Employee(age: 28)
assert emp.getYearOfBirth() == 1991
同様に、クラスに_static_メソッドを追加するには、別の拡張クラスを定義する必要があります。
たとえば、_StaticEmployeeExtension_クラスを定義して、_Employee_クラスに_static_メソッド_getDefaultObj_を追加しましょう。
class StaticEmployeeExtension {
    static Employee getDefaultObj(Employee self) {
        return new Employee(firstName: "firstName", lastName: "lastName", age: 20)
    }
}
次に、_ExtensionModule_ファイルに次の構成を追加して、_StaticEmployeeExtension_を有効にします。
staticExtensionClasses=com.baeldung.metaprogramming.extension.StaticEmployeeExtension
ここで必要なのは、_Employee_クラスで_static_ _getDefaultObj_メソッドをテストすることだけです。
assert Employee.getDefaultObj().firstName == "firstName"
assert Employee.getDefaultObj().lastName == "lastName"
assert Employee.getDefaultObj().age == 20
同様に、拡張機能を使用して、_Integer_や_Long_などのメソッドをプリコンパイル済みのJavaクラスに追加できます。
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. コンパイル時メタプログラミング

特定の注釈を使用すると、コンパイル時にクラス構造を簡単に変更できます。 言い換えれば、*私たちはアノテーションを使用して、コンパイル時にクラスの抽象構文ツリーを変更することができます*。
Groovyでボイラープレートコードを削減するのに非常に便利なアノテーションのいくつかについて説明しましょう。 それらの多くは_groovy.transform_パッケージで利用可能です。
慎重に分析すると、Javaのlink:/intro-to-project-lombok[Project Lombok]に似た機能を提供するアノテーションがいくつかあることがわかります。

4.1. _ @ ToString_

_ @ ToString_アノテーションは、_toString_メソッドのデフォルトの実装をコンパイル時にクラスに追加します。 必要なのは、クラスに注釈を追加することです。
たとえば、_ @ ToString_注釈を_Employee_クラスに追加しましょう。
@ToString
class Employee {
    long id
    String firstName
    String lastName
    int age
}
ここで、_Employee_クラスのオブジェクトを作成し、_toString_メソッドによって返される文字列を確認します。
Employee employee = new Employee()
employee.id = 1
employee.firstName = "norman"
employee.lastName = "lewis"
employee.age = 28

assert employee.toString() == "com.baeldung.metaprogramming.Employee(1, norman, lewis, 28)"
出力文字列を変更するために、_ @ ToString_を使用して_excludes _、_ includes _、_ includePackage _、_ ignoreNulls_などのパラメーターを宣言することもできます。
たとえば、_id_と_package_をEmployeeオブジェクトの文字列から除外しましょう。
@ToString(includePackage=false, excludes=['id'])
assert employee.toString() == "Employee(norman, lewis, 28)"

4.2. _ @ TupleConstructor_

Groovyで_ @ TupleConstructor_を使用して、クラスにパラメーター化されたコンストラクターを追加します。 この注釈は、各プロパティのパラメーターを持つコンストラクターを作成します。
たとえば、_ @ TupleConstructor_を_Employee_クラスに追加しましょう。
@TupleConstructor
class Employee {
    long id
    String firstName
    String lastName
    int age
}
これで、クラスで定義されたプロパティの順序でパラメーターを渡す_Employee_オブジェクトを作成できます。
Employee norman = new Employee(1, "norman", "lewis", 28)
assert norman.toString() == "Employee(norman, lewis, 28)"
オブジェクトの作成中にプロパティに値を提供しない場合、Groovyはデフォルト値を考慮します。
Employee snape = new Employee(2, "snape")
assert snape.toString() == "Employee(snape, null, 0)"
_ @ ToString_と同様に、_ @ TupleConstructor_を使用して_excludes _、_ includes _、_ includeSuperProperties_などのパラメーターを宣言し、必要に応じて関連するコンストラクターの動作を変更できます。

4.3. _ @ EqualsAndHashCode_

_ @ EqualsAndHashCode_を使用して、コンパイル時に_equals_および_hashCode_メソッドのデフォルト実装を生成できます。
_ @ EqualsAndHashCode_の動作を_Employee_クラスに追加して確認しましょう。
Employee normanCopy = new Employee(1, "norman", "lewis", 28)

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

4.4. _ @ Canonical_

  • _ @ Canonical_は、_ @ ToString @ TupleConstructor_、および_ @ EqualsAndHashCode_アノテーションの組み合わせです*。

    追加するだけで、3つすべてをGroovyクラスに簡単に含めることができます。 また、3つのアノテーションすべての特定のパラメーターのいずれかを使用して_ @ Canonical_を宣言できます。

4.5. _ @ AutoClone_

_Cloneable_インターフェースを実装するための迅速で信頼できる方法は、_ @ AutoClone_アノテーションを追加することです。
_Employee_クラスに_ @ AutoClone_を追加した後、_clone_メソッドを確認しましょう。
try {
    Employee norman = new Employee(1, "norman", "lewis", 28)
    def normanCopy = norman.clone()
    assert norman == normanCopy
} catch (CloneNotSupportedException e) {
    e.printStackTrace()
}

4.6. _ @ Log、@ Commons、@ Log4j、@ Log4j2、、および @ Slf4j_によるロギングのサポート

Groovyクラスにロギングサポートを追加するには、_groovy.util.logging_パッケージで使用可能な注釈を追加するだけです。
_Employee_クラスに_ @ Log_アノテーションを追加して、JDKが提供するロギングを有効にしましょう。 その後、_logEmp_メソッドを追加します。
def logEmp() {
    log.info "Employee: $lastName, $firstName is of $age years age"
}
_Employee_オブジェクトで_logEmp_メソッドを呼び出すと、コンソールにログが表示されます。
Employee employee = new Employee(1, "Norman", "Lewis", 28)
employee.logEmp()
INFO: Employee: Lewis, Norman is of 28 years age
同様に、Apache Commonsロギングサポートを追加するために_ @ Commons_注釈を使用できます。 _ @ Log4j_は、Apache Log4j 1.xロギングサポートに使用でき、_ @ Log4j2_はlink:/java-logging-intro[Apache Log4j 2.x]に使用できます。 最後に、_ @ Slf4j_を使用してlink:/slf4j-with-log4j2-logback[Simple Logging Facade for Java]サポートを追加します。

5. 結論

このチュートリアルでは、Groovyでのメタプログラミングの概念を検討しました。
途中で、実行時とコンパイル時の両方でいくつかの注目すべきメタプログラミング機能を見てきました。
同時に、Groovyでよりクリーンで動的なコードを作成するために利用できる便利なアノテーションを追加で検討しました。
いつものように、この記事のコード実装はhttps://github.com/eugenp/tutorials/tree/master/core-groovy-2[GitHub上]で入手できます。