2020年5月 4日 (月)

WicketとSpringを悪魔合体させる その2

0. ではがったいさせるぞ(通算二度目)

WicketとSpringを悪魔合体させるアプローチとして、前回の記事では

  1. WicketをベースにSpringのDIを個別に組み込む
  2. Spring BootをベースにフロントをWicketにする

のうち前者にチャレンジしてみたが、今回は後者をやってみる。

1. 準備

Spring Bootをベースにするので、まずは普通にSpring InitializrでSpring Bootなプロジェクトを作る。
この時のポイントは、テンプレートエンジンは入れないこと。Apache WicketはViewよりの機能を提供するので、画面組立はWicketにさせるためだ。

以下、pom.xmlのdependenciesを抜粋。

  <dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.module</groupId>
<artifactId>jackson-module-kotlin</artifactId>
</dependency>
<dependency>
<groupId>org.jetbrains.kotlin</groupId>
<artifactId>kotlin-reflect</artifactId>
</dependency>
<dependency>
<groupId>org.jetbrains.kotlin</groupId>
<artifactId>kotlin-stdlib-jdk8</artifactId>
</dependency>
<!-- Apache Wicket -->
<dependency>
<groupId>org.apache.wicket</groupId>
<artifactId>wicket-spring</artifactId>
<version>${wicket.version}</version>
</dependency>
<dependency>
<groupId>org.apache.wicket</groupId>
<artifactId>wicket-core</artifactId>
<version>${wicket.version}</version>
</dependency>
<dependency>
<groupId>org.apache.wicket</groupId>
<artifactId>wicket-ioc</artifactId>
<version>${wicket.version}</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
<exclusions>
<exclusion>
<groupId>org.junit.vintage</groupId>
<artifactId>junit-vintage-engine</artifactId>
</exclusion>
</exclusions>
</dependency>
</dependencies>

2. 普通のSpring Bootアプリケーションからいじるところ

まず、Spring BootにすることでKotlinのコードはsrc/main/kotlinに、ページに紐づくViewのHTML等それ以外のファイルはsrc/main/resourcesに、それぞれ配置されるようになるが、WicketのQuick Startで作ったプロジェクトではJavaのコードもページに紐づくHTMLも同じ場所に置かれるようになっている。

ここは趣味の問題と言えるけど、Wicketのしきたりに従うなら、pom.xmlのbuildセクションをいじってHTMLをsrc/main/kotlin以下に置けるようにするといい。

  <build>
<resources>
<resource>
<filtering>false</filtering>
<directory>src/main/resources</directory>
</resource>
<resource>
<filtering>false</filtering>
<directory>src/main/kotlin</directory>
<includes>
<include>**</include>
</includes>
<excludes>
<exclude>**/*.kt</exclude>
</excludes>
</resource>
</resources>
<testResources>
<testResource>
<filtering>false</filtering>
<directory>src/test/resources</directory>
</testResource>
<testResource>
<filtering>false</filtering>
<directory>src/test/kotlin</directory>
<includes>
<include>**</include>
</includes>
<excludes>
<exclude>**/*.kt</exclude>
</excludes>
</testResource>
</testResources>
<!-- 以下略 -->
</build>

またWicketには、クラシックなwarで使われるweb.xmlで設定している設定が必要になる。
Create a Wicket Quickstartで作ることができるmvn archetype:generateの結果には、以下のようなweb.xmlが含まれているはず。

<?xml version="1.0" encoding="ISO-8859-1"?>
<web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://www.oracle.com/webfolder/technetwork/jsc/xml/ns/javaee/web-app_3_1.xsd"
  version="3.1">

  <display-name>test-wicket-app1</display-name>

  <!--
    There are three means to configure Wickets configuration mode and they
    are tested in the order given.

    1) A system property: -Dwicket.configuration
    2) servlet specific <init-param>
    3) context specific <context-param>

    The value might be either "development" (reloading when templates change) or
    "deployment". If no configuration is found, "development" is the default. -->

  <filter>
    <filter-name>wicket.test-wicket-app1</filter-name>
    <filter-class>org.apache.wicket.protocol.http.WicketFilter</filter-class>
    <init-param>
      <param-name>applicationClassName</param-name>
      <param-value>net.formula97.webapps.WicketbootlinApplication</param-value>
    </init-param>
  </filter>

  <filter-mapping>
    <filter-name>wicket.test-wicket-app1</filter-name>
    <url-pattern>/*</url-pattern>
  </filter-mapping>
</web-app>

Spring BootにすることでJava Configで書くことができるようになるので、これをもとにServletContextInitializerの実装クラスをConfigurationにして書いてやる。

  @Configuration
  class ServletInitializer: ServletContextInitializer {
    override fun onStartup(servletContext: ServletContext) {
      // web.xml の設定をもとに書く
      // WicketFilter は Apache Wicket の ServletFilter
      val registration: FilterRegistration = servletContext.addFilter("wicket.wicketbootlin", WicketFilter::class.java)
      registration.setInitParameter(WicketFilter.APP_FACT_PARAM, SpringWebApplicationFactory::class.java.name)

      // メインクラスのFQCNをここで指定する
      registration.setInitParameter("applicationClassName", WicketbootlinApplication::class.java.name)
      // いわゆるサーブレットフィルタ部分
      registration.setInitParameter(WicketFilter.FILTER_MAPPING_PARAM, "/*")
      registration.addMappingForUrlPatterns(null, false, "/*")

      // 起動モード指定
      // spring.profiles.active の値に応じて development と deployment を切り替えるとかもアリだろう
      registration.setInitParameter("configuration", "development")
    }
  }

最後にエントリーポイントになるWebApplicationだが、Spring Bootのエントリーポイントは、作った直後ではこうなっているはず。

  @SpringBootApplication
class WicketbootlinApplication
companion object { @JvmStatic fun main(args: Array) { runApplication(*args) } }

なので、エントリーポイントのクラスをWebApplicationの継承クラスにして、getHomePage()とinit()を実装する。

  @SpringBootApplication
  class WicketbootlinApplication: WebApplication() {
    @Autowired
    private lateinit var applicationContext: ApplicationContext

    companion object {
      @JvmStatic
      fun main(args: Array) {
        runApplication(*args)
      }
    }

    override fun getHomePage(): Class {
      return HomePage::class.java
    }

    override fun init() {
      super.init()

      // レスポンスとマークアップ時のエンコーディングをUTF-8にする
      requestCycleSettings.responseRequestEncoding = CharEncoding.UTF_8
      markupSettings.defaultMarkupEncoding = CharEncoding.UTF_8

      // ComponentScanの結果を反映
      // これで Wicket から Spring による DIコンポーネントを使うことができるようになる
       componentInstantiationListeners.add(SpringComponentInjector(this, applicationContext))

      // todo ページルーティングを書く
      mountPage("/", HomePage::class.java)
    }
  }

これでフロントがWicketのSpring Bootアプリケーションになったので、

  @Service
  interface EnterpriseMessage {
    fun getMessage(): String
    fun getVersionCode(): String
  }

こんなServiceを

  class HomePage(params: PageParameters): WebPage(params) {
    // Page クラスは Spring の配下にあるわけではないので、
    // Autowired ではなく org.apache.wicket.spring.injection.annot.SpringBean を使う
    @SpringBean
    private lateinit var enterpriseMessage: EnterpriseMessage
init { // ページに値を張る add(Label("message", Model.of(enterpriseMessage.getMessage()))) add(Label("versionCode", Model.of(enterpriseMessage.getVersionCode()))) } }

とやることでPageクラスから利用できるようになる。
ページに値を張った結果はこんな感じ。
Injected

あとは、ビジネスロジックやデータアクセスに関する部分は、Springのもつ強力な機能を活用すればいいだろう。

2020年5月 3日 (日)

WicketとSpringを悪魔合体させる その1

0. ではがったいさせるぞ

データアクセスがからむユニットテストをやるには、やはりDIの力を借りるのが手っ取り早い。

CDIでもいいんだけれど、今回はSpringを使ってみることにする。
アプローチとしては

  1. WicketをベースにSpringのDIを個別に組み込む
  2. Spring BootをベースにフロントをWicketにする

の二つがあるが、今回は前者にチャレンジしてみる。

1. 準備

何はなくともpom.xmlの依存設定だ。wicket-springを依存に追加する。
そのほか、SpringのDIをつかうので、Spring Contextとjavax.annotation-apiも加える。

  <dependency>
    <groupId>org.apache.wicket</groupId>
    <artifactId>wicket-spring</artifactId>
    <version>${wicket.version}</version>
  </dependency>
  <dependency>
    <groupId>org.springframework</groupId>
    <artifactid>spring-context</artifactId>
    <version>5.1.10.RELEASE</version>
  </dependency>
  <dependency>
    <groupId>javax.annotation</groupId>
    <artifactid>javax.annotation-api</artifactId>
    <version>1.3.2</version>
  </dependency>

2. 各所の実装

話を簡単にするため、今回はView側にDIでねじ込まれた値を張るだけにしてみる。こんな感じ。

  • HomePage.html
  <!DOCTYPE html>
  <html xmlns:wicket="http://wicket.apache.org" lang="ja">
  <head>
      <meta charset="utf-8" />
      <title></title>
  </head>
  <body>
  <h1>
      <span wicket:id="message">hello</span>
  </h1>
  <p>
      java.version = <span wicket:id="versionCode">8</span> .
  </p>
  </body>
  </html>
  • HomePage.kt
  class HomePage(parameters: PageParameters): WebPage(parameters) {
    // Autowired ではなく org.apache.wicket.spring.injection.annot.SpringBean を使う
    @SpringBean
    private lateinit var enterpriseMessage: EnterpriseMessage

    init {
      add(Label("message", Model.of(enterpriseMessage.message)))
      add(Label("versionCode", Model.of(enterpriseMessage.versionCode)))
    }
  }

エントリーポイントになるWebApplicationの継承クラスは、DIしたいパッケージにコンポーネントスキャンをかけてSpringの管理下に置く処理を書く。

  class WicketApplication(): WebApplication {
    override fun init() {
      super.init()

      val ctx = AnnotationConfigApplicationContext()
      // コンポーネントスキャン対象を指定
      ctx.scan("net.formula97.webapps.beans")
      ctx.refresh()
      // Wicket-Springの機能でSpringの管理下に置く
      getComponentInstantiationListeners().add(SpringComponentInjector(this, ctx))
    }
  }

Spring管理下に置かれたクラスは、今回は定数クラスっぽい扱いにしてみた。

  @ManagedBean
  class EnterpriseMessage {
    val message: String = "Welcome to the Spring-Integrated world!"
    val versionCode: String = System.getProperty("java.version")
  }

こうしてやることで、マネージドビーンをnewすることなく(そもそもKotlinだとインスタンスを作るのにnewというキーワードは使わないのだが)使うことができるようになる。

2019年9月25日 (水)

Apache Wicketを使ってみる[5] -テスト編-

0. Wicketのテストサポート

WicketTester というヘルパークラスがcoreパッケージに含まれていて、Servletコンテナを介さずとも JUnit 4 でユニットテストを簡単に実行できるようになっている。

1. 簡単な使用例を実際に動かしてみる

前述の WicketTester の先頭にある使用例を Kotlin でかいてみた。
シナリオとしては以下のとおり。

  1. MyPage と YourPage のふたつのページがある
  2. MyPage のリンクをクリックすると YourPage へ遷移する
  3. YourPage はコンストラクタ引数のパラメータをラベルに反映する

プロダクションコード

  • MyPage
class MyPage() : WebPage() {
  override fun onInitialize() {
    super.onInitialize()
    add(Label("myMessage", "Hello!"))
    // クリックすると「YourPage」へ遷移するリンク
    add(object : Link("toYourPage") {
      override fun onClick() {
        setResponsePage(YourPage("Hi!"))
      }
    })
  }
}
  • YouePage
class YourPage(private val message: String) : WebPage() {
  override fun onInitialize() {
    super.onInitialize()
    add(Label("yourMessage", message))
    info("Wicket Rocks ;-)")
  }
}

テストコード

  • MyPageTest
class MyPageTest {
  /** WicketTester のインスタンス */
  private var tester: WicketTester? = null

  @Before
  fun setUp() {
    tester = WicketTester(WicketApplication())
  }

  @After
  fun tearDown() { }

  /**
   * MyPageが表示可能なことを確認するテスト。
   */
  @Test
  fun canRenderMyPage() {
    // MyPageを開始
    tester!!.startPage(MyPage::class.java)

    // MyPageが表示可能なことを確認
    tester!!.assertRenderedPage(MyPage::class.java)

    // MyPageに「Hello!」と表示されていることを確認
    tester!!.assertLabel("myMessage", "Hello!")
  }

  /**
   * MyPageのリンクをクリックするとYourPageへ遷移できることを確認するテスト。
   */
  @Test
  fun canFlipToYourPage() {
    tester!!.startPage(MyPage::class.java)
    // 「toYourPage」リンクをクリックするとYourPageへ遷移することを確認
    tester!!.clickLink("toYourPage")
    tester!!.assertRenderedPage(YourPage::class.java)

    // YourPageのメッセージが「Hi!」であることを確認
    tester!!.assertLabel("yourMessage", "Hi!")
  }
}
  • YourPageTest
