This guide walks you through the steps to create asynchronous queries to GitHub. The focus is on the asynchronous part, a feature often used when scaling services.
You’ll build a lookup service that queries GitHub user information and retrieves data through GitHub’s API. One approach to scaling services is to run expensive jobs in the background and wait for the results using Java’s CompletableFuture
interface. Java’s CompletableFuture
is an evolution from the regular Future
. It makes it easy to pipeline multiple asynchronous operations merging them into a single asynchronous computation.
Before you can create a GitHub lookup service, you need to define a representation for the data you’ll retrieve through GitHub’s API.
To model the user representation, you create a resource representation class. Provide a plain old Java object with fields, constructors, and accessors:
src/main/java/hello/User.java
link:complete/src/main/java/hello/User.java[role=include]
Spring uses the Jackson JSON library to convert GitHub’s JSON response into a User
object. The @JsonIgnoreProperties
annotation signals Spring to ignore any attributes not listed in the class. This makes it easy to make REST calls and produce domain objects.
In this guide, we are only grabbing the name
and the blog
URL for demonstration purposes.
Next you need to create a service that queries GitHub to find user information.
src/main/java/hello/GitHubLookupService.java
link:complete/src/main/java/hello/GitHubLookupService.java[role=include]
The GitHubLookupService
class uses Spring’s RestTemplate
to invoke a remote REST point
(api.github.com/users/), and then convert the answer into a User
object. Spring Boot automatically provides a RestTemplateBuilder
that customizes the defaults with any auto-configuration bits (i.e. MessageConverter
).
The class is marked with the @Service
annotation, making it a candidate for Spring’s component scanning to detect it and add it to the application context.
The findUser
method is flagged with Spring’s @Async
annotation, indicating it will run on a separate thread. The method’s return type is CompletableFuture<User>
instead of User
, a requirement for any asynchronous service. This code uses the completedFuture
method to return a CompletableFuture
instance which is already completed with result of the GitHub query.
Note
|
Creating a local instance of the GitHubLookupService class does NOT allow the findUser method to run asynchronously. It must be created inside a @Configuration class or picked up by @ComponentScan .
|
The timing for GitHub’s API can vary. To demonstrate the benefits later in this guide, an extra delay of one second has been added to this service.
To run a sample, you can create an executable jar. Spring’s @Async
annotation works with web apps, but you don’t need all the extra steps of setting up a web container to see its benefits.
src/main/java/hello/Application.java
link:complete/src/main/java/hello/Application.java[role=include]
The @EnableAsync
annotation switches on Spring’s ability to run @Async
methods in a background thread pool. This class also customizes the used Executor
. In our case, we want to limit the number of concurrent threads to 2 and limit the size of the queue to 500. There are many more things you can tune. By default, a SimpleAsyncTaskExecutor
is used.
There is also a CommandLineRunner
that injects the GitHubLookupService
and calls that service 3 times to demonstrate the method is executed asynchronously.
src/main/java/hello/AppRunner.java
link:complete/src/main/java/hello/AppRunner.java[role=include]
https://raw.githubusercontent.com/spring-guides/getting-started-macros/master/build_an_executable_jar_subhead.adoc https://raw.githubusercontent.com/spring-guides/getting-started-macros/master/build_an_executable_jar_with_both.adoc
Logging output is displayed, showing each query to GitHub. With the help of allOf
factory method we create an array of CompletableFutures
on which by calling the join
method makes it possible to wait for the completion of all of the CompletableFutures
.
2016-09-01 10:25:21.295 INFO 17893 --- [ GithubLookup-2] hello.GitHubLookupService : Looking up CloudFoundry 2016-09-01 10:25:21.295 INFO 17893 --- [ GithubLookup-1] hello.GitHubLookupService : Looking up PivotalSoftware 2016-09-01 10:25:23.142 INFO 17893 --- [ GithubLookup-1] hello.GitHubLookupService : Looking up Spring-Projects 2016-09-01 10:25:24.281 INFO 17893 --- [ main] hello.AppRunner : Elapsed time: 2994 2016-09-01 10:25:24.282 INFO 17893 --- [ main] hello.AppRunner : --> User [name=Pivotal Software, Inc., blog=http://pivotal.io] 2016-09-01 10:25:24.282 INFO 17893 --- [ main] hello.AppRunner : --> User [name=Cloud Foundry, blog=https://www.cloudfoundry.org/] 2016-09-01 10:25:24.282 INFO 17893 --- [ main] hello.AppRunner : --> User [name=Spring, blog=http://spring.io/projects]
Note that the first two calls happen in separate threads (GithubLookup-2
, GithubLookup-1
) and the third one is parked until one of the two threads became available. To compare how long this takes without the asynchronous feature, try commenting out the @Async
annotation and run the service again. The total elapsed time should increase noticeably because each query takes at least a second. You can also tune the Executor
to increase the corePoolSize
attribute for instance.
Essentially, the longer the task takes and the more tasks are invoked simultaneously, the more benefit you will see with making things asynchronous. The trade off is handling the CompletableFuture
interface. It adds a layer of indirection because you are no longer dealing directly with the results.
Congratulations! You’ve just developed an asynchronous service that lets you scale multiple calls at once.
The following guides may also be helpful: