InfinispanのGrid File Systemを使った、思い付きのネタです。
Webアプリケーションのテンプレートの配信に、InfinispanのGrid File Systemを使ってみようかなと思いまして。テンプレートエンジンには、Velocityを選択。
あ、そもそもInfinispanのGrid File Systemって、相変わらずexperimental APIですので。そのあたり、ご注意を。
21. Grid File System
http://infinispan.org/docs/6.0.x/user_guide/user_guide.html#_grid_file_system
構成
VelocityViewServletを使った、単純なServletアプリケーションを用意します。ただし、Velocityテンプレートを読み込むResourceLoaderは、InfinispanのGrid File Systemから読み込むような実装を定義します。
テンプレートの配信は、別途クライアント的な簡易プログラムを作成して行います。
つまり、配信側からテンプレートを登録すると、クラスタに参加しているWebアプリケーション側でVelocityのテンプレートが見える、もしくは修正したら反映される、そんな感じですね。
WebアプリケーションにはVelocityテンプレートを含めず、Infinispanが構成するクラスタ内にテンプレートが存在するところがポイントです。
用意
今回、配信側とWebアプリケーション側をsbtのプロジェクトとしては分割しました。いわゆる、マルチプロジェクトですね。実は、初めて定義します。
で、結果こんな感じになりました。
project/Build.scala
import sbt._ import sbt.Keys._ import com.earldouglas.xsbtwebplugin.WebPlugin object BuildSettings { val buildOrganization = "org.littlewings" val buildVersion = "0.0.1-SNAPSHOT" val buildScalaVersion = "2.11.1" val buildScalacOptions = Seq("-Xlint", "-deprecation", "-unchecked", "-feature") val buildSettings = Seq( organization := buildOrganization, version := buildVersion, scalaVersion := buildScalaVersion, scalacOptions ++= buildScalacOptions, incOptions := incOptions.value.withNameHashing(true) ) } object Dependencies { val infinispanVersion = "6.0.2.Final" val infinispan = "org.infinispan" % "infinispan-core" % infinispanVersion excludeAll( ExclusionRule(organization = "org.jgroups", name = "jgroups"), ExclusionRule(organization = "org.jboss.marshalling", name = "jboss-marshalling-river"), ExclusionRule(organization = "org.jboss.marshalling", name = "jboss-marshalling"), ExclusionRule(organization = "org.jboss.logging", name = "jboss-logging"), ExclusionRule(organization = "org.jboss.spec.javax.transaction", name = "jboss-transaction-api_1.1_spec") ) val infinispanAsProvided = "org.infinispan" % "infinispan-core" % infinispanVersion % "provided" excludeAll( ExclusionRule(organization = "org.jgroups", name = "jgroups"), ExclusionRule(organization = "org.jboss.marshalling", name = "jboss-marshalling-river"), ExclusionRule(organization = "org.jboss.marshalling", name = "jboss-marshalling"), ExclusionRule(organization = "org.jboss.logging", name = "jboss-logging"), ExclusionRule(organization = "org.jboss.spec.javax.transaction", name = "jboss-transaction-api_1.1_spec") ) val jgroups = "org.jgroups" % "jgroups" % "3.4.1.Final" val jbossTransactionApi = "org.jboss.spec.javax.transaction" % "jboss-transaction-api_1.1_spec" % "1.0.1.Final" val jbossMarshallingRiver = "org.jboss.marshalling" % "jboss-marshalling-river" % "1.4.4.Final" val jbossMarshalling = "org.jboss.marshalling" % "jboss-marshalling" % "1.4.4.Final" val jbossLogging = "org.jboss.logging" % "jboss-logging" % "3.1.2.GA" val jcipAnnotations = "net.jcip" % "jcip-annotations" % "1.0" val velocity = "org.apache.velocity" % "velocity" % "1.7" val velocityTools = "org.apache.velocity" % "velocity-tools" % "2.0" val javaeeWebApi = "javax" % "javaee-web-api" % "7.0" % "provided" val containerJettyVersion = "9.2.1.v20140609" val containerJettyWebapp = "org.eclipse.jetty" % "jetty-webapp" % containerJettyVersion % "container" val containerJettyPlus = "org.eclipse.jetty" % "jetty-plus" % containerJettyVersion % "container" } object InfinispanVelocityPractice extends Build { import BuildSettings._ import Dependencies._ val templatePublisherDeps = Seq( infinispan, jgroups, jbossTransactionApi, jbossMarshallingRiver, jbossMarshalling, jbossLogging, jcipAnnotations ) val webViewDeps = Seq( containerJettyWebapp, containerJettyPlus, javaeeWebApi, velocity, velocityTools, infinispanAsProvided ) ++ (Seq( jgroups, jbossTransactionApi, jbossMarshallingRiver, jbossMarshalling, jbossLogging, jcipAnnotations ) map ( _ % "provided")) lazy val root = Project("infinispan-velocity-practice", file("."), settings = buildSettings) .aggregate(templatePublisher, webView) lazy val templatePublisher = Project("template-publisher", file("template-publisher"), settings = buildSettings ++ Seq(fork in run := true) ++ Seq(libraryDependencies ++= templatePublisherDeps)) lazy val webView = Project("web-view", file("web-view"), settings = buildSettings ++ Seq( artifactName := { (version: ScalaVersion, module: ModuleID, artifact: Artifact) => "javaee7-web." + artifact.extension }) ++ WebPlugin.webSettings ++ Seq(libraryDependencies ++= webViewDeps)) }
ああ、長い長い(笑)。「template-publisher」が配信側、「web-view」がWebアプリケーション側です。
あと、Webアプリケーション側はxsbt-web-pluginも使用するので、プラグインの追加も行います。
project/plugins.sbt
addSbtPlugin("com.earldouglas" % "xsbt-web-plugin" % "0.9.0")
先のBuild.scalaでの「web-view」側に、しれっとxsbt-web-pluginの設定が追加されています。
Infinispanの設定
配信側とWebアプリケーション側で、同じ設定を持ちます。というわけで、同じ設定をそれぞれコピーして配置…。
template-publisher/src/main/resources/infinispan.xml
web-view/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:6.0 http://www.infinispan.org/schemas/infinispan-config-6.0.xsd" xmlns="urn:infinispan:config:6.0"> <global> <transport clusterName="VelocityTemplateCluster"> <properties> <property name="configurationFile" value="jgroups.xml" /> </properties> </transport> <globalJmxStatistics enabled="true" jmxDomain="org.infinispan" cacheManagerName="DefaultCacheManager" /> <shutdown hookBehavior="REGISTER"/> </global> <namedCache name="templateDataCache"> <clustering mode="dist"> <hash capacityFactor="${template.data.capacityFactor:0}" /> </clustering> </namedCache> <namedCache name="templateMetaDataCache"> <clustering mode="repl" /> </namedCache> </infinispan>
Grid File Systemは、データを管理する部分とメタデータを管理する部分に分かれますが、データをdist、メタデータをreplとして定義。また、データの方はcapacityFactorを使って、システムプロパティで設定しない限りは0のヘテロなクラスタにします。
まあ、配信側はデータを持たなくていいよね?ってことで…。
JGroupsの設定は、端折ります(GitHubにはアップしています)。
配信側
で、テンプレートを配信するプログラム。
template-publisher/src/main/scala/org/littlewings/infinispan/velocity/VelocityTemplatePublisher.scala
package org.littlewings.infinispan.velocity import java.nio.file.{Files, Paths} import org.infinispan.io.{GridFile, GridFilesystem} import org.infinispan.manager.DefaultCacheManager object VelocityTemplatePublisher { def main(args: Array[String]): Unit = { System.setProperty("java.net.preferIPv4Stack", "true") val (fromFilePath, toFilePath) = args.toList match { case f :: t :: Nil => (f, t) case _ => Console.err.println("Input: [fromFilePath] [toFilePath]") sys.exit(1) } publishGridFileSystem(fromFilePath, toFilePath) } private def publishGridFileSystem(fromFilePath: String, toFilePath: String): Unit = { val manager = new DefaultCacheManager("infinispan.xml") try { val templateDataCache = manager.getCache[String, Array[Byte]]("templateDataCache") val metaDataCache = manager.getCache[String, GridFile.Metadata]("templateMetaDataCache") val gridFileSystem = new GridFilesystem(templateDataCache, metaDataCache) val file = gridFileSystem.getFile(toFilePath) if (!file.exists() && file.getParentFile != null) { file.getParentFile.mkdirs() } val os = gridFileSystem.getOutput(file.asInstanceOf[GridFile]) try { for (b <- Files.readAllBytes(Paths.get(fromFilePath))) { os.write(b) } } finally { os.close() } } finally { manager.stop() } } }
これは、単純にローカルファイルの内容をGrid File Systemに放り込むだけです。起動引数で、読み込むファイルとGrid File Systemに登録する時のパスを指定します。
Webアプリケーション側
Webアプリケーション側は、今時ですが単純なServletプログラムで書きました。
で、最初はGrid File Systemというか、クラスタに参加する部分がネックになるので、とりあえずServletContextListener+Scalaのコンパニオンオブジェクトで。
web-view/src/main/scala/org/littlewings/infinispan/velocity/InfinispanGridFileSystemLocator.scala
package org.littlewings.infinispan.velocity import scala.collection.JavaConverters._ import javax.servlet.{ServletContextEvent, ServletContextListener} import org.infinispan.io.{GridFile, GridFilesystem} import org.infinispan.manager.{DefaultCacheManager, EmbeddedCacheManager} object InfinispanGridFileSystemLocator { private var manager: EmbeddedCacheManager = _ private var gridFileSystem: GridFilesystem = _ private def init(): Unit = { manager = new DefaultCacheManager("infinispan.xml") val templateDataCache = manager.getCache[String, Array[Byte]]("templateDataCache") val metaDataCache = manager.getCache[String, GridFile.Metadata]("templateMetaDataCache") gridFileSystem = new GridFilesystem(templateDataCache, metaDataCache) } def getGridFileSystem: GridFilesystem = gridFileSystem private def destroy(): Unit = { try { for (cacheName <- manager.getCacheNames.asScala) { manager.getCache(cacheName).stop() } } finally { manager.stop() } } } class InfinispanGridFileSystemLocator extends ServletContextListener { override def contextInitialized(sce: ServletContextEvent): Unit = InfinispanGridFileSystemLocator.init() override def contextDestroyed(sce: ServletContextEvent): Unit = InfinispanGridFileSystemLocator.destroy() }
これを使用する、VelocityのResourceLoaderの実装を作成します。
web-view/src/main/scala/org/littlewings/infinispan/velocity/InfinispanGridFileResourceLoader.scala
package org.littlewings.infinispan.velocity import java.io.InputStream import org.infinispan.io.GridFilesystem import org.apache.commons.collections.ExtendedProperties import org.apache.velocity.exception.ResourceNotFoundException import org.apache.velocity.runtime.resource.Resource import org.apache.velocity.runtime.resource.loader.ResourceLoader class InfinispanGridFileResourceLoader extends ResourceLoader { private def withGfs[A](fun: GridFilesystem => A): A = fun(InfinispanGridFileSystemLocator.getGridFileSystem) override def init(configuration: ExtendedProperties): Unit = { } override def getLastModified(resource: Resource): Long = withGfs { gfs => val file = gfs.getFile(resource.getName) if (file.exists) file.lastModified else 0 } override def isSourceModified(resource: Resource): Boolean = withGfs { gfs => val file = gfs.getFile(resource.getName) if (file.exists) file.lastModified != resource.getLastModified else true } override def resourceExists(name: String): Boolean = withGfs { gfs => gfs.getFile(name).exists } @throws(classOf[ResourceNotFoundException]) override def getResourceStream(templateName: String): InputStream = withGfs { gfs => val file = gfs.getFile(templateName) if (file.exists) gfs.getInput(templateName) else throw new ResourceNotFoundException(s"Resource[$templateName] Not Found in GridFileSystem") } }
で、Velocityにフォワードを行うServlet。
web-view/src/main/scala/org/littlewings/infinispan/velocity/TemplateViewServlet.scala
package org.littlewings.infinispan.velocity import javax.servlet.http.{HttpServlet, HttpServletRequest, HttpServletResponse} class TemplateViewServlet extends HttpServlet { override protected def doGet(req: HttpServletRequest, res: HttpServletResponse): Unit = { req.setCharacterEncoding("UTF-8") req.setAttribute("name", Option(req.getParameter("name")).getOrElse("かずひら")) req.setAttribute("word", Option(req.getParameter("word")).getOrElse("こんにちは!")) val templateFile = Option(req.getParameter("template")) .getOrElse("default") + ".vm" req .getRequestDispatcher(s"/WEB-INF/template/${templateFile}") .forward(req, res) } }
単純ですが、テンプレート名は外から渡せるように…。
あと、さっきからServlet 3.0より前っぽい書き方してますが、まあweb.xml書かざるをえないので、承知の上で…。
web.xmlは、こんな感じですね。
web-view/src/main/webapp/WEB-INF/web.xml
<?xml version="1.0" encoding="UTF-8"?> <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://xmlns.jcp.org/xml/ns/javaee/web-app_3_1.xsd" version="3.1"> <listener> <listener-class>org.littlewings.infinispan.velocity.InfinispanGridFileSystemLocator</listener-class> </listener> <servlet> <servlet-name>TemplateViewServlet</servlet-name> <servlet-class>org.littlewings.infinispan.velocity.TemplateViewServlet</servlet-class> </servlet> <servlet> <servlet-name>VelocityViewServlet</servlet-name> <servlet-class>org.apache.velocity.tools.view.VelocityViewServlet</servlet-class> </servlet> <servlet-mapping> <servlet-name>TemplateViewServlet</servlet-name> <url-pattern>/view</url-pattern> </servlet-mapping> <servlet-mapping> <servlet-name>VelocityViewServlet</servlet-name> <url-pattern>*.vm</url-pattern> </servlet-mapping> </web-app>
Velocityの設定は、Velocity Viewのデフォルトの設定をちょっとコピーしてきて、ResourceLoaderを先ほど作成したInfinispanのGrid File Systemから取得するものに差し替えます。
web-viewwebapp/WEB-INF/velocity.properties
# default to servletlogger, which logs to the servlet engines log runtime.log.logsystem.class = org.apache.velocity.runtime.log.ServletLogChute,org.apache.velocity.tools.view.ServletLogChute input.encoding = UTF-8 output.encoding = UTF-8 # by default, load resources with webapp resource loader and string resource loader (in that order) resource.loader = infinispan infinispan.resource.loader.class = org.littlewings.infinispan.velocity.InfinispanGridFileResourceLoader # allows getting and setting $request, $session and $application attributes using Velocity syntax, # like in #set($session.foo = 'bar'), instead of $session.setAttribute('foo','bar') runtime.introspector.uberspect = org.apache.velocity.util.introspection.UberspectImpl,org.apache.velocity.tools.view.WebappUberspector
Infinispanはprovidedスコープにしたので、jboss-deployment-structure.xmlに利用する定義を行いました。
web-view/src/main/webapp/WEB-INF/jboss-deployment-structure.xml
<?xml version="1.0" encoding="UTF-8"?> <jboss-deployment-structure xmlns="urn:jboss:deployment-structure:1.2"> <deployment> <dependencies> <module name="org.infinispan" /> </dependencies> </deployment> </jboss-deployment-structure>
が、WildFly 8.1.0.Final(他のバージョンはちゃんと見てませんが)だと、「キミ、内部のモジュール使ってるね?」と怒られました…。
20:36:07,772 WARN [org.jboss.as.dependency.private] (MSC service thread 1-3) JBAS018567: Deployment "deployment.javaee7-web.war" is using a private module ("org.infinispan:main") which may be changed or removed in future versions without notice. 20:36:07,773 WARN [org.jboss.as.dependency.private] (MSC service thread 1-3) JBAS018567: Deployment "deployment.javaee7-web.war" is using a private module ("org.infinispan:main") which may be changed or removed in future versions without notice.
むー、Infinispanも、WARに含めた方がいいのかなぁ…?とりあえず、今回はWildFlyのモジュールを使うものとします。
デプロイ
で、Webアプリケーション側のWARファイルを作成して
> package [info] Updating {file:/xxxx/infinispan-velocity-practice/}web-view... [info] Resolving org.eclipse.jetty#jetty-jndi;9.2.1.v20140609 ... [info] Done updating. [info] Compiling 3 Scala sources to /xxxx/infinispan-velocity-practice/web-view/target/scala-2.11/classes... [info] Packaging /xxxx/infinispan-velocity-practice/web-view/target/scala-2.11/javaee7-web.war ... [info] Done packaging. [success] Total time: 7 s, completed 2014/06/15 21:16:00
WildFlyに放り込みます。
$ cp web-view/target/scala-2.11/javaee7-web.war /path/to/wildfly-8.1.0.Final/standalone/deployments/
WildFlyの起動は、システムプロパティでcapacityFactorを調整しておきます。
$ wildfly-8.1.0.Final/bin/standalone.sh -Dtemplate.data.capacityFactor=1.0
これをやらずに最初に配信側を起動すると、capacityFactorが0のNodeしかいないクラスタになってしまって、コケることになります。
では、curlで確認してみます。
$ curl http://localhost:8080/javaee7-web/view?template=hello <html><head><title>Error</title></head><body>/WEB-INF/template/hello.vm</body></html>
テンプレートはまだ配信していないので、当然Not Foundです。
テンプレートの配信
それでは、Velocityテンプレートを配信してみます。こんなテンプレートを用意。
template-publisher/template/hello.vm
名前: ${name} Word: ${word}
これを、配信側のsbtプロジェクトに移って
> project template-publisher [info] Set current project to template-publisher (in build file:/xxxxx/infinispan-velocity-practice/)
配信します。
> run template/hello.vm /WEB-INF/template/hello.vm
ちなみに、ここで苦労したのはWildFlyにデプロイしたWebアプリケーションが構成するクラスタに、最初入れなかったことですね。最終的に、配信側のプロジェクトをForkするように設定したら入れました。
settings = buildSettings ++ Seq(fork in run := true) ++ Seq(libraryDependencies ++= templatePublisherDeps))
本来は、sbtからじゃなくて普通に起動しろということでしょうか…。
これでテンプレートが登録できたので、先ほどのcurlコマンドを再実行。
$ curl http://localhost:8080/javaee7-web/view?template=hello 名前: かずひら Word: こんにちは!
今度は、結果が返ってきました。
では、テンプレートをちょっと修正して
template-publisher/template/hello.vm
名前: ${name}! Word: ${word}? ちょっと修正
で、再配信。
> run template/hello.vm /WEB-INF/template/hello.vm
確認。
$ curl http://localhost:8080/javaee7-web/view?template=hello 名前: かずひら! Word: こんにちは!? ちょっと修正
反映されてますね。この要領で、さらにテンプレートを追加することもできます。というか、最初からテンプレート追加してましたか…。
今回作成したコードは、こちらにアップしています。
https://github.com/kazuhira-r/infinispan-examples/tree/master/infinispan-velocity-practice
気になるところは、Webアプリケーション側でInfinispanのCacheを作る時にServletContextListener使っちゃいましたが、もうちょっといい方法ないかなぁというところでしょうか。VelocityのResourceLoaderから参照しなくてはいけないので、CDIの@Injectはできないし…BeanManager使うのは、今回パスしました。パッと思い付くのは、そんな感じ。
ちなみに、仕事でもこういうテンプレート配信みたいなことやる機会そこそこあるのですが、その時は普通にrsyncだったりします。
ちょっとやってみたかったネタでした。