レベル: 初級 Scott Davis, Founder, ThirstyHead.com
2009年 9月 15日 連載「Grails をマスターする」の今回の記事では、Scott Davis が独自の Grails プラグインを作成する方法を紹介します。この記事を読んでプラグインをこれほど簡単に作成できることがわかれば、現在 250 を超える Grails プラグインがあり、新しいプラグインも次から次へと追加されているわけが納得できるはずです。
連載「Grails をマスターする」ではこれまで多数の記事にわたってコードを賢く再利用する方法に焦点を当ててきました。例えば GSP (GoovyServer Pages) コードの同じスニペットを何ヶ所かでコピー・アンド・ペーストしていることに気付いたら、部分テンプレートやカスタム TagLib を作成することができます。複数のコントローラーやドメイン・クラスで共通するメソッドが 1 つや 2 つあるとしたら、ExpandoMetaClass を使って、メソッドを直接継承または関連付ける抽象親クラスを作成することができます。また、アプリケーションの機能を共有しているとしたら、その機能をサービスやカスタム・コードにリファクタリングすることができます。
 | この連載について
Grails は、Spring や Hibernate などのよく知られた Java™ 技術に「Convention over Configuration (設定より規約)」といった現代のプラクティスを盛り込んだ最新の Web 開発フレームワークです。Groovy で作成された Grails は既存の Java コードをシームレスに統合するだけでなく、スクリプト言語ならではの柔軟性と動的機能を与えてくれます。Grails を学んだら、Web 開発に対する今までの見方がまったく違ってくるはずです。
|
|
しかしこれらの手法はいずれもミクロ・レベルのものです。機能をマクロ・レベルで共有しているとしたらどうなるでしょうか。マクロ・レベルではコントローラーとドメイン・クラス、サービスとコーデック、そして一般的な Grails アプリケーションを構成するその他すべての部分を組み合わせて連動させなければなりません。つまりマクロ・レベルと言っているのは、プラグインのことです。
「Grails をマスターする: プラグインを理解する」では、既存のプラグインの 1 つとして Searchable について説明しました。Grails Plugins Portal には、現在 250 を超える Grails プラグインが用意されています (「参考文献」を参照)。しかもこの数は増え続けています。それは、プラグインによって既存の Grails アプリケーションを拡張することが、Grails のコアとなる概念だからです。今回の記事で、独自のカスタム・プラグインを作成する方法を学びます。サンプル・プラグインのソース・コードはダウンロードすることができます。
ShortenUrl プラグインの概要
 | テストにテストを重ねること
