第一部分: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调用的示例。
我们需要完成:
- 做一个提供测试测试数据的 controller。
- 创建一个异步的 service,远程调用上面的测试数据 controller。
- 创建一个 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}毫秒"}