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-webspring-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.jarloadAgentJMX エージェントを適用します。

一度 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)
}
・・・