CLOVER🍀

That was when it all began.

組み込みTomcat(on Spring Boot)でJNDIリソースを扱う

ちょっとやりたいことがありまして、(Spring Bootを使った)組み込みTomcatでJNDIリソースを定義する方法を調べてみました。結果としてやりたいことはできなかったのですが、せっかく調べたのでメモということで。

例題1、DataSourceを定義する

まずは容易に出てきそうなお題として、DataSourceをJNDIリソースとして定義するケースを。まあ、なんのことはなくて以下のリポジトリを参考にマネしてみただけです。

https://github.com/wilkinsona/spring-boot-sample-tomcat-jndi

ここでの組み込みTomcatは7だったので、Spring Bootが1.2.0となりTomcatが8となった今では利用するパッケージ名などがちょっと変わっていますが。

pomは、dependenciesのみ紹介します。

  <dependencies>
    <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-web</artifactId>        
    </dependency>
    <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-test</artifactId>
      <scope>test</scope>
    </dependency>

    <dependency>
      <groupId>org.scala-lang</groupId>
      <artifactId>scala-library</artifactId>
      <version>${scala.version}</version>
    </dependency>

    <dependency>
      <groupId>mysql</groupId>
      <artifactId>mysql-connector-java</artifactId>
      <version>5.1.34</version>
      <scope>runtime</scope>
    </dependency>

    <dependency>
      <groupId>org.apache.tomcat</groupId>
      <artifactId>tomcat-dbcp</artifactId>
      <version>8.0.15</version>
    </dependency>
  </dependencies>

Spring Bootは、「spring-boot-starter-web」のみ。データベースはMySQL、コネクションプールはTomcat DBCPです。通常、Spring Boot Dataを選ぶとTomcat JDBCコンフィギュレーションされるらしいですが、今回はこちらを追加。

DataSourceを利用するクラスとしては、RestControllerを用意。
src/main/scala/org/littlewings/springboot/jndi/JndiController.scala

package org.littlewings.springboot.jndi

import javax.naming.InitialContext
import javax.sql.DataSource

import org.springframework.beans.factory.annotation.Autowired
import org.springframework.web.bind.annotation.{ RequestMapping, RequestMethod, RestController }

@RestController
@RequestMapping(Array("jndi"))
class JndiController {
  @Autowired
  var dataSource: DataSource = _

  @RequestMapping(value = Array("inject"), method = Array(RequestMethod.GET))
  def inject: String = select(dataSource)

  @RequestMapping(value = Array("lookup"), method = Array(RequestMethod.GET))
  def lookup: String = {
    val context = new InitialContext
    try {
      val ds = context.lookup("java:comp/env/jdbc/MySqlDataSource").asInstanceOf[DataSource]
      select(ds)
    } finally {
      context.close()
    }
  }

  private def select(ds: DataSource): String = {
    val conn = ds.getConnection
    val ps = conn.prepareStatement("SELECT 'Hello MySQL!'")
    val rs = ps.executeQuery()
    try {
      rs.next()
      rs.getString(1)
    } finally {
      rs.close()
      ps.close()
      conn.close()
    }
  }
}

Autowiredでインジェクションする方と、JNDIルックアップする方の両方用意しています。

JNDIルックアップしようが、インジェクションされるのでは?というところと、varなところはご愛嬌。

続いて、エントリポイントとBean定義。
src/main/scala/org/littlewings/springboot/jndi/App.scala

package org.littlewings.springboot.jndi

import javax.sql.DataSource

import org.apache.catalina.Context
import org.apache.catalina.startup.Tomcat
import org.apache.tomcat.util.descriptor.web.ContextResource
import org.springframework.context.annotation.Bean
import org.springframework.boot.SpringApplication
import org.springframework.boot.autoconfigure.SpringBootApplication
import org.springframework.boot.context.embedded.ServletContextInitializer
import org.springframework.boot.context.embedded.tomcat.{ TomcatEmbeddedServletContainer, TomcatEmbeddedServletContainerFactory }
import org.springframework.jndi.JndiObjectFactoryBean

object App {
  def main(args: Array[String]): Unit = SpringApplication.run(classOf[App], args: _*)
}

