Guava Cache

JavaキャッシュGoogleライブラリローカルキャッシュCacheBuilder安定性

GitHub概要

google/guava

Google core libraries for Java

スター51,221
ウォッチ2,354
フォーク11,085
作成日:2014年5月29日
言語:Java
ライセンス:Apache License 2.0

トピックス

guavajava

スター履歴

google/guava Star History
データ取得日時: 2025/10/22 10:05

ライブラリ

Guava Cache

概要

Guava CacheはGoogle社が開発したJava向けローカルキャッシュライブラリです。Google Core Libraries for Javaの一部として提供され、シンプルで直感的なCacheBuilderパターンによるキャッシュ構築を実現。2025年現在、Caffeineに性能面で劣るものの、Google内部で長年使用された実績と安定性により多くのレガシープロジェクトで継続利用されています。新規プロジェクトではCaffeineが推奨されるものの、既存Guava依存プロジェクトでの安定したキャッシュソリューションとして重要な位置を占めています。

詳細

Guava Cache 33.4.8は2025年現在の最新版で、Java 11+での動作をサポート。長年のGoogle社内での使用により洗練されたAPIと高い安定性を実現しているものの、シングルスレッド設計による性能制約があります。Caffeine登場以降は新規採用が減少していますが、既存の大規模プロジェクトでの継続利用と、Guavaライブラリ全体への依存がある場合の選択肢として活用されています。LoadingCache、refresh機能、統計収集など基本的なキャッシュ機能は網羅しており、学習コストの低いAPIを提供。

主な特徴

  • CacheBuilderパターン: 直感的で読みやすいキャッシュ設定
  • 統合されたエコシステム: Google Guavaライブラリ全体との統合
  • LoadingCache: 自動的なデータローディング機能
  • 統計とモニタリング: 包括的なキャッシュ統計情報
  • 長期安定性: Google社内での長年の使用実績
  • Documentation: 充実したドキュメントとコミュニティサポート

メリット・デメリット

メリット

  • Google社内での長年の安定使用実績(信頼性証明済み)
  • 直感的で理解しやすいCacheBuilderパターン
  • Guavaライブラリ全体との統合による一貫性
  • 豊富なドキュメントと学習リソース
  • 既存プロジェクトでの移行コスト最小化
  • 成熟したAPIによる予期しない動作の少なさ

デメリット

  • Caffeineと比較して性能が大幅に劣る(数倍の差)
  • シングルスレッド設計による並行性制約
  • LRUアルゴリズムによるヒット率の限界
  • 新機能追加やパフォーマンス改善の期待薄
  • モダンなWindow TinyLFUアルゴリズムなし
  • 非同期操作のサポート不足

参考ページ

書き方の例

依存関係の追加

// Gradle でのセットアップ
implementation 'com.google.guava:guava:33.4.8-jre'

// Android の場合
implementation 'com.google.guava:guava:33.4.8-android'
<!-- Maven でのセットアップ -->
<dependency>
  <groupId>com.google.guava</groupId>
  <artifactId>guava</artifactId>
  <version>33.4.8-jre</version>
</dependency>

基本的なキャッシュ作成と操作

import com.google.common.cache.Cache;
import com.google.common.cache.CacheBuilder;
import java.util.concurrent.TimeUnit;

public class BasicGuavaCacheExample {
    public static void main(String[] args) {
        // 基本的なキャッシュ作成
        Cache<String, String> cache = CacheBuilder.newBuilder()
            .maximumSize(1000)                        // 最大サイズ
            .expireAfterWrite(5, TimeUnit.MINUTES)    // 書き込み後5分で期限切れ
            .expireAfterAccess(2, TimeUnit.MINUTES)   // アクセス後2分で期限切れ
            .recordStats()                            // 統計記録を有効化
            .build();

        // キャッシュ操作
        cache.put("user:1", "John Doe");
        String user = cache.getIfPresent("user:1");
        System.out.println("User: " + user); // "User: John Doe"

        // 存在しない場合のデフォルト値
        String defaultUser = cache.get("user:999", () -> "Unknown User");
        System.out.println("Default: " + defaultUser); // "Default: Unknown User"

        // バルク操作
        Map<String, String> users = Map.of(
            "user:2", "Jane Smith",
            "user:3", "Bob Johnson"
        );
        cache.putAll(users);

        // 統計情報の取得
        CacheStats stats = cache.stats();
        System.out.printf("Hit rate: %.2f%%\n", stats.hitRate() * 100);
        System.out.printf("Miss count: %d\n", stats.missCount());
        System.out.printf("Eviction count: %d\n", stats.evictionCount());

        // キャッシュサイズと無効化
        System.out.println("Cache size: " + cache.size());
        cache.invalidate("user:1");  // 個別削除
        cache.invalidateAll();       // 全削除
    }
}

LoadingCache(自動ローディング)

import com.google.common.cache.CacheBuilder;
import com.google.common.cache.CacheLoader;
import com.google.common.cache.LoadingCache;

public class LoadingCacheExample {
    
    // データベースアクセスのシミュレーション
    private static String loadUserFromDatabase(String userId) {
        try {
            Thread.sleep(100); // DB アクセス時間をシミュレーション
            return "User-" + userId + "-from-DB";
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
            throw new RuntimeException(e);
        }
    }
    
    public static void main(String[] args) throws Exception {
        // LoadingCache の作成
        LoadingCache<String, String> userCache = CacheBuilder.newBuilder()
            .maximumSize(10000)
            .expireAfterWrite(30, TimeUnit.MINUTES)
            .refreshAfterWrite(5, TimeUnit.MINUTES)   // 5分後にバックグラウンドでリフレッシュ
            .recordStats()
            .build(new CacheLoader<String, String>() {
                @Override
                public String load(String key) throws Exception {
                    return loadUserFromDatabase(key);
                }
            });

        // 自動ローディング
        String user1 = userCache.get("user:123");     // DB からロード
        String user2 = userCache.get("user:123");     // キャッシュから取得
        
        // バルクローディング
        Map<String, String> users = userCache.getAll(
            List.of("user:100", "user:101", "user:102")
        );
        
        // 手動でのリフレッシュ
        userCache.refresh("user:123");
        
        // 統計情報確認
        CacheStats stats = userCache.stats();
        System.out.printf("Hit rate: %.2f%%\n", stats.hitRate() * 100);
        System.out.printf("Load count: %d\n", stats.loadCount());
        System.out.printf("Average load time: %.2f ms\n", 
            stats.averageLoadTime() / 1_000_000.0);
    }
}

高度なキャッシュ設定とカスタマイズ

import com.google.common.cache.CacheBuilder;
import com.google.common.cache.CacheLoader;
import com.google.common.cache.LoadingCache;
import com.google.common.cache.RemovalListener;
import com.google.common.cache.RemovalNotification;
import com.google.common.cache.Weigher;

public class AdvancedGuavaCacheExample {
    
    public static void main(String[] args) {
        // カスタムRemovalListener
        RemovalListener<String, String> removalListener = 
            new RemovalListener<String, String>() {
                @Override
                public void onRemoval(RemovalNotification<String, String> notification) {
                    System.out.printf("Removed: %s=%s (cause: %s)\n", 
                        notification.getKey(), 
                        notification.getValue(), 
                        notification.getCause());
                    
                    // クリーンアップ処理
                    if (notification.getCause().wasEvicted()) {
                        cleanupExpiredData(notification.getKey(), notification.getValue());
                    }
                }
            };
        
        // 重み付きキャッシュ
        LoadingCache<String, String> weightedCache = CacheBuilder.newBuilder()
            .maximumWeight(100000)  // 最大重み
            .weigher(new Weigher<String, String>() {
                @Override
                public int weigh(String key, String value) {
                    // オブジェクトサイズに基づく重み計算
                    return key.length() + value.length();
                }
            })
            .expireAfterWrite(1, TimeUnit.HOURS)
            .expireAfterAccess(30, TimeUnit.MINUTES)
            .refreshAfterWrite(15, TimeUnit.MINUTES)
            .removalListener(removalListener)
            .recordStats()
            .build(new CacheLoader<String, String>() {
                @Override
                public String load(String key) throws Exception {
                    return fetchDataFromSource(key);
                }
            });
        
        // 弱参照キャッシュ
        Cache<String, Object> weakCache = CacheBuilder.newBuilder()
            .maximumSize(1000)
            .weakKeys()     // キーの弱参照
            .weakValues()   // 値の弱参照
            .expireAfterAccess(10, TimeUnit.MINUTES)
            .build();
        
        // ソフト参照キャッシュ(メモリプレッシャー時に自動削除)
        Cache<String, Object> softCache = CacheBuilder.newBuilder()
            .softValues()   // 値のソフト参照
            .maximumSize(500)
            .build();
    }
    
