Groovy DSL機能を使用してJavaアプリケーションを構成する

背景



みなさんこんにちは! 恐ろしい設定と、それらがどのように櫛で正常になったかについての話をしたいと思います。 私はかなり大規模で比較的古いプロジェクトに取り組んでいます。このプロジェクトは常に更新および拡張されています。 構成は、xmlファイルをJava Beanにマッピングすることで指定されます。 最良の解決策ではありませんが、利点があります。たとえば、サービスを作成するときに、そのセクションを担当する構成を持つBeanをサービスに渡すことができます。 ただし、欠点もあります。 これらの最も重要な点は、構成プロファイルの通常の継承がないことです。 ある時点で、1つの設定を変更するには、各プロファイルに1つずつ、約30のxmlファイルを編集する必要があることに気付きました。 これはこれ以上続けることができず、すべてを書き直すという強い意思が下されました。







必要条件





私はこの設定を次のようにしたいと思います:







典型的なGroovy DSLスクリプト
 name = "MyTest" description = "Apache Tomcat" http { port = 80 secure = false } https { port = 443 secure = true } mappings = [ { url = "/" active = true }, { url = "/login" active = false } ]
      
      





これをどのように達成したか-カットの下で。







たぶん、このためのライブラリはすでにありますか?



おそらくそうです。 しかし、私が見つけて見たもののうち、私にふさわしいものはありませんでした。 それらのほとんどは、構成を読み取り、それらを1つの大きな構成に結合してから、個別のプロッターを介して受信した構成を処理するために設計されています。 ビンにマップする方法を知っている人はほとんどいないため、数十個のアダプターコンバーターの作成には時間がかかりすぎます。 Lightbendの設定は、そのHOCON形式と継承/再定義をそのまま使用できる最も有望なもののようです。 そして、彼女はほとんどJava Beanを埋めることさえできましたが、判明したように、彼女はマップする方法を知らず、非常に貧弱に拡張します。 私が彼女で実験している間、同僚は結果の構成を見て、「ある意味ではGroovy DSLに似ている」と言った。 それで、それを使うことに決めました。







これは何ですか



DSL(ドメイン固有言語)は、特定のアプリケーション分野、この場合はアプリケーションの構成に合わせて調整された言語です。 猫の前のネタバレに例があります。







Javaアプリケーションからgroovyスクリプトを実行するのは簡単です。 たとえば、Gradleに応じて、Groovyを追加する必要があります。







 compile 'org.codehaus.groovy:groovy-all:2.3.11'
      
      





GroovyShellを使用します







  GroovyShell shell = new GroovyShell(); Object value = shell.evaluate(pathToScript);
      
      





どのように機能しますか?



すべての魔法は2つのことに基づいています。







代表団



まず、groovyスクリプトがバイトコードにコンパイルされ、クラスが作成されます。スクリプトが実行されると、すべてのスクリプトコードを含むこのクラスのrun()メソッドが呼び出されます。 スクリプトが値を返す場合、 evaluate()



結果として取得できます。 原則として、スクリプト内の構成でBeanを作成して返すことは可能ですが、この場合、美しい構文は得られません。







代わりに、特別なタイプのスクリプトDelegatingScriptを作成できます。 その特徴は、デリゲートオブジェクトを渡すことができ、すべてのメソッド呼び出しとフィールドの操作がそれに委任されることです。 リンクドキュメントには使用例があります。

設定を含むクラスを作成しましょう







 @Data public class ServerConfig extends GroovyObjectSupport { private String name; private String description; }
      
      





@Data



ロンボクライブラリのアノテーション:フィールドにゲッターとセッターを追加し、toString、equals、hashCodeを実装します。 彼女のおかげで、POJOはビンに変わります。







GroovyObjectSupport



は、「groovyオブジェクトのように見せたいjavaオブジェクト」(ドキュメントに記載されている)の基本クラスです。 後で必要な理由を示します。 この段階では、それなしで実行できますが、すぐに実行できます。







次に、フィールドに入力するスクリプトを作成します。







 name = "MyTestServer" description = "Apache Tomcat"
      
      





ここではすべてが明らかです。 これまでのところ、ご覧のとおり、DSL機能は使用していません。後で説明します。







