首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
您现在的位置: 程式碼之家 >> 編程語言 >  >> Java 程式設計 >> Java 程式設計入門

Java 設計模式實戰:動態代理實現通用緩存(Memoization)組件

     編輯:Java 程式設計入門

什麼是 Memoization?

Memoization(記憶化)源自 Lisp/Python/Perl 等函數式語言,核心思想:緩存純函數的計算結果,相同輸入直接返回緩存值。適用於計算密集型、冪等、無副作用的方法(如遞歸斐波那契、數學函數、昂貴的數據庫聚合查詢)。

從硬編碼緩存到裝飾器模式

階段 1:侵入式緩存(反模式)

public class PiCalculator {
    private Map<Integer, Byte> cache = new HashMap<>();
    public synchronized byte calculate(int n) {
        if (cache.containsKey(n)) return cache.get(n);
        byte result = runBBPAlgorithm(n); // 耗時計算
        cache.put(n, result);
        return result;
    }
}

缺點:業務邏輯與緩存邏輯耦合、難以單元測試(無法驗證算法本身)、緩存策略難以替換、違反單一職責原則。

階段 2:裝飾器模式分離關注點

public interface BinaryDigitsCalculator {
    byte calculateBinaryDigit(int n);
}

public class PiCalculator implements BinaryDigitsCalculator {
    public byte calculateBinaryDigit(int n) { return runBBPAlgorithm(n); }
}

public class CachingCalculator implements BinaryDigitsCalculator {
    private final BinaryDigitsCalculator delegate;
    private final Map<Integer, Byte> cache = new ConcurrentHashMap<>();

    public CachingCalculator(BinaryDigitsCalculator delegate) { this.delegate = delegate; }

    public byte calculateBinaryDigit(int n) {
        return cache.computeIfAbsent(n, delegate::calculateBinaryDigit);
    }
}

優勢:計算邏輯純淨可測、緩存可熱插拔、支持多種緩存策略(LRU、TTL、弱引用)組合。

通用化:JDK 動態代理實現萬能 Memoizer

核心實現

public final class Memoizer {
    private Memoizer() {}

    @SuppressWarnings("unchecked")
    public static <T> T memoize(T target) {
        Class<?>[] interfaces = target.getClass().getInterfaces();
        if (interfaces.length != 1) {
            throw new IllegalArgumentException("Target must implement exactly one interface");
        }
        Class<T> iface = (Class<T>) interfaces[0];
        return (T) Proxy.newProxyInstance(
            iface.getClassLoader(),
            new Class[]{iface},
            new MemoizationInvocationHandler(target)
        );
    }

    private static class MemoizationInvocationHandler implements InvocationHandler {
        private final Object target;
        private final Map<MethodKey, Object> cache = new ConcurrentHashMap<>();

        MemoizationInvocationHandler(Object target) { this.target = target; }

        @Override
        public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
            // 僅緩存無參或所有參數可序列化/可比較的方法
            MethodKey key = new MethodKey(method, args);
            return cache.computeIfAbsent(key, k -> {
                try { return method.invoke(target, args); }
                catch (InvocationTargetException e) { throw e.getCause(); }
                catch (Exception e) { throw new UndeclaredThrowableException(e); }
            });
        }
    }

    // 方法簽名 + 參數值 作為 Key,需正確實現 equals/hashCode
    private static final class MethodKey {
        private final Method method;
        private final Object[] args;
        // ... equals/hashCode 基於 method + Arrays.deepEquals(args)
    }
}

使用示例

public interface ExpensiveService {
    BigDecimal computeRiskScore(String portfolioId, LocalDate asOf);
}

ExpensiveService real = new ExpensiveServiceImpl();
ExpensiveService cached = Memoizer.memoize(real); // 透明添加緩存

// 業務代碼無感知
BigDecimal score = cached.computeRiskScore("PF001", LocalDate.now());

進階議題與生產級增強

議題解決方案
緩存無限增長引入 Cache<K,V> 抽象(Guava Cache / Caffeine / Ehcache),支持最大條目、TTL、基於權重驅逐。
參數包含不可比較對象要求參數實現 equals/hashCode,或提供 KeyGenerator 策略接口(如 JSON 序列化、SpEL 表達式)。
線程安全與併發穿透ConcurrentHashMap.computeIfAbsent 原子性保證單次計算;高併發熱點可用 Future 模式或 StampedLock 優化。
異常結果不應緩存捕獲異常並拋出,不寫入緩存;或引入 CacheLoader 區分「計算失敗」與「計算結果為 null」。
方法副作用/非冪等嚴禁對有副作用方法使用 Memoization;可通過註解 @Memoizable 標記並編譯期檢查。
分佈式環境一致性單機緩存不適用;改用 Redis + Lua 腳本實現分佈式「計算一次」語義,或引入分佈式鎖。

對比:Memoization vs 常見緩存模式

  • @Cacheable (Spring):聲明式、支持 SpEL Key、集成 CacheManager、分佈式友好,但需 Spring 容器。
  • Guava Cache / Caffeine:高性能本地緩存庫、豐富驅逐策略、異步加載,手動調用 get(key, callable)。
  • 動態代理 Memoizer:零依賴、純 JDK、接口級透明增強、適合輕量級微服務/庫開發、無運行時依賴。

總結

動態代理實現的通用 Memoizer,以裝飾器模式 + 接口導向設計,將「緩存」這一橫切關注點從業務邏輯中完全剝離。它展示了 Java 元編程能力在解決橫切關注點時的優雅性:零侵入、可組合、可測試、可替換。生產環境建議在此基礎上集成成熟緩存庫(Caffeine/Redis),並嚴格限定適用於純計算、冪等、參數可比較的接口方法。

  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved