CLOVER🍀

That was when it all began.

Spring FrameworkのTask Executionを詊す

これは、なにをしたくお曞いたもの

Spring Frameworkには、タスク実行ずスケゞュヌリングの機胜がありたす。

Task Execution and Scheduling

今回は、タスク実行にフォヌカスしお芋おいきたいず思いたす。

具䜓的には、TaskExecutorずThreadPoolTaskExecutor、@Asyncアノテヌションを扱いたす。

Task Execution

Spring FrameworkのTask Execution タスクの実行に関するドキュメントは、こちらです。

Task Execution and Scheduling

Spring Frameworkでは、JavaのExecutorを抜象化したTaskExecutorを提䟛しおいたす。

The Spring TaskExecutor Abstraction

Java SE環境であっおもJava EE環境であっおも、こちらを䜿うこずでタスク実行に関する実装の詳现が隠蔜されたす。

TaskExecutorはExecutorを継承したむンタヌフェヌスであり、その実装はSpring Frameworkが提䟛しおいたす。
Spring Frameworkの利甚者自身が、TaskExecutorの実装を䜜成するこずはほずんどないでしょう。

TaskExecutor (Spring Framework 5.3.6 API)

TaskExecutor Types

TaskExecutorの実装には、以䞋の皮類がありたす。

  • SyncTaskExecutor 
 タスクの実行は別スレッドで行うものの、タスクを非同期では実行しない実装。テストなど、マルチスレッドが䞍芁な状況䞋で䜿甚する
  • SimpleAsyncTaskExecutor 
 呌び出しごずに、新しいスレッドを䜜成しお実行する実装。同時に実行できるタスクの数は制限できる
  • ConcurrentTaskExecutor 
 java.util.concurrent.Executorのアダプタヌ。通垞は、ThreadPoolTaskExecutorを䜿甚すればよい
  • ThreadPoolTaskExecutor 
 最も䞀般的な実装。java.util.concurrent.ThreadPoolExecutorをTaskExecutorずしおラップしおいる。他のExecutorを䜿いたい堎合は、ConcurrentTaskExecutorの利甚を怜蚎する
  • WorkManagerTaskExecutor 
 CommonJ WorkManagerWebSphere、WebLogicをバック゚ンドに䜿甚する実装
  • DefaultManagedTaskExecutor 
 Java EE環境におけるManagedExecutorServiceをバック゚ンドに䜿甚する実装

いずれにせよ、TaskExecutorはJavaのExecutorむンタヌフェヌスの拡匵なので、䜿い方も同じになりたす。

Executor (Java SE 11 & JDK 11 )

よく䜿われそうなThreadPoolTaskExecutorはThreadPoolExecutorをラップしおいるので、こちらも参照するず
よいでしょう。

ThreadPoolExecutor (Java SE 11 & JDK 11 )

ThreadPoolTaskExecutor (Spring Framework 5.3.6 API)

Spring Bootを䜿った堎合は、ThreadPoolTaskExecutorが自動構成されたす。

Task Execution and Scheduling

TaskExecutorの䜿甚䟋はこちら。

Using a TaskExecutor

たた、@Asyncはスケゞュヌル実行に関する機胜なのですが、今回䜿っおみたす。

The @Async annotation

@EnableAsyncを䜿っお有効化したうえで、凊理を行うBeanのメ゜ッドに@Asyncアノテヌションを付䞎するず、
指定されたたたはデフォルトのTaskExecutorを䜿っお非同期実行が行われたす。

Enable Scheduling Annotations

今回は、TaskExecutorずThreadPoolTaskExecutor、@Asyncアノテヌションを䜿っおいきたす。

環境

今回の環境は、こちらです。

$ java --version
openjdk 11.0.10 2021-01-19
OpenJDK Runtime Environment (build 11.0.10+9-Ubuntu-0ubuntu1.20.04)
OpenJDK 64-Bit Server VM (build 11.0.10+9-Ubuntu-0ubuntu1.20.04, mixed mode, sharing)