そして最後に、Javaから実行します







 CompilerConfiguration cc = new CompilerConfiguration(); cc.setScriptBaseClass(DelegatingScript.class.getName()); //      groovy     DelegatingScript GroovyShell sh = new GroovyShell(Main.class.getClassLoader(), new Binding(), cc); DelegatingScript script = (DelegatingScript)sh.parse(new File("config.groovy")); ServerConfig config = new ServerConfig(); //     script.setDelegate(config); //    run()  " "  config     name  description script.run(); System.out.println(config.toString());
      
      





ServerConfig(name=MyTestServer, description=Apache Tomcat)



は、toString()のロンボック実装の結果です。







ご覧のとおり、すべてが非常に簡単です。 configは実際の実行可能なgroovyコードであり、その中の言語のすべての機能、たとえば置換を使用できます







 def postfix = "server" name = "MyTest ${postfix}" description = "Apache Tomcat ${postfix}"
      
      





ServerConfig(name=MyTest server, description=Apache Tomcat server)









このスクリプトでは、ブレークポイントと借方を設定することもできます!







メソッド呼び出し



それでは、実際のDSLに移りましょう。 コネクタ設定を構成に追加するとします。 これらは次のようになります。







 @Data public class Connector extends GroovyObjectSupport { private int port; private boolean secure; }
      
      





2つのコネクタ、httpおよびhttpsのフィールドをサーバー構成に追加します。







 @Data public class ServerConfig extends GroovyObjectSupport { private String name; private String description; private Connector http; private Connector https; }
      
      





このグルーヴィーなコードを使用して、スクリプトからコネクタを設定できます







 import org.example.Connector //... http = new Connector(); http.port = 80 http.secure = false
      
      





実行結果

ServerConfig(name=MyTest, description=Apache Tomcat, http=Connector(port=80, secure=false), https=null)









ご覧のとおり、これは機能しましたが、もちろん、このような構文は設定にはまったく適していません。 見たいように設定を書き直します:







 name = "MyTest" description = "Apache Tomcat" http { port = 80 secure = false } https { port = 443 secure = true }
      
      





結果は例外です。

Exception in thread "main" groovy.lang.MissingMethodException: No signature of method: config.http() is applicable for argument types: (config$_run_closure1) values: [config$_run_closure1@780cb77]









http(Closure)



メソッドを呼び出そうとしているように見えますが、groovyはデリゲートオブジェクトまたはスクリプト内でそれを見つけることができません。 もちろん、ServersConfigクラスで宣言することもできます。







  public void http(Closure closure) { http = new Connector(); closure.setDelegate(http); closure.setResolveStrategy(Closure.DELEGATE_FIRST); closure.call(); }
      
      





httpsについても同様です。 今回はすべてが良いです:







実行結果

ServerConfig(name=MyTest, description=Apache Tomcat, http=Connector(port=80, secure=false), https=Connector(port=443, secure=true))









ここでは、これがDSLへの最初のステップであるため、何をしたかを説明する必要があります。 groovy.lang.Closure



パラメーターgroovy.lang.Closure



受け取り、 groovy.lang.Closure



フィールドの新しいオブジェクトを作成し、結果のクロージャーに委任し、クロージャーコードを実行するメソッドを宣言しました。 ひも







 closure.setResolveStrategy(Closure.DELEGATE_FIRST);
      
      





つまり、フィールドまたはメソッドを参照する場合、groovyは最初にデリゲートを調べ、次に適切なものが見つからない場合にのみクロージャーを調べます。 スクリプトの場合、この戦略はデフォルトで使用されます;スクリプトを閉じるには、手動でインストールする必要があります。







ネタバレ見出し

groovyを介して構成可能なログバックライブラリは、このアプローチのみを使用します。 DSLで使用されるすべてのメソッドを明示的に実装しました。







原則として、すでに特定のDSLがありますが、理想からはほど遠いです。 最初に、各フィールドを設定するコードを手動で記述しないようにします。次に、構成で使用されるすべてのクラスのBeanのコードの重複を避けます。 そして、ここでグルーヴィーなDSLマジックの2番目のコンポーネントが助けになります...







methodMissing()



groovyは、オブジェクトにないメソッド呼び出しに遭遇するたびに、methodMissing()の呼び出しを試みます。 パラメータとして、呼び出そうとしたメソッドの名前と引数のリストがそこに渡されます。 ServerConfigクラスからhttpおよびhttpsメソッドを削除し、代わりに次を宣言します。







 public void methodMissing(String name, Object args) { System.out.println(name + " was called with " + args.toString()); }
      
      





