Grails 2.4で追加されたdoWithSpring、doWithConfigについて
※この記事はGrails 2.4.3を元に記述しています
Grails 2.4からdoWithSpring
、doWithConfig
といったユニットテストの仕組みが導入された。
これを使うとユニットテスト内でSpringビーンを定義したり、コンフィグの値を変更したりできる。
doWithSpring、doWithConfigの使い方
まずは実際の使用例から。
import grails.test.mixin.TestMixin import grails.test.mixin.support.GrailsUnitTestMixin import spock.lang.Specification @TestMixin(GrailsUnitTestMixin) class DoWithSpringDoWithConfigSpec extends Specification { static doWithSpring = { myService(MyService) } static doWithConfig(ConfigObject config) { config.myConfigValue = 'Hello' } def "doWithSpringとdoWithConfigの動作確認"() { expect: grailsApplication.mainContext.getBean('myService') != null config.myConfigValue == 'Hello' } }
doWithSpring
はstatic
な変数として定義し引数にクロージャを取る。
クロージャ中では、SpringビーンDSLを使ってビーンを定義できる。
doWithConfig
は変数ではなくメソッドとして定義する(doWithSpring
との統一性を考えるとクロージャでも良かった気がするのだが...)。
このメソッドの引数にはConfigObject
のインスタンスが渡されるので、このインスタンスを操作することでコンフィグの変更ができる。
doWithSpring
とdoWithConfig
の実行順はdoWithConfig
、doWithSpring
の順番になる。
そのため、ビーンの初期化処理でコンフィグの値に依存していたとしてもうまく機能する。
FreshRuntime
ConfigObjectやGrailsApplicationを含むアプリケーションコンテキストの初期化はスペッククラス内で1度だけ実行される。
Spockでキーワードで言えば@Shared
やsetupSpec
で構築されたフィクスチャのように考えれば良い。
もしフィーチャ(テスト)メソッドごとにアプリケーションコンテキストの初期化をしたい場合はgrails.test.runtime.FreshRuntime
アノテーションを使う。
@FreshRuntime @TestMixin(GrailsUnitTestMixin) class DoWithSpringDoWithConfigSpec extends Specification { ...
FreshRuntimeアノテーションは上記のようにクラスレベルに設定するか、フィーチャメソッドに設定できる。 ただし、現状はこの問題によりクラスレベルに設定してもうまく動作しない。 この問題は2.4.4で修正される(2014/10/15時点の最新バージョンは2.4.3)。
モックをSpringビーンとして登録する
先ほどのFreshRuntimeアノテーションとorg.codehaus.groovy.grails.commons.InstanceFactoryBean
を使うとSpockのモックをSpringビーンとして登録することができる。
FreshRuntimeアノテーションと組み合わせる必要があるのは、恐らくフィーチャメソッド間でモックを共有してはならないからだと思う。
Spockではインタラクションを持つモックは特定のフィーチャメソッドへの参照をもっているため、static、または@Sharedを使って共有してはならない。
そのため、モックをSpringビーンとして登録する場合も、FreshRuntimeアノテーションを使ってフィーチャメソッドごとに初期化する必要があるのだろう。
モックをSpringビーンとして登録したい状況としては、テスト対象の背後で動作しているSpringビーンのインスタンスをモックに差し替えたい場合が考えられる。 例えば次のようなコントローラがあったとする。
import grails.compiler.GrailsCompileStatic @GrailsCompileStatic class MyController { MyService myService def index() { render myService.hello() } }
このコントローラのindex
アクションでは、このコントローラにDIされるmyService
に処理を委譲している。
このコントローラをテストしようとした時、myService
をモックに差し替えたい場合がある。
これを、次のように記述できる。
import grails.test.mixin.TestFor import grails.test.runtime.FreshRuntime import org.codehaus.groovy.grails.commons.InstanceFactoryBean import spock.lang.Specification @FreshRuntime @TestFor(MyController) class MockedBeanSpec extends Specification { def myService = Mock(MyService) def doWithSpring = { myService(InstanceFactoryBean, myService, MyService) } def "コントローラの背後で動作するサービスをモックに差し替える"() { when: controller.index() then: response.text == 'hello' and: 1 * myService.hello() >> { "hello" } } }
まずはじめにdef myService = Mock(MyService)
でSpockのモックを定義している。
次にmyService(InstanceFactoryBean, myService, MyService)
といったように、InstanceFactoryBeanのコンストラクタの引数に、このモックのインスタンスとそのクラスの型を指定する。
これで生成したモックを使ってmyService
という名前のSpringビーンを登録できる。
resources.groovyに定義したビーンをユニットテストで使用する
GrailsではSpringビーンDSLを使って、grails-app/conf/spring/resources.groovy
にビーンの定義ができる。
インテグレーションテストや、Grailsのアプリケーションが起動する場合はこのファイルに定義したビーンが自動的に登録される。
しかし、ユニットテストでは自動的には登録されない。
これをstatic loadExternalBeans = true
という設定をスペッククラスに追加することで、自動的にビーンが登録されるようになる。
import grails.test.mixin.TestFor import spock.lang.Specification @TestFor(MyController) class LoadingApplicationBeansSpec extends Specification { static loadExternalBeans = true ... }
そんなとこで。
Grailsの追加、更新、削除処理について深く考える
※この記事はGrails 2.4.2をベースに記述しています
一見簡単にみえるが、意外と奥が深い。 次のようなドメインクラスがあったとする。
class Person { String username static constraints = { username unique: true } }
この時、デフォルトのスキャフォルドは次のようなコントローラを生成する。
@Transactional(readOnly = true) class PersonController { ... @Transactional def save(Person personInstance) { if (personInstance == null) { notFound() return } if (personInstance.hasErrors()) { respond personInstance.errors, view:'create' return } personInstance.save flush:true request.withFormat { form multipartForm { flash.message = message(code: 'default.created.message', args: [message(code: 'person.label', default: 'Person'), personInstance.id]) redirect personInstance } '*' { respond personInstance, [status: CREATED] } } } @Transactional def update(Person personInstance) { if (personInstance == null) { notFound() return } if (personInstance.hasErrors()) { respond personInstance.errors, view:'edit' return } personInstance.save flush:true request.withFormat { form multipartForm { flash.message = message(code: 'default.updated.message', args: [message(code: 'Person.label', default: 'Person'), personInstance.id]) redirect personInstance } '*'{ respond personInstance, [status: OK] } } } @Transactional def delete(Person personInstance) { if (personInstance == null) { notFound() return } personInstance.delete flush:true request.withFormat { form multipartForm { flash.message = message(code: 'default.deleted.message', args: [message(code: 'Person.label', default: 'Person'), personInstance.id]) redirect action:"index", method:"GET" } '*'{ render status: NO_CONTENT } } } ... }
save
/update
/delete
メソッドがそれぞれ追加/更新/削除の処理になる。このコードをベースに説明する。
ドメインクラスをアクションの引数とした場合の動作
save
/update
/delete
メソッドは引数にPerson
クラスの値を取るように定義されいる。
def save(Person personInstance) def update(Person personInstance) def delete(Person personInstance)
これはGrails 2.3から追加された機能で、アクションメソッドの引数にドメインクラスを指定すると
- パラメータ中に
id
があった場合、自動的にデータがロードされる (ない場合は、新規にインスタンスが生成される) - リクエストパラメータがある場合は、自動的にデータがバインドされる
- 自動的にバリデートが行われる
といったことが行われる。
は非常に便利だが、柔軟性にもかける。このロジックは
id
パラメータがあった場合は、ドメインクラスのget
メソッドを使ってデータを取得するという単純なものである。この取得ロジックをカスタマイズできると非常に便利だが、現時点ではない。そのため、id
パラメータ以外の独自のデータ取得ロジックを実装したい場合は、アクションメソッドの引数にドメインクラスを指定することを諦める必要がある。については自動的にバインドされるためbindDataなどを使った時のように、バインドするプロパティを指定することはできない。そのため、ドメインクラスのconstraintsで
bindable
を使ったmass assignment脆弱性の対応をしっかりしておく必要がある。も一見便利なようだが、冗長なバリデーションを引き起こす。バリデーションのコストが低い場合はあまり気にしなくても良いかもしれないが、データベースにアクセスするようなバリデーションが含まれている場合は注意する必要がある。
まず、delete
メソッドではバリデーションは不要だろう (これはバグと言ってもいいかもしれないが)。バインドするパラメータが存在しないからである。
save
/update
についてはバリデーションが必要のため、問題ないように思える。ただ、このあとにsave
メソッドでデータを保存していることが問題になる。
@Transactional def save(Person personInstance) { … personInstance.save flush:true ... } @Transactional def update(Person personInstance) { … personInstance.save flush:true ... }
save
は自動的にバリデーションを実行する。そのため、save
を実行したときにバリデーションエラーになることを考慮しなければならない。はじめにバリデーションを通過済みだからといって、途中で値を変更したり、データベースへのアクセスがある制約の場合は他のトランザクションが完了することにより、save
を実行した時に必ずバリデーションを通過するとは限らない。
またsave
は複数回バリデーションを実行することにも注意する必要がある。これはドメインクラスで用意されているイベントフックの仕組みで値が変更された場合を考慮したものである。
そのため、上記の例で言うとsave
/update
メソッドはそれぞれ合計で3回のバリデーションが実行される。この例のPersonクラスはusername
にユニーク制約をつけているので、バリデーションでデータベースへSELECT文が発行され、計3回のSELECT文が発行されることになる。
この問題を回避するかはどうかは、どこまで最適化が必要かによるが、
- アクションの引数にドメインクラスを指定するのをやめて、自前で取得する
- イベントフックで値が更新されない前提であれば、自前で
validate
を呼んで、save
の時はバリデーションを無効にする
という案がある。
@Transactional def save() { def person = new Person() bindData person, request if (!person.validate()) { respond person.errors, view: 'create' return } person.save validate: false, flush: true ... } @Transactional def update(Long id) { def person = Person.get id if (!person) { notFound() return } bindData person, request if (!person.validate()) { respond person.errors, view: 'edit' return } person.save validate: false, flush: true ... } @Transactional def delete(Long id) { def person = Person.get id if (!person) { notFound() return } person.delete flush: true ... }
だが、person.save validate: false
などは、通常はやり過ぎた最適化なので、ユニーク制約がないなどバリデーションのコストが低い場合は単に
if (!person.validate()) { respond person.errors, view: 'create' return } person.save validate: false, flush: true
は
if (!person.save(flush: true)) { respond person.errors, view: 'create' return }
でよいだろう。または、バインド後にバリデーションエラーになる可能性がない場合は
@Transactional def save(Person personInstance) { ... personInstance.save validate: false, flush:true ... }
とする手もあるだろう。
楽観的排他制御
デフォルトのスキャフォルドが生成するコードには楽観的排他制御の考慮がない。
Grailsではドメインクラスに楽観的排他制御を行うためのversion
プロパティが追加される。このプロパティを使って楽観的排他制御をするには2つの考慮が必要がである。
- 変更画面に
hidden
などでバージョンの値を仕込んでおき、ポストされたときにデータベースのバージョンの値と比較する - Hibernateが楽観的排他エラーを検出したときのハンドリングをする
Hibernateによる楽観的排他制御ではユーザが設定したバージョンの値は完全に無視される。Grailsでversion
プロパティをバインド可能にして、あとはHibernateに任せておけば簡単に楽観的排他制御ができそうだが、恐らくセキュリティ上の理由から残念ながらそうはなっていない。
そのため、前者の考慮はユーザ自身でやらなければならない。Grails 2.2までのデフォルトのスキャフォルドでは考慮されていたのだが、なぜか2.3でREST対応のテンプレートになった時になくなってしまった。
対処は次のようにデータベースのversion
の値と、ユーザから送られてきたversion
の値を比較すればよい。
@Transactional def update(Long id, Long version) { def person = Person.get id if (!person) { notFound() return } if (person.version > version) { person.errors.rejectValue('version', 'default.optimistic.locking.failure', [message(code: 'person.label', default: 'Person')] as Object[], 'Another user has updated this Person while you were editing') respond person.errors, view: 'edit' return } bindData person, request if (!person.validate()) { respond person.errors, view: 'edit' return } person.save validate: false, flush: true ... }
必ずbindData
を実行する前といったように、データベースから取得したperson
の値を変更する前にチェックを入れる必要がある。これは、データをバインドしたあとに比較したら同じバージョンの値を比較してしまうからではない。Grailsのデータバインディングではデフォルトでversion
プロパティがバインディングの対象外になっている。
問題はこのアクションメソッドに@Transactionalが設定されていることだ。このアノテーションはGrails 2.3から追加された新しいトランザクションのアノテーションである。以前はサービスクラス限定だったが、新しいトランザクションアノテーションはコントローラなど、どのような場所で使えるようになっている。
@Transactionalが指定されたメソッドの中で、データベースにすでに保存されている (Merged状態の) ドメインクラスのインスタンスの変更を行うと、明示的にsave
を呼びださなくても、メソッドが終了する時点でインスタンスへの変更がフラッシュされ、トランザクションがコミットされる。トランザクションをロールバックするには、非検査例外をメソッドからスローするなどしなければならない。
上記のアクションメソッドは、バージョンチェックを行ったあとに、単にメソッドをreturnで終了しているだけである。そのため、このアクションメソッドは、実際にはトランザクションがコミットされて終了している。そのため、この前になにか値を変更すると、その変更は自動的にコミットされてしまうのだ。
次はHibernateの楽観的排他エラーの対処である。Hibernateは楽観的排他エラーを検出すると、org.hibernate.StaleObjectStateExceptionをスローする。ただ、Springを経由してHibernateを使っている場合は、Springが例外をラップして再スローするため、実際にはユーザにはorg.springframework.dao.OptimisticLockingFailureExceptionのサブクラスの例外がスローされる (参考: http://d.hatena.ne.jp/yomama/20070802)。
この例外はsave
の呼び出し時にキャッチしてハンドリングすればよい。
@Transactional def update(Long id, Long version) { def person = Person.get id if (!person) { notFound() return } if (person.version > version) { person.errors.rejectValue('version', 'default.optimistic.locking.failure', [message(code: 'person.label', default: 'Person')] as Object[], 'Another user has updated this Person while you were editing') respond person.errors, view: 'edit' return } bindData person, request if (!person.validate()) { respond person.errors, view: 'edit' return } try { person.save validate: false, flush: true } catch (OptimisticLockingFailureException e) { person.errors.rejectValue('version', 'default.optimistic.locking.failure', [message(code: 'person.label', default: 'Person')] as Object[], 'Another user has updated this Person while you were editing') respond person.errors, view: 'edit' return } ... }
このOptimisticLockingFailureExceptionは更新/削除時にデータが存在しなかった場合にもスローされる。
def person = Person.get id if (!person) { notFound() return }
でチェック済みのように思えるが、この処理のあとに別のトランザクションでデータが削除されている場合があるからだ。
楽観的排他エラーの場合も、データが存在しない場合もOptimisticLockingFailureException
がスローされるため、状況に応じて処理を分岐させることは難しいが、そうそう同時にトランザクションが実行されることがないなら、そのあたりは割りきりで良いだろう。もし、割り切れない場合は、データ取得時に悲観的ロックを取得して、同時に処理が走らないようにするなどの考慮が必要になる。
また、データが存在しない場合の考慮はdelete
メソッドでもする必要ある。
@Transactional def delete(Long id) { def person = Person.get id if (!person) { notFound() return } try { person.delete flush: true } catch (OptimisticLockingFailureException e) { notFound() return } ... }
一意制約違反の考慮
Grailsではusername unique: true
といったようなにユニーク制約をつけると、バリデーション実行時にデータベースにSELECT文を発行して制約違反がないかを確認する。そのため通常は一意制約違反の場合はバリデーションエラーになる。
しかし、このチェックは同時に複数のトランザクションが同時に走った場合、ともにSELECT文でのチェックをパスしてしまう可能性ある。このような場合は、そのあとのINSERT文や、UPDATE文を発行した時点でデータベース上で一意制約違反となる。
Hibernateはこのエラーを検出した場合、org.hibernate.exception.ConstraintViolationExceptionをスローするが、先ほどの楽観的排他制御と同様にSpringがorg.springframework.dao.DataIntegrityViolationExceptionのサブクラスでラップするため、アプリケーションではこの例外をキャッチする。
一意制約違反は、追加/更新時に考慮する必要があるため、save
/update
メソッドで対応を行う。
@Transactional def save() { def person = new Person() bindData person, request if (!person.validate()) { respond person.errors, view: 'create' return } try { person.save validate: false, flush: true } catch (DataIntegrityViolationException e) { person.errors.rejectValue('username', 'default.not.unique.message', ['username', message(code: 'person.label', default: 'Person'), person.username] as Object[], 'Username must be unique') respond person.errors, view: 'create' return } ... } @Transactional def update(Long id, Long version) { ... try { person.save validate: false, flush: true } catch (OptimisticLockingFailureException e) { person.errors.rejectValue('version', 'default.optimistic.locking.failure', [message(code: 'person.label', default: 'Person')] as Object[], 'Another user has updated this Person while you were editing') respond person.errors, view: 'edit' return } catch (DataIntegrityViolationException e) { person.errors.rejectValue('username', 'default.not.unique.message', ['username', message(code: 'person.label', default: 'Person'), person.username] as Object[], 'Username must be unique') respond person.errors, view: 'edit' return } ... }
これで動くと言いたいところだが、save
メソッドで実際にDataIntegrityViolationExceptionが発生する状況を発生されると以下のエラーが発生してしまう (何故かupdate
メソッドやOptimisticLockingFailureExceptionの場合は発生しないのだが...) 。
2014-07-27 14:55:01,417 [http-bio-8080-exec-5] ERROR StackTrace - Full Stack Trace: org.hibernate.AssertionFailure: null id in Person entry (don't flush the Session after an exception occurs) ...
これは@Transactional
をつけているため、ロールバック処理をしてあげなければならないのだろう。Springの@Transaction
アノテーションではTransactionAspectSupportを経由して、TransactionStatusのインスタンスを取得し、setRollbackOnly()を呼び出すことで、明示的にトランザクションをロールバックとして扱うことができる。しかし、新しいGrailsの@Transactional
アノテーションではこの手段が使えない。そのため、トランザクションをロールバックするには、先ほど説明したようにアクションメソッドを非検査例外で終了しなければならない (裏技としてAST変換で追加されるtransactionStatus
という変数を使ってtransactionStatus.setRollbackOnly()
とすることもできるが、公開されたAPIではないのでお勧めしない)。
非検査例外をスローすれば良いのだが、これはコントローラではアクションごとに画面遷移を制御しなければならないため、非検査例外をスローしてしまうと扱いが難しくなる。遷移先の情報を詰め込んで例外をスローし、ロールバック後にGrails 2.3から追加されたコントローラでの例外ハンドリング機能を使って例外をキャッチして画面遷移を制御する案もあるが、あまりスマートな解決方法ではない。
そのため、コントローラ内でロールバックの遷移が必要な場合は、ドメインクラスのwithTransaction
を使う、または処理をサービスクラスに移してトランザクションの境界をサービスクラスにすると良いだろう。
ただ、今回の例でいうと、そもそもトランザクションは必要なのだろうか。今回はPersonに対して変更が1度しかないこともあり、トランザクションはなくても良い。ただ、Personのsave
が一度しか無いからといって、常にトランザクションが必要ないわけではない。ドメインクラスに関連がある場合は、他のドメインクラスへ変更がカスケードされることがあるため、このような場合はトランザクションを使うべきだろう。
ということで今回は@Transactional
を消してしまう。
class PersonController { ... def save(Person personInstance) { ... } def update(Person personInstance) { ... } def delete(Person personInstance) { ... } ... }
またDataIntegrityViolationExceptionをキャッチした時、一意制約違反があったプロパティを特定できないことにも注意が必要である。同一ドメイン上に複数の一意制約が存在することはそうそうないと思うが、一律DataIntegrityViolationExceptionがスローされるため、どのプロパティで一意制約が発生したのか特定するのが難しい。このような場合は割り切ったエラー処理が必要になるだろう。テーブルロックをかけて、この例外が発生しないようにもできるが、これは良い選択ではないだろう。
一意制約が発生する可能性のプロパティが1つしかない場合は、それを前提にエラー処理を行うことができる。
そもそも、当たり前だが制約違反が発生しないのであればDataIntegrityViolationExceptionをキャッチする必要はない。
まとめ
追加、更新、削除処理は一見簡単にみえるが、まじめに実装すると色々考慮すべきことがある。 最終的には以下のような形になった。
def save() { def person = new Person() bindData person, request if (!person.validate()) { respond person.errors, view: 'create' return } try { person.save validate: false, flush: true } catch (DataIntegrityViolationException e) { person.errors.rejectValue('username', 'default.not.unique.message', ['username', message(code: 'person.label', default: 'Person'), person.username] as Object[], 'Username must be unique') respond person.errors, view: 'create' return } ... } def update(Long id, Long version) { def person = Person.get id if (!person) { notFound() return } if (person.version > version) { person.errors.rejectValue('version', 'default.optimistic.locking.failure', [message(code: 'person.label', default: 'Person')] as Object[], 'Another user has updated this Person while you were editing') respond person.errors, view: 'edit' return } bindData person, request if (!person.validate()) { respond person.errors, view: 'edit' return } try { person.save validate: false, flush: true } catch (OptimisticLockingFailureException e) { person.errors.rejectValue('version', 'default.optimistic.locking.failure', [message(code: 'person.label', default: 'Person')] as Object[], 'Another user has updated this Person while you were editing') respond person.errors, view: 'edit' return } catch (DataIntegrityViolationException e) { person.errors.rejectValue('username', 'default.not.unique.message', ['username', message(code: 'person.label', default: 'Person'), person.username] as Object[], 'Username must be unique') respond person.errors, view: 'edit' return } ... } def delete(Long id) { def person = Person.get id if (!person) { notFound() return } try { person.delete flush: true } catch (OptimisticLockingFailureException e) { notFound() return } ... }
重複したコードが増えるため、Domain Lockingプラグインや、独自のカスタムメソッドを用意するとよい。
最後にドメインクラスや、処理の内容に応じてこのコードが最適なわけではないことに注意してほしい。 そんなところで。
G* Advent Calendar 2013 1日目 - Gaiden
この記事は毎年恒例の G* Advent Calendar 2013 の1日目の記事です。1日目担当のyamakzuです。こんにちは。
今日は、先月10月頭にリリースしたGaidenについて紹介します。
GaidenはGroovy製の軽量ドキュメンテーションツールです。 この手のドキュメンテーションツールではSphinxやAsciiDocが有名ですが、ちょっとしたドキュメントを記述するには少し大きすぎるツールに感じるときがあります。
Gaidenはもっと簡単に気軽にドキュメントを記述できるようなツールを目指しています。 シンタックスにはMarkdownを採用しています。 Markdownでさっと書いて、ドキュメントを生成できる。そんなツールです。
とかいう表向きのセールストークはありますが、正直なところはGroovy使いとしてG製なツールが欲しかっただけです。
開発は主に@gantawitterと私、そしてid:nobeansのサポートのもと進めています。また、ドキュメントテンプレートの作成にid:labduckに協力してもらっています。
では早速使ってみましょう。
インストール
Groovy界隈ではお馴染みのGVMからインストールできます。
$ gvm install gaiden
WindowsではGVMが使えないのでバイナリをダウンロードして、展開したディレクトリのbin
ディレクトリにPATHを通してください。
プロジェクトの作成
まず始めにドキュメントのプロジェクトを作る必要があります。
create-project
コマンドで生成します。
$ gaiden create-project mydoc
$ cd mydoc
プロジェクトの構成は次のようになっています。
$ tree
mydoc
├── GaidenConfig.groovy : Gaidenの設定ファイル
├── pages : Markdownで書かれたページを格納するディレクトリ
│ ├── index.md
│ └── toc.groovy : 目次を定義するファイル
├── static : CSS、JSなどの静的ファイルを格納するディレクトリ
│ ├── ...
└── templates
└── layout.html : ページの雛形となるHTMLファイル
ページを記述する
ページはpages
ディレクトリ配下にファイル名.md
というファイル名でMarkdownで記述します。
試しにpages/mypage.md
を作り、次のように記述します。
# はじめてのGaiden
Gaidenやばい
ドキュメントを生成する
ページを記述したらドキュメントを生成してみましょう。
ドキュメントを生成するにはbuild
コマンドを実行します。
$ gaiden build
ドキュメントはbuild
ディレクトリに生成されます。
build/mypage.html
をブラウザで開いてみると次のような画面が生成されているはずです。
目次を作る
目次はpages/toc.groovy
に記述します。
次のように記述します。
"index.md"(title: "はじめに") {
"mypage.md"(title: "はじめてのGaiden")
}
もう一度gaiden build
を実行してbuild/toc.html
を開いてみてください。
次のようなページが表示されるはずです。
このtoc.groovy
はGroovyのDSLで記述します。
ページ名の文字列に続けて引数にtitle
という属性を設定していきます。
階層構造を持つ場合はクロージャ{}
を使って、ブロック内でページを指定します。
もう少し複雑な例では次ようになります。
"introduction.md"(title: "はじめに") {
"introduction/whatis.md"(title: "Gaidenとはなにか")
"introduction/install.md"(title: "インストール")
}
"quickstart/quickstart.md"(title: "クイックスタート") {
"quickstart/addingcontent.md"(title: "ページの生成")
}
テンプレートを編集する
ページのデザインを変更したい場合はtemplates/layout.html
を編集します。
Gaiden 0.3ではデフォルトで次のようなテンプレートになっています。
<html>
<head>
<meta charset="UTF-8">
<title>$title</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="stylesheet" href="${resource('/css/bootstrap.css')}">
<link rel="stylesheet" href="${resource('/css/prettify.css')}">
<link rel="stylesheet" href="${resource('/css/style.css')}">
</head>
<body>
<div class="navbar navbar-static-top">
<div class="container">
<a class="navbar-brand" href="$tocPath">$title</a>
<a class="toc btn btn-primary btn-lg" href="$tocPath"><span class="glyphicon glyphicon-list"></span></a>
</div>
</div>
<div class="container">
<section class="page-content">
$content
</section>
</div>
<footer id="footer" class="footer">
<div class="container">
<p class="credit text-muted">Powered by <a href="#">Gaiden</a>.</p>
</div>
</footer>
<script src="${resource('/js/jquery-1.10.2.min.js')}"></script>
<script src="${resource('/js/bootstrap.js')}"></script>
<script src="${resource('/js/prettify.js')}"></script>
<script src="${resource('/js/application.js')}"></script>
</body>
</html>
テンプレートの中では${resource(..)}
や$content
、$title
、$tocPath
といった拡張変数、メソッドが使えるようになっています。
詳しくはリファレンスを参照してください。
このHTMLを変更することで自由にデザインを変更できます。
今後について
まだまだ機能が少ない状況ですが少しずつ改善していく予定です。
年内には0.4をリリース予定です。 是非使ってフィードバックをください!
明日からは...
なんと3日間連続できょんくんです!どうしてそうなった! ではでは、よろしくお願いしますー
JJUG CCC 2013 FallのJVM言語パネルディスカッションにGroovy代表として参加してきた
あれ、気がつけば半年ぐらいブログ書いてなかった...
ということでJJUG CCC 2013 FallのJVM言語パネルディスカッションにGroovy代表として参加してきました。 いろいろあってJVM言語パネルディスカッションのGroovy代表として声をかけてもらったのですが、 Groovyについて語る自信があまりなかったのでid:nobeansに一緒に参加してもらいました。ありがとうございます!
緊張しまくって自分の資料説明するのが精一杯で、ほとんどディスカッションらしいことはできなかった気がしますが、良い経験になりました。
次は一人で参加できるように頑張ります(?)
GrailsのSpring Security Core Pluginでパスワードのソルトとストレッチング
パスワードをハッシュで保存して置くのは当たり前ですが、レインボーテーブル使用した総当たり探索の対策として、ソルトとストレッチングを組み合わせ、より安全にパスワードを保存するのが一般的になってきました。
GrailsのSpring Security Core PluginはデフォルトではSHA-256
のハッシュでパスワードを保存しますが、ソルトやストレッチングは使用されていません。しかし、そこは認証のデパートSpring Security、ソルトとストレッチングのサポートが組み込まれています。
まずはソルトのサポートから見て行きましょう。
usernameをソルトとする
usernameをソルトとして扱うにはConfig.groovy
に以下の設定を追加します。
grails.plugins.springsecurity.dao.reflectionSaltSourceProperty = 'username'
これで認証時に、usernameをソルトとしてパスワードに付与しハッシュを計算するようになります。ただし、この設定はあくまで認証時の設定であり、ユーザのパスワードを保存する時にソルトが付与されるわけではありません。
もし、s2-quickstart
コマンドを使って生成したユーザドメインクラスを使用している場合は、このドメインクラスに手を入れます。デフォルトでは以下のようにパスワードがエンコードされています。
protected void encodePassword() { password = springSecurityService.encodePassword(password) }
これを以下のように変更します。
protected void encodePassword() { password = springSecurityService.encodePassword(password, username) }
この様にspringSecurityService#encodePasswordの第2引数にソルトとしてusernameを指定します。
これでパスワード保存時にusernameをソルトとして付与してハッシュが計算されます。
ソルト専用のプロパティを使う
徳丸先生の資料によるとソルトには一定の長さが必要です。usernameとpasswordの組み合わせで一定以上の長さが確保できない場合は、一定の長さをもつソルト専用のプロパティが欲しくなります。
以下のようにユーザドメインクラスを変更します。
import org.apache.commons.lang.RandomStringUtils class User { ... String salt String getSalt() { // 新規作成時にsaltがない場合にsaltを生成 if (!salt) { // ランダムである必要ないが一定の長さをもつ文字列として // ランダムが扱いやすいのでRandomStringUtilsを使用 salt = RandomStringUtils.randomAlphanumeric(20) // ランダムな20文字 } return salt } static constraints = { ... salt blank: false } static mapping = { ... // 安全のためinsert以外でsaltが更新されないようにする salt updateable: false } protected void encodePassword() { // usernameをsaltに変更 password = springSecurityService.encodePassword(password, salt) } }
合わせてConfig.groovy
のreflectionSaltSourceProperty
も変更しましょう。
grails.plugins.springsecurity.dao.reflectionSaltSourceProperty = 'salt'
これで設定完了!と言いたいところですが、まだいくつかの準備が必要です。
上記のreflectionSaltSourceProperty
はSpringSecurityのUserDetails
を実装したクラスのプロパティを指定しています。GrailsのSpring Security Core PluginのデフォルトではUserDetails
の実装クラスにGrailsUser
が使われます。
しかし、GrailsUser
にはsalt
というプロパティは存在しないため、新たにクラスを用意する必要があります。
また、このUserDetails
のインスタンスはUserDetailsService
の実装クラスで生成されます。GrailsのSpring Security Core Pluginでは、これがGormUserDetailsService
というクラスになります。
つまり独自のUserDetailsService
を作成し、salt
というプロパティを持つ独自のUserDetails
を生成する必要があります。
クラスはsrc/groovy
などに作成すると良いでしょう。
まずはUserDetails
の実装クラスを以下のよう作成します。
import org.codehaus.groovy.grails.plugins.springsecurity.GrailsUser import org.springframework.security.core.userdetails.UserDetails class MyUser implements UserDetails { @Delegate GrailsUser grailsUser String salt }
次に上記のMyUser
クラスを生成するUserDetailsService
を作ります。既存のGormUserDetailsService
を継承して作るがカンタンです。
import org.codehaus.groovy.grails.plugins.springsecurity.GormUserDetailsService import org.springframework.security.core.GrantedAuthority import org.springframework.security.core.userdetails.UserDetails class MyUserDetailsService extends GormUserDetailsService { @Override protected UserDetails createUserDetails(user, Collection<GrantedAuthority> authorities) { new MyUser(grailsUser: super.createUserDetails(user, authorities), salt: user.salt) } }
groovyの@Delegate
を使用したお手軽実装にしてみました。
あとはこのサービスをSpringのビーンとして登録します。resource.groovy
に以下のように追加してください。
beans = { ... userDetailsService(MyUserDetailsService) { grailsApplication = ref('grailsApplication') } }
これでソルト専用のプロパティを使うことができます。
ストレッチング
GrailsのSpring Security Core PluginのデフォルトではpasswordEncoder
にorg.springframework.security.authentication.encoding.MessageDigestPasswordEncoder
が使われています。
このクラスはストレッチングをサポートしています。
しかし、プラグインの設定ではgrails.plugins.springsecurity.password.algorithm
というハッシュアルゴリズムの設定があるだけで、ストレッチング回数の設定はありません(デフォルトでは1回です)。
自分で新たにビーンを定義することもできますが、ここはGrailsが持つ、設定ファイルからSpringビーンのプロパティを上書きする仕組みを活用します。
Config.groovy
に以下の設定を追加します。
beans.passwordEncoder.iterations = 1000
これで1000回ストレッチングします。
テスト
ソルトとストレッチングが機能しているか不安になるのでテストを書いてみましょう。
def "ソルトとストレッチングの確認"() { setup: def password = "password" def user = new User(username: "test", password: password, enabled: true) and: "パスワードにランダム文字列をソルトとして付与" def digest = "$password{$user.salt}".getBytes("UTF-8") and: "1000回ストレッチング" 1000.times { digest = MessageDigest.getInstance("SHA-256").digest(digest) } def hashedPassword = new String(Hex.encode(digest)) when: "新規Userを保存" user.save() then: "ソルトとストレッチングを使用したハッシュになっていること" user.password == hashedPassword }
うまく動いているようです。
おわりに
ということでソルトとストレッチングで安全にパスワードを保存しましょう。 プラグインのリファレンスは以下を詳しく見ておくとよいです。
なお、この記事の内容は自己責任でご利用ください。ではでは。
この記事はGrails2.2.1、Spring Security Core Plugin1.2.7.3をもとに記述しています。
G*ワークショップZ May 2013 - Spockハンズオン
でしゃべってきました。
http://jggug.doorkeeper.jp/events/3872
さっそく@orangecloverさんがまとめてくれています。ありがとうございます!
http://d.hatena.ne.jp/orangeclover/20130518/1368845593
当日の資料はGitHubにおいてあります。
モックの説明以降はずいぶん駆け足になって申し訳なかったのですが、それを見越してドキュメントと学習テストを充実させておいたので、それで勘弁して下さいw
ドキュメントは先程のGitHubのdocs
ディレクトリに、学習テストはsrc/test/groovy
ディレクトリにおいてあります。参加しなかった人でもなんとなく眺めればSpockがわかったような気になれる教材に仕上がっています。
また、当日@uehajから質問されてた「oldはどのように値を保持しているのか」という質問に対してコピーして持ってるのではないかと適当なことを言ってしまいましたが、裏で@kiy0takaさんが調べてくれていました。
oldは先に評価して結果を保持してるっぽい。#jggug gist.github.com/kiy0taka/55983…
— Kiyotaka Okuさん (@kiy0taka) 2013年5月17日
ということでAST変換で先に評価して値を保持しているそうです。
ではでは、新運営委員長の@y_u、grails連携説明している暇ないからLTよろしくと無茶ぶりした@gantawitter、参加者のみなさんおつかれさまでした!
Struts1ユーザにGrailsをオススメする6の理由 - The search is over -
Struts1のEOLがアナウンスされました。 最後のリリースから長く時間が経過しており、実質開発は終了している状態でしたが、このタイミングでのアナウンスとなりました。
アナウンスの中では、次の乗り換え先として、Struts2・Spring Web MVC・Grails・Stripesといったフレームワークがお薦めされていますが、私はダンチな生産性を提供するGrailsをお勧めします。
私のフレームワーク遍歴は、Struts1、Wicket、Seasar2、Springと色々渡り歩いてきましたが、ここ1年はがっつり業務でGrailsを使用しています。 1年がっつり使ってみた経験から、Struts1ユーザにGrailsをお勧めする理由をいくつか考えてみました。
Grailsは既存Javaフレームワークの延長線上にある
Grailsを支える基盤は、Spring・Hibernateといった、Javaの世界でよく知られ、また広く使われているフレームワークです。 ビューはSpring MVCをベースとしており、モデルはHibernateをベースとしています。 このため、既存のJavaシステムで、これらフレームワークの知識を有している場合は、そのノウハウを思う存分活用できます。
Struts1ユーザからみてGrailsをお勧めする理由の1つは、Grailsのコントローラがリクエスト駆動ベースであることです。 WicketやJSFなどのように、コンポーネントベースではありません。 このため、Struts1でActionクラスを実装してしてきたユーザにとって、GrailsのControllerは非常に馴染みやすいはずです。
さらに、Java EE互換である点も重要です。
Grailsでは、単にgrails war
とコマンドを実行するだけでwarファイルを生成できます。
このwarファイルは、Tomcatなど今までStrutsアプリケーションをデプロイしてきたアプリケーションサーバにデプロイ可能です。
既存のアプリケーションサーバが使用できるため、今までの運用ノウハウも引き続き活用できます。
GroovyはJava開発者のための言語である
Ruby、Pythonといった言語に比べて、Javaコードの冗長さは、よく批判の対象になります。 Groovyの目標は、常に、このJavaの定型的な冗長さを排除し、Java開発者に異次元の生産性を提供することです。
GroovyはJava言語の延長にある、Java開発者のための言語です。 Java開発者のための言語である証拠に、一部の例外を除き、Javaのコードは、Groovyのコードとして実行可能です。 これは、Groovyのシンタックスは、Javaのシンタックスと互換があるということです。 そのため、Javaプログラマであれば、今すぐにでもGroovyのコードが書き始められます! 世界中の多くのJavaエンジニアは、潜在的なGroovyエンジニアといっても過言ではないでしょう。 若干言い過ぎたかも知れませんが、Javaエンジニアにとって非常に学びやすい言語であることは間違いありません。
もちろん、Javaコードとの親和性だけでなく、GroovyにはJava開発者に異次元の生産性を提供する多くの機能が含まれています。 型宣言の省略、クロージャ、便利なコレクション操作、メタプログラミング、演算子のオーバーロードなど、Rubyや他言語で羨ましかった機能、またはそれ以上の機能がGroovyで使用できます。
Grailsは、このGroovyでコードを書きます。 Struts1ユーザにとっては、このGroovyが逆に障壁になるかと思います。 いくらJavaとの親和性が高いとはいえ、言語が変わるというのは大きな話です。 しかし、新たなフォースを手に入れるには常に痛みが付き物です。 Java開発者にとって、比較的少ない痛みで、異次元の生産性が手に入るのはGroovy以外にありません。
既存のJava資産をそのまま活用できる
GroovyとJavaの親和性の高さも、既存のJava開発者にとって大きな魅力の1つです。 Groovyからは、既存のJavaのライブラリ・フレームワークといった、Javaのコードを簡単に呼び出すことができます。 これは、Javaのコード内でJavaのコードを呼ぶように、Groovyのコード内でJavaコードを呼び出せます。
また、GrailsでもJavaライブラリや、Javaのコードと連携する仕組みが予め用意されています。
jar形式のJavaライブラリは、単にlib
ディレクトリにファイルを置くだけで、簡単にアプリケーションの依存関係として追加できます。
Javaコードには、デフォルトでsrc/java
というディレクトリがGrailsによって用意されており、このディレクトリでJavaコードをすぐに書き始めることができます。
もし、Strutsアプリケーションで使用してた、ライブラリや、ビジネスロジックなどの資産がある場合は、 Grails上でもその資産を活用できます。
フレームワークの連携で悩むことはありません
Struts1を単独で使用していたユーザもいるかも知れませんが、SpringやHibernateといった、他のフレームワークと組み合わせて使用していたユーザも多くいると思います。 これらフレームワークを組み合わせて使用する場合は、自分で連携の設定をしなければなりません。 この連携の設定を調べるために、インターネットを彷徨い、気がつくと1日2日経過していた、という話は珍しくありません。 一度連携できたら、使い回すだけでしょ?と思いがちですが、フレームワークのバージョンに伴い設定方法が変更になり、また1日2日インターネットにダイブする羽目になります。
たかが数日と思うかもしれませんが、迅速な開発が求められる昨今においては、これは非常に足かせになります。ぐぐってる暇なんてありません。
Grailsはフルスタックフレームワークです。create-app
とコマンドを打てば、数秒で全てのフレームワーク連携が完了した雛形が手に入ります。
Struts1のように、フレームワークの連携で悩むことは、Grailsではありません。
進化し続けるフレームワーク
Struts1ユーザにとって、乗り換えたフレームワークが、数年で使い物にならなくなる自体はあまり嬉しくないでしょう。 そのため、現在Grailsの開発が活発に行われているか、今後もメンテンナスが続くかは関心の1つかと思います。
現在Grailsは、VMwareとEMCとの合併会社Pivotalの配下にある、SpringSourceで開発が行われています。 開発自体はオープンに行われており、ベンダー依存を気にする必要は今のところ無いかと思います。 ソースコードもGithubで公開されています。
開発は活発に行われており、世の中の動向に合わせて新しい機能が今なお追加されています。 ただ、新しい機能がどんどん追加されているため、正直枯れているとは言えない状況です。
Sturts1ユーザにとっては、安定性が心配になるかと思いますが、商用レベルでの使用に十分耐えるレベルではあるので、安心してください。
学習環境
Grailsのドキュメントは非常にしっかりと書かれており、日本語版も鋭意翻訳中です。
まとめ
GrailsはJava開発者のための、フレームワークです。 Java開発者が魅力を感じないとしたら、ほんと訴求先がないと言っていいかもしれません(言語、フレームワークは素晴らしいのは間違いないのですが、Java開発者以外には訴求力が弱い)。
ということでSturts1ユーザの皆さん、一緒にGrailsやりましょう!