$ mvn --version
Apache Maven 3.8.1 (05c21c65bdfed0f71a2f2ada8b84da59348c4c5d)
Maven home: $HOME/.sdkman/candidates/maven/current
Java version: 11.0.10, vendor: Ubuntu, runtime: /usr/lib/jvm/java-11-openjdk-amd64
Default locale: ja_JP, platform encoding: UTF-8
OS name: "linux", version: "5.4.0-72-generic", arch: "amd64", family: "unix"

Spring Bootは2.4.5を䜿い、䟝存するSpring Frameworkは5.3.6ずなりたす。

Spring Bootプロゞェクトの䜜成

Spring Initializrを䜿っお、Spring Bootプロゞェクトを䜜成したす。

今回のアプリケヌションはWebではなく、CommandLineRunnerで確認するくらいで良いかな、ず思ったので䟝存関係は
特に指定しおいたせん。

$ curl -s https://start.spring.io/starter.tgz \
  -d bootVersion=2.4.5 \
  -d javaVersion=11 \
  -d name=task-execution \
  -d groupId=org.littlewings \
  -d artifactId=task-execution \
  -d version=0.0.1-SNAPSHOT \
  -d packageName=org.littlewings.spring.task \
  -d baseDir=task-execution | tar zxvf -


$ cd task-execution

dependenciesをなにも指定しない堎合は、䟝存関係がこれくらいのpom.xmlになりたす。

 <properties>
        <java.version>11</java.version>
    </properties>
    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>

起動クラスは、こちらで。

src/main/java/org/littlewings/spring/task/App.java

package org.littlewings.spring.task;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.scheduling.annotation.EnableAsync;

@SpringBootApplication
//@EnableAsync
public class App {
    public static void main(String[] args) {
        SpringApplication.run(App.class, args);
    }
}

コメントアりトしおいる@EnableAsyncアノテヌションは、埌述したす。

実行は、以䞋のコマンドで行っおいるものずしたす。

$ mvn spring-boot:run

あず、CommandLineRunnerむンタヌフェヌスの実装クラスは詊したい機胜ごずに䜜っおいくのですが、そのたた増やすず
党郚実行しおいっおしたうので、玹介が終わったら以䞋のように@Componentアノテヌションをコメントアりトしおいく
ものずしたす。

// @Component
public class SimpleRunner implements CommandLineRunner {

たずは䜿っおみる

では、䜿っおいっおみたす。

こちらを参考に

Using a TaskExecutor

こんなクラスを䜜成。

src/main/java/org/littlewings/spring/task/SimpleRunner.java

package org.littlewings.spring.task;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.boot.CommandLineRunner;
import org.springframework.core.task.TaskExecutor;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
import org.springframework.stereotype.Component;

@Component
public class SimpleRunner implements CommandLineRunner {
    Logger logger = LoggerFactory.getLogger(SimpleRunner.class);

    TaskExecutor taskExecutor;

    public SimpleRunner(TaskExecutor taskExecutor) {
        this.taskExecutor = taskExecutor;
    }

    @Override
    public void run(String... args) throws Exception {
        taskExecutor.execute(() -> {
            logger.info("Hello World!!");
            logger.info("TaskExecutor name = {}", taskExecutor.getClass().getName());
        });

        taskExecutor.execute(() -> logger.info("Oops!!"));

        ThreadPoolTaskExecutor threadPoolTaskExecutor = (ThreadPoolTaskExecutor) taskExecutor;
        logger.info("core pool size = {}, max pool size = {}, keep-alive seconds = {}", threadPoolTaskExecutor.getCorePoolSize(), threadPoolTaskExecutor.getMaxPoolSize(), threadPoolTaskExecutor.getKeepAliveSeconds());
    }
}

DIする型は、TaskExecutorむンタヌフェヌスでも倧䞈倫です。

    TaskExecutor taskExecutor;

    public SimpleRunner(TaskExecutor taskExecutor) {
        this.taskExecutor = taskExecutor;
    }

TaskExecutor#executeメ゜ッドに、Runnableを枡すこずでタスクを実行できたす。今回は、ログ出力をしおいるだけです。
TaskExecutorの実装クラス名も出力しおいたす。

        taskExecutor.execute(() -> {
            logger.info("Hello World!!");
            logger.info("TaskExecutor name = {}", taskExecutor.getClass().getName());
        });