class YourPageTest {

  /** WicketTester のインスタンス */
  private var tester: WicketTester? = null

  @Before
  fun setUp() {
    tester = WicketTester(WicketApplication())
  }

  @After
  fun tearDown() { }

  /**
   * コンストラクタ引数を画面反映できることを確認するテスト。
   */
  @Test
  fun canShowConstructorOperand() {
    // コンストラクタ引数に「Unit Testing...」を指定するして起動できることを確認
    tester!!.startPage(YourPage("Unit Testing..."))
    tester!!.assertRenderedPage(YourPage::class.java)

    // コンストラクタ引数に指定した文字列がラベルに反映されていることを確認
    tester!!.assertLabel("yourMessage","Unit Testing...")

    // コンソールに INFO レベルのメッセージが書かれていることを確認
    tester!!.assertInfoMessages("Wicket Rocks ;-)")
  }
}

そのほか、UI部品の有効、無効なども同様の方法でテストできる。

2. Formのテスト

前述の WicketTester から FormTester という、サブミットを含めたFormの操作とテストを行うヘルパークラスを得ることができる。

  /**
   * 正しいユーザー名とパスワードならログオンできることを確認するテスト。
   */
  @Test
  fun canLonginCorrectUserAndPW() {
    // 「form」という名で定義されているFormを、空文字で埋めないよう取得
    val formTester = tester!!.newFormTester("form", false)
    // ユーザー名とパスワードに「user」をセットしてサブミット
    formTester.setValue("username", "user")
    formTester.setValue("password", "user")
    formTester.submit()

    // コンソールにログイン成功が出力されていることを確認
    tester!!.assertInfoMessages("Username and password are correct!")
  }

