SpringBoot异步调用

一、Spring框架为我们提供了基于线程池的异步调用支持,用法也很简单。

特别注意:通常调用方法写在contorller类中,而异步执行业务逻辑放在service类中

1.controller方法本身就在servlet容器的线程池中同步执行。
2.若controller方法被标记为异步执行,则这个方法会被提交到非servlet容器线程池。
3.若controller方法为同步执行,而被调用方法又需要异步执行,则被调用方法必须放在另一个类中(如service类),异步方法才会被提交到非servlet容器线程池,否则,被调用方法仍然在servlet容器线程池中与controller方法一起同步执行,违背目的。

更正补充:异步失效的原因,由于调用方法和被调用方法在同一个对象所导致,具体细节可搜索【@Transactional和@Async注解失效】,由于spring中的事务注解和异步注解提供的增强逻辑都是基于动态代理实现的,要想让增强逻辑生效,被调用方法必须来自代理对象。
从代理对象这个点出发,解决上述注解失效的方法:
1. 将被调用方法单独放在一个类里,由spring容器构造代理bean,然后注入使用。
2. 不单独出类,在本类中依赖注入本类的bean(这个bean由spring容器托管,实为代理类的bean),作为成员变量,被调用方法由这个成员变量的bean提供。
3. 显式使用cglib,从AopContext.currentProxy()获取代理对象。

二、使用默认线程池

如果用户未声明异步线程池,spring将开启一个默认线程池来处理异步调用。

  1. 用@EnableAsync注解开启应用的异步调用功能。
@EnableAsync
@Configuration
public class AsyncConfig {
}
  1. 在controller类中给需要异步执行的方法上打上@Async注解,告诉spring此方法需要异步执行(此方法必须可被Overied)。
/**
 * @author: Yang
 * @date: 2020/4/6 15:09
 * @description:
 */
@Slf4j
@RestController
@RequestMapping("/async")
public class AsyncController {

    @Resource
    private AsyncService asyncService;

    /**
     * 无调用关系,无返回值的异步方法
     */
    @Async
    @GetMapping("/sameClass")
    public void sameClass() {
        log.error(Thread.currentThread().getName());
    }

    /**
     * 有调用关系,无返回值的异步调用
     */
    @GetMapping("/noResult")
    public void noResult() {
        this.asyncService.no();
    }

    /**
     * 有调用关系,有返回值的异步调用
     *
     * @return
     */
    @GetMapping("/haveResult")
    public Long haveResult() {
        Long kk = null;
        Future<Long> have = this.asyncService.have();
        if (have.isDone()) {
            try {
                kk = have.get();
            } catch (InterruptedException e) {
                e.printStackTrace();
            } catch (ExecutionException e) {
                e.printStackTrace();
            } finally {
                Thread.interrupted();
            }
        }
        return kk;
    }
}
  1. 在service类中给需要异步执行的方法上打上@Async注解,告诉spring此方法需要异步执行(此方法必须可被Overied)。
@Slf4j
@Component
class AsyncService {
    @Async
    protected void no() {
        log.error(Thread.currentThread().getName());
    }

    @Async
    protected Future<Long> have() {
        log.error(Thread.currentThread().getName());
        log.error("---------{}", Thread.currentThread().getId());
        return new AsyncResult<>(Thread.currentThread().getId());
    }
}
  1. 分别在浏览器访问几次下列接口进行测试:
http://localhost:8081/async/sameClass //控制台打印出线程name【task-1、task-2、task-3】
http://localhost:8081/async/noResult  //控制台打印出线程name【task-1、task-2、task-3】
http://localhost:8081/async/haveResult  //能够得到池中线程id【45,12,44】
注:servlet容器线程池的线程名类似【http-nio-8081-exec-1】

三、使用自定义线程池

  1. 默认线程池的方式使用的是容器的线程池,不能灵活的控制和业务隔离,建议实践中自定义线程池来处理。
  2. 只需另行申明一个自定义线程池,配置类实现AsyncConfigurer接口,异步执行的方法声明无区别。
/**
 * @author: Yang
 * @date: 2020/4/6 14:04
 * @description:
 */
@EnableAsync
@Configuration
public class AsyncConfig implements AsyncConfigurer {

    private static final int MAX_SIZE = 16;
    private static final int QUEUE_SIZE = 16;
    private static final int KEEP_ALIVE_SECOND = 600;
    private static final int AWAIT_TERMINAL_SECOND = 60;
    private static final int CORE_SIZE = Runtime.getRuntime().availableProcessors() * 2;

    @Bean
    @Override
    public Executor getAsyncExecutor() {
        ThreadPoolTaskExecutor pool = new ThreadPoolTaskExecutor();
        pool.setMaxPoolSize(MAX_SIZE);
        pool.setCorePoolSize(CORE_SIZE);
        pool.setQueueCapacity(QUEUE_SIZE);
        pool.setKeepAliveSeconds(KEEP_ALIVE_SECOND);
        pool.setAllowCoreThreadTimeOut(false);
        /*
         *线程池对拒绝任务的处理策略:这里采用了CallerRunsPolicy策略,
         *表示当线程池没有处理能力的时候,该策略会直接在execute方法的调用线程中运行这个任务;
         *如果执行程序已关闭,则会丢弃该任务
         */
        pool.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
        pool.setThreadNamePrefix("async-");
        // 该方法用来设置线程池中任务的等待时间,如果超过这个时候还没有销毁就强制销毁,以确保应用最后能够被关闭,而不是阻塞住。
        pool.setWaitForTasksToCompleteOnShutdown(true);
        pool.setAwaitTerminationSeconds(AWAIT_TERMINAL_SECOND);
        //初始化线程池
        pool.initialize();
        return pool;
    }

    @Override
    public AsyncUncaughtExceptionHandler getAsyncUncaughtExceptionHandler() {
        return null;
    }
}
  1. 分别在浏览器访问几次下列接口进行测试:
http://localhost:8081/async/sameClass //控制台打印出线程name【async----1、async----2、async----3】
http://localhost:8081/async/noResult  //控制台打印出线程name【async----1、async----2、async----3】
http://localhost:8081/async/haveResult  //能够得到池中线程id【45,12,44】

四、多个异步线程池

  1. 有时候一个应用中需要异步调用的业务模块不止一个,建议各业务模块的异步线程池分开配置,异步执行方法分开提交,方便管理和错误排查。
  2. 开启和配置方式都差不多,只是需要在各个自定义线程池上声明池的名称,异步提交执行的方法上也带上对应的池名称。
@Override
@Bean("myAsyncPool")
public Executor getAsyncExecutor() {
    //自定义线程池声明,略
}
@Async("myAsyncPool")
protected void no() {
  //异步执行方法声明,略
}
posted @ 2020-04-06 20:33  JaxYoun  阅读(857)  评论(0编辑  收藏  举报