        taskExecutor.execute(() -> logger.info("Oops!!"));

TaskExecutorむンタヌフェヌスができるのは、これだけです。

TaskExecutor (Spring Framework 5.3.6 API)

タスクからの戻り倀も取埗できたせん。

実䜓はThreadPoolTaskExecutorなので、キャストしお蚭定を芋おみたしょう。

        ThreadPoolTaskExecutor threadPoolTaskExecutor = (ThreadPoolTaskExecutor) taskExecutor;
        logger.info("core pool size = {}, max pool size = {}, keep-alive seconds = {}", threadPoolTaskExecutor.getCorePoolSize(), threadPoolTaskExecutor.getMaxPoolSize(), threadPoolTaskExecutor.getKeepAliveSeconds());

実行結果。

2021-04-25 22:36:55.048  INFO 43283 --- [         task-1] o.littlewings.spring.task.SimpleRunner   : Hello World!!
2021-04-25 22:36:55.049  INFO 43283 --- [         task-2] o.littlewings.spring.task.SimpleRunner   : Oops!!
2021-04-25 22:36:55.049  INFO 43283 --- [         task-1] o.littlewings.spring.task.SimpleRunner   : TaskExecutor name = org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor
2021-04-25 22:36:55.048  INFO 43283 --- [           main] o.littlewings.spring.task.SimpleRunner   : core pool size = 8, max pool size = 2147483647, keep-alive seconds = 60
2021-04-25 22:37:55.052  INFO 43283 --- [extShutdownHook] o.s.s.concurrent.ThreadPoolTaskExecutor  : Shutting down ExecutorService 'applicationTaskExecutor'

ログのスレッド名を芋るず、task-ずなっおいお、別スレッドで実行されおいるこずがわかりたす。

䜿甚されおいるThreadPoolTaskExecutorがAutoConfigureされおいるのは、こちらですね。

https://github.com/spring-projects/spring-boot/blob/v2.4.5/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/task/TaskExecutionAutoConfiguration.java

プロパティ蚭定は、こちらを。

Core Properties

デフォルトでは8スレッドをコアサむズずしお持ち、最倧スレッド数はInteger.MAX_VALUEたで広がるスレッドプヌルが
構成されたす。

https://github.com/spring-projects/spring-boot/blob/v2.4.5/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/task/TaskExecutionProperties.java

ずころでこのプログラム、実行するずしばらく終了したせん。

停止たで1分かかっおいたす。

2021-04-25 22:36:55.048  INFO 43283 --- [           main] o.littlewings.spring.task.SimpleRunner   : core pool size = 8, max pool size = 2147483647, keep-alive seconds = 60
2021-04-25 22:37:55.052  INFO 43283 --- [extShutdownHook] o.s.s.concurrent.ThreadPoolTaskExecutor  : Shutting down ExecutorService 'applicationTaskExecutor'

spring.task.execution.pool.allow-core-thread-timeoutがデフォルトでtrueになっおいおタスクを実行したスレッドはタむムアりト
する蚭定になっおいるのですが、spring.task.execution.pool.keep-aliveがデフォルトで60秒なので、この間は埅ち続けたす。

ちょっず長いので、今回は3秒にしおおきたしょう。

src/main/resources/application.properties

spring.task.execution.pool.keep-alive=3s

今床は、3秒で終了するようになりたした。

2021-04-25 22:44:36.363  INFO 43800 --- [         task-1] o.littlewings.spring.task.SimpleRunner   : Hello World!!
2021-04-25 22:44:36.364  INFO 43800 --- [         task-2] o.littlewings.spring.task.SimpleRunner   : Oops!!
2021-04-25 22:44:36.364  INFO 43800 --- [         task-1] o.littlewings.spring.task.SimpleRunner   : TaskExecutor name = org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor
2021-04-25 22:44:36.364  INFO 43800 --- [           main] o.littlewings.spring.task.SimpleRunner   : core pool size = 8, max pool size = 2147483647, keep-alive seconds = 3
2021-04-25 22:44:39.369  INFO 43800 --- [extShutdownHook] o.s.s.concurrent.ThreadPoolTaskExecutor  : Shutting down ExecutorService 'applicationTaskExecutor'

ThreadPoolTaskExecutorを䜿う

先ほどはTaskExecutorむンタヌフェヌスのたたタスクを実行したしたが、TaskExecutor#executeだずRunnableしか
実行できないのでちょっず䞍䟿です 。

もう、ThreadPoolTaskExecutorでDIしおいいのではないのでしょうかずいう気分になりたす。

ずいうわけで、そのように曞いおみたした。

src/main/java/org/littlewings/spring/task/ThreadPoolTaskExecutorRunner.java

package org.littlewings.spring.task;

import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.boot.CommandLineRunner;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
import org.springframework.stereotype.Component;

@Component
public class ThreadPoolTaskExecutorRunner implements CommandLineRunner {
    Logger logger = LoggerFactory.getLogger(ThreadPoolTaskExecutorRunner.class);

