網頁

2020/6/14

Spring Boot @Async 非同步方法範例

Spring的@Asyncannotation 可令方法為非同步(asynchronous)執行。

範例環境:

  • Java 8
  • Spring Boot 2.2.4.RELEASE
  • Maven
  • Lombok

建立一個Spring Boot專案(IntelliJ IDEAEclipse 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;

}

新增DemoControllerEmployeeContrller模擬外部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才有非同步效果,並設valueAsyncConfig中配置的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-1async-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


4 則留言:

  1. 想請問如果想用for迴圈執行同一個方法(非同步)待不同參數,用這個@Async有辦法嗎@@"?

    回覆刪除
  2. 有喔,例如上面demoService.getEmployeeData()可以以改成以帶入不同的參數例如id取得員工資料,呼叫時以迴圈帶入多個id,則該方法會以多個執行緒進行。

    回覆刪除
  3. wow...請問您,示意圖是手打的嗎?也太強了!

    回覆刪除
  4. @sh 這是用 asciiflow.com 的上一版搭配手工畫的

    回覆刪除