Springboot如何使用@Async实现异步任务
作者:唐宋xy
前言
在查询大批量的数据的时候,如果需要查询多个表中的数据,或者不仅查询数据库,还需要取其他的系统中查询数据,然后将所有查询到的数据一起返回,这个时候,如果是单线程查询效率慢,这个时候多线程就可以解决这个查询效率慢的问题,Springboot中提供了@Async注解,一键实现异步操作~
实战
一、@Async配置
异步任务配置类
@Configuration @EnableAsync public class AsyncConfig implements AsyncConfigurer { /** * 异步任务线程池配置 * @return */ @Override public Executor getAsyncExecutor() { ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor(); executor.setCorePoolSize(10); // 设置线程初始化数量 executor.setMaxPoolSize(100); // 设置线程池最大数量 executor.setQueueCapacity(100); // 设置等待队列的大小 executor.setThreadNamePrefix("AsyncExecutorThread-"); // 设置线程名称前缀 executor.initialize(); //如果不初始化,导致找到不到执行器 return executor; } /** * 异步任务异常处理 * 如果需要自定义对异步任务的异常进行处理,则自定义异常处理(实现AsyncUncaughtExceptionHandler接口),并在这个方法返回该对象 * @return */ @Override public AsyncUncaughtExceptionHandler getAsyncUncaughtExceptionHandler() { return null; } }
二、实现异步任务
在需要开启异步的方法上面加上注解@Async表示调用该方法的时候,通过线程池获取线程去执行,如果在Class上加@Async则表示该类的所有方法都是异步执行的方法
异步任务调用示例
Controller中调用service中的异步方法更新数据,并直接返回
@Slf4j @Controller @RequestMapping("/api/dashboard") public class DashboardController { @Autowired public DashboardService dashboardService; @RequestMapping(value = "/update", method = RequestMethod.PUT) @ResponseBody public Response update(DashboardLeadsDataVo vo) { dashboardService.update(vo); log.info("主线程执行完毕,返回结果") return new Response(); } }
在Service中直接定义异步方法
@Slf4j @Transactional @Service public class DashboardServiceImpl implements DashboardService { @Autowired private DashboardRepository dashboardRepository; /** * 异步执行更新 * @param vo */ @Async @Override public void update(DashboardLeadsDataVo vo) { log.info("线程:【{}】执行更新【线索数据】异步任务【开始】", Thread.currentThread().getName()); DashboardLeadsData dl = new DashboardLeadsData() BeanUtils.copyProperties(vo, dl); dashboardRepository.update(dl) log.info("线程:【{}】执行更新【线索数据】异步任务【结束】", Thread.currentThread().getName()); } }
这样可以通过日志看到,开启了新的线程去执行@Async注解的方法,主线程在调用完方法之后就直接可以返回了,子线程在异步执行方法。
如果有大量的逻辑运算需要进行,那么就可以开启多个子线程去异步执行,然后主线程直接返回结果,响应速度快。
三、等待所有子线程完成,主线程返回数据
上面的异步任务没有问题,但是如果在查询数据的时候,因为需要查询多个数据,并组装之后返回页面,所以可以开启多个异步任务去执行查询,但是主线程在调用异步任务的方法之后,就直接返回了,子线程还没有执行完成,所以导致返回页面的数据是没有值的。
这个问题需要通过使用到异步任务的返回值:Future接口来实现返回值和主线程等待子线程执行完毕返回
需求:
按照不同的条件查询多个表中的数据,并返回到页面中展示
分析:
因为有多个表,并且查询的数据量比较大,查询时间肯定会比较长,那么如果是单线程执行,页面点击查询的时候,页面白屏或者Loading的时间会比较长,所以需要开启多线程异步去查询,并在多个线程都查询完数据之后主线程统一返回页面渲染
实现:
Controller中直接调用service中的方法 @Controller @RequestMapping("/api/dashboard") public class DashboardController { @Autowired public DashboardService dashboardService; /** * 根据时间查询显示的各种线索量 * @return */ @RequestMapping(value = "/cardsData", method = RequestMethod.GET) @ResponseBody public Response<DashboardDataDto> getCardsData() { DashboardDataDto dto = dashboardService.getCardsDataByDate(); return new Response<>(dto); } }
由service中调用多个异步任务方法查询数据并统一返回Controller
@Slf4j @Transactional @Service public class DashboardServiceImpl implements DashboardService { @Autowired private DashboardRepository dashboardRepository; /** * 查询各种线索量 */ @Override public DashboardDataDto getCardsDataByDate() { List<Future> futureList = new ArrayList<>(); DashboardDataDto dto = new DashboardDataDto(); // 从Spring容器中获取当前类的对象,不能直接调用自身方法,否则@Async注解无效 或者在其他spring管理类中调用 DashboardServiceImpl springProxyBean = SpringUtil.getBean(DashboardServiceImpl.class); // 获取总数据 Future<String> totalFuture = springProxyBean.getTotalCount(dto); futureList.add(totalFuture); // 获取年份数据 Future<String> yearFuture = springProxyBean.getTerritoryYearCount(dto); futureList.add(yearFuture); // 判断异步任务中的所有子线程是否执行完成 // Future接口不仅可以用来判断线程的状态,还可以获取到异步任务执行的返回结果 while (true) { if(futureList.size() > 0) { boolean isAllThreadDone = true; for (Future future : futureList) { // 判断是否所有的线程都已经执行完成 if(future == null || !future.isDone()) { isAllThreadDone = false; } /** 如果需要获取异步任务的返回值,则先判断是否执行完成 * if (future.isDone()) { * future.get(); * } */ } // 都已经执行完成则break if(isAllThreadDone) { break; } } } log.info("主线程结束。。。。。。。"); return dto; } /** * 查询总数据 * @param dto */ @Async public Future<String> getTotalCount(DashboardDataDto dto) { log.info("线程:【{}】执行查询【总数据】异步任务【开始】", Thread.currentThread().getName()); // 查询领界累计线索量 List<Object> totalLeadsCount = dashboardRepository.findTotalLeadsCount(); if(!CollectionUtils.isEmpty(totalLeadsCount)) { dto.setTotalLeadsCount(((Number) totalLeadsCount.get(0)).intValue()); } log.info("线程:【{}】执行查询【总数据】异步任务【结束】", Thread.currentThread().getName()); // 只可以返回Future接口实现类,并且将需要返回的数据传递 return new AsyncResult<>(Thread.currentThread().getName()); } /** * 查询年份领界线索数据量 * @param dto */ @Async public Future<String> getYearCount(DashboardDataDto dto) { log.info("线程:【{}】执行查询【年份数据量】异步任务【开始】", Thread.currentThread().getName()); String currentYearStart = DateUtils.yearStart(0); String nextYearStart = DateUtils.yearStart(1); // 查询年份累计线索量 List<Object> yearsCountList = dashboardRepository.findYearsCountByDate(currentYearStart, nextYearStart); if(!CollectionUtils.isEmpty(yearsCountList)) { dto.setYearsCount(((Number) yearsCountList.get(0)).intValue()); } log.info("线程:【{}】执行查询【年份数据量】异步任务【结束】", Thread.currentThread().getName()); return new AsyncResult<>(Thread.currentThread().getName()); } }
Service中用到的 SpringUtil
如果是自身调用自身的异步方法(如果是调用其他的spring管理类中异步方法,无须通过手动获取该对象,直接调用即可),需要通过Spring的容器中获取到当前类的对象,否则自身调用方法无效,因为没有走spring的代理,@Async注解无法生效
@Component public class SpringUtil implements ApplicationContextAware { private static ApplicationContext applicationContext = null; public static ApplicationContext getApplicationContext() { return applicationContext; } /** * Spring容器启动后,会把 applicationContext 给自动注入进来,然后我们把 applicationContext * 赋值到静态变量中,方便后续拿到容器对象 * @see org.springframework.context.ApplicationContextAware#setApplicationContext(org.springframework.context.ApplicationContext) */ @Override public void setApplicationContext(ApplicationContext applicationContext) throws BeansException { SpringUtil.applicationContext = applicationContext; } @SuppressWarnings("unchecked") public static <T> T getBean(String beanId) { return (T) applicationContext.getBean(beanId); } public static <T> T getBean(Class<T> requiredType) { return (T) applicationContext.getBean(requiredType); } }
调用说明
经测试,主线程的log,会在所有的需要等待的子线程执行完毕之后,才会返回到Controller,返回到页面中的数据就是所有线程已查询出来的数据对象,响应速度加快
TODO
实现多个线程同时开始执行某个任务或者多个线程都同时处于等待状态时开始执行某个任务,可以使用CountDownLatch、CyclicBarrier来实现,可以用来实现等待所有的子线程返回数据,主线程再返回功能,后续再说明~~
四、@ASync无效说明
没有在@SpringBootApplication启动类或者异步任务配置类当中添加注解@EnableAsync注解。
异步方法使用注解@Async的返回值只能为void或者Future。
没有走Spring的代理类。因为@Transactional和@Async注解的实现都是基于Spring的AOP,而AOP的实现是基于动态代理模式实现的。那么注解失效的原因就很明显了,有可能因为调用方法的是对象本身而不是代理对象,因为没有经过Spring容器。
注解的方法必须是public方法,编译时非public方法会报错
如果需要从类的内部调用,需要先获取其代理类
ApplicationContext.getBean(Class);
调用的是静态(static)方法,被@Async修饰的方法不能是static,否则不会生效
总结
以上为个人经验,希望能给大家一个参考,也希望大家多多支持脚本之家。