    ThreadPoolTaskExecutor taskExecutor;

    public ThreadPoolTaskExecutorRunner(ThreadPoolTaskExecutor taskExecutor) {
        this.taskExecutor = taskExecutor;
    }

    @Override
    public void run(String... args) throws Exception {
        List<Future<?>> futures = new ArrayList<>();

        futures.add(taskExecutor.submit(() -> {
            logger.info("calc task start");

            try {
                TimeUnit.SECONDS.sleep(3L);
            } catch (InterruptedException e) {
                // ignore
            }

            return 1 + 2;
        }));

        futures.add(taskExecutor.submit(() -> {
            logger.info("message task start");

            try {
                TimeUnit.SECONDS.sleep(5L);
            } catch (InterruptedException e) {
                // ignore
            }

            return "Hello World!!";
        }));

        for (Future<?> future : futures) {
            logger.info("future get = {}", future.get());
        }
    }
}

やっおいるこずは、Callableを䜿っお結果を返すようにしおFutureを受け取り、

        futures.add(taskExecutor.submit(() -> {
            logger.info("calc task start");

            try {
                TimeUnit.SECONDS.sleep(3L);
            } catch (InterruptedException e) {
                // ignore
            }

            return 1 + 2;
        }));

        futures.add(taskExecutor.submit(() -> {
            logger.info("message task start");

            try {
                TimeUnit.SECONDS.sleep(5L);
            } catch (InterruptedException e) {
                // ignore
            }

            return "Hello World!!";
        }));

最埌に埅ち合わせおいるだけです。

        for (Future<?> future : futures) {
            logger.info("future get = {}", future.get());
        }

実行結果。

2021-04-25 22:53:40.708  INFO 44289 --- [         task-1] o.l.s.task.ThreadPoolTaskExecutorRunner  : calc task start
2021-04-25 22:53:40.709  INFO 44289 --- [         task-2] o.l.s.task.ThreadPoolTaskExecutorRunner  : message task start
2021-04-25 22:53:43.710  INFO 44289 --- [           main] o.l.s.task.ThreadPoolTaskExecutorRunner  : future get = 3
2021-04-25 22:53:45.710  INFO 44289 --- [           main] o.l.s.task.ThreadPoolTaskExecutorRunner  : future get = Hello World!!

ログを芋おも、別スレッドで実行されおいるこずが確認できたすし、結果も取埗できおいたす。

@Asyncを䜿っお非同期実行する

次は、@Asyncを䜿っお非同期実行しおみたしょう。@Asyncアノテヌションは、スケゞュヌル実行偎の機胜です。

The @Async annotation

最初に、うたくいかない䟋を曞いおみたす。

src/main/java/org/littlewings/spring/task/MessageService.java

package org.littlewings.spring.task;

import java.util.concurrent.ExecutionException;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.scheduling.annotation.Async;
import org.springframework.scheduling.annotation.AsyncResult;
import org.springframework.stereotype.Service;

@Service
public class MessageService {
    Logger logger = LoggerFactory.getLogger(MessageService.class);

