第一部分:SpringBoot异步任务实现方法



异步任务使用场景

在很多情况下,比如邮件发送,文件上传或者下载的时候,由于时间较长,经常会出现用户需要很长时间才会得到响应,为了优化用户体验,比如先进行页面跳转,显示已经开始进行邮件发送或者下载上传文件等信息,等任务结束再返回成功信息,这就采取了异步的方式进行任务调度。



异步任务的实现

首先我们可以模拟一个网站跳转的过程,假设某一个线程执行某个任务需要3秒,结束以后才会进行下一步,我们令线程休眠三秒,然后通过controller进行页面跳转,代码如下:

Service层

@Service
public class AsyncService {
    
    public void hello(){
        try {
            Thread.sleep(3000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("数据正在处理");
    }
}

Controller层

@RestController
public class AsyncController {

    @Autowired
    private AsyncService asyncService;

    @RequestMapping("/hello")
    public String hello(){
        asyncService.hello();
        return "OK";
    }
}

这时用户点击需要等待三秒才会跳转到新页面并显示OK,此时由于用户体验极差,大概率下用户会直接关闭该网页,因此需要通过多线程的方式来实现异步任务调度,而SpringBoot则可以使用更简便的方式来实现异步任务调度。我们只需要在Service层需要多线程处理的方法上加上@Async注解。

@Service
public class AsyncService {
    @Async
    public void hello(){
        try {
            Thread.sleep(3000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("数据正在处理");
    }
}

然后在主启动类上加上**@EnableAsync**注解来开启异步注解功能即可执行异步任务调度,此时执行可立即跳转然后再执行hello方法来输出“数据正在处理”。

//开启异步注解功能
@EnableAsync
@SpringBootApplication
public class SpringbootTestApplication {

    public static void main(String[] args) {
        SpringApplication.run(SpringbootTestApplication.class, args);
    }

}

 



第二部分:Spring Boot实现异步调用(多线程)

启动加上@EnableAsync,需要执行的异步方法上加上@Async   @Async实际上就是多线程封装的
  使用场景例如,发送短信验证码
  异步线程执行方法有可能会非常消耗CPU资源,所以大的项目建议使用MQ异步实现
  失效问题:如果异步注解写成当前自己类,有可能aop会失效,无法拦截注解,最终导致异步失效,需要经过代理类调用接口,所以需要将异步的代码单独抽取成一个类调用接口。



多线程使用示例(不使用注解),不建议使用
package com.zq.controller;
 
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
 
import lombok.extern.slf4j.Slf4j;
 
@RestController
@Slf4j
public class Myxiancheng {
 
    @RequestMapping("/d1xiancheng")
    public String d1xiancheng() {
        log.info("======1=======");
 
        // 发送短信
        // 单线程
        // sms();
        // 多线程
        new Thread(new Runnable() {
 
            @Override
            public void run() {
                sms();
            }
        }).start();
 
        log.info("======4=======");
        return "我是返回结果";
    }
 
    public String sms() {
        log.info("======2=======");
        try {
            log.info("正在发送短信======");
            Thread.sleep(3000);
        } catch (Exception e) {
 
        }
        log.info("======3=======");
        return "短信发送完成";
    }
}



多线程使用示例(使用注解)

失效问题:如果异步注解写成当前自己类,有可能aop会失效,无法拦截注解,最终导致异步失效,需要经过代理类调用接口,所以需要将异步的代码单独抽取成一个类调用接口(不要在一个包中)。异步方法和controller不要在一个包中

线程方法类

package com.zq.async;
 
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Component;
 
import lombok.extern.slf4j.Slf4j;
 
@Component
@Slf4j
public class MyAsync {
    @Async
    public String sms() {
        log.info("======2=======");
        try {
            log.info("正在发送短信======");
            Thread.sleep(3000);
        } catch (Exception e) {
 
        }
        log.info("======3=======");
        return "短信发送完成";
    }
}

调用controller类

package com.zq.controller;
 
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
 
import com.zq.async.MyAsync;
 
import lombok.extern.slf4j.Slf4j;
 
@RestController
@Slf4j
public class Myxiancheng {
 
    @Autowired
    private MyAsync myAsync;
 
    @RequestMapping("/d1xiancheng")
    public String d1xiancheng() {
        log.info("======1=======");
 
        // 发送短信
        myAsync.sms();
 
        log.info("======4=======");
        return "我是返回结果";
    }
 
}

启动类

package com.zq;
 
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.scheduling.annotation.EnableAsync;
 
@SpringBootApplication
@EnableAsync // 开启异步注解
@MapperScan("com.zq.mapper") // 默认不会扫描mapper,需要此注解指定
public class App {
    public static void main(String[] args) {
        SpringApplication.run(App.class, args);
    }
 
}



整合线程池

不建议频繁创建线程,频繁的创建线程效率非常低,所以使用线程池

创建线程池配置类

package com.zq.config;
 
import java.util.concurrent.ThreadPoolExecutor;
 
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.task.TaskExecutor;
import org.springframework.scheduling.annotation.EnableAsync;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
 
@Configuration
@EnableAsync
public class ThreadPoolConfig {
 
    /**
     * 每秒需要多少个线程处理
     * tasks/(1/taskcost)
     */
    private int corePoolSize = 3;
     
     
    /**
     * 线程池维护线程的最大数量
     * (max(tasks)- queueCapacity)/(1/taskcost)
     */
    private int maxPoolSize = 3;
     
    /**
     * 缓存队列
     * (coreSizePool/taskcost)*responsetime
     */
    private int queueCapacity = 10;
     
    /**
     * 允许的空闲时间
     * 默认为60
     */
    private int keepAlive = 100;
     
    @Bean
    public TaskExecutor taskExecutor() {
        ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
        //设置核心线程数
        executor.setCorePoolSize(corePoolSize);
        //设置最大线程数
        executor.setMaxPoolSize(maxPoolSize);
        //设置队列容量
        executor.setQueueCapacity(queueCapacity);
        //设置允许的空闲时间(秒)
        //executor.setKeepAliveSeconds(keepAlive);
        //设置默认的线程名称
        executor.setThreadNamePrefix("thread-");
        //设置拒绝策略rejection-policy:当pool已经达到max size的时候,如何处理新任务
        executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
        executor.setWaitForTasksToCompleteOnShutdown(true);
        return executor;
    }
}

使用线程池

package com.zq.async;
 
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Component;
 
import lombok.extern.slf4j.Slf4j;
 
@Component
@Slf4j
public class MyAsync {
    @Async("taskExecutor")  //使用taskExecutor线程池
    public String sms() {
        log.info("======2=======");
        try {
            log.info("正在发送短信======");
            Thread.sleep(3000);
        } catch (Exception e) {
 
        }
        log.info("======3=======");
        return "短信发送完成";
    }
}

 



第三部分:SpringBoot 异步 controller 示例


下面的示例是在 springboot 的 controller 中整合异步service调用的示例。

我们需要完成:

  1. 做一个提供测试测试数据的 controller。
  2. 创建一个异步的 service,远程调用上面的测试数据 controller。
  3. 创建一个 controller,调用多个异步 service,并等待异步调用全部完成,输出结果。


1. 测试数据 controller


package com.example.demoasync;

import java.util.ArrayList;
import java.util.List;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class EmployeeDataController {
    private static Logger log = LoggerFactory.getLogger(EmployeeDataController.class);

    @RequestMapping(value = "/addresses", method = RequestMethod.GET)
    public List<String> getAddresses() {
        log.info("get addresses Start");

        ArrayList<String> employeeAddresses = new ArrayList<String>();
        employeeAddresses.add("addr1");
        employeeAddresses.add("addr2");
        employeeAddresses.add("addr3");

        return employeeAddresses;
    }

    @RequestMapping(value = "/phones", method = RequestMethod.GET)
    public List<String> getPhoneNumbers() {
        log.info("get phones Start");

        ArrayList<String> phoneNumberList = new ArrayList<String>();

        phoneNumberList.add("100000");
        phoneNumberList.add("200000");

        return phoneNumberList;
    }

    @RequestMapping(value = "/names", method = RequestMethod.GET)
    public List<String> getEmployeeName() {
        log.info("get names Start");

        List<String> employeeList = new ArrayList<String>();

        employeeList.add("Santa");
        employeeList.add("Banta");

        return employeeList;
    }
}



2. 异步 service

package com.example.demoasync;

import java.util.List;
import java.util.concurrent.CompletableFuture;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Service;
import org.springframework.web.client.RestTemplate;

@Service
public class AsyncService {

    private static Logger log = LoggerFactory.getLogger(AsyncService.class);

    @Autowired
    private RestTemplate restTemplate;

    @Bean
    public RestTemplate restTemplate() {
        return new RestTemplate();
    }

    @Async("asyncExecutor")
    public CompletableFuture<List<String>> getEmployeeName() throws InterruptedException {
        log.info("getEmployeeName starts");

        List<String> employeeNameData = restTemplate.getForObject("http://localhost:8080/names", List.class);

        log.info("employeeNameData, {}", employeeNameData);
        Thread.sleep(1000L); // Intentional delay
        log.info("employeeNameData completed");
        return CompletableFuture.completedFuture(employeeNameData);
    }

    @Async("asyncExecutor")
    public CompletableFuture<List<String>> getEmployeeAddress() throws InterruptedException {
        log.info("getEmployeeAddress starts");

        List<String> employeeAddressData = restTemplate.getForObject("http://localhost:8080/addresses", List.class);

        log.info("employeeAddressData, {}", employeeAddressData);
        Thread.sleep(1000L); // Intentional delay
        log.info("employeeAddressData completed");
        return CompletableFuture.completedFuture(employeeAddressData);
    }

    @Async("asyncExecutor")
    public CompletableFuture<List<String>> getEmployeePhone() throws InterruptedException {
        log.info("getEmployeePhone starts");

        List<String> employeePhoneData = restTemplate.getForObject("http://localhost:8080/phones", List.class);

        log.info("employeePhoneData, {}", employeePhoneData);
        Thread.sleep(1000L); // Intentional delay
        log.info("employeePhoneData completed");
        return CompletableFuture.completedFuture(employeePhoneData);
    }
}



3. controller

package com.example.demoasync;

import java.util.List;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutionException;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class AsyncController {
 
    private static Logger log = LoggerFactory.getLogger(AsyncController.class);
 
    @Autowired
    private AsyncService service;
 
    @RequestMapping(value = "/testAsynch", method = RequestMethod.GET)
    public void testAsynch() throws InterruptedException, ExecutionException
    {
        log.info("testAsynch Start");
 
        CompletableFuture<List<String>> employeeAddress = service.getEmployeeAddress();
        CompletableFuture<List<String>> employeeName = service.getEmployeeName();
        CompletableFuture<List<String>> employeePhone = service.getEmployeePhone();
 
        // 等待每个异步调用都完成
        CompletableFuture.allOf(employeeAddress, employeeName, employeePhone).join();
         
        log.info("EmployeeAddress--> " + employeeAddress.get());
        log.info("EmployeeName--> " + employeeName.get());
        log.info("EmployeePhone--> " + employeePhone.get());
    }
}

 



第四部分:SpringBoot异步任务, 以及带返回值的异步任务(@Async 不起作用的原因)



第一部分: 无返回值异步任务

 



第二部分:有返回值的异步任务

返回值用Futrue变量封装起来,下面是service层的代码

@Async
    public Future<String> doReturn(int i){
        try {
            // 这个方法需要调用500毫秒
            Thread.sleep(500);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        // 消息汇总
        return new AsyncResult<>(String.format("这个是第{%s}个异步调用的证书", i));
    }

读取的时候,记得要批量读取不能单独读取, 下面是controller层的代码

@GetMapping("/hi")
    public Map<String, Object> testAsyncReturn() throws ExecutionException, InterruptedException {
        long start = System.currentTimeMillis();
 
        Map<String, Object> map = new HashMap<>();
        List<Future<String>> futures = new ArrayList<>();
        for (int i = 0; i < 10; i++) {
            Future<String> future = asyncService.doReturn(i);
            futures.add(future);
        }
        List<String> response = new ArrayList<>();
        for (Future future : futures) {
            String string = (String) future.get();
            response.add(string);
        }
        map.put("data", response);
        map.put("消耗时间", String.format("任务执行成功,耗时{%s}毫秒", System.currentTimeMillis() - start));
        return map;
    }

结果如下: 耗时500多毫秒的意思代表,springboot自带异步任务线程池是小于10的大小的

{"data":["这个是第{0}个异步调用的证书","这个是第{1}个异步调用的证书","这个是第{2}个异步调用的证书","这个是第{3}个异步调用的证书","这个是第{4}个异步调用的证书","这个是第{5}个异步调用的证书","这个是第{6}个异步调用的证书","这个是第{7}个异步调用的证书","这个是第{8}个异步调用的证书","这个是第{9}个异步调用的证书"],"消耗时间":"任务执行成功,耗时{508}毫秒"}



重要的事情说三遍: 一定要批量读取结果, 否则不能达到异步的效果!! 一定要批量读取结果, 否则不能达到异步的效果!! 一定要批量读取结果, 否则不能达到异步的效果!!

1、异步方法和调用类不要在同一个类中

2、注解扫描时,要注意过滤,避免重复实例化,因为存在覆盖问题,@Async就失效了

否则效果如下:

{"data":["这个是第{0}个异步调用的证书","这个是第{1}个异步调用的证书","这个是第{2}个异步调用的证书","这个是第{3}个异步调用的证书","这个是第{4}个异步调用的证书","这个是第{5}个异步调用的证书","这个是第{6}个异步调用的证书","这个是第{7}个异步调用的证书","这个是第{8}个异步调用的证书","这个是第{9}个异步调用的证书"],"消耗时间":"任务执行成功,耗时{5012}毫秒"}