Form のサブミットには、ほぼ間違いなくその後ろに DB アクセスが発生するだろうけれど、これを JUnit でテストしようと思ったら 既にあるデータを消さないとテスト結果がそれに依存してしまうし、 DB への書き込みをすべてロールバックしないと、ほかの開発データも書きつぶしてしまうことになる。

こういうとき、 DI で DB アクセス部分を分離してやればテストしやすい構造になるんだろうけれど、それは今度試してみよう....。

2019年9月16日 (月)

Apache Wicketを使ってみる[4] -認証/認可編-

0. Wicketで認証/認可を扱うには

wicket-auth-roles というdependencyが必要。

<dependency>
  <groupId>org.apache.wicket</groupId>
  <artifactId>wicket-auth-roles</artifactId>
  <version>8.5.0</version>
</dependency>

1. Wicketでの認証

通常の WebApplication に認証情報を処理できるような拡張が施された AuthenticatedWebApplication と、認証処理と認証情報のセッションストアへの格納を行う AuthenticatedWebSession で構成される。

二つとも abstract なので、使うには継承したうえで実装しなければならない処理がある。

AuthenticatedWebApplication AuthenticatedWebSession
  • protected Class<? extends WebPage> getSignInPageClass()
    サインインページを構成するクラスを返す
  • protected boolean authenticate(String username, String password)
    ユーザー名とパスワードから認証を行う
  • protected Class<? extends AbstractAuthenticatedWebSession> getWebSessionClass()
    アプリケーションで使うセッションストアのクラスを返す

上記のユーザーガイドからの抜粋をKotlinで書いてみると、こんな感じになる。

  • セッションストアクラス
class BasicAuthenticationSession(request: Request) : AuthenticatedWebSession(request) {
  override fun authenticate(username: String, password: String) : Boolean {
    // ここで認証処理を行う
    // 通常であれば、DBアクセスしてユーザーの有無やパスワード比較とかをする、はず
    return username == password && username == "wicketer"
  }

  override fun getRoles() : Roles {
    // ロールベースのアクセス制御用に、ログイン中ユーザーのロールを返す
    // 通常であれば、セッションストアのユーザー情報からロールを引いたりする、はず
    return Roles()
  }
}
  • アプリケーションクラス
class WicketApplication() : AuthenticatedWebApplication() {
  override fun getHomePage() : Class<out WebPage> {
    // いわゆる「スタートページ」を構成するクラスを返す
    return HomePage::class.java
  }
  override fun getWebSessionClass() : Class<out AbstractAuthenticatedWebSession> {
    // アプリケーションでつかうセッションストアを返す
    return BasicAuthenticationSession::class.java
  }
  override fun getSignInPageClass() : Class<out WebPage> {
    // いわゆる「ログインページ」を構成するクラスを返す
    return SignInPage::class.java
  }
}
  • サインインページ
class SignInPage() : WebPage() {
  private var username: String = ""
  private var password: String = ""

  override fun onInitialize() {
    super.onInitialize()

    // ログインフォームを構成するStatelessForm
    val form: StatelessForm<Unit> = object : StatelessForm<Unit>("form") {
      override fun onSubmit() {
        if (username.length == 0) {
          return
        }

        // 認証処理を呼び出し、認証成功のときは前ページで設定されたリダイレクト先へ遷移する
        val authResult = AuthenticatedWebSession.get().signIn(username, password)
        if (authResult) {
          // ここではそのままリダイレクトしているが、ここでユーザーロールのチェックとかをはさむ、はず
          continueToOriginalDestination()
        }
      }
    }

    // ModelとTextFieldをformに追加する
    form.model = CompoundPropertyModel(this)
    form.add(TextField<String>("username"))
    form.add(PasswordTextField("password"))

    add.(form)
  }
}

サインインページのデフォルト実装はあるけれど、同じくデフォルト実装のパネルと組み合わせて使う想定のようで、自力で書くのと大差ないかも....

2. 途中で認証をはさみたいときは

「トップページとかは普通に見れるようにするが、あるページは会員だけに見せたい」といった場合、途中でユーザー認証を行い、認証に成功したらユーザーがもといたページに戻す、といった処理をやることになる。

そういった場合は、以下の二つの方法をとる、とのこと。[出典]

いずれも、実行時に遷移前の状態をセッションストアに保存したあと、認証に成功した場合認証前の状態に復元してくれるが、認証ページへの遷移直前に RestartResponseAtInterceptPageException が内部的にスローされてページ遷移するので、以後の処理は行われなくなる。

3. Wicketでの認可

いわゆる処理に対する権限チェックである。先ほどの例で出てきた AuthenticatedWebSession#getRoles() も、認可に関する処理の一つ。
「ログインしているか否か」も、認可の範囲に入る模様。

基本処理は IAuthorizationStrategy で規定されている

boolean isActionAuthorized(Component, Action) アクションが許可されているかどうかを返す。
boolean isInstantiationAuthorized(Class<T extends IRequestableComponent>) 指定されたコンポーネントをインスタンス化できるかどうかを返す。
boolean isResourceAuthorized(IResource, PageParameters) リクエストのパラメータが、リソースへのアクセスが許可されているかどうかを返す。

だが、すべてを許可するALLOW_ALLという実装のほか、ページ単位のチェックを行う規定処理がすでに組み込まれている。個人的には、一番しっくりきたのがロールベース戦略として紹介されている MetaDataRoleAuthorizationStrategyAnnotationsRoleAuthorizationStrategy だった。
それぞれ、用途的には以下のような違いがある。

MetaDataRoleAuthorizationStrategy アプリケーション起動時に、あらかじめ使用可能な権限をページごとに列挙しておく
AnnotationsRoleAuthorizationStrategy ページクラスに対し、アノテーションで権限を宣言していく

両者とも一長一短ではあるけど、たとえば、 AdminOperatablePage というページに「サインインしていたら表示可能、スーパーユーザーなら中のFormを操作可能」という権限を設定する場合、

  • MetaDataRoleAuthorizationStrategy のばあい
class WicketApplication() : AuthenticatedWebApplication() {
  override fun init() {
    securitySettings.authorizationStrategy = MetaDataRoleAuthorizationStrategy(this)

    // ログインしていたら "AdminOperatablePage" はインスタンス化を許可
    MetaDataRoleAuthorizationStrategy.authorize(AdminOperatablePage::class.java, "SIGNED_IN")

    // adminForm というコンポーネントは、"ADMIN"なら操作可能にする
    // "ADMIN"でないときはロックされる
    MetaDataRoleAuthorizationStrategy.authorize(adminForm, Action.ENABLE, "ADMIN")
  }
}
  • AnnotationsRoleAuthorizationStrategy のばあい
@AuthorizeInstantiation("SIGNED_IN")
@AuthorizeAction(action = "ENABLE", roles = {"ADMIN"})
class AdminOperatablePage(params: PageParameters) : WebPage(params) {
  // @AuthorizeInstantiation で "SIGNED_IN" を持つ場合にインスタンス化を許可
  // ここの @AuthorizeAction は、ページ全体に効果がある
  ...
}

....といった感じになるはず。

なお、権限は AuthenticatedWebSession#getRoles() で取得されるので、先ほどのセッションストアクラスを書き直してみると、

class BasicAuthenticationSession(request: Request) : AuthenticatedWebSession(request) {

