Java SpringBoot
    在实际的项目中,对于一些用时比较长的代码片段或者函数,可以采用异步的方式来执行,这样就不会影响整体的流程了。比如在一个用户请求中需要上传一些文件,但是上传文件的耗时会相对来说比较长,这个时候如果上传文件的成功与否不影响主流程的话,就可以把上传文件的操作异步化,在SpringBoot中比较常见的方式就是把要异步执行的代码片段封装成一个函数,然后在函数头使用@Async注解,就可以实现代码的异步执行(当然首先得在启动类上加上@EnableAsync注解了)。
    具体的使用方式就不再演示了,网上教大家使用@Async的很多。在实际开发过程中遇到的一个坑分享一下,
    那么这个坑是什么呢?就是如果在同一个类里面调用一个自己的被@Async修饰的函数时,这个函数将不会被异步执行,它依然是同步执行的!所以如果没有经过测试就想当然的以为只要在方法头加上@Async就能达到异步的效果,那么很有可能会得到相反的效果。这个是很要命的。
    首先先看一个正确使用的方式,建一个SpringBoot项目,如果是用Intellij IDEA新建的项目,记得勾上web的依赖。
    项目建好后,在启动类上加上@EnableAsync注解:

    1. import org.springframework.boot.SpringApplication;
    2. import org.springframework.boot.autoconfigure.SpringBootApplication;
    3. import org.springframework.scheduling.annotation.EnableAsync;
    4. @SpringBootApplication
    5. @EnableAsync
    6. public class AsyncdemoApplication {
    7. public static void main(String[] args) {
    8. SpringApplication.run(AsyncdemoApplication.class, args);
    9. }
    10. }

    然后再新建一个类Task,用来放三个异步任务doTaskOne、doTaskTwo、doTaskThree:

    1. import org.springframework.scheduling.annotation.Async;
    2. import org.springframework.stereotype.Component;
    3. import java.util.Random;
    4. /**
    5. * @author https://www.chuckfang.top
    6. * @date Created on 2019/11/12 11:34
    7. */
    8. @Component
    9. public class Task {
    10. public static Random random = new Random();
    11. @Async
    12. public void doTaskOne() throws Exception {
    13. System.out.println("开始做任务一");
    14. long start = System.currentTimeMillis();
    15. Thread.sleep(random.nextInt(10000));
    16. long end = System.currentTimeMillis();
    17. System.out.println("完成任务一,耗时:" + (end - start) + "毫秒");
    18. }
    19. @Async
    20. public void doTaskTwo() throws Exception {
    21. System.out.println("开始做任务二");
    22. long start = System.currentTimeMillis();
    23. Thread.sleep(random.nextInt(10000));
    24. long end = System.currentTimeMillis();
    25. System.out.println("完成任务二,耗时:" + (end - start) + "毫秒");
    26. }
    27. @Async
    28. public void doTaskThree() throws Exception {
    29. System.out.println("开始做任务三");
    30. long start = System.currentTimeMillis();
    31. Thread.sleep(random.nextInt(10000));
    32. long end = System.currentTimeMillis();
    33. System.out.println("完成任务三,耗时:" + (end - start) + "毫秒");
    34. }
    35. }

    在单元测试类上注入Task,在测试用例上测试这三个方法的执行过程:

    1. @SpringBootTest
    2. class AsyncdemoApplicationTests {
    3. public static Random random = new Random();
    4. @Autowired
    5. Task task;
    6. @Test
    7. void contextLoads() throws Exception {
    8. task.doTaskOne();
    9. task.doTaskTwo();
    10. task.doTaskThree();
    11. Thread.sleep(10000);
    12. }
    13. }

    为了让这三个方法执行完,需要再单元测试用例上的最后一行加上一个延时,不然等函数退出了,异步任务还没执行完。
    启动看看效果:

    1. 开始做任务三
    2. 开始做任务二
    3. 开始做任务一
    4. 完成任务一,耗时:4922毫秒
    5. 完成任务三,耗时:6778毫秒
    6. 完成任务二,耗时:6960毫秒

    可以看到三个任务确实是异步执行的,那再看看错误的使用方法。
    可以在测试类里面把这三个函数再写一遍,并在测试用例上调用测试类自己的方法:

    1. @SpringBootTest
    2. class AsyncdemoApplicationTests {
    3. public static Random random = new Random();
    4. @Test
    5. void contextLoads() throws Exception {
    6. doTaskOne();
    7. doTaskTwo();
    8. doTaskThree();
    9. Thread.sleep(10000);
    10. }
    11. @Async
    12. public void doTaskOne() throws Exception {
    13. System.out.println("开始做任务一");
    14. long start = System.currentTimeMillis();
    15. Thread.sleep(random.nextInt(10000));
    16. long end = System.currentTimeMillis();
    17. System.out.println("完成任务一,耗时:" + (end - start) + "毫秒");
    18. }
    19. @Async
    20. public void doTaskTwo() throws Exception {
    21. System.out.println("开始做任务二");
    22. long start = System.currentTimeMillis();
    23. Thread.sleep(random.nextInt(10000));
    24. long end = System.currentTimeMillis();
    25. System.out.println("完成任务二,耗时:" + (end - start) + "毫秒");
    26. }
    27. @Async
    28. public void doTaskThree() throws Exception {
    29. System.out.println("开始做任务三");
    30. long start = System.currentTimeMillis();
    31. Thread.sleep(random.nextInt(10000));
    32. long end = System.currentTimeMillis();
    33. System.out.println("完成任务三,耗时:" + (end - start) + "毫秒");
    34. }
    35. }

    再看看效果

    1. 开始做任务一
    2. 完成任务一,耗时:9284毫秒
    3. 开始做任务二
    4. 完成任务二,耗时:8783毫秒
    5. 开始做任务三
    6. 完成任务三,耗时:943毫秒

    它们竟然是顺序执行的!也就是同步执行,并没有达到异步的效果,这要是在生产上使用,岂不凉凉。
    这种问题如果不进行测试还是比较难发现的,特别是想要异步执行的代码并不会执行太久,也就是同步执行也察觉不出来,或者说根本发现不了它是不是异步执行。这种错误也很容易犯,特别是把一个类里面的方法提出来想要异步执行的时候,并不会想着新建一个类来放这个方法,而是会在当前类上直接抽取为一个方法,然后在方法头上加上@Async注解,以为这样就完事了,其实并没有起到异步的作用!
    其实@Async的这个性质在官网上已经有过说明了,官网:https://www.baeldung.com/spring-async是这样说的:

    First – let’s go over the rules – @Async has two limitations:

    • it must be applied to public methods only
    • self-invocation – calling the async method from within the same class – won’t work

    The reasons are simple – 「the method needs to be public so that it can be proxied. And 「self-invocation doesn’t work」 because it bypasses the proxy and calls the underlying method directly.

    在一开始就提到了@Async的两个限制,其中第二个就是调用自己类上的异步方法是不起作用的。下面也讲了原因,就是这种使用方式绕过了代理而直接调用了方法,所以肯定是同步的了。从这里,也知道了另外一个知识点,就是@Async注解其实是通过代理的方式来实现异步调用的。