    @Async
    public String getMessage() {
        logger.info("get message start");

        Thread.dumpStack();

        try {
            TimeUnit.SECONDS.sleep(3L);
        } catch (InterruptedException e) {
            // ignore
        }

        return "Hello World";
    }
}

こんな感じで、非同期実行したいメ゜ッドに@Asyncアノテヌションを付䞎したす。スタックトレヌスの様子も芋たいので、
Thread#dumpStackも含めおおきたした。

    @Async
    public String getMessage() {
        logger.info("get message start");

        Thread.dumpStack();

        try {
            TimeUnit.SECONDS.sleep(3L);
        } catch (InterruptedException e) {
            // ignore
        }

        return "Hello World";
    }

こちらを䜿うクラスを䜜成。このパタヌンでは、TaskExecutorやThreadPoolTaskExecutorは、最埌たで゜ヌスコヌドに珟れたせん。

src/main/java/org/littlewings/spring/task/UseAsyncRunner.java

package org.littlewings.spring.task;

import java.util.concurrent.Future;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.boot.CommandLineRunner;
import org.springframework.stereotype.Component;

@Component
public class UseAsyncRunner implements CommandLineRunner {
    Logger logger = LoggerFactory.getLogger(UseAsyncRunner.class);

    MessageService messageService;

    public UseAsyncRunner(MessageService messageService) {
        this.messageService = messageService;
    }

    @Override
    public void run(String... args) throws Exception {
        String message = messageService.getMessage();

        logger.info("return message = {}", message);
    }
}

実行しおログをよく芋るず、単に同じスレッドで動いおいるだけです。

2021-04-25 23:06:59.317  INFO 45078 --- [           main] o.l.spring.task.MessageService           : get message start
java.lang.Exception: Stack trace
    at java.base/java.lang.Thread.dumpStack(Thread.java:1388)
    at org.littlewings.spring.task.MessageService.getMessage(MessageService.java:22)
    at org.littlewings.spring.task.UseAsyncRunner.run(UseAsyncRunner.java:22)
    at org.springframework.boot.SpringApplication.callRunner(SpringApplication.java:819)
    at org.springframework.boot.SpringApplication.callRunners(SpringApplication.java:803)
    at org.springframework.boot.SpringApplication.run(SpringApplication.java:346)
    at org.springframework.boot.SpringApplication.run(SpringApplication.java:1340)
    at org.springframework.boot.SpringApplication.run(SpringApplication.java:1329)
    at org.littlewings.spring.task.App.main(App.java:11)
2021-04-25 23:07:02.318  INFO 45078 --- [           main] o.l.spring.task.UseAsyncRunner           : return message = Hello World

この機胜を䜿うには、@EnableAsyncアノテヌションを䜿う必芁があるからです。

Enable Scheduling Annotations

これで、@Asyncアノテヌションが機胜するようになりたす。

@SpringBootApplication
@EnableAsync
public class App {

再床実行。

2021-04-25 23:08:43.523  INFO 45212 --- [           main] o.l.spring.task.UseAsyncRunner           : return message = null
2021-04-25 23:08:43.534  INFO 45212 --- [         task-1] o.l.spring.task.MessageService           : get message start
java.lang.Exception: Stack trace
    at java.base/java.lang.Thread.dumpStack(Thread.java:1388)
    at org.littlewings.spring.task.MessageService.getMessage(MessageService.java:22)
    at org.littlewings.spring.task.MessageService$$FastClassBySpringCGLIB$$ace3701c.invoke(<generated>)
    at org.springframework.cglib.proxy.MethodProxy.invoke(MethodProxy.java:218)
    at org.springframework.aop.framework.CglibAopProxy$CglibMethodInvocation.invokeJoinpoint(CglibAopProxy.java:779)
    at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:163)
    at org.springframework.aop.framework.CglibAopProxy$CglibMethodInvocation.proceed(CglibAopProxy.java:750)
    at org.springframework.aop.interceptor.AsyncExecutionInterceptor.lambda$invoke$0(AsyncExecutionInterceptor.java:115)
    at java.base/java.util.concurrent.FutureTask.run(FutureTask.java:264)
    at java.base/java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1128)
    at java.base/java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:628)
    at java.base/java.lang.Thread.run(Thread.java:834)

今床は、別スレッドで動くようになりたした。スタックトレヌスを芋るず、むンタヌセプタヌが入っおいたすね。