  override fun getRoles() : Roles {
    // ロールは中身がHashSet<String>なので、条件に応じたロール識別文字列をaddしていく
    // @see https://ci.apache.org/projects/wicket/apidocs/8.x/org/apache/wicket/authroles/authorization/strategies/role/Roles.html
    val resultRoles = Roles()

    if (isSignedIn()) {
      // たとえば、サインインしていたら"SIGNED_IN"をおく、とか
      resultRoles.add("SIGNED_IN")
    }
    if (username == "superuser") {
      // スーパーユーザーだったら"ADMIN"を置く、とか
      resultRoles.add("ADMIN")
    }
    // このほか、ユーザーのオブジェクトを保管しているなら、そのオブジェクトにぶら下がっている
    // ロールをaddしていくことになる、はず

    return resultRoles
  }
}

...といった具合になるはず。

4. 認可に失敗したときは

認可に失敗したとき、デフォルトでは

  • ユーザーがサインインしていないときは、既定のサインインページへ遷移させる
  • サインインしているがコンポーネントが認可できない場合は、 UnauthorizedInstantiationException がスローされる

....という挙動だが、カスタム処理にしたい場合は IUnauthorizedComponentInstantiationListener というイベントリスナがあるので、これの中で処理する。

  securitySettings.unauthorizedComponentInstantiationListener = IUnauthorizedComponentInstantiationListener {
    // 認可失敗を処理する "AuthWarningPage" というページをレスポンスにする
    it.setResponsePage(AuthWarningPage::class.java)
  }

ロールの検証を追加するなら、こんな感じになるはず。

  securitySettings.unauthorizedComponentInstantiationListener = object : IRoleCheckingStrategy, IUnauthorizedComponentInstantiationListener {
    override fun onUnauthorizedInstantiation(component: Component?) {
      // 認可失敗を処理する "AuthWarningPage" というページをレスポンスにする
      component!!.setResponsePage(AuthWarningPage::class.java)
    }

    override fun hasAnyRole(roles: Roles): Boolean {
      // 指定ロールが見つかったらtrueを返す処理を書く
      var result = roles.hasRole("ADMIN")
      return result
    }
  }

まぁ、このあたりはデフォルト実装でもどうにかなるレベルなので、やるかどうかはケースバイケースか。

2019年9月14日 (土)

MavenのJettyにDataSourceを設定するのに苦労した話

0. やっぱりデータソースは外に出したいのです

前回のデータアクセスの話では、

val database: Sql2o = Sql2o("jdbc:mysql://localhost:3306/mydb", "mydbadmin", "mydbadmin")

と、ソースコードに直接書いていたけれど、実際の運用を考えるとDBの接続先設定とプロダクションコードは極力分離したいところ。

幸いにも、Mavenで作ったプロジェクトのアーキタイプにはJettyがdependencyに組み込まれていて、test側に開発サーバとしてJettyを起動するときの処理が最初から入っている。
ここに DataSource 設定を書いてやれば、理屈の上ではプロダクションコードからDBの接続先設定を分離できることになる。

で、前置きが長くなってしまったけど、Mavenで依存に入っているJettyにDataSourceを設定するのに結構苦労をしたので、その時の備忘録的なメモを書いてみる。

1. やること(というかやったこと)

やったことをざっくりまとめると、以下のとおり。

  1. pom.xmlに依存とプラグイン設定を追加
  2. Jettyの設定ファイル追加
  3. web.xmlにDataSourceルックアップ設定を追加
  4. DataSourceを使うようデータアクセス部分を変更

1. pom.xmlに依存とプラグイン設定を追加

なにはなくともdependencyに必要なライブラリを入れなければならない。
今回はコネクションプールライブラリを挟む方針で、高性能を謳っていることで話題のHikariCPを使ってみることにする。

<dependencies>
  <dependency>
    <groupId>mysql</groupId>
    <artifactId>mysql-connector-java</artifactId>
    <version>8.0.17</version>
    <!-- Jettyがtestスコープで動くのでそちらに合わせる -->
    <scope>test</scope>
  </dependency>
  <dependency>
    <groupId>com.zaxxer</groupId>
    <artifactId>HikariCP</artifactId>
    <version>3.3.1</version>
    <!-- Jettyがtestスコープで動くのでそちらに合わせる -->
    <scope>test</scope>
  </dependency>
</dependencies<

Jettyがtestスコープで動くので、コネクションプールとJDBCドライバもそれに合わせておく。

