Spring を使った Web アプリケーションへ Ehcache を適用し JMX でモニタリング
Spring を使った Web アプリケーションへ Ehcache を適用し、JMX でキャッシュ状況を取得できるようにしてみました。
サンプルソースは http://github.com/fits/try_samples/tree/master/blog/20150508/
Spring へ Ehcache を適用
Spring には Cache Abstraction 機能が用意されており、Cache 用のアノテーションをメソッドへ付ければメソッドの戻り値をキャッシュできます。 (デフォルトでは、メソッドの引数がキャッシュキーとなります)
実際のキャッシュ処理には以下のようなライブラリや API が利用でき、今回は Ehcache を使用します。
- ConcurrentMap
- Ehcache
- Guava
- GemFire
- JSR-107 (JCache)
なお、キャッシュ用アノテーションは以下が利用でき、今回は JSR-107 のアノテーションを使用します。
Service クラス
まずはキャッシュを適用するメソッドを実装します。
今回は JSR-107 の @CacheResult
アノテーションを使いました。
src/main/java/sample/service/SampleService.java
package sample.service; import org.springframework.stereotype.Service; import javax.cache.annotation.CacheResult; @Service public class SampleService { @CacheResult(cacheName = "sample") public String sample(String id) { return "sample: " + id + ", " + System.currentTimeMillis(); } }
Controller クラス
次に、キャッシュを適用したメソッドを呼び出す処理を実装します。
今回のようなケースでは、CGLIB によるプロキシを使ってキャッシュ処理を差し込んでおり、内部的なメソッド呼び出しにはキャッシュを適用しないようなのでご注意ください。
src/main/java/sample/controller/SampleController.java
package sample.controller; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.ResponseBody; import javax.inject.Inject; import sample.service.SampleService; @Controller public class SampleController { @Inject private SampleService sampleService; @RequestMapping("/sample/{id}") @ResponseBody public String sample(@PathVariable("id") String id) { // キャッシュを適用したメソッドの実行 return sampleService.sample(id); } }
設定クラス
設定クラスでは、下記を実施することで Ehcache を適用できます。
@EnableCaching
でキャッシュを有効化EhCacheCacheManager
を Bean 定義
Ehcache の設定ファイル ehcache.xml
をクラスパスから取得するように ClassPathResource
を使っています。
今回は JMX でモニタリングできるように Ehcache の ManagementService
も Bean 定義しています。
src/main/java/sample/config/WebConfig.java
package sample.config; import net.sf.ehcache.management.ManagementService; import org.springframework.cache.CacheManager; import org.springframework.cache.annotation.EnableCaching; import org.springframework.cache.ehcache.EhCacheCacheManager; import org.springframework.cache.ehcache.EhCacheManagerFactoryBean; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.core.io.ClassPathResource; import org.springframework.jmx.support.MBeanServerFactoryBean; import org.springframework.web.servlet.config.annotation.EnableWebMvc; @Configuration @EnableWebMvc @EnableCaching //キャッシュ機能の有効化 public class WebConfig { // CacheManager の定義 @Bean public CacheManager cacheManager() { EhCacheCacheManager manager = new EhCacheCacheManager(); manager.setCacheManager(ehcache().getObject()); return manager; } @Bean public EhCacheManagerFactoryBean ehcache() { EhCacheManagerFactoryBean ehcache = new EhCacheManagerFactoryBean(); ehcache.setConfigLocation(new ClassPathResource("ehcache.xml")); return ehcache; } // JMX 設定 @Bean public MBeanServerFactoryBean mbeanServer() { MBeanServerFactoryBean factory = new MBeanServerFactoryBean(); factory.setLocateExistingServerIfPossible(true); return factory; } // Ehcache 用の JMX 設定 @Bean public ManagementService managementService() { ManagementService service = new ManagementService(ehcache().getObject(), mbeanServer().getObject(), true, true, true, true); service.init(); return service; } }
実行クラス
Spring Boot で実行するための実行クラスです。
src/main/java/sample/Application.java
package sample; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.EnableAutoConfiguration; import org.springframework.context.annotation.ComponentScan; @ComponentScan @EnableAutoConfiguration public class Application { public static void main(String... args) { SpringApplication.run(Application.class, args); } }
ビルド定義
Spring の Cache Abstraction 機能は 2つのモジュールに分かれており、Ehcache を使用するには spring-context-support
が必要です。
モジュール | 備考 |
---|---|
spring-context | Cache の基本機能 |
spring-context-support | 各種キャッシュライブラリ用の実装 |
今回使用した spring-boot-starter-web
は spring-context-support
を依存関係に含んでいなかったため、依存定義をする必要がありました。
build.gradle
apply plugin: 'spring-boot' def enc = 'UTF-8' tasks.withType(AbstractCompile)*.options*.encoding = enc buildscript { repositories { jcenter() } dependencies { classpath 'org.springframework.boot:spring-boot-gradle-plugin:1.2.3.RELEASE' } } repositories { jcenter() } dependencies { compile 'org.springframework.boot:spring-boot-starter-web:1.2.3.RELEASE' // 以下は org.springframework.cache.ehcache のために必要 compile 'org.springframework:spring-context-support:4.1.6.RELEASE' // @Inject アノテーションのために必要 compile 'javax:javaee-api:7.0' // @CacheResult アノテーションのために必要 compile 'javax.cache:cache-api:1.0.0-PFD' compile 'net.sf.ehcache:ehcache:2.10.0' }
キャッシュ設定
Ehcache の設定は以下の通り。 キャッシュの有効期間を 10 秒としています。
src/main/resources/ehcache.xml
<?xml version="1.0" encoding="UTF-8"?> <ehcache> <cache name="sample" maxElementsInMemory="100" eternal="false" timeToLiveSeconds="10" /> </ehcache>
実行
bootRun タスクで Web アプリケーションを起動します。
起動
> gradle bootRun ・・・ :bootRun ・・・ 2015-05-04 18:37:07.807 INFO 6704 --- [ main] sample.Application : Started Application in 5.674 seconds (JVM running for 6.24) 2015-05-04 18:37:13.436 INFO 6704 --- [nio-8080-exec-1] o.a.c.c.C.[Tomcat].[localhost].[/] : Initializing Spring FrameworkServlet 'dispatcherServlet' ・・・
何度か同じ URL へアクセスしてみると、10秒間のキャッシュが効いている事を確認できました。
動作確認
$ curl http://localhost:8080/sample/aaa sample: aaa, 1430732146256 $ curl http://localhost:8080/sample/aaa sample: aaa, 1430732146256 $ curl http://localhost:8080/sample/aaa sample: aaa, 1430732146256 $ curl http://localhost:8080/sample/aaa sample: aaa, 1430732158070
JMX でキャッシュ状況を取得
JMX を使って Ehcache のキャッシュヒット状況などを確認してみます。
Attach API による JMX エージェント (MBean サーバー) の適用
Java の Attach API を使えば、ローカルで実行中の Java プロセスへアタッチして JMX エージェント (MBean サーバー) を動的に適用できます。
そのため、アプリケーションの起動時 (gradle bootRun) に JMX 用の実行時オプションを指定しておく必要はありません。
Attach API を使って JMX エージェントを適用する手順は以下の通りです。
- (1) Java VM の ID (プロセスID) を指定して VM へアタッチ
- (2) (1) を使って JMX 用のサービス URL を取得
- (3) (2) が null の場合に JMX エージェントを適用し (2) を再実施
(2) の戻り値が null の場合、JMX エージェントが未適用という事ですので、management-agent.jar
を loadAgent
し JMX エージェントを適用します。
一度 JMX エージェントを適用しておけば、それ以降はサービス URL を取得できるので、その URL を使って MBean サーバーへ接続します。
Ehcache の JMX
Ehcache の JMX オブジェクト名は以下のようになります。
net.sf.ehcache:type=CacheStatistics,CacheManager=__DEFAULT__,name=<キャッシュ名>
net.sf.ehcache:type=CacheStatistics,*
をオブジェクト名に指定して queryNames
すれば Ehcache に関する全オブジェクト名を取得できます。
そうして取得したオブジェクト名を使って CacheHits
等を getAttribute
すればキャッシュのヒット数などを取得できます。
Groovy で実装
今回は Ehcache の JMX からキャッシュヒット数などを取得する処理を Groovy で実装してみました。
ehcache_jmx1.groovy
import javax.management.ObjectName import javax.management.remote.JMXConnectorFactory import javax.management.remote.JMXServiceURL import com.sun.tools.attach.VirtualMachine // JMX のサービス URL 取得処理 def getServiceUrl = { it.agentProperties.getProperty('com.sun.management.jmxremote.localConnectorAddress') } def pid = args[0] // (1) Java VM へアタッチ def vm = VirtualMachine.attach(pid) // (2) JMX のサービス URL 取得 def url = getServiceUrl(vm) if (url == null) { def javaHome = vm.systemProperties.getProperty('java.home') // (3) JMX エージェントを適用 vm.loadAgent("${javaHome}/lib/management-agent.jar") // JMX のサービス URL を再取得 url = getServiceUrl(vm) } vm.detach() // MBean サーバーへの接続 def con = JMXConnectorFactory.connect(new JMXServiceURL(url)) def server = con.getMBeanServerConnection() def cacheName = new ObjectName('net.sf.ehcache:type=CacheStatistics,*') // EhCache に関する JMX オブジェクト名の取得 server.queryNames(cacheName, null).each { name -> println "# ${name}" // ヒット回数などの属性値を取得 def res = [ 'CacheHits', // キャッシュのヒット回数 'CacheMisses', // キャッシュのミス回数 'InMemoryHits', 'InMemoryMisses' ].collectEntries { attr -> [attr, server.getAttribute(name, attr)] } println res } con.close()
実行例
> groovy ehcache_jmx1.groovy 6704 # net.sf.ehcache:type=CacheStatistics,CacheManager=__DEFAULT__,name=sample [CacheHits:7, CacheMisses:3, InMemoryHits:8, InMemoryMisses:2]
また、JMX のサービス URL は ConnectorAddressLink.importFrom(<プロセスID>)
で取得する事も可能です。
ehcache_jmx2.groovy
・・・ import com.sun.tools.attach.VirtualMachine import sun.management.ConnectorAddressLink // JMX のサービス URL 取得処理 (ConnectorAddressLink 利用版) def getServiceUrl = { ConnectorAddressLink.importFrom(it as int) } def pid = args[0] def url = getServiceUrl(pid) if (url == null) { def vm = VirtualMachine.attach(pid) def javaHome = vm.systemProperties.getProperty('java.home') vm.loadAgent("${javaHome}/lib/management-agent.jar") vm.detach() url = getServiceUrl(pid) } ・・・