負荷テストその2(Grails 0.5)
昨日のHttpSessionがいっぱいになってしまう点については、大量アクセスが発生するサイトではよくあることだ。大量アクセスが短期に発生するようなページで無駄にHttpSessionを使用するとTomcatの場合、OutOfMemoryErrorが発生し、発生したスレッドによってはそのままハングしてしまうという事象はTomcat 5.0 ではよくおきていた。その後の5.5、6.0では検証していないので分からない。ただ、昨日の負荷テストの結果を見る限り同じ?なのではないかと思われる。
まあ、これに対してはよくあることなので独自のHttpSessionの実装が実は用意してある。フィルタをかましてHttpServletRequestをラップして独自のHttpSessionを返す、というやつだ。これをGrailsのプラグインでweb.xmlにちょちょいと追加するようなプラグインを作ってインストールした。
ちなみにセッションの永続化についてはehcacheを利用している。テストで使用した際にはehcacheについてはレプリケーション等の設定は行っていない。
再度、同じように負荷をかけてみた。そのときの結果はこれ(GSPなし)。
$ ab -c 300 -n 30000 http://localhost:8080/pagetest-0.1-c/test01 This is ApacheBench, Version 2.0.40-dev <$Revision: 1.146 $> apache-2.0 Copyright 1996 Adam Twiss, Zeus Technology Ltd, http://www.zeustech.net/ Copyright 2006 The Apache Software Foundation, http://www.apache.org/ Benchmarking localhost (be patient) Completed 3000 requests Completed 6000 requests Completed 9000 requests Completed 12000 requests Completed 15000 requests Completed 18000 requests Completed 21000 requests Completed 24000 requests Completed 27000 requests Finished 30000 requests Server Software: Apache-Coyote/1.1 Server Hostname: localhost Server Port: 8080 Document Path: /pagetest-0.1-c/test01 Document Length: 90 bytes Concurrency Level: 300 Time taken for tests: 73.788111 seconds Complete requests: 30000 Failed requests: 0 Write errors: 0 Total transferred: 8098725 bytes HTML transferred: 2700270 bytes Requests per second: 406.57 [#/sec] (mean) Time per request: 737.881 [ms] (mean) Time per request: 2.460 [ms] (mean, across all concurrent requests) Transfer rate: 107.17 [Kbytes/sec] received Connection Times (ms) min mean[+/-sd] median max Connect: 0 401 2053.1 1 45024 Processing: 7 314 276.2 253 9336 Waiting: 6 299 264.1 245 9238 Total: 74 716 2091.4 263 45322 Percentage of the requests served within a certain time (ms) 50% 263 66% 301 75% 345 80% 394 90% 885 95% 3264 98% 4140 99% 9270 100% 45322 (longest request)
若干スループットが上がってるかも。誤差の範囲ともいえるが。プロファイラの結果で気が付くのは、
- コントローラの生成が遅い
- コントローラのgetProperty関連処理が遅い
ような気がする。
負荷テスト(Grails 0.5)
以前、やろうとしていたスループットのテストをやってみる。groovy-all-1.1-BEATA-1.jarをgroovy-all-1.1-BETA-2-SNAPSHOT.jarに入れ替えて実行した。実行したアプリケーションはこんな感じ。
まず、コントローラ
class Test01Controller { def index = { render """ <html> <head> <title>Hello</title> </head> <body> Hello World </body> </html> """ } def view = { render(view:'hello') } def layout = { render(view:'hellolayout') } }
views/test01/hello.gspはこんな感じ
<html> <head> <title>Hello</title> </head> <body> Hello World </body> </html>
views/test01/hellolayout.gspはこんな感じ
<html> <head> <meta name="layout" content="simple" /> </head> <body>Hello World</body> </html>
views/layouts/simple.gspはこんな感じだ。
<html> <head> <title>Hello</title> <g:layoutHead /> </head> <body> <g:layoutBody /> </body> </html>
何がやりたいかと言うと、
- コントローラのrenderでHTML出力した場合
- GSPでHTML出力した場合(レイアウトなし)
- GSPでHTML出力した場合(レイアウトあり)
でどの程度の違いがあるのか、ということだ。
テストは、grails war でWARファイルを作成して、Tomcat 6.0.13にデプロイし、最初に対象URLをブラウザで表示して、その後ApacheBenchで負荷をかけることとした。
最初に、コントローラのrenderで出力した場合のテスト結果はこんな感じだった。
第1回目
$ ab -c 100 -n 10000 http://localhost:8080/pagetest-0.1/test01 This is ApacheBench, Version 2.0.40-dev <$Revision: 1.146 $> apache-2.0 Copyright 1996 Adam Twiss, Zeus Technology Ltd, http://www.zeustech.net/ Copyright 2006 The Apache Software Foundation, http://www.apache.org/ Benchmarking localhost (be patient) Server Software: Apache-Coyote/1.1 Server Hostname: localhost Server Port: 8080 Document Path: /pagetest-0.1/test01 Document Length: 90 bytes Concurrency Level: 100 Time taken for tests: 26.640011 seconds Complete requests: 10000 Failed requests: 1 (Connect: 0, Length: 1, Exceptions: 0) Write errors: 0 Non-2xx responses: 1 Total transferred: 2891115 bytes HTML transferred: 901010 bytes Requests per second: 375.38 [#/sec] (mean) Time per request: 266.400 [ms] (mean) Time per request: 2.664 [ms] (mean, across all concurrent requests) Transfer rate: 105.97 [Kbytes/sec] received Connection Times (ms) min mean[+/-sd] median max Connect: 0 6 17.4 1 301 Processing: 12 256 155.7 210 1394 Waiting: 7 248 154.9 204 1392 Total: 36 263 154.6 215 1419 Percentage of the requests served within a certain time (ms) 50% 215 66% 264 75% 304 80% 335 90% 459 95% 559 98% 752 99% 918 100% 1419 (longest request)
続けて2度目
$ ab -c 100 -n 10000 http://localhost:8080/pagetest-0.1/test01 This is ApacheBench, Version 2.0.40-dev <$Revision: 1.146 $> apache-2.0 Copyright 1996 Adam Twiss, Zeus Technology Ltd, http://www.zeustech.net/ Copyright 2006 The Apache Software Foundation, http://www.apache.org/ Benchmarking localhost (be patient) Total of 6052 requests completed
むむむ、タイムアウトしてしまった。Tomcatの方はというと、OutOfMemoryErrorが出ている。ま、原因は予想がつくのだが。
Tomcatを再起動して、GSP(レイアウトなし)のテスト。
1回目
$ ab -c 100 -n 10000 http://localhost:8080/pagetest-0.1/test01/view This is ApacheBench, Version 2.0.40-dev <$Revision: 1.146 $> apache-2.0 Copyright 1996 Adam Twiss, Zeus Technology Ltd, http://www.zeustech.net/ Copyright 2006 The Apache Software Foundation, http://www.apache.org/ Benchmarking localhost (be patient) Server Software: Apache-Coyote/1.1 Server Hostname: localhost Server Port: 8080 Document Path: /pagetest-0.1/test01/view Document Length: 97 bytes Concurrency Level: 100 Time taken for tests: 59.736206 seconds Complete requests: 10000 Failed requests: 0 Write errors: 0 Total transferred: 3600000 bytes HTML transferred: 970000 bytes Requests per second: 167.40 [#/sec] (mean) Time per request: 597.362 [ms] (mean) Time per request: 5.974 [ms] (mean, across all concurrent requests) Transfer rate: 58.84 [Kbytes/sec] received Connection Times (ms) min mean[+/-sd] median max Connect: 0 0 5.4 0 128 Processing: 95 593 340.9 480 3809 Waiting: 77 590 339.4 477 3808 Total: 130 594 341.0 480 3812 Percentage of the requests served within a certain time (ms) 50% 480 66% 584 75% 677 80% 748 90% 967 95% 1227 98% 1588 99% 1920 100% 3812 (longest request)
2回目はハングして返ってこなかった(ように見えた)ので強制終了した。
再度Tomcatを再起動(停止中にOutOfMemoryError発生)してGSP(レイアウトあり)のテスト。
1回目
$ ab -c 100 -n 10000 http://localhost:8080/pagetest-0.1/test01/layout This is ApacheBench, Version 2.0.40-dev <$Revision: 1.146 $> apache-2.0 Copyright 1996 Adam Twiss, Zeus Technology Ltd, http://www.zeustech.net/ Copyright 2006 The Apache Software Foundation, http://www.apache.org/ Benchmarking localhost (be patient) Server Software: Apache-Coyote/1.1 Server Hostname: localhost Server Port: 8080 Document Path: /pagetest-0.1/test01/layout Document Length: 140 bytes Concurrency Level: 100 Time taken for tests: 169.166464 seconds Complete requests: 10000 Failed requests: 0 Write errors: 0 Total transferred: 4040000 bytes HTML transferred: 1400000 bytes Requests per second: 59.11 [#/sec] (mean) Time per request: 1691.665 [ms] (mean) Time per request: 16.917 [ms] (mean, across all concurrent requests) Transfer rate: 23.32 [Kbytes/sec] received Connection Times (ms) min mean[+/-sd] median max Connect: 0 0 3.4 0 55 Processing: 28 1681 7113.9 632 72432 Waiting: 27 1678 7112.7 630 72432 Total: 71 1682 7113.8 633 72432 Percentage of the requests served within a certain time (ms) 50% 633 66% 970 75% 1277 80% 1546 90% 2174 95% 3014 98% 5148 99% 71387 100% 72432 (longest request)
2回目はやはりハングしたように見えるほど遅かったので強制終了した。同様にOutOfMemoryErrorが発生した。
回数を重ねると遅く、メモリを消費していくのは多分HttpSessionのせいだろう。
しかし、Cookieが使えないクライアントから毎秒1000以上のリクエストが来るようなサイトでGrailsが果たして使えるのか?ということが実は最大の関心事なのだけれども。
一応、最初のテスト(コントローラのrenderでHTMLを出力)をプロファイラで調べてみた。やはりメモリを圧迫しているのはHttpSessionのインスタンスのようだ。1セッションで1800バイトくらい使っているようだ。確かにデフォルトのメモリ設定だと、20000セッションは厳しいかもしれない。
次に、CPU時間の結果を見てみた。全体の63.8%がSimpleGrailsControllerHelper#handleURI()で消費されている。そのうち、SimpleGrailsControllerHelper#getControllerInstance()で実に39.1%を消費している。SimpleGrailsControllerHelper#handleAction()が9.1%の消費となっているので、コントローラの実行の実に4倍のCPU時間を消費していることになる。まあ、今回のコントローラは殆ど何もしていないので、こんな結果になったのではないかと思われる。なお、SimpleGrailsControllerHelper#getControllerInstance()の消費時間はクロージャの数に比例するようだ。
次にGSPでHTML出力した場合(レイアウトなし)の場合のプロファイルを取る。
ここで新たに登場してくるのが、全体の40.2%のCPU時間を消費しているGrailsLayoutDecoratorMapper#getNamedDecorator()だ。これが増えたおかげで先ほどのSimpleGrailsControllerHelper#handleURI()の消費時間が26.6%まで落ちた。このメソッドは結果をキャッシュできるような気がするなぁ(少なくともProductionでは)。
ソースを見てみると、
public Decorator getNamedDecorator(HttpServletRequest request, String name) { if(StringUtils.isBlank(name))return null; if(decoratorMap.containsKey(name)) { return (Decorator)decoratorMap.get(name); } else { String decoratorName = name; if(!name.matches("(.+)(\\.)(\\w{2}|\\w{3})")) { name += DEFAULT_VIEW_TYPE; } String decoratorPage = DEFAULT_DECORATOR_PATH + '/' + name; ResourceLoader resourceLoader = establishResourceLoader(); Resource res = resourceLoader.getResource(decoratorPage); if(!res.exists()) { PathMatchingResourcePatternResolver matcher = new PathMatchingResourcePatternResolver(resourceLoader); String pattern = GrailsResourceUtils.WEB_INF + "/plugins/*/grails-app/views/layouts/" + name; if(LOG.isDebugEnabled()) LOG.debug("No decorator found at " + decoratorPage+". Trying plug-ins with pattern: " + pattern); try { Resource[] layouts = matcher.getResources(pattern); if(layouts.length>0) { if(layouts.length>1) { LOG.warn("Multiple matching layouts found in plug-ins for name ["+name+"] using first from ["+ ArrayUtils.toString(layouts) +"]"); } String url = layouts[0].getURL().toString(); url = GrailsResourceUtils.WEB_INF + url.substring(url.indexOf("/plugins"),url.length()); Decorator d = new DefaultDecorator(name, request.getRequestURI(), url, Collections.EMPTY_MAP); decoratorMap.put(decoratorName, d); return d; } } catch (IOException e) { // ignore, if there was a problem here its going to be a FNFE which is ok } return null; } else { if(LOG.isDebugEnabled()) LOG.debug("Using decorator " + decoratorPage); Decorator d = new DefaultDecorator(decoratorName,request.getRequestURI(),decoratorPage, Collections.EMPTY_MAP); decoratorMap.put(decoratorName,d); return d; } } }
をを、キャッシュしているぞ。ただし、見つかったものだけだな。return nullのものに関してはキャッシュしていないということか。確かに妥当な気もする。nameとして与えられるパラメタの内容が有限ならばネガティブキャッシュしてもよさそうだが。
最後にGSPでHTML出力した場合(レイアウトあり)の場合。
新たに登場するのは、GrailsPageFilter#applyDecorator()だ(31.7%)。これはレイアウトGSPページを実行するのだから当然といえば当然か。逆にGrailsLayoutDecoratorMapper#getNamedDecorator()は姿を消した。おそらく、レイアウトが指定されているので何も検索しないのだろう。ちなみに、SimpleGrailsControllerHelper#handleURI()は、32.2%のCPU消費時間となっている。
むぅ、劇的なパフォーマンスアップは望めないかも。。。
ハングしちゃうんですけど!を調べた
昨日の日記でGrailsがハングすることを書きましたが調べてみました。スタックトレースを見ると、Groovyで書かれたクラス(コントローラ)のClass#newInstance()で止まっているみたいで、怖くて使えないです。もう、Grailsの話題じゃないです。
スタックトレースを見る限りでは、org.codehaus.groovy.runtime.metaclass.MemoryAwareConcurrentReadMapが怪しい。Grails 0.5に付属のGroovy(1.1 BETA-1)のこのクラスのソースはここ。その中で、wait()したままの箇所はここ。
private void waitForWriteState() { synchronized (writeQueue) { while (concurrentReads!=0) { try { writeQueue.wait(); } catch (InterruptedException e) {} } } }
MemoryAwareConcurrentReadMapって言うのはいわゆるReaderWriterLock機能を持ったMapなのだろう。とすると、concurrentReadsはその名のとおり現在getしているスレッド数で、put時にはgetしているやつがいなくなるまで待つのだろうと思われる。で、concurrentReadsの定義はこれ、
private volatile long concurrentReads = 0;
volatile宣言してあってもマルチスレッド環境では、synchronizedが必要。Java初心者の方は調べてくださいね。ここでは説明しませんけれど。とすると、synchronizedされているのはどのオブジェクト?writeQueue?他のconcurrentReadsをアクセスしているところはというと、
private void lockWrite() { synchronized (writeLock) { concurrentReads++; } } private void unlockWrite(){ synchronized (writeLock) { concurrentReads--; } synchronized (writeQueue) { writeQueue.notify(); } } public Object get(Object key) { int hash = key.hashCode(); lockWrite(); try { int index = index(hash,table.length); for (Entry current = table[index]; current!=null; current=current.next) { if (hash!=current.hash) continue; Object oldKey = current.getKey(); Object oldValue = current.getValue(); if (!current.isValid()) continue; if (key==oldKey || key.equals(oldKey)) { return oldValue; } } } finally { unlockWrite(); } return null; }
おやおや、writeLockで排他しているみたいだ。それなら、waitForWriteState()の呼出元はどうなっているんだろう?いくつかあるが、例えばput()
public void put(Object key, Object value) { if (value == null) { remove(key); return; } synchronized(writeLock) { waitForWriteState(); putNonBlocking(key,value,false); } }
呼び出す前にsynchronized(writeLock)してるのね。でも....
なるほど、ハイ、わかりました原因。バグですね。それじゃあ、また来週!!
ちなみに最新のソースでは直っているみたい。入れ替えた方がいいかも。
ハングしちゃうんですけど!(Grails 0.5+オリジナルパッチ)
コントローラ中でrender "hogehoe"したときと、GSPに飛ばしたとき(レイアウトがあるとき、ないとき)とでどれくらいスループットが変わるかをテストすることにした。
Windowsだとそもそもソケット周りが耐え切れないと思って、Linux(CentOS) on VMWare(仮想CPU数2)上のTomcatでアプリケーションを実行し、ApacheBench(ab)で -c 100 -n 10000 とかのパラメタで実行。
ところが、どうもハングしたみたいで、いつまでたっても返ってこない。
JProfilerを使用して、再度Tomcatを実行してApacheBenchで負荷をかけてみた。
2103秒間ブロックされてるって(スクリーンショットを参照のこと。見えないと思うけど)!
待っているスレッドのスタックトレースは以下のとおり。
org.codehaus.groovy.runtime.metaclass.MemoryAwareConcurrentReadMap.lockWrite() org.codehaus.groovy.runtime.metaclass.MemoryAwareConcurrentReadMap.get(java.lang.Object) org.codehaus.groovy.runtime.metaclass.MetaClassRegistryImpl.getMetaClass(java.lang.Class) groovy.lang.MetaClassRegistry.getMetaClass(java.lang.Class) org.codehaus.groovy.runtime.Invoker.getMetaClass(java.lang.Object) org.codehaus.groovy.runtime.InvokerHelper.getMetaClass(java.lang.Object) groovy.lang.Closure.<init>(java.lang.Object, java.lang.Object) Test01Controller$_closure2.<init>(java.lang.Object, java.lang.Object) Test01Controller.<init>() java.lang.Class.newInstance() org.codehaus.groovy.grails.commons.AbstractGrailsClass.newInstance() java.lang.reflect.InvocationHandler.invoke(java.lang.Object, java.lang.reflect.Method, java.lang.Object[ ]) $Proxy3.newInstance() org.springframework.context.ApplicationContext.getBean(java.lang.String) org.codehaus.groovy.grails.web.servlet.mvc.SimpleGrailsControllerHelper.getControllerInstance(org.codehaus.groovy.grails.commons.GrailsControllerClass) org.codehaus.groovy.grails.web.servlet.mvc.SimpleGrailsControllerHelper.handleURI(java.lang.String, org.codehaus.groovy.grails.web.servlet.mvc.GrailsWebRequest, java.util.Map) org.codehaus.groovy.grails.web.servlet.mvc.SimpleGrailsControllerHelper.handleURI(java.lang.String, org.codehaus.groovy.grails.web.servlet.mvc.GrailsWebRequest) org.codehaus.groovy.grails.web.servlet.mvc.SimpleGrailsController.handleRequest(javax.servlet.http.HttpServletRequest, javax.servlet.http.HttpServletResponse) org.springframework.web.servlet.HandlerAdapter.handle(javax.servlet.http.HttpServletRequest, javax.servlet.http.HttpServletResponse, java.lang.Object) org.codehaus.groovy.grails.web.servlet.GrailsDispatcherServlet.doDispatch(javax.servlet.http.HttpServletRequest, javax.servlet.http.HttpServletResponse) javax.servlet.http.HttpServlet.service(javax.servlet.http.HttpServletRequest, javax.servlet.http.HttpServletResponse) URL: /pagetest-0.1/grails/test01.dispatch javax.servlet.http.HttpServlet.service(javax.servlet.ServletRequest, javax.servlet.ServletResponse) org.codehaus.groovy.grails.web.mapping.filter.UrlMappingsFilter.doFilterInternal(javax.servlet.http.HttpServletRequest, javax.servlet.http.HttpServletResponse, javax.servlet.FilterChain) com.opensymphony.module.sitemesh.filter.PageFilter.parsePage(javax.servlet.http.HttpServletRequest, javax.servlet.http.HttpServletResponse, javax.servlet.FilterChain) com.opensymphony.module.sitemesh.filter.PageFilter.doFilter(javax.servlet.ServletRequest, javax.servlet.ServletResponse, javax.servlet.FilterChain) org.codehaus.groovy.grails.web.servlet.mvc.GrailsWebRequestFilter.doFilterInternal(javax.servlet.http.HttpServletRequest, javax.servlet.http.HttpServletResponse, javax.servlet.FilterChain) org.apache.tomcat.util.net.JIoEndpoint$Worker.run()
ブロックして(待たせて)いるほうのスレッドのスタックトレースは以下のとおり。
java.lang.Object.wait() org.codehaus.groovy.runtime.metaclass.MemoryAwareConcurrentReadMap.waitForWriteState() org.codehaus.groovy.runtime.metaclass.MemoryAwareConcurrentReadMap.put(java.lang.Object, java.lang.Object) org.codehaus.groovy.runtime.metaclass.MetaClassRegistryImpl.getMetaClass(java.lang.Class) groovy.lang.MetaClassRegistry.getMetaClass(java.lang.Class) org.codehaus.groovy.runtime.Invoker.invokeStaticMethod(java.lang.Class, java.lang.String, java.lang.Object) org.codehaus.groovy.runtime.InvokerHelper.invokeStaticMethod(java.lang.Class, java.lang.String, java.lang.Object) org.codehaus.groovy.runtime.ScriptBytecodeAdapter.invokeStaticMethodN(java.lang.Class, java.lang.Class, java.lang.String, java.lang.Object[ ]) Test01Controller.<init>() java.lang.Class.newInstance() org.codehaus.groovy.grails.commons.AbstractGrailsClass.newInstance() java.lang.reflect.InvocationHandler.invoke(java.lang.Object, java.lang.reflect.Method, java.lang.Object[ ]) $Proxy3.newInstance() org.springframework.context.ApplicationContext.getBean(java.lang.String) org.codehaus.groovy.grails.web.servlet.mvc.SimpleGrailsControllerHelper.getControllerInstance(org.codehaus.groovy.grails.commons.GrailsControllerClass) org.codehaus.groovy.grails.web.servlet.mvc.SimpleGrailsControllerHelper.handleURI(java.lang.String, org.codehaus.groovy.grails.web.servlet.mvc.GrailsWebRequest, java.util.Map) org.codehaus.groovy.grails.web.servlet.mvc.SimpleGrailsControllerHelper.handleURI(java.lang.String, org.codehaus.groovy.grails.web.servlet.mvc.GrailsWebRequest) org.codehaus.groovy.grails.web.servlet.mvc.SimpleGrailsController.handleRequest(javax.servlet.http.HttpServletRequest, javax.servlet.http.HttpServletResponse) org.springframework.web.servlet.HandlerAdapter.handle(javax.servlet.http.HttpServletRequest, javax.servlet.http.HttpServletResponse, java.lang.Object) org.codehaus.groovy.grails.web.servlet.GrailsDispatcherServlet.doDispatch(javax.servlet.http.HttpServletRequest, javax.servlet.http.HttpServletResponse) javax.servlet.http.HttpServlet.service(javax.servlet.http.HttpServletRequest, javax.servlet.http.HttpServletResponse) URL: /pagetest-0.1/grails/test01.dispatch javax.servlet.http.HttpServlet.service(javax.servlet.ServletRequest, javax.servlet.ServletResponse) org.codehaus.groovy.grails.web.mapping.filter.UrlMappingsFilter.doFilterInternal(javax.servlet.http.HttpServletRequest, javax.servlet.http.HttpServletResponse, javax.servlet.FilterChain) com.opensymphony.module.sitemesh.filter.PageFilter.parsePage(javax.servlet.http.HttpServletRequest, javax.servlet.http.HttpServletResponse, javax.servlet.FilterChain) com.opensymphony.module.sitemesh.filter.PageFilter.doFilter(javax.servlet.ServletRequest, javax.servlet.ServletResponse, javax.servlet.FilterChain) org.codehaus.groovy.grails.web.servlet.mvc.GrailsWebRequestFilter.doFilterInternal(javax.servlet.http.HttpServletRequest, javax.servlet.http.HttpServletResponse, javax.servlet.FilterChain) org.apache.tomcat.util.net.JIoEndpoint$Worker.run()
これって、Groovyかな。pageEncodingパッチのせいじゃないよねぇ? http://jira.codehaus.org/browse/GRAILS みても、issueが多くて既に登録されているのか良く分からない...
GSPで定義タグって使えるの?(Grails 0.5+オリジナルパッチ)
こんな感じのGSPを作ってみた。
<%! def square = { x -> x * x } %> <html> <head> <title>Test</title> </head> <body> 3 ^ 2 = <%= square(3) %> </body> </html>
これ、コンパイルできないみたい。Parse.javaとかみると、<%! %>の処理はあるみたいなんだけど..未実装?それともGSPでは使えない?それともバグ?
ParseTest.javaに以下のようなテストを追加して実行してみた。
public void testParseWithDeclare() throws Exception { String output = parseCode("myTest", "<%! def hoge = 'hoge' %>" + "<div>Hello</div>"); System.out.println(output); }
結果として表示されたのは、
import org.codehaus.groovy.grails.web.pages.GroovyPage import org.codehaus.groovy.grails.web.taglib.* def hoge = 'hoge' class myTest extends GroovyPage { public Object run() { out.print('<div>Hello</div>') } }
だった。def hogeの文がmyTestクラスの中にあるのが正しいのかな。
T.Yamamotoさんのブログで紹介されてた!
T.Yamamotoさんのブログでこのブログが紹介されていました。Grailsもどんどんメジャーになっていって欲しいですね。
そうですか、濃いですか...。どちらかというと特殊な環境での使用を考えているのでいろいろと検証中なのです、ハイ。その名のとおり備忘録的な内容なので読んでいて楽しい&役に立つ内容じゃないかもです。
プラグインでコマンドを追加(Grails 0.5)
grails hoge-hoge とかすると、何かやってくれるようなコマンドをプラグインに追加したくって。例えば、grails configure-hogehoge とかすると、対話式にプラグインの設定ができるとか。
そりゃあ、多分できるのでしょう。なんといっても、開発者ガイドには、「コマンドラインインターフェイスからランタイムコンフィグレーションエンジンにいたるどんな拡張も可能にする方法を提供します」ですから。
さて、やってみる。まずはプラグインプロジェクトを。
grails create-plugin hoge
作成したプロジェクトのscriptsディレクトリに以下のHogeHoge.groovyを作成。
task ('default': "Print hoge message.") { println 'Hello from Grails plugin hoge' }
これで完成。次にプラグインのZipファイルを作成。
cd hoge grails package-plugin
パッケージ作成完了。あとは適当なGrailsプロジェクトでこのプラグインをインストールして、コマンドラインから
grails hoge-hoge
とすればOK。Hello from Grails plugin hoge って出力されます。なんて簡単なんだ。
でも、同じ名前があったらどうなるんだろう。AプラグインとBプラグインで同じコマンドがあった場合とか。
作ってみるのが手っ取り早いんだろうが、それも何なので、ソースをみた。grails.batで起動されているのは、org.codehaus.groovy.grails.cli.GrailsScriptRunnerだ。mainメソッド中に以下のような箇所が。
try { callPluginOrGrailsScript(scriptName) } catch(Throwable t) { println "Error executing script ${scriptName}: ${t.message}" t.printStackTrace(System.out) }
そのものずばりの名前。で、callPluginOrGrailsScript()はというと、
private static callPluginOrGrailsScript(scriptName) { def potentialScripts = [] def userHome = ANT.antProject.properties."user.home" def scriptLocations = ["${baseDir.absolutePath}/scripts", "${grailsHome}/scripts", "${userHome}/.grails/scripts"] scriptLocations.each { def scriptFile = new File("${it}/${scriptName}.groovy") if(scriptFile.exists()) { potentialScripts << scriptFile } } try { def pluginScripts = RESOLVER.getResources("file:${baseDir.absolutePath}/plugins/**/scripts/${scriptName}.groovy") potentialScripts += pluginScripts.collect { it.file } } catch(Exception e) { println "Note: No plugin scripts found" } if(potentialScripts.size()>0) { potentialScripts = potentialScripts.unique() if(potentialScripts.size() == 1) { println "Running script ${potentialScripts[0].absolutePath}" Gant.main(["-c","-d","${userHome}/.grails/${version}/scriptCache","-f", potentialScripts[0].absolutePath] as String[]) } else { println "Multiple options please select:" def validArgs = [] potentialScripts.eachWithIndex { f, i -> println "[${i+1}] $f " validArgs << i+1 } ANT.input(message: "Enter # ",validargs:validArgs.join(","), addproperty:"grails.script.number") def number = ANT.antProject.properties."grails.script.number".toInteger() println "Running script ${potentialScripts[number-1].absolutePath}" Gant.main(["-f", potentialScripts[number-1].absolutePath] as String[]) } } else { println "Script $scriptName not found." } }
ほほう、複数あるときは、「Multiple options please select:」とかでて選択することになるっぽい。
これならどんな名前でも心配しなくていい。