次に、JettyがHikariCPとconnector-jを扱う設定を読み込めるようにするのだけど、これはMavenプラグインの設定をいじる必要がある。
追加するとこんな感じになるはず。

<plugins>
  <plugin>
    <groupId>org.eclipse.jetty</groupId>
    <artifactId>jetty-maven-plugin</artifactId>
    <version>${jetty9.version}</version>
    <configuration>
      <-- 追加、testスコープのclasspathを優先する設定 -->
      <useTestScope>true</useTestScope>
      <-- 追加ここまで -->
      <systemProperties>
        <systemProperty>
          <name>maven.project.build.directory.test-classes</name>
          <value>${project.build.directory}/test-classes</value>
        </systemProperty>
      </systemProperties>
      <jettyXml>
        ${project.basedir}/src/test/jetty/jetty.xml,${project.basedir}/src/test/jetty/jetty-ssl.xml,${project.basedir}/src/test/jetty/jetty-http.xml,${project.basedir}/src/test/jetty/jetty-https.xml
      </jettyXml>
      <!-- 以下追加 -->
      <webApp>
        <!-- web.xml を読み込む先を指定する設定 -->
        <descriptor>${project.basedir}/src/main/webapp/WEB-INF/web.xml</descriptor>
        <!-- 「データソースとして定義する内容」を書いたファイルを読み込む設定 -->
        <jettyEnvXml>${project.basedir}/src/test/jetty/jetty-env.xml</jettyEnvXml>
      </webApp>
      <!-- 追加ここまで -->
    </configuration>
  </plugin>
</plugins>

2. Jettyの設定ファイル編集

前述のpom.xml

に追加したjetty-env.xmlを、src/test/jettyに作る。

設定例をググっていると、 Configure の class に org.eclipse.jetty.webapp.WebAppContext を使う例が結構ひっかっかると思うけど、これをやると ClassCastException で設定に失敗するので org.eclipse.jetty.maven.plugin.JettyWebAppContext に変更している。

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE Configure PUBLIC "-//Jetty//Configure//EN" "http://www.eclipse.org/jetty/configure_9_0.dtd">
<!-- Configure の class は、 Maven プラグインから動くのでこっちを使う -->
<Configure id="wac" class="org.eclipse.jetty.maven.plugin.JettyWebAppContext">
  <New id="myDS" class="org.eclipse.jetty.plus.jndi.Resource">
    <Arg><Ref refid="wac" /></Arg>
    <!-- ここが DataSource の名前になる -->
    <Arg>jdbc/datasource</Arg>
    <Arg>
      <New class="com.zaxxer.hikari.HikariDataSource">
        <Arg>
          <New class="com.zaxxer.hikari.HikariConfig">
            <!-- 以下 setter 部分の意味は JavaDoc とかを参照のこと -->
            <Set name="maximumPoolSize">20</Set>
            <Set name="driverClassName">com.mysql.cj.jdbc.Driver</Set>
            <Set name="jdbcUrl">jdbc:mysql://localhost:3306/mydb</Set>
            <Set name="username">mydbadmin</Set>
            <Set name="password">mydbadmin</Set>
          </New>
        </Arg>
      </New>
    </Arg>
  </New>
</Configure>

3. web.xmlにDataSourceルックアップ設定を追加

下回りは設定できたので、プロダクションコードに関連する箇所をいじって、 DataSource をルックアップできるようにする。

pom.xmlに追加したとおり、/src/main/webapp/WEB-INF/web.xmlに以下を追加。

<web-app xmlns="http://java.sun.com/xml/ns/javaee" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://www.oracle.com/webfolder/technetwork/jsc/xml/ns/javaee/web-app_3_1.xsd"
    version="3.1">

  <!-- "jdbc/datasource" をデータソースとして名付ける設定 -->
  <resource-ref>
    <res-ref-name>jdbc/datasource</res-ref-name>
    <res-type>javax.sql.DataSource</res-type>
    <res-auth>Container</res-auth>
  </resource-ref>

</web-app>

4. DataSourceを使うようデータアクセス部分を変更

ここまでくれば DataSource をルックアップできるようになるので、接続設定を DataSource を使うように変更する。
例によって Kotlin で書いている。

open class DatabaseHelper {
    // init ブロックで初期化されるので空でよい
    //val database: Sql2o = Sql2o("jdbc:mysql://localhost:3306/mydb", "mydbadmin", "mydbadmin")
    val database: Sql2o