ですが、メ゜ッドの戻り倀が取埗できおいたせん。

2021-04-25 23:08:43.523  INFO 45212 --- [           main] o.l.spring.task.UseAsyncRunner           : return message = null

ドキュメントをちゃんず読むず、倀を返す堎合はFutureを䜿う必芁があるようです。

Even methods that return a value can be invoked asynchronously. However, such methods are required to have a Future-typed return value. This still provides the benefit of asynchronous execution so that the caller can perform other tasks prior to calling get() on that Future.

The @Async annotation

ドキュメントを芋おいるず戻り倀がvoidの䟋ばかりですが、voidの堎合だず結果を埅たない埅おない非同期実行になりたす。
メ゜ッドの戻り倀が䞍芁であれば、voidにしお実行しおもよいでしょう。

今回は戻り倀を䜿いたいので、メ゜ッドの戻り倀がFutureずなるように倉曎しおみたす。

src/main/java/org/littlewings/spring/task/MessageService.java

package org.littlewings.spring.task;

import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.scheduling.annotation.Async;
import org.springframework.scheduling.annotation.AsyncResult;
import org.springframework.stereotype.Service;

@Service
public class MessageService {
    Logger logger = LoggerFactory.getLogger(MessageService.class);

    @Async
    public Future<String> getMessage() {
        logger.info("get message start");

        Thread.dumpStack();

        try {
            TimeUnit.SECONDS.sleep(3L);
        } catch (InterruptedException e) {
            // ignore
        }

        // return "Hello World";
        return new AsyncResult<>("Hello World");
    }
}

ドキュメントがvoidの䟋ばかりだったので、Futureを返すにはどうしたら ずいう気分になりたしたが、AsyncResultを
䜿うのが良さそうです。

AsyncResult (Spring Framework 5.3.6 API)

呌び出し元も倉曎。

src/main/java/org/littlewings/spring/task/UseAsyncRunner.java

package org.littlewings.spring.task;

import java.util.concurrent.Future;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.boot.CommandLineRunner;
import org.springframework.stereotype.Component;

@Component
public class UseAsyncRunner implements CommandLineRunner {
    Logger logger = LoggerFactory.getLogger(UseAsyncRunner.class);

    MessageService messageService;

    public UseAsyncRunner(MessageService messageService) {
        this.messageService = messageService;
    }

    @Override
    public void run(String... args) throws Exception {
        Future<String> message = messageService.getMessage();

        logger.info("return message = {}", message.get());
    }
}

では、実行。

2021-04-25 23:43:14.929  INFO 47028 --- [         task-1] o.l.spring.task.MessageService           : get message start
java.lang.Exception: Stack trace
    at java.base/java.lang.Thread.dumpStack(Thread.java:1388)
    at org.littlewings.spring.task.MessageService.getMessage(MessageService.java:37)
    at org.littlewings.spring.task.MessageService$$FastClassBySpringCGLIB$$ace3701c.invoke(<generated>)
    at org.springframework.cglib.proxy.MethodProxy.invoke(MethodProxy.java:218)
    at org.springframework.aop.framework.CglibAopProxy$CglibMethodInvocation.invokeJoinpoint(CglibAopProxy.java:779)
    at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:163)
    at org.springframework.aop.framework.CglibAopProxy$CglibMethodInvocation.proceed(CglibAopProxy.java:750)
    at org.springframework.aop.interceptor.AsyncExecutionInterceptor.lambda$invoke$0(AsyncExecutionInterceptor.java:115)
    at java.base/java.util.concurrent.FutureTask.run(FutureTask.java:264)
    at java.base/java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1128)
    at java.base/java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:628)
    at java.base/java.lang.Thread.run(Thread.java:834)
2021-04-25 23:43:17.931  INFO 47028 --- [           main] o.l.spring.task.UseAsyncRunner           : return message = Hello World

OKですね。

ちなみに、Future以倖にもSpringが提䟛するListenableFuture、そしおJava 8以降のCompletableFutureも戻り倀ずしお
䜿えたす。

ドキュメントに蚘茉がありたすし、