@SpringBootApplication
class App {
  @Bean
  def tomcatFactory: TomcatEmbeddedServletContainerFactory =
    new TomcatEmbeddedServletContainerFactory {
      override protected def getTomcatEmbeddedServletContainer(tomcat: Tomcat): TomcatEmbeddedServletContainer = {
        tomcat.enableNaming()
        super.getTomcatEmbeddedServletContainer(tomcat)
      }

      override protected def postProcessContext(context: Context): Unit = {
        val resource = new ContextResource
        resource.setName("jdbc/MySqlDataSource")
        resource.setType(classOf[DataSource].getName)
        resource.setProperty("driverClassName", "com.mysql.jdbc.Driver")
        resource.setProperty("url", "jdbc:mysql://localhost:3306/practice?useUnicode=true&characterEncoding=utf-8&characterSetResults=utf-8&useServerPrepStmts=true&useLocalSessionState=true&elideSetAutoCommits=true&alwaysSendSetIsolation=false")
        resource.setProperty("username", "kazuhira")
        resource.setProperty("password", "password")
        context.getNamingResources.addResource(resource)
      }
    }

  @Bean
  def dataSource: DataSource = {
    val bean = new JndiObjectFactoryBean
    bean.setJndiName("java:comp/env/jdbc/MySqlDataSource")
    bean.setProxyInterface(classOf[DataSource])
    bean.setLookupOnStartup(false)
    bean.afterPropertiesSet()
    bean.getObject.asInstanceOf[DataSource]
  }
}

ポイントはいくつかあって、TomcatEmbeddedServletContainerFactoryクラスのサブクラスを作って、Tomcatの設定を行うこと。

    new TomcatEmbeddedServletContainerFactory {

getTomcatEmbeddedServletContainerメソッドをオーバーライドして、ここでTomcat#enableNamingを呼び出しておくこと。

      override protected def getTomcatEmbeddedServletContainer(tomcat: Tomcat): TomcatEmbeddedServletContainer = {
        tomcat.enableNaming()
        super.getTomcatEmbeddedServletContainer(tomcat)
      }

これをしないと、組み込みTomcatではJNDIが使えないようです。

そして、ContextResourceを使用して、DataSourceの定義を行います。

      override protected def postProcessContext(context: Context): Unit = {
        val resource = new ContextResource
        resource.setName("jdbc/MySqlDataSource")
        resource.setType(classOf[DataSource].getName)
        resource.setProperty("driverClassName", "com.mysql.jdbc.Driver")
        resource.setProperty("url", "jdbc:mysql://localhost:3306/practice?useUnicode=true&characterEncoding=utf-8&characterSetResults=utf-8&useServerPrepStmts=true&useLocalSessionState=true&elideSetAutoCommits=true&alwaysSendSetIsolation=false")
        resource.setProperty("username", "kazuhira")
        resource.setProperty("password", "password")
        context.getNamingResources.addResource(resource)
      }

少なくとも、ここまででInitialContextを使用したJNDIルックアップはできるようになります。

Autowiredでインジェクションしたい場合は、以下も定義します。

  @Bean
  def dataSource: DataSource = {
    val bean = new JndiObjectFactoryBean
    bean.setJndiName("java:comp/env/jdbc/MySqlDataSource")
    bean.setProxyInterface(classOf[DataSource])
    bean.setLookupOnStartup(false)
    bean.afterPropertiesSet()
    bean.getObject.asInstanceOf[DataSource]
  }

JndiObjectFactoryBean#setProxyInterfaceの指定と、JndiObjectFactoryBean#setLookupOnStartupをfalseに設定しているところがポイントで、これで先ほど定義したJNDIリソースを参照するようになります。

動作確認。

$ mvn spring-boot:run
## JNDIルックアップ
$ curl http://localhost:8080/jndi/lookup
Hello MySQL!

## Autowired
$ curl http://localhost:8080/jndi/inject
Hello MySQL!

OKそうですね。

例題2、ObjectFactoryを作成して定義する

続いて、Tomcat DBCPのようにそれなりにサポートのあるものではなく、任意のJNDIリソースを定義するためにObjectFactoryを使用する例を載せたいと思います。

今回、JNDIリソースとするものは、InfinispanのEmbeddedCacheManagerとします。

pomのdependencies。

  <dependencies>
    <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-web</artifactId>        
    </dependency>
    <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-test</artifactId>
      <scope>test</scope>
    </dependency>

    <dependency>
      <groupId>org.scala-lang</groupId>
      <artifactId>scala-library</artifactId>
      <version>${scala.version}</version>
    </dependency>

    <dependency>
      <groupId>org.infinispan</groupId>
      <artifactId>infinispan-core</artifactId>
      <version>7.0.2.Final</version>
    </dependency>
    <dependency>
      <groupId>net.jcip</groupId>
      <artifactId>jcip-annotations</artifactId>
      <version>1.0</version>
      <scope>provided</scope>
    </dependency>
  </dependencies>

JDBCまわりは削除して、Infinispan関係を追加しました。

動作確認用のRestController。今回もInitialContextからルックアップするものと、Autowiredしてインジェクションする例にしています。
src/main/scala/org/littlewings/springboot/jndi/JndiController.scala

package org.littlewings.springboot.jndi

import javax.naming.InitialContext

import org.infinispan.manager.EmbeddedCacheManager
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.web.bind.annotation.{ RequestMapping, RequestMethod, RestController }

@RestController
@RequestMapping(Array("jndi"))
class JndiController {
  @Autowired
  var cacheManager: EmbeddedCacheManager = _

  @RequestMapping(value = Array("inject"), method = Array(RequestMethod.GET))
  def inject: String = "Count: " + count(cacheManager)

  @RequestMapping(value = Array("lookup"), method = Array(RequestMethod.GET))
  def lookup: String = {
    val context = new InitialContext
    try {
      val manager = context.lookup("java:comp/env/infinispan/CacheContainer").asInstanceOf[EmbeddedCacheManager]
      "Count: " + count(manager)
    } finally {
      context.close()
    }
  }

  private def count(manager: EmbeddedCacheManager): Integer = {
    val cache = manager.getCache[String, Integer]("countingCache")
    cache.get("counter") match {
      case null =>
        val c: Integer = 0
        cache.put("counter", c)
        c
      case n =>
        val c: Integer = n + 1
        cache.put("counter", c)
        c
    }
  }
}

そして、InfinispanのEmbeddedCacheManagerを作成するための、ObjectFactoryを作成します。
src/main/scala/org/littlewings/springboot/jndi/EmbeddedCacheManagerFactory.scala

package org.littlewings.springboot.jndi

import java.util.Hashtable

import javax.naming.{ Context, Name, Reference }
import javax.naming.spi.ObjectFactory

import org.infinispan.manager.{ DefaultCacheManager, EmbeddedCacheManager}

class EmbeddedCacheManagerFactory extends ObjectFactory {
  override def getObjectInstance(obj: AnyRef, name: Name, nameCtx: Context, environment: Hashtable[_, _]): AnyRef =
    obj match {
      case null => null
      case ref: Reference if ref.getClassName == classOf[EmbeddedCacheManager].getName =>
        val ra = ref.get("resourceFileName")
        val resourceFileName = ra.getContent.toString
        new DefaultCacheManager(resourceFileName)
      case _ => null
    }
}

パラメータ「resourceFileName」で設定ファイルが指定される、ということにしています。ちなみに、ObjectFactoryを実装したクラスは初めて作成しました。

ちなみに、設定ファイルはこちら。
src/main/resources/infinispan.xml

<?xml version="1.0" encoding="UTF-8"?>
<infinispan
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="urn:infinispan:config:7.0 http://www.infinispan.org/schemas/infinispan-config-7.0.xsd"
    xmlns="urn:infinispan:config:7.0">
  <jgroups>
    <stack-file name="udp" path="jgroups.xml" />
  </jgroups>

  <cache-container name="jndiCacheManager" shutdown-hook="REGISTER">
    <transport cluster="cluster" stack="udp" />
    <distributed-cache name="countingCache" />
  </cache-container>
</infinispan>

ムダにDistributed Cacheです。JGroupsの設定は端折ります。

最後に、エントリポイントとBean定義。
src/main/scala/org/littlewings/springboot/jndi/App.scala

package org.littlewings.springboot.jndi

import org.apache.catalina.Context
import org.apache.catalina.startup.Tomcat
import org.apache.tomcat.util.descriptor.web.ContextResource
import org.infinispan.manager.EmbeddedCacheManager
import org.springframework.context.annotation.Bean
import org.springframework.boot.SpringApplication
import org.springframework.boot.autoconfigure.SpringBootApplication
import org.springframework.boot.context.embedded.ServletContextInitializer
import org.springframework.boot.context.embedded.tomcat.{ TomcatEmbeddedServletContainer, TomcatEmbeddedServletContainerFactory }
import org.springframework.jndi.JndiObjectFactoryBean

object App {
  def main(args: Array[String]): Unit = SpringApplication.run(classOf[App], args: _*)
}

@SpringBootApplication
class App {
  @Bean
  def tomcatFactory: TomcatEmbeddedServletContainerFactory =
    new TomcatEmbeddedServletContainerFactory {
      override protected def getTomcatEmbeddedServletContainer(tomcat: Tomcat): TomcatEmbeddedServletContainer = {
        tomcat.enableNaming()
        super.getTomcatEmbeddedServletContainer(tomcat)
      }

      override protected def postProcessContext(context: Context): Unit = {
        val resource = new ContextResource
        resource.setName("infinispan/CacheContainer")
        resource.setType(classOf[EmbeddedCacheManager].getName)
        resource.setProperty("factory", classOf[EmbeddedCacheManagerFactory].getName)
        resource.setProperty("resourceFileName", "infinispan.xml")
        context.getNamingResources.addResource(resource)
      }
    }

  @Bean
  def cacheContainer: EmbeddedCacheManager = {
    val bean = new JndiObjectFactoryBean
    bean.setJndiName("java:comp/env/infinispan/CacheContainer")
    bean.setProxyInterface(classOf[EmbeddedCacheManager])
    bean.setLookupOnStartup(false)
    bean.afterPropertiesSet()
    bean.getObject.asInstanceOf[EmbeddedCacheManager]
  }
}

先ほどの例とだいたい同じなのですが、ContextResourceでリソース定義をする時に、「factory」で先ほど作成したObjectFactoryを指定してるところがポイントですかね。

      override protected def postProcessContext(context: Context): Unit = {
        val resource = new ContextResource
        resource.setName("infinispan/CacheContainer")
        resource.setType(classOf[EmbeddedCacheManager].getName)
        resource.setProperty("factory", classOf[EmbeddedCacheManagerFactory].getName)
        resource.setProperty("resourceFileName", "infinispan.xml")
        context.getNamingResources.addResource(resource)
      }

「resourceFileName」もここで指定しています。

それでは、動作確認。

$ mvn spring-boot:run

アクセスしてみます。

## Autowired
$ curl http://localhost:8080/jndi/inject
Count: 0

## Autowired
$ curl http://localhost:8080/jndi/inject
Count: 1

## JNDIルックアップ
$ curl http://localhost:8080/jndi/lookup
Count: 2

## JNDIルックアップ
$ curl http://localhost:8080/jndi/lookup
Count: 3

ちゃんと動いているようです。

また、裏で別のNodeも起動していないので、AutowiredとJNDIルックアップで同じEmbeddedCacheManagerを見ていることも確認できました。

2014-12-28 04:26:29.709  INFO 61787 --- [nio-8080-exec-1] o.i.r.t.jgroups.JGroupsTransport         : ISPN000094: Received new cluster view for channel cluster: [xxxxx-358|0] (1) [xxxxx-358]

OKそうです。

そもそもやりたかったこと

今回これを調べてみようとしたきっかけは、前回のエントリの応用編?としてやってみたかったからです。

Spring Boot×Hibernate Searchで、インデックスを複数Nodeで共有する
http://d.hatena.ne.jp/Kazuhira/20141223/1419347497

この時は、application.ymlにInfinispanの定義をこのようにしましたが

        search:
          default:
            directory_provider: infinispan
          infinispan.configuration_resourcename: infinispan.xml

これだとCacheManagerのライフサイクルがHibernate Searchに握られてしまいますし、Cache自体を使いたい場合はHibernate Searchと別に持つなりしなくてはいけないので、微妙だなぁと思い、こうできたらいいなぁと思ったのがきっかけでした。

        search:
          default:
            directory_provider: infinispan
          infinispan.cachemanager_jndiname: java:comp/env/infinispan/CacheContainer

なのですが、先ほどのコードでは普通にRestControllerからはJNDIルックアップ可能なものの、JPAが起動する時点ではまだContextResourceで定義したJNDIリソースが見えないようで、エラーになって起動しませんでした…。
*「java:comp」の時点でわからない、と言われます

で、今回のようにRestControllerから参照すると見えるので、起動と設定のタイミングなんだろうなぁと思ってここまでにしました。

その他、context.xmlを読ませてみたりContextResourceEnvRefを使ってみたりいろいろ試してはみましたけどね。

これでも、あまり慣れてないJNDIまわりのコードを読んだり調べたりして、なかなか勉強になりました。これはこれで、よしとしましょう。やりたかったら、WildFlyにデプロイすればなんとかなるはずですし。