@Async注解标注private方法时遇到的坑

在Spring中,如果想要异步调用一个方法,可以使用@Async注解,代码如下所示:

@Component
public class JustBean {
    @Async
    public void doAsync() {
        System.out.print("doAsync() in ");
        System.out.println(Thread.currentThread().getName());
    }
}

public class Main {
    public static ApplicationContext applicationContext;
    static {
        applicationContext = new AnnotationConfigApplicationContext(AppConfig.class);
    }
    public static void main(String[] args) {
        JustBean bean = applicationContext.getBean(JustBean.class);
        System.out.println(Thread.currentThread().getName());
        bean.doAsync();
    }
}

执行main方法,输出如下所示,可以看到main和doAsync()在不同的线程中执行。

// 输出:
main
doAsync() in pool-1-thread-1

这里在main中获取了JustBean的代理对象,调用的是代理对象上的doAsync()方法。

而如果想在一个JustBean的方法中,异步调用同一个类下的另一个方法,像下面这种方式直接调用,是行不通的:

@Component
public class JustBean {
    public void invokeDoAsync() {
        System.out.print("invokeDoAsync() in ");
        System.out.println(Thread.currentThread().getName());
        doAsync(); // 调用被代理对象本身的方法,未经过AOP
    }
    @Async
    public void doAsync() {
        System.out.print("doAsync() in ");
        System.out.println(Thread.currentThread().getName());
    }
}

public class Main {
    //...
    public static void main(String[] args) {
        JustBean bean = applicationContext.getBean(JustBean.class);
        System.out.println(Thread.currentThread().getName());
        bean.invokeDoAsync();
    }
}
// 输出:
main
invokeDoAsync() in main
doAsync() in main

可以看到三个方法都在main线程中执行。因为这里invokeDoAsync()对doSync()的调用并没有通过代理对象,从而没有执行切面的逻辑。

使用@Transactional时,也应注意不要像这样调用到被代理对象本身的方法。

我们可以使用ApplicationContext.getBean()方法得到代理对象,然后再对其进行调用,即可达到预期效果:

@Component
public class JustBean {
    public void invokeDoAsync() {
        System.out.print("invokeDoAsync() in ");
        System.out.println(Thread.currentThread().getName());
        Main.applicationContext.getBean(JustBean.class).doAsync(); // 获取代理对象并调用其方法
    }
    @Async
    public void doAsync() {
        System.out.print("doAsync() in ");
        System.out.println(Thread.currentThread().getName());
    }
}
// 输出:
main
invokeDoAsync() in main
doAsync() in pool-1-thread-1

事情看起来到这就结束了,但这里还有一个小坑:

首先,这里我们的JustBean类没有实现接口,Spring会使用CGLib为其创建一个子类作为代理对象。

代理对象中会持有被代理对象的引用,并且重写方法,其中会执行一些切面的逻辑,并且调用被代理对象自身的方法。

上面的代码中,doAsync()方法是public的,调用时,先执行代理对象中生成的切面逻辑,然后调用被代理对象的doAsync(),这不会有什么问题。

而对于private方法,由于子类无法访问,也就无法进行代理。

通过这里的方法,找到了生成的代理类字节码,发现里面也没有对父类的private字段与方法进行覆盖。

然而,在Java编译器的视角,代理类就是其父类的一个对象,通过静态绑定,可以访问private方法,如下所示:

public void invokeDoAsync() {
      System.out.print("invokeDoAsync() in ");
      System.out.println(Thread.currentThread().getName());
      Main.applicationContext.getBean(JustBean.class).doAsync(); // 代理类中没有这个方法,但依然能调用
  }

@Async
private void doAsync() {
      System.out.print("doAsync() in ");
      System.out.println(Thread.currentThread().getName());
}
// 输出:
main
invokeDoAsync() in main
doAsync() in main

父类的方法就是原始方法,所以这里异步逻辑也不生效。

可以推断,这里doAsync()中的this对象是代理类对象,而在public版本中是被代理对象。

这里就会有个问题,代理类在private方法中对字段的访问全部为null,因为代理类中根本没有这些字段:

@Component
public class JustBean {
    @Autowired
    private JustAnotherBean justAnotherBean;

    public void invokeDoAsync() {
        System.out.println("被代理对象的this: " + System.identityHashCode(this));
        System.out.println("被代理对象的this.justAnotherBean: " + this.justAnotherBean);
        System.out.print("invokeDoAsync() in ");
        System.out.println(Thread.currentThread().getName());
        Main.applicationContext.getBean(JustBean.class).doAsync();
    }
    @Async
    private void doAsync() {
        System.out.println("代理对象的this: " + System.identityHashCode(this));
        System.out.println("代理对象的this.justAnotherBean: " + this.justAnotherBean); // null
        System.out.print("doAsync() in ");
        System.out.println(Thread.currentThread().getName());
    }
}
// 输出:
main
被代理对象的this: 2114684409
被代理对象的this.justAnotherBean: com.async.JustAnotherBean@63355449
invokeDoAsync() in main
代理对象的this: 154482552
代理对象的this.justAnotherBean: null
doAsync() in main

注意:代理类重写了toString(),如果直接输出this,会发现代理对象和被代理对象输出的结果相同,所以这里使用System.identityHashCode()进行区分。

综上,不要使用@Async修饰private方法,首先异步会失效,其次会有访问字段为null,从而NPE的风险。

posted @ 2023-05-14 03:33  zaqny  阅读(329)  评论(0编辑  收藏  举报