@Async methods may not only declare a regular java.util.concurrent.Future return type but also Spring’s org.springframework.util.concurrent.ListenableFuture or, as of Spring 4.2, JDK 8’s java.util.concurrent.CompletableFuture, for richer interaction with the asynchronous task and for immediate composition with further processing steps.

The @Async annotation

このあたりを芋ればわかりたす。

https://github.com/spring-projects/spring-framework/blob/v5.3.6/spring-aop/src/main/java/org/springframework/aop/interceptor/AsyncExecutionAspectSupport.java#L271-L293

呌び出し元は、こちら。メ゜ッドをCallableでラップしお実行したす。

https://github.com/spring-projects/spring-framework/blob/v5.3.6/spring-aop/src/main/java/org/springframework/aop/interceptor/AsyncExecutionInterceptor.java#L100-L130

この時䜿われるTaskExecutorは、これたで䜿っおきたSpring Bootでデフォルト構成されたThreadPoolTaskExecutorです。

たた、今回は扱いたせんが、䟋倖凊理に぀いおはこちらのようです。

Exception Management with @Async

自分でThreadPoolTaskExecutorのBeanを䜜成する

最埌に、自分でThreadPoolTaskExecutorのBeanを䜜成しおみたしょう。

こんな感じで、2぀のThreadPoolTaskExecutorのBeanを登録。

src/main/java/org/littlewings/spring/task/MyTaskExecutorConfiguration.java

package org.littlewings.spring.task;

import java.time.Duration;

import org.springframework.boot.task.TaskExecutorBuilder;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;

@Configuration
public class MyTaskExecutorConfiguration {
    @Bean("largePoolTaskExecutor")
    public ThreadPoolTaskExecutor largePoolTaskExecutor() {
        TaskExecutorBuilder builder = new TaskExecutorBuilder();

        return builder
                .corePoolSize(16)
                .maxPoolSize(16)
                .keepAlive(Duration.ofSeconds(2L))
                .queueCapacity(Integer.MAX_VALUE)
                .threadNamePrefix("large-pool-task-")
                .allowCoreThreadTimeOut(true)
                .build();
    }

    @Bean("smallPoolTaskExecutor")
    public ThreadPoolTaskExecutor smallPoolTaskExecutor() {
        TaskExecutorBuilder builder = new TaskExecutorBuilder();

        return builder
                .corePoolSize(2)
                .maxPoolSize(2)
                .keepAlive(Duration.ofSeconds(2L))
                .queueCapacity(Integer.MAX_VALUE)
                .threadNamePrefix("small-pool-task-")
                .allowCoreThreadTimeOut(true)
                .build();
    }
}

ThreadPoolTaskExecutorの䜜成には、TaskExecutorBuilderを䜿うのが良いでしょう。

The auto-configured TaskExecutorBuilder allows you to easily create instances that reproduce what the auto-configuration does by default.

Both a TaskExecutorBuilder bean and a TaskSchedulerBuilder bean are made available in the context if a custom executor or scheduler needs to be created.

Task Execution and Scheduling

TaskExecutorBuilder (Spring Boot 2.4.5 API)

䜿い方は、Spring Boot自身が参考になりたす。

https://github.com/spring-projects/spring-boot/blob/v2.4.5/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/task/TaskExecutionAutoConfiguration.java

今回は、サむズ固定のスレッドプヌルを2぀甚意したした。

    @Bean("largePoolTaskExecutor")
    public ThreadPoolTaskExecutor largePoolTaskExecutor() {
        TaskExecutorBuilder builder = new TaskExecutorBuilder();

        return builder
                .corePoolSize(16)
                .maxPoolSize(16)
                .keepAlive(Duration.ofSeconds(2L))
                .queueCapacity(Integer.MAX_VALUE)
                .threadNamePrefix("large-pool-task-")
                .allowCoreThreadTimeOut(true)
                .build();
    }

    @Bean("smallPoolTaskExecutor")
    public ThreadPoolTaskExecutor smallPoolTaskExecutor() {
        TaskExecutorBuilder builder = new TaskExecutorBuilder();

        return builder
                .corePoolSize(2)
                .maxPoolSize(2)
                .keepAlive(Duration.ofSeconds(2L))
                .queueCapacity(Integer.MAX_VALUE)
                .threadNamePrefix("small-pool-task-")
                .allowCoreThreadTimeOut(true)
                .build();
    }

