Spring Bootで非同期処理

Spring Bootで非同期処理を実装する方法を説明する。

非同期で実行したいメソッドを作成する

Service実装クラスで、非同期で実行したいメソッドを実装する。

  • メソッドに@Asyncアノテーションを付与する
    属性値に非同期処理の設定のbean名を指定する
  • メソッドの戻り値はCompletableFutureでラップする
import java.util.concurrent.CompletableFuture;

import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Service;

@Service
public class SampleServiceImpl implements SampleService {
    
    privateLogger logger;
    
    private Map<String, String> copyMdc;
    
    public SampleServiceImpl(Logger logger) {
        this.logger = logger;
        copyMdc = MDC.getCopyOfContextMap();
    }

    /**
     * 非同期で実行したいメソッド。
     * @Asyncの属性値には非同期処理の設定のbean名を指定する。
     */
    @Async("Thread1")
    @Override
    public CompletableFuture<String> getNameAsync(String id) {
        MDC.setContextMap(copyMdc);
        
        logger.info("getNameAsync start!");
        try {
            Thread.sleep(3000);
        } catch (InterruptedException e) {
            logger.error(e);
        }
        logger.info("getNameAsync end!");
        return CompletableFuture.completedFuture("Bob");
    }
}
import java.util.concurrent.CompletableFuture;

import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Service;

@Service
public class MyServiceImpl implements MyService {
    
    private Logger logger;
    
    private Map<String, String> copyMdc;
    
    public MyServiceImpl(Logger logger) {
        this.logger = logger;
        copyMdc = MDC.getCopyOfContextMap();
    }
    
    /**
     * 非同期で実行したいメソッド。
     * @Asyncの属性値には非同期処理の設定のbean名を指定する。
     */
    @Async("Thread1")
    @Override
    public CompletableFuture<Integer> getAgeAsync(String id) {
        MDC.setContextMap(copyMdc);
        
        logger.info("getAgeAsync start!");
        try {
            Thread.sleep(3000);
        } catch (InterruptedException e) {
            logger.error(e);
        }
        logger.info("getAgeAsync end!");
        return CompletableFuture.completedFuture(35);
    }
}

非同期処理の設定を行うConfigurationクラスを作成する

クラスに@Configuration@EnableAsyncを付与する。
@EnableAsyncはSpringBootにおいて非同期処理を有効にするアノテーションのため、付与しないと非同期処理は行われない。

ThreadPoolTaskExecutorクラスをbean登録する。
最大スレッド数などの設定を行う。スレッド数の設定方法についてはここを参照。

※複数スレッドを超えるリクエストがあった場合は、現在実行中の処理が終わるまで待機する。

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.annotation.EnableAsync;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;

/**
 * 非同期処理の設定を行うConfigurationクラス
 */
@EnableAsync
@Configuration
public class AsyncConfiguration {

    // @Asyncの属性名と等しい方の設定が使用される
    @Bean("Thread1")
    ThreadPoolTaskExecutor threadPoolTaskExecutor() {
        ThreadPoolTaskExecutor taskExecutor = new ThreadPoolTaskExecutor();
        taskExecutor.setCorePoolSize(2);
        taskExecutor.setQueueCapacity(2);
        taskExecutor.setMaxPoolSize(5);
        return taskExecutor;
    }
    
    // @Asyncの属性で指定されない場合は使用されない
    @Bean("Thread2")
    ThreadPoolTaskExecutor threadPoolTaskExecutor() {
        ThreadPoolTaskExecutor taskExecutor = new ThreadPoolTaskExecutor();
        taskExecutor.setCorePoolSize(4);
        taskExecutor.setQueueCapacity(4);
        taskExecutor.setMaxPoolSize(8);
        return taskExecutor;
    }
}

非同期処理のメソッドを呼び出す

CompletableFutureクラスのメソッドを利用して非同期処理のハンドリングを行うことが出来る。
CompletableFutureが出来ることについてはJavadocここを参照。

以下の例では、全ての非同期処理の終了を待ってから画面遷移するように実装している。

import java.util.concurrent.CompletableFuture;

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;

@Controller
public class SampleController {

    private SampleService service;
    
    private MyService myService;

    public RegisterConfirmationController(SampleService service, MyService myService) {
        this.service = service;
        this.myService = myService;
    }

    @RequestMapping(value = "/result", method = RequestMethod.POST)
    public String result() {
        String id = "A001";

        // 非同期で実行
        CompletableFuture<String> nameAsync = service.getNameAsync(id);
        CompletableFuture<Integer> ageAsync = myService.getAgeAsync(id);
        
        // 指定した処理が終わったらこれ以降の処理が実行される
        CompletableFuture.allOf(nameAsync, ageAsync).join();

        return "index";
    }
}

実際に実行した際のログは以下の通り。非同期にメソッドが実行されていることが分かる。

getNameAsync start!
getAgeAsync start!
getAgeAsync end!
getNameAsync end!