自分で作成した Grails アプリケーションをテストすることは常に重要ですが、プラグインを作成するとなると、テストがとりわけ重要になります。プラグインのバグが不幸な相乗効果を招き、そのプラグインをインストールした新しい Grails アプリケーションのそれぞれを台無しにする可能性があるからです。そのため、この記事ではテストにかなりの重要性を置いています。
|
|
Twitter.com や携帯電話を利用したテキスト・メッセージのやりとりが盛んなこの時代、メッセージに設けられた文字制限 (例えば Twitter の 140 文字) に収まらない長々とした多くの URL は面倒な問題になります。ありがたいことに、カスタム・プラグインとして事実上 Grails に統合できる URL 短縮用の Web サービスが巷に出回っています。
カスタム・プラグインを作成するには、Grails でのいつもの方法を多少変える必要があります。通常は grails create-app と入力するところを、リスト 1 に記載するように grails create-plugin と入力しなければなりません (このコマンドは既存の Grails ディレクトリーで入力するのではなく、必ず新しい空のディレクトリーで入力するようにします。この新規プラグインを既存の Grails アプリケーションに統合する方法については、記事の最後で説明します)。
リスト 1. カスタム・プラグインを作成する
$ grails create-plugin shortenurl
|
上記の結果作成されるディレクトリー構造は、通常の Grails アプリケーションとまったく同じです。ただしルート・ディレクトリーには、このプロジェクトがプラグインであることを識別する新しいファイルが 1 つあります。それが、ShortenurlGrailsPlugin.groovy です。リスト 2 にこのファイルのスニペットを記載します。
リスト 2. プラグインの構成ファイル
class ShortenurlGrailsPlugin {
// the plugin version
def version = "0.1"
// the version or versions of Grails the plugin is designed for
def grailsVersion = "1.1.1 > *"
// the other plugins this plugin depends on
def dependsOn = [:]
// resources that are excluded from plugin packaging
def pluginExcludes = [
"grails-app/views/error.gsp"
]
// TODO Fill in these fields
def author = "Your name"
def authorEmail = ""
def title = "Plugin summary/headline"
def description = '''\\
Brief description of the plugin.
'''
//snip
}
|
このファイルには、バージョン番号、プラグインが依存する Grails のバージョン、そしてプラグインが依存する他のプラグインなど、新規に作成するプラグインのメタデータが含まれます (構成ファイルの詳細については、「参考文献」で紹介しているオンライン・マニュアルを参照してください)。
このプラグインを公開プラグインにして、他の開発者が Grails Plugins Portal からダウンロードできるようにしたい場合には、作成者の詳細を入力し、興味をそそるような説明を加えてください。すると、プラグインを Subversion のリポジトリーにチェックインするたびにファイルの内容が読み取られ、Grails Web サイトに自動的に表示されます (プラグインを公開する方法についての詳細は、「参考文献」を参照してください)。この記事では個人で使用するプラグインのままにしておくので、作成者の詳細を入力する部分はそれほど重要ではありません。
ShortenUrl プラグインのために ShortenurlGrailsPlugin.groovy を変更する必要はありませんが、だからといって作業が終わったというわけではありません。ディレクトリー構造の scaffold は生成されているので、次のステップでは実装を作成します。
TinyUrl クラスの作成
TinyUrl.com はよく使われている URL 短縮サービスです。ユーザーが短縮したい URL を TinyUrl.com に送信すると、TinyUrl.com ではその URL が公式の短縮 URL として保存され、その後のすべてのリクエストではその短縮 URL が使用されます。例えば、TinyUrl.com にアクセスして http://www.grails.org/The+Plug-in+Developers+Guide と入力し、Make TinyURL! ボタンをクリックしてみてください。すると図 1 に示されているように、URL は http://tinyurl.com/73495c に短縮されます。これは、元の URL の半分の長さです。
図 1. TinyURL.com によって短縮された URL
TinyURL.com がどのように機能するかわかったところで、このサイトの基礎となるサービスを ShortenUrl プラグインに統合する作業に入ります。まずは以下の URL を Web ブラウザーに入力してください。
http://tinyurl.com/api-create.php?url=http://www.grails.org/The+Plug-in+Developers+Guide |
このシンプルな Web サービス・インターフェースは、HTML ではなく、指定されたページの短縮された URL だけを返します。
次のステップでは、新たに得た知識を Groovy クラスにカプセル化します。このクラスはまさに真の意味での POGO (Plain Old Groovy Object) であって、つまりサービスでも、コントローラーでも、他の何らかの特殊な目的を持つGrails コンポーネントでもありません。したがって、このクラスを配置するのに最適な場所は src/groovy です。src/groovy の配下に org/grails/shortenurl ディレクトリーを作成し、それから TinyUrl.groovy を作成してリスト 3 のコードを追加します。
リスト 3. TinyUrl ユーティリティー・クラス
package org.grails.shortenurl
class TinyUrl{
static String shorten(String longUrl){
def addr = "http://tinyurl.com/api-create.php?url=${longUrl}"
return addr.toURL().text
}
}
|
 | プラグインのなかのパッケージ