定矩したThreadPoolTaskExecutorを䜿う偎のクラス。

src/main/java/org/littlewings/spring/task/CustomTaskExecutorRunner.java

package org.littlewings.spring.task;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.boot.CommandLineRunner;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
import org.springframework.stereotype.Component;

@Component
public class CustomTaskExecutorRunner implements CommandLineRunner {
    Logger logger = LoggerFactory.getLogger(CustomTaskExecutorRunner.class);

    ThreadPoolTaskExecutor largePoolTaskExecutor;
    ThreadPoolTaskExecutor smallPoolTaskExecutor;

    public CustomTaskExecutorRunner(@Qualifier("largePoolTaskExecutor") ThreadPoolTaskExecutor taskExecutor1,
                                    @Qualifier("smallPoolTaskExecutor") ThreadPoolTaskExecutor taskExecutor2) {
        this.largePoolTaskExecutor = taskExecutor1;
        this.smallPoolTaskExecutor = taskExecutor2;
    }

    @Override
    public void run(String... args) throws Exception {
        largePoolTaskExecutor.execute(() -> logger.info("hello"));
        smallPoolTaskExecutor.execute(() -> logger.info("world"));

        logger.info("pool prefix = {}, core pool size = {}, max pool size = {}, keep-alive seconds = {}", largePoolTaskExecutor.getThreadNamePrefix(), largePoolTaskExecutor.getCorePoolSize(), largePoolTaskExecutor.getMaxPoolSize(), largePoolTaskExecutor.getKeepAliveSeconds());
        logger.info("pool prefix = {}, core pool size = {}, max pool size = {}, keep-alive seconds = {}", smallPoolTaskExecutor.getThreadNamePrefix(), smallPoolTaskExecutor.getCorePoolSize(), smallPoolTaskExecutor.getMaxPoolSize(), smallPoolTaskExecutor.getKeepAliveSeconds());
    }
}

ThreadPoolTaskExecutorの区別は、@Qualifierアノテヌションで行うこずにしたした。

    public CustomTaskExecutorRunner(@Qualifier("largePoolTaskExecutor") ThreadPoolTaskExecutor taskExecutor1,
                                    @Qualifier("smallPoolTaskExecutor") ThreadPoolTaskExecutor taskExecutor2) {
        this.largePoolTaskExecutor = taskExecutor1;
        this.smallPoolTaskExecutor = taskExecutor2;
    }

今回のThreadPoolTaskExecutorの䜿い道は、単にメッセヌゞのログ出力ず、蚭定倀の確認のためのログ出力くらいですけどね。

実行結果。

2021-04-26 00:07:16.757  INFO 48670 --- [rge-pool-task-1] o.l.s.task.CustomTaskExecutorRunner      : hello
2021-04-26 00:07:16.758  INFO 48670 --- [all-pool-task-1] o.l.s.task.CustomTaskExecutorRunner      : world
2021-04-26 00:07:16.758  INFO 48670 --- [           main] o.l.s.task.CustomTaskExecutorRunner      : pool prefix = large-pool-task-, core pool size = 16, max pool size = 16, keep-alive seconds = 2
2021-04-26 00:07:16.758  INFO 48670 --- [           main] o.l.s.task.CustomTaskExecutorRunner      : pool prefix = small-pool-task-, core pool size = 2, max pool size = 2, keep-alive seconds = 2

蚭定した内容が反映されおいるようです。

OKですね。

このように自分で定矩したTaskExecutorを@Asyncアノテヌションで指定するには、@Asyncアノテヌションのvalueに
指定すればよいみたいです。

Executor Qualification with @Async

今回は、扱いたせんが。

たずめ

Spring FrameworkのTask Executionを、TaskExecutorずThreadPoolTaskExecutor、@Asyncアノテヌションに絞っお
䜿っおみたした。

JavaのExecutorServiceずそう倉わらないのだろうず思っおいたのですが、それが正解でもあり、固有のこずもあった感じですね。

勉匷になりたした、ず。