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を取埗したす。


おわりに


もちろん、ここで䞎えられたコヌドは、蚭定のために実際のラむブラリに必芁なもののほんの小さな最も単玔な郚分であり、ナヌスケヌスが耇雑になるほど、手動凊理ずチェックを远加する必芁が増えたす。 たずえば、マップ、列挙、ネストされたゞェネリックなどのサポヌト。 リストはどんどん増えおいきたすが、私のニヌズには、蚘事で提䟛したもので十分でした。 これもあなたの助けになり、あなたの蚭定がより矎しく、䟿利になるこずを願っおいたす



Source: https://habr.com/ru/post/J358594/


All Articles