笔者在 Leetcode 中使用 Python 刷题时,总能体会到 @Cache 装饰器的强大,在写动态规划时省去了很多记忆化的麻烦。
与此同时,我好奇 Java 中注解的工作原理,想着能不能自己实现一个 Java 中的 @Cache 注解,为我的函数自动生成以函数参数为 key,函数返回值为 value 的 map 缓存。
常见的解决方案
在查阅资料后发现,在 Java 自定义一个注解,并实现函数调用前后逻辑处理的功能,使用的是动态代理的技术。下面是一些常见的动态代理技术:
JDK 动态代理:可以生成指定接口的代理类;
CGLIB 字节码增强:基于字节码生成子类,重写方法,支持任何类;
Spring AOP:基于 Spring 的切面机制,但是依赖 Spring 框架;
综上所述,由于我的场景是在任何一个方法上使用 @Cache 注解实现缓存功能,并不需要一个接口,因此最终选择 CGLIB 方案。
CGLIB 字节码增强
一、导入依赖
<dependency>
<groupId>cglib</groupId>
<artifactId>cglib</artifactId>
<version>3.3.0</version>
</dependency>
二、定义注解
定义我们的 @Cache 注解,RUNTIME 表示编译后保留在 class 文件中,运行时可通过反射获取,这是我们需要的。METHOD 表示这个注解只能用于方法上。
package interview.java.design.proxy;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Retention(RetentionPolicy.RUNTIME) // 运行时可见
@Target(ElementType.METHOD) // 表示使用在方法上
public @interface Cache {
}
三、编写代理逻辑
要实现函数级缓存的核心思路是:通过动态代理拦截被 @Cache 标注的方法调用,在调用前先检查缓存中是否存在结果,如果有则直接返回,否则执行方法并保存结果到缓存中。
我们使用 CGLIB 提供的 MethodInterceptor 来完成拦截,并使用一个 ConcurrentHashMap 来作为缓存存储结构。
import net.sf.cglib.proxy.MethodInterceptor;
import net.sf.cglib.proxy.MethodProxy;
import java.lang.reflect.Method;
import java.util.Arrays;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
public class CacheInterceptor implements MethodInterceptor {
// HashMap 存储缓存
private final Map<String, Object> cache = new ConcurrentHashMap<>();
@Override
public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable {
if (method.isAnnotationPresent(Cache.class)) {
// 函数参数构成 key
String key = method.getName() + Arrays.toString(args);
// 缓存存在,从缓存中获取
if (cache.containsKey(key)) {
return cache.get(key);
} else {
Object result = proxy.invokeSuper(obj, args);
cache.put(key, result);
return result;
}
} else {
return proxy.invokeSuper(obj, args);
}
}
}
四、定义被代理的类
我们写一个 MathUtil 类,递归计算斐波那契数列。如果没有 Cache 那么耗时将非常恐怖,以此来验证我们的 @Cache 注解。
public class MathUtil {
@Cache
public long fib(int n) {
if (n <= 1) return n;
return fib(n - 1) + fib(n - 2);
}
public long fibNoCache(int n) {
if (n <= 1) return n;
return fibNoCache(n - 1) + fibNoCache(n - 2);
}
}
五、生成代理对象
接下来我们使用 CGLIB 的 Enhancer
来为目标类生成代理对象,使得原本的类方法调用能够被拦截器拦截。并测试计算斐波那契数列 40 的计算时间。
import interview.java.design.proxy.CacheInterceptor;
import interview.java.design.proxy.MathUtil;
import net.sf.cglib.proxy.Enhancer;
import org.junit.jupiter.api.Test;
public class CacheTest {
@Test
void testCacheProxy() {
// 通过 Enhancer 生成动态代理类
Enhancer enhancer = new Enhancer();
enhancer.setSuperclass(MathUtil.class);
enhancer.setCallback(new CacheInterceptor());
MathUtil mathUtil = (MathUtil) enhancer.create();
// 无 @Cache 注解
long start = System.currentTimeMillis();
System.out.println("no cache, fib(40) = " + mathUtil.fibNoCache(40));
long end = System.currentTimeMillis();
System.out.println("无缓存耗时: " + (end - start) + "ms\n");
// 有 @Cache 注解
start = System.currentTimeMillis();
System.out.println("fib(40) = " + mathUtil.fib(40));
end = System.currentTimeMillis();
System.out.println("缓存耗时: " + (end - start) + "ms\n");
}
}
测试输出如下,耗时可以说天差地别,@Cache 注解成功实现缓存功能。
no cache, fib(40) = 102334155
无缓存耗时: 2521ms
fib(40) = 102334155
缓存耗时: 1ms
总结
使用 CGLIB 动态代理实现 map 缓存的步骤如下:
创建注解,标记为运行时注解;
实现 MethodInterceptor,完成缓存的逻辑处理;
通过 Enhancer 生成代理类。
总结来说我们确实实现了 map 缓存的功能,但是还是比较繁琐,并不能像 Python 那样直接使用,还需要手动获取代理类。