こちらのページで環境を構築した 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
設定内容についてはこちらをご参照ください。
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')
}
アプリケーションのエントリーポイントとなるクラスです。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 が定める既定値にしたがいます。 |
こちらのページでも利用した 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());
}
}
本ページの目的である、非同期処理を実装するクラスです。@Async
アノテーションを設定した非同期処理メソッドは Future を返します。Future については、Scala の『Akka 基本的な使い方』や『HTTP 通信』と同様の扱いになります。
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);
前述の 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 + "]";
}
}