argsは実際にはObject[]



型ですが、groovyはそのシグネチャだけのメソッドを探しています。 チェック:







 http was called with [Ljava.lang.Object;@16aa0a0a https was called with [Ljava.lang.Object;@691a7f8f ServerConfig(name=MyTest, description=Apache Tomcat, http=null, https=null)
      
      





必要なもの! 引数を展開し、パラメータのタイプに応じてフィールド値を設定するためだけに残ります。 この例では、Closureクラスの1つの要素の配列がそこに渡されます。 たとえば、次のようにします。







  public void methodMissing(String name, Object args) { MetaProperty metaProperty = getMetaClass().getMetaProperty(name); if (metaProperty != null) { Closure closure = (Closure) ((Object[]) args)[0]; Object value = getProperty(name) == null ? metaProperty.getType().getConstructor().newInstance() : getProperty(name); closure.setDelegate(value); closure.setResolveStrategy(Closure.DELEGATE_FIRST); closure.call(); setProperty(name, value); } else { throw new IllegalArgumentException("No such field: " + name); } }
      
      





チェックと例外

コードを乱雑にしないために、ほとんどすべてのチェックを省略し、例外をキャッチします。 もちろん、実際のプロジェクトでは、これを直接行うことはできません。







ここでは、groovyオブジェクトに固有のいくつかの呼び出しを確認します。









これまで、1つのクラスServerConfigにmethodMissingとすべてのdslバンを追加しました。 Connectionにも同じメソッドを実装できますが、なぜコードを複製するのですか? GroovyConfigurableなどのすべての構成ビンの基本クラスを作成し、methodMissingを転送して、ServerConfigとConnectorを継承しましょう。







そのようなもの
 public class GroovyConfigurable extends GroovyObjectSupport { @SneakyThrows public void methodMissing(String name, Object args) { MetaProperty metaProperty = getMetaClass().getMetaProperty(name); if (metaProperty != null) { Closure closure = (Closure) ((Object[]) args)[0]; Object value = getProperty(name) == null ? metaProperty.getType().getConstructor().newInstance() : getProperty(name); closure.setDelegate(value); closure.setResolveStrategy(Closure.DELEGATE_FIRST); closure.call(); setProperty(name, value); } else { throw new IllegalArgumentException("No such field: " + name); } } } @Data public class ServerConfig extends GroovyConfigurable { private String name; private String description; private Connector http; private Connector https; } @Data public class Connector extends GroovyConfigurable { private int port; private boolean secure; }
      
      





GroovyConfigurableはその相続人のフィールドについて何も知らないにもかかわらず、これはすべて機能します!







継承



次のステップでは、設定に親設定を含め、個々のフィールドを再定義できるようにします。 次のようになります。







 include 'parent.groovy' name = "prod" https { port = 8080 }
      
      





Groovyでは、クラスをインポートできますが、スクリプトはインポートできません。 最も簡単な方法は、GroovyConfigurableクラスにincludeメソッドを実装することです。 スクリプト自体へのパスといくつかのメソッドを追加します。







Groovyconfigurable
  private URI scriptPath; @SneakyThrows public void include(String path) { //        URI uri = Paths.get(scriptPath).getParent().resolve(path).toUri(); runFrom(uri); } @SneakyThrows public void runFrom(URI uri) { this.scriptPath = uri; //  ,     main- CompilerConfiguration cc = new CompilerConfiguration(); cc.setScriptBaseClass(DelegatingScript.class.getName()); GroovyShell sh = new GroovyShell(Main.class.getClassLoader(), new Binding(), cc); DelegatingScript script = (DelegatingScript)sh.parse(uri); script.setDelegate(this); script.run(); }
      
      





parent.groovy構成を作成しましょう。ここでは、いくつかの基本的な構成について説明します。







 name = "PARENT NAME" description = "PARENT DESCRIPTION" http { port = 80 secure = false } https { port = 443 secure = true }
      
      





config.groovyでは、オーバーライドするもののみを残します。







 include "parent.groovy" name = "MyTest" https { port = 8080 }
      
      





実行結果

ServerConfig(name=MyTest, description=PARENT DESCRIPTION, http=Connector(port=80, secure=false), https=Connector(port=8080, secure=true))









ご覧のとおり、名前はhttpsのポートフィールドのように再定義されています。 その中の安全なフィールドは、親設定から残ります。







さらに進んで、構成全体ではなく、個々の部分を含めることができます! これを行うには、設定するフィールドもGroovyConfigurableであることをmethodMissingにチェックを追加し、親スクリプトへのパスを指定します。







ネタバレ見出し
  public void methodMissing(String name, Object args) { MetaProperty metaProperty = getMetaClass().getMetaProperty(name); if (metaProperty != null) { Closure closure = (Closure) ((Object[]) args)[0]; Object value = getProperty(name) == null ? metaProperty.getType().getConstructor().newInstance() : getProperty(name); if (value instanceof GroovyConfigurable) { ((GroovyConfigurable) value).scriptPath = scriptPath; } closure.setDelegate(value); closure.setResolveStrategy(Closure.DELEGATE_FIRST); closure.call(); setProperty(name, value); } else { throw new IllegalArgumentException("No such field: " + name); } }
      
      





これにより、スクリプト全体だけでなく、その一部も含めることができます! たとえば







 http { include "http.groovy" }
      
      





http.groovyは







 port = 90 secure = true
      
      





これはすでに素晴らしい結果ですが、小さな問題があります。







ジェネリック



サーバーの設定にマッピングとそのステータスを追加するとします。







config
 name = "MyTest" description = "Apache Tomcat" http { port = 80 secure = false } https { port = 443 secure = true } mappings = [ { url = "/" active = true }, { url = "/login" active = false } ]
      
      





Mapping.java
 @Data public class Mapping extends GroovyConfigurable { private String url; private boolean active; }
      
      





Serverconfig.java
 @Data public class ServerConfig extends GroovyConfigurable { private String name; private String description; private Connector http; private Connector https; private List<Mapping> mappings; }
      
      





始めます...

ServerConfig(name=MyTest, description=Apache Tomcat, http=Connector(port=80, secure=false), https=Connector(port=443, secure=true), mappings=[config$_run_closure3@14ec4505, config$_run_closure4@53ca01a2])









おっと すべての栄光に消去を入力します。 残念ながら、魔法はここで終わり、手で読んだものを修正する必要があります。 たとえば、別個のGroovyConfigurable#postProcess()



メソッドを使用する







コード
 public void postProcess() { for (MetaProperty metaProperty : getMetaClass().getProperties()) { Object value = getProperty(metaProperty.getName()); if (Collection.class.isAssignableFrom(metaProperty.getType()) && value instanceof Collection) { //      ParameterizedType collectionType = (ParameterizedType) getClass().getDeclaredField(metaProperty.getName()).getGenericType(); //       ,  ,    ,    //  ,      Class itemClass = (Class)collectionType.getActualTypeArguments()[0]; //      ,       GroovyConfigurable //   , ,    if (GroovyConfigurable.class.isAssignableFrom(itemClass)) { Collection collection = (Collection) value; //      ,    ,      Collection newValue = collection.getClass().newInstance(); for (Object o : collection) { if (o instanceof Closure) { //      Object item = itemClass.getConstructor().newInstance(); ((GroovyConfigurable) item).setProperty("scriptPath", scriptPath); ((Closure) o).setDelegate(item); ((Closure) o).setResolveStrategy(Closure.DELEGATE_FIRST); ((Closure) o).call(); ((GroovyConfigurable) item).postProcess(); //     ? newValue.add(item); } else { newValue.add(o); } } setProperty(metaProperty.getName(), newValue); } } } }
      
      





もちろん、それはいものになりましたが、その仕事はします。 さらに、これは1つの基本クラスに対してのみ記述しているため、継承者に対して繰り返す必要はありません。 config.postProcess();



呼び出した後config.postProcess();



使用可能なBeanを取得します。







おわりに



もちろん、ここで与えられたコードは、設定のために実際のライブラリに必要なもののほんの小さな(最も単純な)部分であり、ユースケースが複雑になるほど、手動処理とチェックを追加する必要が増えます。 たとえば、マップ、列挙、ネストされたジェネリックなどのサポート。 リストはどんどん増えていきますが、私のニーズには、記事で提供したもので十分でした。 これもあなたの助けになり、あなたの設定がより美しく、便利になることを願っています!








All Articles