[Grails/Adobe AIR] リロード
Flex Plugin を利用すると、Grails のサービスを Adobe AIR から利用できるようになります。といっても Flex 自体のコーディングが私は初めてなので四苦八苦している最中であり、詳しい話はまた別の機会にします。
というわけで、今回は小ねた。
FLEX で AIR のアプリを開発して気づいたのはFLEX で AIR のアプリを作ると実行しながら UI 側のコードを編集していくこと(リロード)ができなくなるということです・・・。Grails の問題ではなく、Flex Builder の現在の仕様というところでしょうか。将来的には、リロードできるようになって欲しい!
試すことができていませんが、HTML と JavaScript ベースの AIR アプリだと可能なのでしょうか?
jBPM
[FYI]
まだ試していませんが、Jbpm Pluginが出ました。
http://www.grails.org/Jbpm+Plugin
-
-
- -
-
現在、SpaceCard の trunk には、jBPM を利用する為の設定や jar などを配置しています。しかし、現状、問題があります。実際に Grails のプロジェクトに jBPM を組み込んだ方ならご存知と思いますが、grails run-app を実行すると、次のような画面出力がされます。
[10328] cfg.GrailsHibernateUtil did not find superclass names when mapping inheritance.... [10328] cfg.GrailsHibernateUtil did not find superclass names when mapping inheritance.... [10328] cfg.GrailsHibernateUtil did not find superclass names when mapping inheritance.... [10328] cfg.GrailsHibernateUtil did not find superclass names when mapping inheritance.... (さらに続く)
この出力は、Grails が出しているのですが、原因は、 abstract がついたドメインクラスに対する GORM の考慮不足にあります。jBPM は、Hibernate を利用しているので、本来問題なく GORM に取り込めるはずなのですが、jBPM のドメインクラスには abstract が付いたものが多く含まれている為、先の考慮不足の問題によりこの画面出力がでます。
SpaceCard では、jBPM の利用を考えているのですが、bug fix がうまく出来ずこの画面出力を取り除くことができない場合は、jBPM の利用は諦めごりごり実装しようと思っています。
テストとリロード
先日、Grails の Artefact は、開発モードで実行している時に編集したり新規追加するとリロードあるいはロードされるといった記事を書きました(Seasar の Hot Deploy のようなもの)。
以下、この事を単に「リロード」と書きます。
Grails は、書こうと思えば、全てのレイヤのテストを書くことができます。機能テスト以外は、Grails 自体がその仕組みを持っており、機能テストに関しては webtest Plugin を利用します。
さてさて、これらの仕組みを使って完璧なテスト駆動開発を行った場合、リロードという仕掛けは必要でしょうか?
完璧なビヘイビア駆動開発ではどうでしょうか?
恐らくきっと、不要なはずなのです。
つまり、リロードは、テスト駆動開発やビヘイビア駆動開発でない開発スタイルの場合に本来必要とされるものと言えるでしょう。けれども、なかなか完璧なモノやコトはありませんので、完璧でないテスト駆動開発やビヘイビア駆動開発というのが大方でしょう。従って、それらの完璧でないケースでも、リロードはあれば便利な仕掛けとやはり言えるのでしょう。
ただ私は不満があります。上で書いた意味での完璧でないテスト駆動開発やビヘイビア駆動開発において、リロードの仕掛けにより、次のような流れで作業ができると良いのにと思っているのですが、Grails ではできません。
1. アプリを起動する (grails run-app)
2. Artefact を追加あるいは修正する
3. テストコードを書いてテストを実行 (grails test-app; これは、必要と思った時)
4. アプリで動作確認(もちろんアプリは起動したまま)
5. 2 から繰り返し
Grails の場合、3. でテストを実行すると次のようなログが出力されてコンテナがリフレッシュします。
2008-04-04 00:02:32.326:/pagination:INFO: Initializing Spring root WebApplicati onContext [242187] spring.GrailsWebApplicationContext Refreshing org.codehaus.groovy.grail s.commons.spring.GrailsWebApplicationContext@111cab5: display name [org.codehaus .groovy.grails.commons.spring.GrailsWebApplicationContext@111cab5]; startup date [Fri Apr 04 00:02:32 JST 2008]; parent: org.springframework.web.context.support .XmlWebApplicationContext@46f93 [242187] spring.GrailsWebApplicationContext Bean factory for application context [org.codehaus.groovy.grails.commons.spring.GrailsWebApplicationContext@111cab5] : org.springframework.beans.factory.support.DefaultListableBeanFactory@1b766c1 2008-04-04 00:02:33.920:/pagination:INFO: Initializing Spring FrameworkServlet 'grails' 2008-04-04 00:02:33.936::INFO: Started SelectChannelConnector@0.0.0.0:8080
完璧でないテスト駆動開発の下でコードを書いている時に、テストを書くまでもないと思う所と、ここはテストを書いて確認しておきたいと思う所があります。後者がアプリを起動した状態で出来るとかなり満足なのですが・・・。
Pagination
次のようなドメインクラスを作ります。
class DomainA { static constraints = { text(blank:false) } String text }
このクラスは、text という属性を持っているだけの Domain クラスです。
次に、View と Controller を作ります。
これは、実験用のアプリですので、scaffolding した方が楽チンです。
次のように入力します。
grails generate-all DmainA
この状態でアプリケーションを起動して、DomainA クラスのオブジェクトを 11個作ります。
View でみると次のようになります。
ブラウザのアドレス欄の URL は、次のようになっています。
http://localhost:8080/pagination/domainA/list
この状態で、画面の左下にある[次へ]ボタンを押してページ送りします。
すると、オブジェクトが一つだけリスト表示され、ブラウザのアドレス欄の URL は次のようになります。
http://localhost:8080/pagination/domainA/list?offset=10&max=10&sort=id&order=asc
ここまで正常です。
ここで、ブラウザのアドレス欄の URL を次のように変えます。
http://localhost:8080/pagination/domainA/list?offset=A&max=10&sort=id&order=asc
offset のところを `10' から `A' に変えています。
おもむろに、変更した URL に移動すると次の例外が発生します。
org.codehaus.groovy.runtime.InvokerInvocationException: org.springframework.beans.TypeMismatchException: Failed to convert value of type [java.lang.String] to required type [java.lang.Integer]; nested exception is java.lang.NumberFormatException: For input string: "A" at org.codehaus.groovy.reflection.CachedMethod.invoke(CachedMethod.java:92) at groovy.lang.MetaMethod.doMethodInvoke(MetaMethod.java:226) at groovy.lang.MetaClassImpl.invokeMethod(MetaClassImpl.java:899) at groovy.lang.ExpandoMetaClass.invokeMethod(ExpandoMetaClass.java:946) at groovy.lang.MetaClassImpl.invokeMethod(MetaClassImpl.java:740) at groovy.lang.Closure.call(Closure.java:292) at groovy.lang.Closure.call(Closure.java:287) (省略) Caused by: org.springframework.beans.TypeMismatchException: Failed to convert value of type [java.lang.String] to required type [java.lang.Integer]; nested exception is java.lang.NumberFormatException: For input string: "A" at DomainAController$_closure2.doCall(DomainAController.groovy:11) at DomainAController$_closure2.doCall(DomainAController.groovy) Caused by: java.lang.NumberFormatException: For input string: "A" at java.lang.NumberFormatException.forInputString(NumberFormatException.java:48) at java.lang.Integer.parseInt(Integer.java:447) at java.lang.Integer.valueOf(Integer.java:526) at java.lang.Integer.decode(Integer.java:919) ... 2 more
Integer.parseInt に失敗して例外が発生したのですが、これは パラメタ `max' でも同様に同じ例外が発生します。
この例外が発生する Controller の該当場所は、次のようになっています。
def list = { if(!params.max) params.max = 10 [ domainAList: DomainA.list( params ) ] }
このコードは、Grails の `grails generate-all' で生成したものです。問題の箇所は params を与えている DomainA.list() です。
このメソッドは、起動に HibernateGrailsPlugin クラスによって自動的に付与されるダイナミックメソッドです。
次のようにダイナミックメソッドを登録しています。
def listMethod = new ListPersistentMethod(sessionFactory, classLoader) metaClass.'static'.list = {-> listMethod.invoke(domainClassType, "list", [] as Object[])} metaClass.'static'.list = {Map args -> listMethod.invoke(domainClassType, "list", [args] as Object[])}
ListPersistentMethod クラスを見てみると、最終的には GrailsHibernateUtilクラスの populateArgumentsForCriteria() で例外が発生していることが分かります。
コードを見ると、Integer.parseInt() で int 型に変換できることをあてにしているコードになっています。
例えば、params に「変換で失敗する場合はこの値にしてください」というものを与えることが出来たら安心して利用できるわけですが、今はそうはなっていません。pagination が可能なメソッド全てがそうです。
要するに、pagination に関係するクエリパラメタの書き換えにより、例外画面になっては困る場合、pagination 可能なメソッドを呼び出す前に、変換に失敗しないような考慮を呼び出し側で行う必要があるということでした…。(Grails 1.0.2での話)
Grails Plugin の可能性
「The Seasar Project」のサイトを訪れると、そこにはたくさの Seasar に関係したプロダクトが紹介されています。それらは(漏れがあるかもしれませんが)Mayaa を除いて、概ね『DI Container という基盤技術の上に要素技術や仕様実装を載せることを目的にしたプロダクト』が占めているように見えます。
一方、「Grails の Plugins」 のサイトを訪れると、Grails が採用している Spring Framework の『DI Container という基盤技術の上に要素技術や仕様実装を載せることを目的とした Plugin 』以外のものが存在することに気が付きます。例えば、Searchable Plugin や RichUI Plugin といったものですが、これら以外にも半数以上がそのような Plugin となっています。
Plugin という仕掛けは目新しいものではありませんし、そのアーキテクチャもシンプルなものから複雑なものまで、それこそ古今東西色々と今まで現れました。Grails もこの目新しくない Plugin という仕掛けを持っているのですが、実際に Grails の Plugin を実装してみると、Plugin として必要な振舞いをとても簡単に実装できることを実感できます。そして、ここにLL言語によるパワーを私は感じています。言い換えると、関わり易い仕掛けの構築においてLL言語は有用な材料となりえると Grails の Plugin を実装してみて感じています。
さて、いくら簡単に実装ができても、作られたものがそれほど有用でないならあまり意味もありません。
現在公開されている Grails Plugin の中で、多くの人に有用性を実感してもらい易いものは、先に挙げた Searchable Plugin ではないかと思っています。
このPluginは、インストールした後、簡単な設定を Domain クラスにするだけで Domain クラスを Compass(Lucene) によって検索可能にすることができる Plugin です。この Plugin のソースを覗いて見ると Compass をよく知っている人でも最低1ヶ月は掛かってしまうだろうというボリュームがあります。つまりまったく知らない人が学び利用しようとすると、その工数見積りは殆どあてにならないものになる可能性が高いと思います。しかし、この Plugin を使えばインストールして、必要な設定を Domain クラスに行い、View をちょこちょこ直すだけで、とりあえずそれなりのものが出来てしまいます。皆さんは、実際に試してみて、有用性についてどのように感じられるでしょうか。
最後に、ここまで書いて思った事と、最初に書いた「要素技術や仕様実装を載せることを目的とした Plugin 以外のもの」についてそれはどういうものなのかを書いておきます。
Grails の Plugin 周辺で起きていることを見ていると、Open Source の Application Framework に対するコミッタの裾野が広がったように見えます。
うまい表現が思いつかないので語弊があるかもしれませんが、今までは、ドキュメンテーションやマーケティングなどを除外すると、要素技術の内部に関心がある人がコミッタになっていたように見えるのですが、どうも Grails は 要素技術をうまく利用することに関心がある人が新たなコミッタとして加わったように感じています。そこから考えると、再利用可能な Grails の Plugin は『再利用ができる「使い易さに拘った要素技術の組み込みノウハウ+利用規約」』といえるのかもしれません。肝心な所は「使い易さに拘った」という部分なのですが、この部分にLL言語が効いています。
ざくざくと書きましたが、Grails 1.1 (←2.0から訂正しました) ではこの Plugin の仕掛けが色々と改善されるようですので、今後さらにどのようなものになっていくのか興味深く思っています。
Service クラスのトランザクション
Grails は、 Artefact という概念を持っています。以下、この文章では、この概念の具体クラスを Artefact クラスと書きます。
Artefactクラスの特徴は、アプリケーションを起動した状態で Artefactクラスのソースコードの修正を行うと、アプリケーションにその修正が反映されることです。Artefact の正確な定義は見つけられていないのですが、私はリロード可能な実装要素と理解しています(新規作成したArtefactクラスも実行時にロードされます)。
もう少し具体的に書くと、Grails は、Service, Domain, Controller, TagLib, Codec, Filters, UrlMappings といった Artefactを組み込みで提供していますが、これらの概念としての Artefact の具体が Artefact クラスです。このArtefact クラスは Grails の利用者が実装します。そして、Artefact クラスは実行時に編集するとアプリケーションに反映されます。とまぁ、こういう調子で理解しています(実際はいくらかうまくリロードしないケースがあります)。
少し内部に入りますと、上で挙げた概念としての各 Artefact には、 AbstractGrailsClass を継承したサブクラス(他にも協調するクラスあり)とそれらを管理する Plugin が存在します。そして、これらの実装により個々の Artefactクラスに個性を加えています(AbstractGrailsClassは、Bean に責務を加える Wrapper クラスになります)。
少し分かり難いと思いますので、Service クラスを例にしますと、個々の Servise クラスは、利用側から見ると、単に Spring Framework の DI Container で管理された Bean に見えるのですが、実際は、AbstractGrailsClassというラッパークラス(のサブクラス)でそれらのクラスはラッピングされ、このラッパークラスから間接的に使用していることになります。
AbstractGrailsClass でラッピングする目的は、当然いくつかの責務を加わえることにあるのですが、その主要なものとして ExpandoMetaClass の提供があります。この責務により、例えばダイナミックメソッドを個々のServiceクラス(のラッパー)に登録することが実行時に可能になります。実際には、この登録処理は、Service クラス専用の Plugin である ServicesGrailsPlugin( のdoWithDynamicMethodsクロージャ)で通常行われます。余談ですが、「通常」と書いたのは、現在の ServicesGrailsPlugin は、doWithDynamicMethods クロージャを持っていないためです。
リロードに関しても Plugin が絡みますが、これはまた別の記事で書きたいと思います。
本題に入ります。
全ての Service クラスには、トランザクション制御の為の仕組みがデフォルトで加えられます。
少しそれますが、次のコードの様に`transactional=false' と記述することにより、その仕組みを加えないようにすることもできます。
Grailsのリファレンスから引用:
class CountryService { static transactional = false }
さてさて、先に Plugin の doWithDynamicMethodsクロージャの記述によってダイナミックメソッドを加えることができると書きましたが、Service クラスのトランザクション制御の仕組みは doWithSpring クロージャの中の記述により加えられています。記述する内容は Spring Framework の Bean 定義そのものといって良いものです。
実際の該当コードは、次のようになっています。
ServicesGrailsPluginクラスのdoWithSpringクロージャ:
def doWithSpring = { application.serviceClasses.each { serviceClass -> def scope = serviceClass.getPropertyValue("scope") "${serviceClass.fullName}ServiceClass"(MethodInvokingFactoryBean) { targetObject = ref("grailsApplication", true) targetMethod = "getArtefact" arguments = [ServiceArtefactHandler.TYPE, serviceClass.fullName] } def hasDataSource = (application.config?.dataSource || application.domainClasses.size() > 0) if(serviceClass.transactional && hasDataSource) { def props = new Properties() props."*"="PROPAGATION_REQUIRED" "${serviceClass.propertyName}"(TransactionProxyFactoryBean) { bean -> if(scope) bean.scope = scope target = { innerBean -> innerBean.factoryBean = "${serviceClass.fullName}ServiceClass" innerBean.factoryMethod = "newInstance" innerBean.autowire = "byName" if(scope) innerBean.scope = scope } proxyTargetClass = true transactionAttributes = props transactionManager = ref("transactionManager") } } else { "${serviceClass.propertyName}"(serviceClass.getClazz()) { bean -> bean.autowire = true if(scope) { bean.scope = scope } } } } }
※ 見やすいように適当に改行を加えています。が、まだ見難いですね。
TransactionProxyFactoryBean という FactoryBean を使って TransactionInterceptor で Serviceクラス をラッピングすることでトランザクション制御を加えています。
さらに、TransactionProxyFactoryBean に関係する設定コードを見て気づくのは、Hibernate のトランザクションアノテーションが利用できない設定になっていることです。つまり、Service クラスでトランザクション制御を利用する場合、トランザクションがロールバックするのは、RuntimeException が発生した時だけに限定されるということです。
ここで考えないといけないのは、『Groovy は RuntimeException 系以外の例外に対しても try/catch 文を要求しない為にそれを省いたコードを書ける』という点です。
例えば、意味のないものですが、DomainAService という Service クラスを作り、次のようなメソッドを書いたとします。
def create(def params) { def domainA = new DomainA(params) domainA.save() if(true) throw new Exception("exception") // 例外を発生させる return domainA }
そして、Controller の save で次のように呼び出します。
def save = { def domainA = domainAService.create(params) if(!domainA.hasErrors()) { flash.message = "DomainA ${domainA.id} created" redirect(action:show,id:domainA.id) } else { render(view:'create',model:[domainA:domainA]) } }
すると次のような例外がブラウザ上に現れます。
[249766] errors.GrailsExceptionResolver java.lang.reflect.UndeclaredThrowableException org.codehaus.groovy.runtime.InvokerInvocationException: java.lang.reflect.UndeclaredThrowableException at org.codehaus.groovy.reflection.CachedMethod.invoke(CachedMethod.java:92) at groovy.lang.MetaMethod.doMethodInvoke(MetaMethod.java:226) at groovy.lang.MetaClassImpl.invokeMethod(MetaClassImpl.java:899) at groovy.lang.ExpandoMetaClass.invokeMethod(ExpandoMetaClass.java:946) at groovy.lang.MetaClassImpl.invokeMethod(MetaClassImpl.java:740) at groovy.lang.Closure.call(Closure.java:292) at groovy.lang.Closure.call(Closure.java:287) at org.codehaus.groovy.grails.web.servlet.mvc.SimpleGrailsControllerHelper.handleAction(SimpleGrailsControllerHelper.java:525) (省略) Caused by: java.lang.reflect.UndeclaredThrowableException at DomainAServiceService$$EnhancerByCGLIB$$657c2eb6.create(<generated>) at DomainAController$_closure8.doCall(DomainAController:76) at DomainAController$_closure8.doCall(DomainAController) Caused by: java.lang.Exception: exception at DomainAService.create(DomainAService.groovy:8) at DomainAServiceService$$FastClassByCGLIB$$68b8337d.invoke(<generated>) at net.sf.cglib.proxy.MethodProxy.invoke(MethodProxy.java:149) ... 3 more
ブラウザ上あるいはログ出力からは、 InvokerInvocationException という RuntimeException をキャッチしたかに見え、トランザクションのロールバックが行われていそうですが、実際に実験してみるとロールバックは行われず DomainA のオブジェクトは DB に保存されてしまいます。
このことから、Serviceクラス のトランザクション制御を利用する場合には、『本来必要な try/catch を省いてはいけない』ということが言えそうです。しかし、この方針を明確にしただけでは十分ではなく、完全に問題がない状態であることを確認できなければ安心ができない方もいらっしゃるかもしれません。なぜなら、稀有かもしれませんが、Grails 本体あるいは利用している PluginのGroovy コードに問題が潜んでいるかもしれないからです。
SpaceCardプロジェクトでは、(外部で開発された)機能を拡張するような Plugin を差込むことでアプリケーション機能が拡張されることを(無くなってしまうかもしれませんが)構想として持っています。このことを含めて考えると、良いアイデアが見つからず、Service クラスでトランザクション制御を利用することを諦め、他の要件と絡めて独自の Logic という Artefact を設計し、Logic クラスレベルでトランザクション制御を可能にしました。その実装では Throwable オブジェクトを全てキャッチしロールバックするようにしています。
※断定的に書いているところにも間違いがあるやもしれません。間違いのご指摘お待ちしています。