如何避免反射绕过Groovy的invokeMethod拦截器实现懒加载?
Groovy懒加载代理捕获Java反射调用的解决方案
问题背景
通过Groovy的metaClass重写getProperty和invokeMethod实现对象懒加载:仅在调用属性getter时才加载数据。正常Groovy语法调用getter时代理工作正常,但使用Java反射(如Method.invoke)或Jackson序列化(内部用反射调用_accessorMethod.invoke(bean))时,拦截逻辑完全不触发,导致数据仍为null。
现有代理代码
static <T> T lazyProxyFor(T object) { Class clazz = object.class object.metaClass.getProperty = { String name -> println "getting property $name" def currentVal = clazz.metaClass.getMetaProperty(name).getProperty(delegate) if (!currentVal) { setProperty('testing') // 实际为从服务获取数据的逻辑 } return currentVal } object.metaClass.invokeMethod = { String name, Object args -> println "calling $name" if (name ==~ /get[A-Z][a-zA-Z0-9_]*/ && args?.size() == 0) { object[name.drop(3).uncapitalize()] // 调用getProperty方法 } else { clazz.metaClass.getMetaMethod(name, args)?.invoke(delegate, args) // 交给原始对象处理 } } return object }
测试复现代码
Address address = lazyProxyFor(new Address()) address.class.declaredMethods.each{ method -> if (method.name.startsWith('get')) { method.invoke(address) // 反射调用:无拦截输出 // address."$method.name"() // Groovy语法调用:正常触发拦截 } }
原因分析
Groovy的metaClass仅覆盖Groovy语法层面的动态方法分派和属性访问,比如直接调用address.street或address.getStreet()(Groovy语法糖)。但Java反射调用Method.invoke时,会直接调用类的原始方法实现,完全绕过Groovy的元方法拦截机制,所以不会触发你的懒加载逻辑。
解决方案:使用动态代理拦截所有调用路径
要同时覆盖Groovy调用和Java反射调用,必须使用动态代理(Java原生Proxy或CGLIB类代理),这类代理会拦截所有通过代理对象的方法调用,包括反射方式。
方案1:Java原生动态代理(适用于实现接口的类)
如果目标类实现了接口,用Java原生Proxy创建代理对象,所有方法调用(包括反射)都会经过InvocationHandler的invoke方法:
import java.lang.reflect.InvocationHandler import java.lang.reflect.Method import java.lang.reflect.Proxy static <T> T lazyProxyFor(T object) { Class<?> clazz = object.getClass() // 创建代理,需目标类实现至少一个接口 return Proxy.newProxyInstance( clazz.getClassLoader(), clazz.getInterfaces(), new InvocationHandler() { @Override Object invoke(Object proxy, Method method, Object[] args) throws Throwable { String methodName = method.getName() // 处理getter方法(含反射调用) if (methodName.startsWith("get") && (args == null || args.length == 0)) { String propName = methodName.drop(3).uncapitalize() println "calling getter via reflection: $methodName" def currentVal = clazz.metaClass.getMetaProperty(propName).getProperty(object) if (!currentVal) { // 替换为实际的数据加载逻辑 object.setProperty(propName, "loaded from service") } return clazz.metaClass.getMetaProperty(propName).getProperty(object) } // 处理Groovy的getProperty调用 else if (methodName == "getProperty" && args.length == 1 && args[0] instanceof String) { String propName = args[0] println "getting property $propName" def currentVal = clazz.metaClass.getMetaProperty(propName).getProperty(object) if (!currentVal) { object.setProperty(propName, "loaded from service") } return currentVal } // 其他方法直接委托给原始对象 else { return method.invoke(object, args) } } } ) as T }
方案2:CGLIB类代理(适用于无接口的类)
如果目标类没有实现接口,使用CGLIB创建类级别的代理(继承目标类并重写方法),同样能拦截反射调用:
首先确保项目引入CGLIB依赖(如Maven):
<dependency> <groupId>cglib</groupId> <artifactId>cglib</artifactId> <version>3.3.0</version> </dependency>
代理实现代码:
import net.sf.cglib.proxy.Enhancer import net.sf.cglib.proxy.MethodInterceptor import net.sf.cglib.proxy.MethodProxy static <T> T lazyProxyFor(T object) { Class<?> clazz = object.getClass() Enhancer enhancer = new Enhancer() enhancer.setSuperclass(clazz) enhancer.setCallback(new MethodInterceptor() { @Override Object intercept(Object proxy, Method method, Object[] args, MethodProxy methodProxy) throws Throwable { String methodName = method.getName() // 处理getter方法(含反射调用) if (methodName.startsWith("get") && (args == null || args.length == 0)) { String propName = methodName.drop(3).uncapitalize() println "calling getter via reflection/CGLIB: $methodName" def currentVal = clazz.metaClass.getMetaProperty(propName).getProperty(object) if (!currentVal) { object.setProperty(propName, "loaded from service") } return currentVal } // 处理Groovy的getProperty调用 else if (methodName == "getProperty" && args.length == 1 && args[0] instanceof String) { String propName = args[0] println "getting property $propName" def currentVal = clazz.metaClass.getMetaProperty(propName).getProperty(object) if (!currentVal) { object.setProperty(propName, "loaded from service") } return currentVal } // 其他方法调用父类(原始类)实现 else { return methodProxy.invokeSuper(proxy, args) } } }) return enhancer.create() as T }
测试验证
修改测试代码后,无论是Groovy语法调用还是Java反射调用,都会触发懒加载拦截逻辑:
// 假设Address实现了AddressInterface(方案1)或无接口(方案2) Address address = lazyProxyFor(new Address()) // 反射调用:现在会输出拦截日志,加载数据 address.class.declaredMethods.each { method -> if (method.name.startsWith('get')) { method.invoke(address) } } // Groovy调用:依然正常触发拦截 address.street
内容的提问来源于stack exchange,提问作者Brian Bos
相关产品推荐
相关产品推荐

