缓存注解对象能显著提速,因其避免每次调用getAnnotation()时JVM重复创建动态代理、初始化Map及安全检查等开销;使用ConcurrentHashMap以Method/Field为key懒加载缓存,配合MethodHandle可将属性读取压至60纳秒内。
直接缓存注解对象是最简单有效的优化手段。因为每次调用 getannotation() 都会触发 jvm 动态代理创建、属性 map 初始化、安全检查等开销,而多数注解在运行期是不可变的,重复读取时完全可复用已解析结果。
为什么缓存注解对象能显著提速
Java 运行时注解本质是 JVM 通过 AnnotationInvocationHandler 创建的动态代理实例,其内部用 memberValues(一个 Map)存储属性值。每次反射获取注解,JVM 都需:
- 检查类是否加载、注解是否存在于运行时常量池
- 构造代理类并实例化 handler
- 解析默认值、填充 memberValues
这些步骤在高频调用(如 Web 请求拦截、序列化遍历)中会成为明显瓶颈。
用 ConcurrentHashMap 缓存字段/方法级注解
针对类成员(Field、Method)的注解,按成员对象作 key 缓存最自然:
- 使用
ConcurrentHashMap<field t></field>或ConcurrentHashMap<method t></method>,避免线程安全问题 - 用
computeIfAbsent()实现懒加载,首次访问才解析,后续直接返回缓存值 - 不缓存
Class级注解到全局 map 中——类对象生命周期长,易引发内存泄漏;应按业务域隔离或配合弱引用
示例代码:
private static final MapApiEndpoint getEndpoint(Method m) {
return ENDPOINT_CACHE.computeIfAbsent(m, method -> method.getAnnotation(ApiEndpoint.class));
}
避免缓存失效与内存泄漏
注解本身不可变,但缓存载体可能出问题:
- 不要用字符串拼接(如
clazz.getName() + "#" + methodName)作 key——易冲突且无法保证唯一性;优先用Method或Field对象本身(它们重写了equals/hashCode) - 若框架支持热部署(如 Spring DevTools),需监听类重载事件清空对应缓存,否则旧类的 Method 对象仍被引用,导致 ClassLoader 无法卸载
- 对泛型丰富或嵌套深的注解(如含
@Nested属性),缓存的是顶层注解实例,其嵌套属性仍走代理访问,无需额外处理
进阶:结合 MethodHandle 进一步加速属性访问
即使缓存了注解对象,调用 ann.path() 仍是一次接口方法调用,背后是 InvocationHandler.invoke() 分发。对超高频场景(如每毫秒调用数百次),可进一步:
- 用
MethodHandle绑定注解实例的某个属性 getter(如path方法) - 将 handle 缓存为
ConcurrentMap<method methodhandle></method>,跳过反射查找和安全校验 - 实测显示,相比原始反射调用,
MethodHandle+ 注解对象缓存组合可将单次属性读取压至 60 纳秒内


















