ちょっとやりたいことがありまして、(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にデプロイすればなんとかなるはずですし。