动态代理导致注解未生效
今天在使用mybatis-plus
作者苞米豆的另一个项目lock4j
用于项目中的分布式锁,解决多实例情况下对接口进行上锁,使得业务上的共享资源在一个时间节点里相互竞争时只会被一个线程获取到资源。
lock4j
和mybatis-plus
差不多,秉承着人性化使用的初衷,lock4j
使用上还是非常简单的,只需要在需要上锁的接口方法上使用@Lock4j
进行标记并设置一些简单的超时参数,默认情况下使用全路径限定+方法名作为上锁的key
,支持自定义上锁key
,通过实现LockKeyBuilder
接口来自定义key
。
使用中发现有一个需要上锁的接口是一个私有方法(原本单机情况下,采用ReentrantLock
),所以改造时仅仅只是对改接口上添加@Lock4j
注解。
public void saveInfo(String userId){
// ..业务逻辑
this.finalSave(userId);
}
@Lock4j(keys = {"#userId"}, expire = "100", acquireTimeout = "100")
private void finalSave(String userId) {
checkInfo(..);
mapper.insert(..);
}
跑起来测试后发现根本没有进入到对应@Lock4j
处理的代理方法中,马上反应到这不是和常见@Transaction
注解标记的坑一样,不能使用this.xxx
的方式调用,只能通过对象调用的方式来调用,修改代码后再次执行,果然正常上锁,那么为什么this.xxx
会导致异常?
为了知其所以然,不再盲猜硬记,我决定结合@Transaction
注解去了解底层的机制。
在spring
中的aop
说白了就是通过动态代理实现,而动态代理有两种实现方式(jdk
动态代理和cglib
动态代理)。这里简单模拟一下两种动态代理的使用。
创建一个顶层接口,简单定义两个方法插入用户信息和获取用户信息。
public interface UserFacade {
void insertUserInfo();
void getUserInfo();
}
具体实现:在获取用户信息方法上使用模拟注解标记,且在插入用户信息的方法中通过this
的方式调用获取用户信息的方法。
public class UserService implements UserFacade{
@Override
public void insertUserInfo() {
getUserInfo();
log.info("插入用户信息");
}
@MockAnnotation
@Override
public void getUserInfo() {
log.info("查询图书信息");
}
}
模拟注解:创建一个注解用来模拟注解标记的场景。
public @interface MockAnnotation {
}
1. jdk动态代理
在动态拦截的接口invoke
中判断接口是否标记模拟注解MockAnnotation
,若标记了该注解,则进行相应的前后置业务逻辑的处理。
public class UserFacadeJdkProxy implements InvocationHandler {
private Object target;
public Object getProxy(Object target){
this.target = target;
// ... jdk动态代理的创建(省略)
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
Annotation annotation = target.getClass()
.getDeclaredMethod(method.getName(), method.getParameterTypes())
.getAnnotation(MockAnnotation.class);
if(ObjectUtils.isEmpty(annotation)){
return method.invoke(target, args);
}
log.info("前置处理");
Object result = method.invoke(target, args);
log.info("后置处理");
return result;
}
}
测试:通过代理对象调用插入用户信息接口,这时是不会打印拦截方法中的"前置处理"和"后置处理"字眼。
public static void main(String[] args) {
UserFacadeJdkProxy proxy = new UserFacadeJdkProxy();
UserFacade userFacade = (UserFacade) proxy.getProxy(new UserService());
userFacade.insertUserInfo();
}
执行结果:
进入调用方法 查询图书信息 插入用户信息
2.cglib动态代理
public class UserFacadeCglibProxy implements MethodInterceptor {
private Object target;
public Object getProxy(Object target){
this.target = target;
// cglib 动态代理的创建(省略)
}
@Override
public Object intercept(Object o, Method method, Object[] args, MethodProxy methodProxy) throws Throwable {
log.info("进入调用方法");
Annotation annotation = target.getClass().getDeclaredMethod(method.getName(), method.getParameterTypes())
.getAnnotation(MockAnnotation.class);
if(ObjectUtils.isEmpty(annotation)){
return methodProxy.invoke(target, args);
}
log.info("前置处理");
Object result = methodProxy.invoke(target, args);
//Object result = methodProxy.invokeSuper(o, args);
log.info("后置处理");
return result;
}
}
测试:通过代理对象调用插入用户信息接口,这时同样不会打印"前置处理"和"后置处理"字眼。所以不论是通过JDK
动态代理还是通过cglib
动态代理,使用this
方式的调用,都不会进入到对应的拦截方法中。
public static void main(String[] args) {
UserFacadeCglibProxy proxy = new UserFacadeCglibProxy();
UserFacade userFacade = (UserFacade)proxy.getProxy(new UserService());
userFacade.insertUserInfo();
}
执行结果: 进入调用方法 查询图书信息 插入用户信息
由以上可得出结论:不论是通过哪一种动态代理实现AOP
,使用this.xxx
的写法都无法使得注解生效。且如果注解标记的方法为final
或者是private
方法也是不能进入代理方法,原因是jdk
动态代理是基于接口代理、cglib
动态代理是基于继承的方式,不论是那种方式的动态代理的代理对象其实都是无法进入target
的私有方法和final
方法。所以根据以上动态代理可以得出结论,通过this.xxx
的方式调用本类接口是通过被代理对象直接调用本类接口,而不是通过代理对象,也就无法进入对应的invoke
方法或者是intercept
方法,从而无法解析到对应的注解,如果觉得将this.xxx
修改为A
类调用B
类,代码需要被迫转移到其他类影响理解,在实际中可以通过ApplicationContextUtil
的方式获取一次代理对象。将this
调用修改为以下写法。
UserFacade userFacade = ApplicationContextUtil.getBean("userService");
userFacade.getUserInfo();
但是,好玩的来了,注意看UserFacadeCglibProxy
类中有两行注释,采用的是methodProxy.invokeSuper(o, args);
的方式调用,运行之后可以发现通过this.xxx
的方式竟然可以进入invoice()
方法。
Task :UserFacadeCglibProxy.main() 进入调用方法 进入调用方法 前置处理 查询图书信息 后置处理 插入用户信息
这边简单对这两个调用进行一个区别:
使用invoke()
方法的整个执行过程为:
- 客户端调用了代理对象的
insertUserInfo()
方法 - 进入代理对象的
intercept
方法 - 通过
methodProxy.invoke(target, args)
执行被代理对象的insertUserInfo()
- 这时的
this.getUserInfo()
中的this
是被代理对象,所以调用时不会触发intercept
方法 - 调用结束
使用invokeSuper()
方法的整个执行过程为:
- 客户端调用了代理对象
insertUserInfo()
方法 - 进入代理对象的
intercept
方法,进行拦截业务逻辑处理 - 通过
methodProxy.invokeSuper(o, args)
进入被代理对象的insertUserInfo()
- 这时的
this.getUserInfo()
中的this
是代理对象,所以getUserInfo()
会再次触发intercept()
- 进入被代理对象的
getUserInfo()
- 调用结束
所以最终作怪的是this
,使用者需要明确知道这个this
代表的到底是代理对象(proxy
)还是被代理对象(target
)。
转载自:https://juejin.cn/post/7060062710213378061