    private static void cleanupExpiredData(String key, String value) {
        // 期限切れデータのクリーンアップ処理
        System.out.println("Cleaning up expired data: " + key);
    }
    
    private static String fetchDataFromSource(String key) {
        // データソースからの取得処理
        return "Fresh data for " + key;
    }
}

Spring Boot での統合

// Spring Boot Configuration
@Configuration
@EnableCaching
public class GuavaCacheConfiguration {
    
    @Bean
    public CacheManager cacheManager() {
        GuavaCacheManager cacheManager = new GuavaCacheManager();
        cacheManager.setCacheBuilder(
            CacheBuilder.newBuilder()
                .maximumSize(1000)
                .expireAfterAccess(Duration.ofMinutes(10))
                .recordStats()
        );
        return cacheManager;
    }
    
    // 複数キャッシュの設定
    @Bean
    public CacheManager multipleCacheManager() {
        GuavaCacheManager manager = new GuavaCacheManager();
        
        // ユーザーキャッシュ
        manager.setCacheNames(Arrays.asList("users", "sessions"));
        manager.setCacheBuilder(
            CacheBuilder.newBuilder()
                .maximumSize(1000)
                .expireAfterWrite(Duration.ofHours(1))
                .recordStats()
        );
        
        return manager;
    }
    
    // カスタムキャッシュBean
    @Bean
    public LoadingCache<String, User> userCache() {
        return CacheBuilder.newBuilder()
            .maximumSize(1000)
            .expireAfterWrite(1, TimeUnit.HOURS)
            .recordStats()
            .build(new CacheLoader<String, User>() {
                @Override
                public User load(String userId) throws Exception {
                    return userService.findById(userId);
                }
            });
    }
}

// Service クラスでの使用
@Service
public class UserService {
    
    @Autowired
    private LoadingCache<String, User> userCache;
    
    @Cacheable(value = "users", key = "#userId")
    public User getUserById(String userId) {
        // データベースからユーザー取得
        return userRepository.findById(userId);
    }
    
    @CacheEvict(value = "users", key = "#user.id")
    public User updateUser(User user) {
        return userRepository.save(user);
    }
    
    // LoadingCache を直接使用
    public User getUserWithLoadingCache(String userId) throws Exception {
        return userCache.get(userId);
    }
    
    // 統計情報の取得
    public CacheStats getCacheStats() {
        return userCache.stats();
    }
}

パフォーマンス監視と最適化

public class GuavaCacheMonitoringExample {
    
    private final LoadingCache<String, String> cache;
    private final ScheduledExecutorService scheduler;
    
    public GuavaCacheMonitoringExample() {
        this.cache = CacheBuilder.newBuilder()
            .maximumSize(1000)
            .expireAfterWrite(10, TimeUnit.MINUTES)
            .recordStats()
            .removalListener(this::onRemoval)
            .build(this::loadValue);
            
        this.scheduler = Executors.newScheduledThreadPool(1);
        setupMonitoring();
    }
    
    private void setupMonitoring() {
        // 定期的な統計レポート
        scheduler.scheduleAtFixedRate(() -> {
            CacheStats stats = cache.stats();
            
            System.out.println("=== Guava Cache Statistics ===");
            System.out.printf("Hit Rate: %.2f%%\n", stats.hitRate() * 100);
            System.out.printf("Miss Rate: %.2f%%\n", stats.missRate() * 100);
            System.out.printf("Hit Count: %d\n", stats.hitCount());
            System.out.printf("Miss Count: %d\n", stats.missCount());
            System.out.printf("Load Count: %d\n", stats.loadCount());
            System.out.printf("Eviction Count: %d\n", stats.evictionCount());
            System.out.printf("Average Load Time: %.2f ms\n", 
                stats.averageLoadTime() / 1_000_000.0);
            System.out.printf("Load Exception Count: %d\n", stats.loadExceptionCount());
            System.out.println("===============================");
            
        }, 0, 30, TimeUnit.SECONDS);
    }
    
    private void onRemoval(RemovalNotification<String, String> notification) {
        System.out.printf("Removed: %s (cause: %s)\n", 
            notification.getKey(), notification.getCause());
    }
    
    private String loadValue(String key) {
        // 模擬的なデータローディング
        try {
            Thread.sleep(50); // ローディング時間のシミュレーション
            return "Loaded value for " + key;
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
            throw new RuntimeException(e);
        }
    }
    