プラグインのクラスはパッケージに含めることが賢いプラクティスとなっています。これによって、ユーザーの Grails プロジェクトで使用されている既存のクラスとうっかり競合させてしまう可能性が大幅に減るからです。
また、ドメイン・クラスやコントローラーなどもパッケージ化することができます。単純なプロジェクトでは、このようなあまり一般的ではないプラクティスによって必要以上に複雑になると思いますが、経験豊かな Grails 開発者たちは、このプラクティスに絶大な信頼を置いています。
|
|
TinyUrl クラスのテスト
どんなコードでも、本番に移行する前には対応するテストを行うことが必須だと思いませんか?このコードでは直接 Web 呼び出しを行っているため、結合テストをする必要があります。前に作成した org/grails/shortenurl ディレクトリー構造と同じディレクトリー構造を test/integration の下に作成してください。次に TinyUrlTests.groovy を作成し、リスト 4 のコードを追加します (この単純な例では、短いとされる URL は、皮肉なことにエンコードしている元の URL より長くなっています)。
リスト 4. TinyUrl クラスをテストする
package org.grails.shortenurl
class TinyUrlTests extends GroovyTestCase{
def transactional = false
void testShorten(){
def shortUrl = TinyUrl.shorten("http://grails.org")
assertEquals "http://tinyurl.com/3xfpkv", shortUrl
}
}
|
この結合テストの def transactional = false という行に注意してください。このように設定しないと、リスト 5 に記載する意地の悪いエラー・メッセージが表示されます。
リスト 5. テストで def transactional = false を設定していない場合に発生するエラー
Error running integration tests: java.lang.RuntimeException:
There is no test TransactionManager defined
and integration test ${test.name} does not set transactional = false
|
Grails は各テストをデータベース・トランザクションのなかで完了させようとします。通常の Grails アプリケーションでは問題ありませんが、この場合は完全な Grails アプリケーションではなく、プラグイン内での操作なので、データベースが配置済みであるという前提に立つことはできません。そのため、Hibernate プラグインをインストールするか、あるいはエラー・メッセージの指示に従って結合テストに def transactional = false を設定して対処します。
grails test-app と入力して、テストにパスすることを確認してください。
この後、もう 1 つの URL 短縮サービスを実装しますが、その目的は、このプラグインのユーザーにサービスの選択肢を与えることだけにすぎません。
IsGd クラスの作成
Is.Gd (「イズ・グッド」と発音) サービスの自慢は、TinyUrl.com よりも短いドメイン名、そしてさらに短くエンコードされる URL です。http://is.gd にアクセスして、その Web インターフェースを試してみてください。
これもまた皮肉なことですが、この機会に、TinyUrl.groovy で使用した 2 行のメソッド (リスト 3 を参照) よりも長い実装を以下に記載します。この実装のほうが、サービスの呼び出しに失敗した場合に対処するための情報が多少なりとも多くなります。src/groovy/org/grails/shortenurl 内に IsGd.groovy を作成してください (リスト 6 を参照)。
リスト 6. IsGd ユーティリティー・クラス
package org.grails.shortenurl
class IsGd{
static String shorten(String longUrl){
def addr = "http://is.gd/api.php?longurl=${longUrl}"
def url = addr.toURL()
def urlConnection = url.openConnection()
if(urlConnection.responseCode == 200){
return urlConnection.content.text
}else{
return "An error occurred: ${addr}\n" +
"${urlConnection.responseCode} : ${urlConnection.responseMessage}"
}
}
}
|
ご覧のように、このコードではレスポンス・コードが 200 (正常であることを意味する HTTP レスポンス・コード) であることを明示的に確認しています (HTTP レスポンス・コードについての詳細は、「参考文献」を参照してください)。簡単のため、呼び出しが失敗した場合にはエラー・メッセージしか返していませんが、この拡張構造を用意しておけば、呼び出しをさらに数回再試行したり、別の URL 短縮サービスに切り替えたりするという方法で、メソッドをさらに堅牢にすることができます。
このコードに対応する IsGdTests.groovy ファイルを test/integration/org/grails/shortenurl ディレクトリーに作成します (リスト 7 を参照)。grails test-app と入力し、IsGd クラスが正常に機能することを確認します。
リスト 7. IsGd クラスをテストする
package org.grails.shortenurl
class IsGdTests extends GroovyTestCase{
def transactional = false
void testShorten(){
def shortUrl = IsGd.shorten("http://grails.org")
assertEquals "http://is.gd/2oCZR", shortUrl
}
void testBadUrl(){
def shortUrl = IsGd.shorten("IAmNotAValidUrl")
println shortUrl
assertTrue shortUrl.startsWith("An error occurred:")
}
}
|
IAmNotAValidUrl を渡したときに IsGd サービスが失敗する具体的な理由を詳細に確認するには、コマンドラインでリスト 8 に記載する curl を使用することをお勧めします (この cURL ユーティリティーは UNIX®/Linux®/Mac OS X のネイティブ・ユーティリティーであり、Windows® バージョンはダウンロード可能です。「参考文献」を参照してください)。ブラウザーで不適切な URL のテストをすると、エラー・メッセージを確認することはできますが、エラー・コードを確認することはできません。cURL を使用すれば、Web サービスが返すコードが、期待される 200 ではなく 500 であることが明らかにわかります。
リスト 8. curl を使用して失敗した Web サービス・クラスの詳細を確認する
$ curl --verbose "http://is.gd/api.php?longurl=IAmNotAValidUrl"
* About to connect() to is.gd port 80 (#0)
* Trying 78.31.109.147... connected
* Connected to is.gd (78.31.109.147) port 80 (#0)
> GET /api.php?longurl=IAmNotAValidUrl HTTP/1.1
> User-Agent: curl/7.16.3 (powerpc-apple-darwin9.0) libcurl/7.16.3
OpenSSL/0.9.7l zlib/1.2.3
> Host: is.gd
> Accept: */*
>
< HTTP/1.1 500 Internal Server Error
< X-Powered-By: PHP/5.2.6
< Content-type: text/html; charset=UTF-8
< Transfer-Encoding: chunked
< Date: Wed, 19 Aug 2009 17:33:04 GMT
< Server: lighttpd/1.4.22
<
* Connection #0 to host is.gd left intact
* Closing connection #0
Error: The URL entered was not valid.
|
これで、プラグインのコア機能の実装とテストは完了しました。次は、この 2 つのユーティリティー・クラスを Grails が使いやすいように公開する便利なサービスを作成します。
ShortenUrl サービスの作成
ShortenUrl サービスを作成するには、grails create-service ShortenUrl と入力します。そして grails-app/services/ShortenUrlService.groovy に、リスト 9 のコードを追加します。
リスト 9. ShortenUrl サービス
import org.grails.shortenurl.*
class ShortenUrlService {
boolean transactional = false
def tinyurl(String longUrl) {
return TinyUrl.shorten(longUrl)
}
def isgd(String longUrl) {
def shortUrl = IsGd.shorten(longUrl)
if(shortUrl.contains("error")){
log.error(shortUrl)
}
return shortUrl
}
}
|
前に行った結合テストと同じく、transactional フラグは必ず false に設定してください。これらの呼び出しにはデータベースは一切関与しないため、これらの呼び出しをトランザクションで処理する必要はありません。
isgd() メソッドでは、無効な URL の短縮が試みられた場合は、その試行を必ずログに記録します。すべての Grails 成果物には、実行時に log オブジェクトが注入されます。この log オブジェクトでは、目的のログ・レベル (debug、info、error など) に対応するメソッドを呼び出すことができます (ロギングについての詳細は、「参考文献」を参照してください)。この後すぐにわかるように、単体テストを作成する際に、この注入された log オブジェクトを処理するには追加のステップが必要です。
Grails がこのサービスを自動作成したときに、サービスに対応するテストも test/unit ディレクトリーに追加されています。このテストは意味的には単体テストではなく結合テストであり、外部リソースに依存してサービスをテストするため、通常であれば、ShortenUrlServiceTests.groovy を test/integration ディレクトリーに移す必要があるところです。しかし今回は単体テストの技をいくつか披露したいので、ShortenUrlServiceTests.groovy ファイルは test/unit ディレクトリーに置いたまま、このファイルにリスト 10 のコードを追加してください。
リスト 10. ShortenUrl サービスをテストする
import grails.test.*
class ShortenUrlServiceTests extends GrailsUnitTestCase {
def transactional = false
def shortenUrlService
protected void setUp() {
super.setUp()
shortenUrlService = new ShortenUrlService()
}
protected void tearDown() {
super.tearDown()
}
void testTinyUrl() {
def shortUrl = shortenUrlService.tinyurl("http://grails.org")
assertEquals "http://tinyurl.com/3xfpkv", shortUrl
}
void testIsGd() {
def shortUrl = shortenUrlService.isgd("http://grails.org")
assertEquals "http://is.gd/2oCZR", shortUrl
}
void testIsGdWithBadUrl() {
def shortUrl = shortenUrlService.isgd("IAmNotAValidUrl")
assertTrue shortUrl.startsWith("An error occurred:")
}
}
|
transactional フラグを false に設定した後、shortenUrlService 変数を宣言していることに注目してください。続いて setUp() メソッドでサービスの初期化を行います。そして、それぞれのテストごとに setUp() メソッドと tearDown() メソッドを呼び出します。
これが結合テストだとすればエラーなしで実行されるはずですが、これは単体テストなので、testIsGdWithBadUrl() メソッドは No such property: log for class: ShortenUrlService というエラーで失敗します。Web ブラウザーで test/reports/html/index.html を開くと、図 2 に示すエラー・メッセージが表示されます。
図 2. log オブジェクトが注入されていないために失敗した単体テスト
エラーの原因は、単体テストの対象となっているサービスに log オブジェクトが注入されていないことにあります (単体テストは完全に独立して実行されるように意図されていることを思い出してください)。ありがたいことに、このエラーは mockLogging(ShortenUrlService) というたった 1 行を追加することで回避することができます (リスト 11 を参照)。
リスト 11. log オブジェクトが注入されたことにするためのモックの追加
protected void setUp() {
super.setUp()
mockLogging(ShortenUrlService)
shortenUrlService = new ShortenUrlService()
}
|
mockLogging() メソッドは疑似 log オブジェクトをサービスに注入します。この疑似ロガーは、log4j のアペンダー (appender) に指定された任意の出力先にではなく、System.out に出力を送信します。出力 (図 3 を参照) を表示するには、もう一度 grails test-app と入力し、ShortenUrlServiceTests の HTML レポート・ページの下にある System.out リンクをクリックしてください。
図 3. 疑似ロガーの出力
このプラグインには、GSP で URL を短縮するカスタム TagLib やカスタム・コーデックなど、他にも多くの Grails 成果物をバンドルすることができますが、プラグインで実現可能な内容は、これで十分把握できたはずです。次は、このプラグインを現状のままパッケージ化し、別の Grails プロジェクトに統合します。
プラグインのパッケージ化とデプロイメント
デプロイメント用の完全な Grails アプリケーションを準備するには grails war と入力しますが、プラグインの場合には grails package-plugin と入力します。すると、プロジェクトのルート・ディレクトリーに grails-shortenurl-0.1.zip ファイルが生成されます。
「Grails をマスターする: プラグインを理解する」で説明したように、すべての Grails プラグインは ZIP ファイルとして配布されます。ホーム・ディレクトリーのなかにある grails/1.1.1/plugins ディレクトリーを見てみると、grails-hibernate-1.1.1.zip や grails-searchable-0.5.5.zip など、同じような名前が付いたプラグインがあることがわかります。
ShortenUrl が公開プラグインだったとしたら、grails release-plugin と入力することで、変更を加えたプラグインを Grails Plugins Portal にプッシュすることもできます。そうすれば、grails install-plugin shortenurl と入力することによって、誰でもこのプラグインをプロジェクトに統合することができます。一方、専用プラグインをローカルにインストールするのも同じく簡単で、ローカル・ファイル・システム上の ZIP へのフル・パスを指定するだけで、プラグインをインストールすることができます。
プラグインを実際にインストールしてみるため、shortenurl ディレクトリーの外に新しい空のディレクトリーを作成します。grails create-app foo と入力して単純なアプリケーションを作成し、カレント・ディレクトリーを foo ディレクトリーに変更して grails install-plugin /local/path/to/grails-shortenurl-0.1.zip と入力します。もちろん、パスの部分はプラグインへの実際のパスに置き換えてください。すると、リスト 12 のような出力が表示されます。
リスト 12. ローカル・プラグインをインストールする
$ grails install-plugin /code/grails-shortenurl-0.1.zip
Welcome to Grails 1.1.1 - http://grails.org/
Licensed under Apache Standard License 2.0
Grails home is set to: /opt/grails
Base Directory: /code/foo
Running script /opt/grails/scripts/InstallPlugin.groovy
Environment set to development
[copy] Copying 1 file to /Users/sdavis/.grails/1.1.1/plugins
Installing plug-in shortenurl-0.1
[mkdir] Created dir:
/Users/sdavis/.grails/1.1.1/projects/foo/plugins/shortenurl-0.1
[unzip] Expanding:
/Users/sdavis/.grails/1.1.1/plugins/grails-shortenurl-0.1.zip into
/Users/sdavis/.grails/1.1.1/projects/foo/plugins/shortenurl-0.1
Executing shortenurl-0.1 plugin post-install script ...
Plugin shortenurl-0.1 installed
|
上記を見るとわかるように、ローカルにインストールする専用プラグインのライフサイクルも、公開プラグインのライフサイクルとまったく同じです。
foo/application.properties ファイルをテキスト・エディターで開いて、plugins.shortenurl が記載されていることを確認してください (リスト 13 を参照)。
リスト 13 プラグインが application.properties に記載されていることを確認する
#utf-8
#Wed Aug 19 14:38:24 MDT 2009
app.version=0.1
app.servlet.version=2.4
app.grails.version=1.1.1
plugins.hibernate=1.1.1
plugins.shortenurl=0.1
app.name=foo
|
プラグインがインストールされていることは確実になったので、今度はこのプラグインが期待通りの動作をすることを確認します。grails create-controller test と入力した後、grails-app/controllers/TestController.groovy を開いてリスト 14 に記載するコードを追加します。
リスト 14. サービスをコントローラーに注入する
class TestController {
def shortenUrlService
def index = {
render "This is a test for the ShortenUrl plug-in " +
"Type test/tinyurl?q=http://grails.org to try it out."
}
def tinyurl = {
render shortenUrlService.tinyurl(params.q)
}
}
|
このサービスをコントローラーに注入するのは、def shortenUrlService です。grails run-app と入力してアプリケーションを開始し、Web ブラウザーで http://localhost:9090/foo/test/tinyurl?q=http://grails.org にアクセスすると、図 4 の結果が表示されます。
図 4. プラグインが正常にインストールされたことを確認する
これで http://tinyurl.com/3xfpkv にアクセスすれば、間違いなく grails.org にアクセスすることができます。
まとめ
以上を読んでおわかりのように、Grails プラグインを作成する方法は通常の Grails アプリケーションを作成する方法とそれほど変わりません。grails create-app と入力する代わりに grails create-plugin と入力し、grails war と入力する代わりに grails package-plugin と入力するといった具合です。そして GrailsPlugin.groovy 記述子ファイルに重要な詳細を追加するという点を除けば、Grails プラグインを作成する場合でも、Grails アプリケーションを作成する場合でも、手順 (サービスの作成、テストの作成など) はすべて同じです。
今回の記事では、mockLogging() メソッドによる Grails 単体テストのモック作成機能について簡単に触れましたが、次回は mockDomain() や mockForConstraintsTests() をはじめ、さらに多くの非常に有益な mock メソッドについて説明します。それまでは、Grails を楽しみながらマスターしてください。
ダウンロード | 内容 | ファイル名 | サイズ | ダウンロード形式 |
|---|
| Source code | j-grails09159.tar | 820KB | HTTP |
|---|
参考文献 学ぶために
製品や技術を入手するために
- Grails: Grails の最新リリースをダウンロードしてください。
- cURL: ほとんどの UNIX、Linux、および Mac OS X システムでは、cURL がデフォルトでインストールされます。Windows やその他ほとんどすべての OS に対応したバージョンもダウンロードできます。
議論するために
著者について
記事の評価
|