Spring的@Async
annotation 可令方法為非同步(asynchronous)執行。
範例環境:
- Java 8
- Spring Boot 2.2.4.RELEASE
- Maven
- Lombok
建立一個Spring Boot專案(IntelliJ IDEA、Eclipse STS)。
application.properties
設定如下。
application.properties
server.servlet.context-path=/demo
server.port=8080
所以應用程式路徑為http://localhost:8080/demo/
。
新增配置類別AsyncConfig
,類別名稱前加上@EnableAsync
啟用非同步方法功能。
並在配置類中配置一個名稱為executor
的Bean。Bean的實例ThreadPoolTaskExecutor
做為@Async
使用的Executor
。若不設定executor則Spring預設使用SimpleAsyncTaskExecutor
來執行非同步方法。
AsyncConfig
package com.abc.demo.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.annotation.EnableAsync;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
import java.util.concurrent.Executor;
@EnableAsync
@Configuration
public class AsyncConfig {
@Bean(name = "executor")
public Executor executor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(3);
executor.setMaxPoolSize(3);
executor.setQueueCapacity(100);
executor.setThreadNamePrefix("async-");
executor.initialize();
return executor;
}
}
新增傳遞資料用的DTO物件。注意本範例使用Lombok生成建構式及getter setter。
BasicInfoDto
package com.abc.demo.dto;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.util.List;
@Data
@NoArgsConstructor
@AllArgsConstructor
public class BasicInfoDto {
private List<String> employeeNameList;
private List<String> departmentNameList;
}
DepartmentDto
package com.abc.demo.dto;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@NoArgsConstructor
@AllArgsConstructor
public class DepartmentDto {
private Long id;
private String name;
}
DepartmentDataDto
package com.abc.demo.dto;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.util.List;
@Data
@NoArgsConstructor
@AllArgsConstructor
public class DepartmentDataDto {
private List<DepartmentDto> departmentDtoList;
}
EmployeeDto
package com.abc.demo.dto;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@NoArgsConstructor
@AllArgsConstructor
public class EmployeeDto {
private Long id;
private String name;
}
EmployeeDataDto
package com.abc.demo.dto;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.util.List;
@Data
@NoArgsConstructor
@AllArgsConstructor
public class EmployeeDataDto {
private List<EmployeeDto> employeeDtoList;
}
新增DemoController
及EmployeeContrller
模擬外部API來顯示非同步取得資料的效果。
APIhttp://localhost:8080/demo/department
取得部門資料。
DepartmentController
package com.abc.demo.controller;
import com.abc.demo.dto.DepartmentDataDto;
import com.abc.demo.dto.DepartmentDto;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.Arrays;
import java.util.List;
@RestController
public class DepartmentController {
@GetMapping("/department")
public DepartmentDataDto getDepartmentData() {
List<DepartmentDto> departmentDtoList = Arrays.asList(
new DepartmentDto(1L, "Marketing"),
new DepartmentDto(2L, "HR")
);
return new DepartmentDataDto(departmentDtoList);
}
}
APIhttp://localhost:8080/demo/employee
取得員工資料。
EmployeeController
package com.abc.demo.controller;
import com.abc.demo.dto.EmployeeDataDto;
import com.abc.demo.dto.EmployeeDto;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.Arrays;
import java.util.List;
@RestController
public class EmployeeController {
@GetMapping("/employee")
public EmployeeDataDto getEmployeeData() {
List<EmployeeDto> employeeDtoList = Arrays.asList(
new EmployeeDto(1L, "john"),
new EmployeeDto(2L, "gary"));
return new EmployeeDataDto(employeeDtoList);
}
}
新增有非同步方法的DemoService
,此類別的非同步方法分別呼叫以上的外部API。方法前要掛上@Async
才有非同步效果,並設value
為AsyncConfig
中配置的executor
Bean。
DemoService
package com.abc.demo.service;
import com.abc.demo.dto.DepartmentDataDto;
import com.abc.demo.dto.EmployeeDataDto;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.web.client.RestTemplateBuilder;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Service;
import org.springframework.web.client.RestTemplate;
import java.util.concurrent.CompletableFuture;
@Service
public class DemoService {
private final RestTemplate restTemplate;
@Autowired
public DemoService(RestTemplateBuilder restTemplateBuilder) {
this.restTemplate = restTemplateBuilder.build();
}
@Async("executor")
public CompletableFuture<EmployeeDataDto> getEmployeeData() throws InterruptedException {
String threadName = Thread.currentThread().getName();
System.out.println(threadName + ":get employee data start");
EmployeeDataDto employeeDataDto = restTemplate
.getForObject("http://localhost:8080/demo/employee", EmployeeDataDto.class);
Thread.sleep(2000);
System.out.println(threadName + ":get employee data end");
return CompletableFuture.completedFuture(employeeDataDto);
}
@Async("executor")
public CompletableFuture<DepartmentDataDto> getDepartmentData() throws InterruptedException {
String threadName = Thread.currentThread().getName();
System.out.println(threadName + ":get department data start");
DepartmentDataDto departmentDataDto = restTemplate
.getForObject("http://localhost:8080/demo/department", DepartmentDataDto.class);
Thread.sleep(1000);
System.out.println(threadName + ":get department data end");
return CompletableFuture.completedFuture(departmentDataDto);
}
}
非同步方法中利用Thread.sleep()
模擬呼叫外部API等待回應的時間,然後把取得的資料放入CompletableFuture
。
建立測試APIDemoController
。
當client對http://localhost:8080/demo/get
發出請求時,DemoController.getBasicInfo()
會調用DemoService
的非同步方法來呼叫外部API取得員工資料及部門資料。
DemoController
package com.abc.demo.controller;
import com.abc.demo.dto.*;
import com.abc.demo.service.DemoService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import java.util.List;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutionException;
import java.util.stream.Collectors;
@RestController
public class DemoController {
@Autowired
private DemoService demoService;
@GetMapping(value = "/get")
public BasicInfoDto getBasicInfo() throws InterruptedException, ExecutionException {
CompletableFuture<EmployeeDataDto> employeeDataDtoCompletableFuture =
demoService.getEmployeeData();
CompletableFuture<DepartmentDataDto> departmentDataDtoCompletableFuture =
demoService.getDepartmentData();
CompletableFuture.allOf(
employeeDataDtoCompletableFuture,
departmentDataDtoCompletableFuture
).join();
List<EmployeeDto> employeeDtoList =
employeeDataDtoCompletableFuture.get().getEmployeeDtoList();
List<DepartmentDto> departmentDtoList =
departmentDataDtoCompletableFuture.get().getDepartmentDtoList();
return new BasicInfoDto(
employeeDtoList.stream().map(EmployeeDto::getName)
.collect(Collectors.toList()),
departmentDtoList.stream().map(DepartmentDto::getName)
.collect(Collectors.toList()));
}
}
啟動應用程式並以Postman或瀏覽器對http://localhost:8080/demo/get
發出GET
請求。
應用程式收到請求並執行後會在console印出下面結果。可以觀察到外部API分別由兩條不同的執行緒async-1
及async-2
非同步執行。
async-1:get employee data start
async-2:get department data start
async-2:get department data end
async-1:get employee data end
示意圖
| |
| |
| @Async async-2 | http://localhost:8080/demo/department
| +------------------------+ | +------------------------+
| http://localhost:8080 +---->| DemoService |------>| DepartmentController |
| /demo/get / | .getDepartmentData() | | | .getDepartmentData() |
+----------+ | +------------------+ +------------------------+ | +------------------------+
| Client |------>| DemoController | |
| | | | .getBasicInfo()| @Async async-1 | http://localhost:8080/demo/employee
+----------+ | +------------------+ +------------------------+ | +------------------------+
| \ | DemoService |------>| EmployeeController |
| +---->| .getEmployeeData() | | | .getEmployeeData() |
| +------------------------+ | +------------------------+
| |
| |
如果把@Async
拿掉,則在console印出下面結果,可以看到外部API都是由同一條執行緒http-nio-8080-exec-2
依順序同步執行。
http-nio-8080-exec-2:get employee data start
http-nio-8080-exec-2:get employee data end
http-nio-8080-exec-2:get department data start
http-nio-8080-exec-2:get department data end
返回結果。
{
"employeeNameList": [
"john",
"gary"
],
"departmentNameList": [
"Marketing",
"HR"
]
}
範例程式請參考github。
想請問如果想用for迴圈執行同一個方法(非同步)待不同參數,用這個@Async有辦法嗎@@"?
回覆刪除有喔,例如上面demoService.getEmployeeData()可以以改成以帶入不同的參數例如id取得員工資料,呼叫時以迴圈帶入多個id,則該方法會以多個執行緒進行。
回覆刪除wow...請問您,示意圖是手打的嗎?也太強了!
回覆刪除@sh 這是用 asciiflow.com 的上一版搭配手工畫的
回覆刪除