    init {
        // InitialContext から "jdbc/datasource" をルックアップする
        val context = InitialContext()
        val datasource = context.lookup("java:comp/env/jdbc/datasource") as DataSource
        // ルックアップした DataSource オブジェクトで Sql2o を初期化
        database = Sql2o(datasource)
    }
}

この状態で mvn package を実行して war を作り、 Tomcat とかのアプリケーションコンテナにデプロイしてもちゃんと動く。ここまで来るのに長かった....。

2019年9月 2日 (月)

続:Apache Wicketを使ってみる -データアクセス編-

0. データアクセスをどうするか

Quick Start Wizardで作成したMavenプロジェクトには、当然のことながらデータアクセスに関するライブラリがないので、自力でどうにかするしかない。

とはいえ、Spring Bootのような至れり尽くせりな環境でもないけど、ローレベルなJDBCをスクラッチで書き起こす元気もないので、今回はsql2oというライブラリを使ってみることにする。

1. sql2oとは

sql2o本家、およびGitHubの記述をざっくりまとめると、こんな感じ。

位置づけ的にはDapper.NETRoom Persisitence Libraryに近いかも。

  • 小さく軽量なデータアクセス用ライブラリ
  • JDBCのResultSetをPOJOに変換する機能を提供するが、SQLを生成する機能はない
  • 他の著名なライブラリに比べ、高速に(モノによっては6,7倍高速)動作する

導入はMavenで。執筆時点の最新版は1.6.0だった。
今回はDBMSにMySQLを使ってみることにするので、Connector/Jも一緒に設定しておく。

<dependency>
  <groupId>org.sql2o</groupId>
  <artifactId>sql2o</artifactId>
  <version>1.6.0</version>
</dependency>
<dependency>
  <groupId>mysql</groupId>
  <artifactId>mysql-connector-java</artifactId>
  <version>8.0.17</version>
</dependency>

2. 検索系処理

データベースへの接続は、Sql2oのコンストラクタに接続文字列を渡すことで作ってくれる。
なお、あえてすべてKotlinで書いてみた。

val database: Sql2o = Sql2o("jdbc:mysql://localhost:3306/mydb", "mydbadmin", "mydbadmin")

オープン処理とかは、DAOの基底クラスにプロパティを書いておくと扱いやすいかも。

/** トランザクションなしのConnection */
val connection: Connection
    get() {
        return database.open()
    }
/** トランザクション開始ありのConnection */
val connectionWithTran: Connection
    get() {
database.beginTransaction() }

で、検索系はSQLをそのまま書き、それを Connection#createQuery(String) に渡して Query オブジェクトを作り、 Query#executeAndFetch(Class) を実行すると ResultSet を POJO へバインドしてくれる。

// SQLをそのまま書く。長いSQLの場合はヒアドキュメントを使うと楽かも
val sql = "select todo_id, todo_title, created_at, finished from todo"
// 接続をオープン
val conn = connection
try {
    // use 関数でクエリ実行(Javaでいうところのtry-with-resources)
    conn.use {
        // setAutoDeriveColumnNames はスネークケース→キャメルケースを自動変換する設定
        val query = conn.createQuery(sql).setAutoDeriveColumnNames(true)
        val ret: MutableList<ToDo> = query.executeAndFetch(ToDo::class.java)
    }
} catch (e: Exception) {
    // do something...
}

3. 更新系処理

更新系処理も、検索系処理同様 createQuery(String) でプリペアードステートメントを作り、 Connection#executeUpdate() で実行する。

val sql = "insert into todo (todo_title, created_at, finished) values (:todoTitle, :createdAt, :finished)"
// トランザクション開始ありのConnectionを取得
val tran = connectionWithTran
try {
    tran.use {
        val stmt = tran.createQuery(sql)
                // プリペアードステートメントへバインドするパラメータを追加していく
                .addParameter("todoTitle", todo.todoTitle)
                .addParameter("createdAt", todo.createdAt)
                .addParameter("finished", todo.finished)
        stmt.executeUpdate()
        // use 関数内で commit()を発行
        tran.commit()
    }
} catch (e: Exception) {
    // do something...
    // ロールバックは暗黙的に行われる
}

なお、sql2oには宣言的トランザクションはないので、トランザクション制御は自分でやらなければならない。

4. 最後に

自力でSQLのチューニングをしたほうが手っ取り早い場合なんかには、こういうライブラリのほうがいいかもしれない。

2023年12月
          1 2
3 4 5 6 7 8 9
10 11 12 13 14 15 16
17 18 19 20 21 22 23
24 25 26 27 28 29 30
31            

最近のトラックバック

無料ブログはココログ