likes
comments
collection
share

动态代理导致注解未生效

作者站长头像
站长
· 阅读数 12

​ 今天在使用mybatis-plus作者苞米豆的另一个项目lock4j用于项目中的分布式锁,解决多实例情况下对接口进行上锁,使得业务上的共享资源在一个时间节点里相互竞争时只会被一个线程获取到资源。

lock4jmybatis-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()方法的整个执行过程为:

  1. 客户端调用了代理对象的insertUserInfo()方法
  2. 进入代理对象的intercept方法
  3. 通过methodProxy.invoke(target, args)执行被代理对象的insertUserInfo()
  4. 这时的this.getUserInfo()中的this是被代理对象,所以调用时不会触发intercept方法
  5. 调用结束

使用invokeSuper()方法的整个执行过程为:

  1. 客户端调用了代理对象insertUserInfo()方法
  2. 进入代理对象的intercept方法,进行拦截业务逻辑处理
  3. 通过methodProxy.invokeSuper(o, args)进入被代理对象的insertUserInfo()
  4. 这时的this.getUserInfo()中的this是代理对象,所以getUserInfo()会再次触发intercept()
  5. 进入被代理对象的getUserInfo()
  6. 调用结束

所以最终作怪的是this,使用者需要明确知道这个this代表的到底是代理对象(proxy)还是被代理对象(target)。

转载自:https://juejin.cn/post/7060062710213378061
评论
请登录