    // ベンチマークテスト
    public void benchmarkCache() {
        int iterations = 10000;
        long startTime = System.nanoTime();
        
        // 書き込みベンチマーク
        for (int i = 0; i < iterations; i++) {
            cache.put("key" + i, "value" + i);
        }
        
        long writeTime = System.nanoTime() - startTime;
        
        // 読み込みベンチマーク
        startTime = System.nanoTime();
        for (int i = 0; i < iterations; i++) {
            cache.getIfPresent("key" + (i % 1000));
        }
        
        long readTime = System.nanoTime() - startTime;
        
        System.out.printf("Write time: %.2f ms\n", writeTime / 1_000_000.0);
        System.out.printf("Read time: %.2f ms\n", readTime / 1_000_000.0);
        System.out.printf("Write throughput: %.0f ops/sec\n", 
            iterations * 1_000_000_000.0 / writeTime);
        System.out.printf("Read throughput: %.0f ops/sec\n", 
            iterations * 1_000_000_000.0 / readTime);
    }
    
    public void shutdown() {
        scheduler.shutdown();
    }
}

カスタムCacheLoaderとエラーハンドリング

import com.google.common.cache.CacheBuilder;
import com.google.common.cache.CacheLoader;
import com.google.common.cache.LoadingCache;
import com.google.common.util.concurrent.ListenableFuture;
import com.google.common.util.concurrent.ListenableFutureTask;

public class CustomCacheLoaderExample {
    
    public static void main(String[] args) throws Exception {
        // エラーハンドリング付きCacheLoader
        LoadingCache<String, String> resilientCache = CacheBuilder.newBuilder()
            .maximumSize(1000)
            .expireAfterWrite(30, TimeUnit.MINUTES)
            .refreshAfterWrite(5, TimeUnit.MINUTES)
            .recordStats()
            .build(new CacheLoader<String, String>() {
                @Override
                public String load(String key) throws Exception {
                    return loadWithRetry(key, 3);
                }
                
                @Override
                public ListenableFuture<String> reload(String key, String oldValue) {
                    // 非同期リロード
                    ListenableFutureTask<String> task = ListenableFutureTask.create(() -> {
                        try {
                            return loadWithRetry(key, 3);
                        } catch (Exception e) {
                            // エラー時は古い値を返す
                            System.err.println("Reload failed for " + key + ", using old value");
                            return oldValue;
                        }
                    });
                    
                    // バックグラウンドで実行
                    Executors.newSingleThreadExecutor().execute(task);
                    return task;
                }
            });
        
        // タイムアウト付きキャッシュ
        LoadingCache<String, String> timeoutCache = CacheBuilder.newBuilder()
            .maximumSize(500)
            .expireAfterWrite(15, TimeUnit.MINUTES)
            .build(new CacheLoader<String, String>() {
                @Override
                public String load(String key) throws Exception {
                    return loadWithTimeout(key, 5000); // 5秒タイムアウト
                }
            });
        
        // フォールバック戦略付きキャッシュ
        LoadingCache<String, String> fallbackCache = CacheBuilder.newBuilder()
            .maximumSize(1000)
            .expireAfterWrite(1, TimeUnit.HOURS)
            .build(new CacheLoader<String, String>() {
                @Override
                public String load(String key) throws Exception {
                    try {
                        return loadFromPrimarySource(key);
                    } catch (Exception primaryError) {
                        System.err.println("Primary source failed: " + primaryError.getMessage());
                        try {
                            return loadFromSecondarySource(key);
                        } catch (Exception secondaryError) {
                            System.err.println("Secondary source failed: " + secondaryError.getMessage());
                            return getDefaultValue(key);
                        }
                    }
                }
            });
    }
    
    private static String loadWithRetry(String key, int maxRetries) throws Exception {
        Exception lastException = null;
        
        for (int i = 0; i < maxRetries; i++) {
            try {
                return loadFromExternalService(key);
            } catch (Exception e) {
                lastException = e;
                System.err.printf("Load attempt %d failed for key %s: %s\n", 
                    i + 1, key, e.getMessage());
                
                if (i < maxRetries - 1) {
                    // 指数バックオフ
                    long delay = (long) Math.pow(2, i) * 100;
                    Thread.sleep(delay);
                }
            }
        }
        
        throw new Exception("Failed to load after " + maxRetries + " attempts", lastException);
    }
    
    private static String loadWithTimeout(String key, long timeoutMs) throws Exception {
        ExecutorService executor = Executors.newSingleThreadExecutor();
        
        try {
            Future<String> future = executor.submit(() -> loadFromExternalService(key));
            return future.get(timeoutMs, TimeUnit.MILLISECONDS);
        } catch (TimeoutException e) {
            throw new Exception("Load timeout for key: " + key);
        } finally {
            executor.shutdown();
        }
    }
    
    private static String loadFromExternalService(String key) throws Exception {
        // 外部サービスからのデータ取得をシミュレーション
        Thread.sleep(100);
        
        // 20%の確率でエラーをシミュレーション
        if (Math.random() < 0.2) {
            throw new Exception("External service error");
        }
        
        return "Data for " + key + " from external service";
    }
    
    private static String loadFromPrimarySource(String key) throws Exception {
        // プライマリソースからのデータ取得
        return loadFromExternalService(key);
    }
    
    private static String loadFromSecondarySource(String key) throws Exception {
        // セカンダリソースからのデータ取得
        Thread.sleep(50);
        return "Fallback data for " + key;
    }
    
    private static String getDefaultValue(String key) {
        // デフォルト値の取得
        return "Default value for " + key;
    }
}

CaffeineからGuavaへのダウングレード対応

/**
 * Caffeine から Guava Cache へのダウングレード支援クラス
 * レガシーシステムでの互換性維持用
 */
public class CaffeineToGuavaMigration {
    
    // Caffeineライクなインターフェースでラッピング
    public static class GuavaCompatibilityWrapper<K, V> {
        private final LoadingCache<K, V> guavaCache;
        
        public GuavaCompatibilityWrapper(LoadingCache<K, V> guavaCache) {
            this.guavaCache = guavaCache;
        }
        
        // Caffeine風のAPIメソッド
        public V get(K key) {
            try {
                return guavaCache.get(key);
            } catch (Exception e) {
                throw new RuntimeException(e);
            }
        }
        
        public V getIfPresent(K key) {
            return guavaCache.getIfPresent(key);
        }
        
        public void put(K key, V value) {
            guavaCache.put(key, value);
        }
        
        public void invalidate(K key) {
            guavaCache.invalidate(key);
        }
        
        public void invalidateAll() {
            guavaCache.invalidateAll();
        }
        
        public long estimatedSize() {
            return guavaCache.size();
        }
        
        public CacheStats stats() {
            return guavaCache.stats();
        }
    }
    
    // Guava Cache ビルダーのファクトリメソッド
    public static <K, V> GuavaCompatibilityWrapper<K, V> createCompatibleCache(
            int maxSize, 
            Duration expireAfterWrite,
            Function<K, V> loader) {
        
        LoadingCache<K, V> cache = CacheBuilder.newBuilder()
            .maximumSize(maxSize)
            .expireAfterWrite(expireAfterWrite.toMillis(), TimeUnit.MILLISECONDS)
            .recordStats()
            .build(new CacheLoader<K, V>() {
                @Override
                public V load(K key) throws Exception {
                    return loader.apply(key);
                }
            });
        
        return new GuavaCompatibilityWrapper<>(cache);
    }
    
    // 設定変換ヘルパー
    public static CacheBuilder<Object, Object> convertCaffeineSpec(String caffeineSpec) {
        // Caffeine設定文字列をGuava設定に変換
        CacheBuilder<Object, Object> builder = CacheBuilder.newBuilder();
        
        // 簡易的な変換ロジック(実際の実装では正規表現等を使用)
        if (caffeineSpec.contains("maximumSize=")) {
            String sizeStr = extractValue(caffeineSpec, "maximumSize=");
            builder.maximumSize(Long.parseLong(sizeStr));
        }
        
        if (caffeineSpec.contains("expireAfterWrite=")) {
            String durationStr = extractValue(caffeineSpec, "expireAfterWrite=");
            long duration = parseDuration(durationStr);
            builder.expireAfterWrite(duration, TimeUnit.MILLISECONDS);
        }
        
        if (caffeineSpec.contains("recordStats")) {
            builder.recordStats();
        }
        
        return builder;
    }
    
    private static String extractValue(String spec, String key) {
        int start = spec.indexOf(key) + key.length();
        int end = spec.indexOf(',', start);
        if (end == -1) end = spec.length();
        return spec.substring(start, end);
    }
    
    private static long parseDuration(String duration) {
        // 簡易的なDuration解析
        if (duration.endsWith("m")) {
            return Long.parseLong(duration.substring(0, duration.length() - 1)) * 60 * 1000;
        } else if (duration.endsWith("s")) {
            return Long.parseLong(duration.substring(0, duration.length() - 1)) * 1000;
        }
        return Long.parseLong(duration);
    }
}