モーダルを閉じる工作HardwareHub ロゴ画像

工作HardwareHubは、ロボット工作や電子工作に関する情報やモノが行き交うコミュニティサイトです。さらに詳しく

利用規約プライバシーポリシー に同意したうえでログインしてください。

工作HardwareHub ロゴ画像 (Laptop端末利用時)
工作HardwareHub ロゴ画像 (Mobile端末利用時)

Spring Boot における非同期処理

モーダルを閉じる

ステッカーを選択してください

モーダルを閉じる

お支払い内容をご確認ください

購入商品
」ステッカーの表示権
メッセージ
料金
(税込)
決済方法
GooglePayマーク
決済プラットフォーム
確認事項

利用規約をご確認のうえお支払いください

※カード情報はGoogleアカウント内に保存されます。本サイトやStripeには保存されません

※記事の執筆者は購入者のユーザー名を知ることができます

※購入後のキャンセルはできません

作成日作成日
2017/06/06
最終更新最終更新
2021/10/07
記事区分記事区分
一般公開

目次

    アカウント プロフィール画像 (サイドバー)

    Spring Bootでの実用的な機能をわかりやすく解説中

    0
    ステッカーを贈るとは?

    Spring Boot について、非同期処理の基本的な実装方法をまとめます。

    関連する公式ドキュメント

    サンプルプロジェクト構成

    .
    |-- build.gradle
    |-- gradle
    |   `-- wrapper
    |       |-- gradle-wrapper.jar
    |       `-- gradle-wrapper.properties
    |-- gradlew
    |-- gradlew.bat
    `-- src
        `-- main
            `-- java
                `-- hello
                    |-- AppRunner.java
                    |-- Application.java
                    |-- GitHubLookupService.java
                    `-- User.java
    

    build.gradle

    buildscript {
        ext {
            springBootVersion = '1.5.3.RELEASE'
        }
        repositories {
            mavenCentral()
        }
        dependencies {
            classpath("org.springframework.boot:spring-boot-gradle-plugin:${springBootVersion}")
        }
    }
    
    apply plugin: 'java'
    apply plugin: 'eclipse'
    apply plugin: 'idea'
    apply plugin: 'org.springframework.boot'
    
    jar {
        baseName = 'gs-spring-boot'
        version =  '0.1.0'
    }
    
    repositories {
        mavenCentral()
    }
    
    sourceCompatibility = 1.8
    targetCompatibility = 1.8
    
    dependencies {
        compile('org.springframework.boot:spring-boot-starter-web')
    
        // 非同期処理とは直接関係ありません。今回はサンプルで JSON を扱うため利用します。
        compile('com.fasterxml.jackson.core:jackson-databind')
    }
    

    Java ソースコード

    Application.java

    アプリケーションのエントリーポイントとなるクラスです。Web アプリケーションとして起動しますが、今回のサンプルにおいては、起動時の CommandLineRunner 内で実行される処理が重要であって、Web アプリケーションであることは非同期処理とは直接関係ありません。

    package hello;
    
    import java.util.concurrent.Executor;
    
    import org.springframework.boot.SpringApplication;
    import org.springframework.boot.autoconfigure.SpringBootApplication;
    import org.springframework.scheduling.annotation.AsyncConfigurerSupport;
    import org.springframework.scheduling.annotation.EnableAsync;
    import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
    
    @SpringBootApplication
    @EnableAsync
    public class Application extends AsyncConfigurerSupport {
    
        public static void main(String[] args) {
            SpringApplication.run(Application.class, args);
        }
    
        @Override
        public Executor getAsyncExecutor() {
            ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
            executor.setCorePoolSize(2); // 並列処理数を 2 に制限
            executor.setMaxPoolSize(2);
            executor.setQueueCapacity(500); // 非同期処理を待つキューの長さを 500 までに制限
            executor.setThreadNamePrefix("GithubLookup-"); // 非同期処理用のスレッド名を設定
            executor.initialize();
            return executor;
        }
    }
    
    項目 概要
    @EnableAsync 後述の @Async を利用するために、@Configuration 設定のある、いずれかのクラスへの必要が設定です。Javadoc はこちらです。
    @SpringBootApplication @Configuration, @EnableAutoConfiguration, @ComponentScan を統合したアノテーションです。
    AsyncConfigurerSupport AsyncConfigurer@Configuration および @EnableAsync が設定されたクラスで実装することで、非同期処理のパラメータを設定できます。AsyncConfigurerSupport は AsyncConfigurer を実装したクラスです。今回はこれを継承して必要な設定だけを変更します。他の設定は AsyncConfigurerSupport が定める既定値にしたがいます。

    AppRunner.java

    CommandLineRunner を実装するクラスです。アプリケーション起動時に一度だけ実行される処理を設定できます。処理の内部で後述の非同期処理を 3 つ実行しています。CommandLineRunner 自体は、今回の目的である非同期処理とは直接関係ありません。

    AppRunner クラスは @Service が設定された GitHubLookupService に依存しており、このような場合は @Autowired アノテーションによって dependency injection します。特に『Spring Beans and dependency injection』に記載のとおり、AppRunner() コンストラクタは一つしか存在しないため、@Autowired アノテーションの省略が可能となっています。

    package hello;
    
    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 AppRunner implements CommandLineRunner {
    
        private static final Logger logger = LoggerFactory.getLogger(AppRunner.class);
    
        private final GitHubLookupService gitHubLookupService;
    
        public AppRunner(GitHubLookupService gitHubLookupService) {
            this.gitHubLookupService = gitHubLookupService;
        }
    
        @Override
        public void run(String... args) throws Exception { // 可変長引数であることを意味する Java の記法です。
    
            // 処理の開始時刻を記憶します。
            long start = System.currentTimeMillis();
    
            // 非同期処理を 3 つ開始します。
            Future<User> page1 = gitHubLookupService.findUser("PivotalSoftware");
            Future<User> page2 = gitHubLookupService.findUser("CloudFoundry");
            Future<User> page3 = gitHubLookupService.findUser("Spring-Projects");
    
            // 今回のサンプルではすべてが完了するまで待ちます。
            while (!(page1.isDone() && page2.isDone() && page3.isDone())) {
                Thread.sleep(10); // millisecond
            }
    
            // 処理に要した時間をログ出力します。
            logger.info("Elapsed time: " + (System.currentTimeMillis() - start));
            logger.info("--> " + page1.get());
            logger.info("--> " + page2.get());
            logger.info("--> " + page3.get());
        }
    }
    

    GitHubLookupService.java

    本ページの目的である、非同期処理を実装するクラスです。@Async アノテーションを設定した非同期処理メソッドは Future を返します。Future については、Scala の Akkaと同様の扱いになります。

    AppRunner.java と同様に、GitHubLookupService()@Autowired が省略された DI (Dependency Injection) 用のコンストラクタです。

    RestTemplate は Spring フレームワークが提供する HTTP クライアントのようなものです。チュートリアルはこちらです。返された JSON 文字列を後述の User クラスのオブジェクトに変換して返します。

    package hello;
    
    import java.util.concurrent.Future;
    
    import org.slf4j.Logger;
    import org.slf4j.LoggerFactory;
    import org.springframework.boot.web.client.RestTemplateBuilder;
    import org.springframework.scheduling.annotation.Async;
    import org.springframework.scheduling.annotation.AsyncResult;
    import org.springframework.stereotype.Service;
    import org.springframework.web.client.RestTemplate;
    
    @Service
    public class GitHubLookupService {
    
        private static final Logger logger = LoggerFactory.getLogger(GitHubLookupService.class);
    
        private final RestTemplate restTemplate;
    
        public GitHubLookupService(RestTemplateBuilder restTemplateBuilder) {
            this.restTemplate = restTemplateBuilder.build();
        }
    
        @Async // コメントアウトすると、メインスレッドで実行されるようになります。非同期処理はなされなくなりますが、コンパイルエラーにはなりません。
        public Future<User> findUser(String user) throws InterruptedException {
            logger.info("Looking up " + user);
            String url = String.format("https://api.github.com/users/%s", user);
            User results = restTemplate.getForObject(url, User.class);
    
            // 非同期処理を並列実行することによって、処理時間が短縮されることを分かりやすくするための sleep です。
            Thread.sleep(1000L);
            return new AsyncResult<>(results);
        }
    }
    

    上記のうち以下の URL 文字列の構築処理は、専用の UriComponentsBuilder を利用することもできます。

    String url = String.format("https://api.github.com/users/%s", user);
    User results = restTemplate.getForObject(url, User.class);
    

    +import java.net.URI;
    +import org.springframework.web.util.UriComponentsBuilder;
    ...
    
    +UriComponentsBuilder builder = UriComponentsBuilder.fromUriString("https://api.github.com/users/").path(user);
    +URI url = builder.build().toUri();
    User results = restTemplate.getForObject(url, User.class);
    

    User.java

    前述の restTemplate で JSON 文字列を変換する対象となるクラスです。@JsonIgnoreProperties(ignoreUnknown=true) は、JSON 文字列に存在して User クラスに存在しない項目を変換対象から除外するための設定です。除外すべき項目が存在しない場合は設定不要です。

    package hello;
    
    import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
    
    @JsonIgnoreProperties(ignoreUnknown=true)
    public class User {
    
        private String name;
        private String blog;
    
        public String getName() {
            return name;
        }
    
        public void setName(String name) {
            this.name = name;
        }
    
        public String getBlog() {
            return blog;
        }
    
        public void setBlog(String blog) {
            this.blog = blog;
        }
    
        @Override
        public String toString() {
            return "User [name=" + name + ", blog=" + blog + "]";
        }
    }
    

    定期処理

    別スレッドで定期的に処理を実行するためには @Scheduled アノテーションを利用します。

    src/main/java/hello/Application.java

    @EnableAsync ではなく @EnableScheduling を設定します。後述の @Scheduled を利用するために、@Configuration 設定のある、いずれかのクラスへの必要が設定です。Javadoc はこちらです。

    package hello;
    
    import org.springframework.boot.SpringApplication;
    import org.springframework.boot.autoconfigure.SpringBootApplication;
    import org.springframework.scheduling.annotation.EnableScheduling;
    
    @SpringBootApplication
    @EnableScheduling
    public class Application {
    
        public static void main(String[] args) throws Exception {
            SpringApplication.run(Application.class);
        }
    }
    

    src/main/java/hello/ScheduledTasks.java

    @Scheduled(fixedRate = 5000) によって 5 秒毎に実行されるメソッドとして設定されます。以下のような設定が可能です

    • @Scheduled(fixedRate = 5000) 処理時間を考慮せず 5 秒毎に実行
    • @Scheduled(fixedDelay = 5000) 処理が完了してから 5 秒後に再度実行
    • @Scheduled(initialDelay = 1000, fixedRate = 5000) アプリケーションが起動した 1 秒後から、処理時間を考慮せず 5 秒毎に実行
    • @Scheduled(cron="*/5 * * * * MON-FRI") cron 形式の設定。平日のみ 5 秒毎に実行

    ScheduledTasks.java

    package hello;
    
    import java.text.SimpleDateFormat;
    import java.util.Date;
    
    import org.slf4j.Logger;
    import org.slf4j.LoggerFactory;
    import org.springframework.scheduling.annotation.Scheduled;
    import org.springframework.stereotype.Component;
    
    @Component
    public class ScheduledTasks {
    
        private static final Logger log = LoggerFactory.getLogger(ScheduledTasks.class);
    
        private static final SimpleDateFormat dateFormat = new SimpleDateFormat("HH:mm:ss");
    
        @Scheduled(fixedRate = 5000)
        public void reportCurrentTime() {
            log.info("The time is now {}", dateFormat.format(new Date()));
        }
    }
    
    0
    詳細設定を開く/閉じる
    アカウント プロフィール画像 (本文下)

    Spring Bootでの実用的な機能をわかりやすく解説中

    記事の執筆者にステッカーを贈る

    有益な情報に対するお礼として、またはコメント欄における質問への返答に対するお礼として、 記事の読者は、執筆者に有料のステッカーを贈ることができます。

    さらに詳しく →
    ステッカーを贈る コンセプト画像

    Feedbacks

    Feedbacks コンセプト画像

      ログインするとコメントを投稿できます。

      関連記事

      • Spring Security フォームログインのサンプルコード
        Spring フレームワークによる Web アプリケーション開発で、ログイン処理を実装する際は Spring Security が便利です。ここでは特に Spring Boot で Web アプリケーションを開発する場合を対象とし、フォームによる ID/Password ログインを行うためのサンプルコードをまとめます。 公式ドキュメント [Spring Security チュートリアル](http...
        えびちゃんえびちゃん12/4/2019に更新
        いいねアイコン画像0
      • Java配列の宣言方法 (C/C++との違い)
        Javaの配列 Javaの配列宣言方法はC/C++と似ているようで若干異なる。 初期化しない場合 C/C++の int array[10]; はJavaでは int array[] = new int[10]; となる。同様にC/C++の int array[3][3]; はJavaでは int array[][] = new int[3][3]; となる。 初期化
        てんとうむしてんとうむし5/13/2018に更新
        いいねアイコン画像0
      • PlantUML による UML 図の描き方
        サムネイル画像-c788fffde5
        PlantUML はテキスト形式で表現されたシーケンス図やクラス図といった UML (Unified Modeling Language) 図の情報から画像を生成するためのツールです。簡単な使い方をまとめます。 インストール方法の選択 Atom や Eclipse のプラグインをインストールしてエディタから利用する方法、JAR をダウンロードして Java コマンドで実行する方法、Redmine ...
        kentakenta1/21/2020に更新
        いいねアイコン画像0
      • Akka HTTP サンプルコード (Scala)
        サムネイル画像-a98142497c
        Akka アクターを用いて実装された汎用 HTTP フレームワークです。Spray の後継です。コアモジュールである akka-http-core は 2016/2/17 に experimental が外れました。akka-http などのいくつかのサブモジュールは 2016/3/1 現在 experimental のままですが、基本的な
        雄太雄太10/7/2021に更新
        いいねアイコン画像0
      • Kestrel の使用例
        Kestrel は Message Queue (MQ) の実装のひとつです。一般に MQ はアプリケーション間やプロセス間、スレッド間で非同期に通信するために用いられます。メッセージの送信側は MQ に書き込めば受信側の応答を待たずに次の処理に非同期に進むことができます。Kestrel はわずか 2500 行程の Scala で実装されており JVM で動作します。MQ 自体はメモリ上に存在する...
        したくんしたくん10/12/2017に更新
        いいねアイコン画像0