It is not unusual that your web service needs to communicate with another web service in order to serve its clients. In the old days, that would imply that an incoming request to your server would capture one servlet connection, and perform a blocking call to the remote service before it can send a response to the client. It works, but it does not scale very well if you have multiple concurrent clients. However, as of Servlet 3.0 we have support for asynchronous servlets (see my colleague Henrik’s blog post), and as of Servlet 3.1 it also supports non-blocking I/O which means that we can get a significantly increased throughput.
Problem
In this blog post, we will implement an asynchronous servlet that talks to a remote server using Spring. For demonstration purposes, GitHub’s repository search API will be used as the remote service. The problem is basically divided into three main classes:
- A GitHubRepoListService has been implemented using the AsyncRestTemplate to communicate with a remote service.
- The RepositoryListDtoAdapter is responsible for converting the response extracted from the
AsyncRestTemplate
from Pojos specific to the GitHub response to other Pojos more suitable for the clients of our service. As such, it also acts as an anti-corruption layer between GitHub and the rest of our service implementation. - Lastly, there is the AsyncController that is the entry point for our clients.
Of course, the service would not compile without some glue code and a build script. For reference, the project is available on GitHub.
Remote Server Communication
First, we implement a server that uses a AsyncRestTemplate to communicate with GitHub:
@Service
class GitHubRepoListService implements RepoListService {
private static final String SEARCH_URL = "https://api.github.com/search/repositories?q={query}";
@Autowired
private AsyncRestTemplate asyncRestTemplate;
@Override
public ListenableFuture search(String query) {
ListenableFuture> gitHubItems =
asyncRestTemplate.getForEntity(SEARCH_URL, GitHubItems.class, query);
return new RepositoryListDtoAdapter(query, gitHubItems);
}
}
The response of the AsyncRestTemplate
request is a ListenableFuture that eventually will reference a GitHubItems
, which is then wrapped in a custom RepositoryListDtoAdapter
that converts it to a ListenableFuture<RepoListDto>
, see below.
Asynchronous Object Transformation
If you have not used the AsyncRestTemplate
or if you are not used to asynchronous Java programming, you may find that transforming one object to another is more complex than one would expect. It may be tempting to just call gitHubItems.get()
to fetch the response entity and then continue from there, but remember that we are dealing with a future which means that the get()
method is a blocking call that waits for the future to complete before returning. A better solution is to use the ListenableFutureAdapter which can then do the transformation in an asynchronous way:
class RepositoryListDtoAdapter extends ListenableFutureAdapter> {
private final String query;
public RepositoryListDtoAdapter(String query, ListenableFuture> gitHubItems) {
super(gitHubItems);
this.query = query;
}
@Override
protected RepoListDto adapt(ResponseEntity responseEntity) throws ExecutionException {
GitHubItems gitHubItems = responseEntity.getBody();
List repoDtos =
gitHubItems.items().stream().map(toRepositoryDto).collect(Collectors.toList());
return new RepoListDto(query, gitHubItems.totalCount(), repoDtos);
}
private static Function toRepositoryDto = item -> {
GitHubOwner owner = item.owner();
return new RepoDto(item.fullName(), item.getUrl(), item.description(),
owner.userName(), owner.url(), owner.avatarUrl());
};
}
Controller
The frontend of our service is a controller that maps incoming requests, extracts the query
parameter and delegate to the RepoListService
to do the remote call as described earlier. We need to convert the ListableFuture
to a DeferredResult before returning it to our clients in order to take advantage of Spring’s asynchronous request processing (see also Considerations). For this reason, we create a ListenableFutureCallback that we add to the ListenableFuture
:
@RestController
class AsyncController {
private static final Logger log = LoggerFactory.getLogger(AsyncController.class);
@Autowired
private RepoListService repoListService;
@RequestMapping("/async")
DeferredResult> async(@RequestParam("q") String query) {
DeferredResult> deferredResult = new DeferredResult<>();
ListenableFuture repositoryListDto = repoListService.search(query);
repositoryListDto.addCallback(
new ListenableFutureCallback() {
@Override
public void onSuccess(RepoListDto result) {
ResponseEntity responseEntity =
new ResponseEntity<>(result, HttpStatus.OK);
deferredResult.setResult(responseEntity);
}
@Override
public void onFailure(Throwable t) {
log.error("Failed to fetch result from remote service", t);
ResponseEntity responseEntity =
new ResponseEntity<>(HttpStatus.SERVICE_UNAVAILABLE);
deferredResult.setResult(responseEntity);
}
}
);
return deferredResult;
}
}
The ListenableFutureCallback
interface has two methods that we need to implement. If all goes well, the onSuccess()
method is called. Consequently, the response that is transformed by the RepositoryListDtoAdapter
is wrapped in a ResponseEntity together with a success status, which in turn is passed to the DeferredResult
instance. If something goes wrong, the onFailure()
method is called, an empty response entity with a failure status code is created instead.
Glue Code
Since the project is implemented using Spring Boot, we use a simple Application
class to configure Spring:
@Configuration
@ComponentScan
@EnableAutoConfiguration
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
@Bean
AsyncRestTemplate asyncRestTemplate() {
return new AsyncRestTemplate();
}
}
The rest of the code is basically plumbing to handle the serialization and deserialization of requests and responses. The JSON response from GitHub is mapped to GitHubItems
by the AsyncRestTemplate
:
@JsonIgnoreProperties(ignoreUnknown = true)
class GitHubItems {
@JsonProperty("total_count")
private int totalCount;
@JsonProperty("items")
private List items;
int totalCount() {
return totalCount;
}
List items() {
return items;
}
}
Where GitHubItem
is implemented like:
@JsonIgnoreProperties(ignoreUnknown = true)
class GitHubItem {
@JsonProperty("full_name")
private String fullName;
@JsonProperty("html_url")
private URL url;
@JsonProperty("description")
private String description;
@JsonProperty("owner")
private GitHubOwner owner;
String fullName() {
return fullName;
}
URL getUrl() {
return url;
}
String description() {
return description;
}
GitHubOwner owner() {
return owner;
}
}
and the GitHubOwner
is implemented as:
@JsonIgnoreProperties(ignoreUnknown = true)
class GitHubOwner {
@JsonProperty("login")
private String userName;
@JsonProperty("html_url")
private URL url;
@JsonProperty("avatar_url")
private URL avatarUrl;
String userName() {
return userName;
}
URL url() {
return url;
}
URL avatarUrl() {
return avatarUrl;
}
}
The response that is sent to the client consists of two Pojos. At the top level, there is a RepoListDto
that contains information about the client’s search query, the total number of repositories at GitHub that matched the query and a list of RepoDto
s that represents each repository:
public class RepoListDto {
@JsonProperty("query")
private final String query;
@JsonProperty("nbr_of_repositories")
private final int nbrOfRepositories;
@JsonProperty("repositories")
private final List repositories;
public RepoListDto(String query, int nbrOfRepositories, List repositories) {
this.query = query;
this.nbrOfRepositories = nbrOfRepositories;
this.repositories = repositories;
}
}
Each individual GitHub repository is presented as a RepoDto
:
public class RepoDto {
@JsonProperty("name")
private final String name;
@JsonProperty("url")
private final URL url;
@JsonProperty("description")
private final String description;
@JsonProperty("owner")
private final String owner;
@JsonProperty("owner_url")
private final URL ownerUrl;
@JsonProperty("owner_avatar")
private final URL ownerAvatar;
public RepoDto(String name, URL url, String description,
String owner, URL ownerUrl, URL ownerAvatar) {
this.name = name;
this.url = url;
this.description = description;
this.owner = owner;
this.ownerUrl = ownerUrl;
this.ownerAvatar = ownerAvatar;
}
}
Build script
The project was built using Maven and the following pom.xml
:
4.0.0
com.jayway.asyncservlet
demo
0.0.1-SNAPSHOT
jar
AsyncServlet
Demo project of how an asynchronous server can use an AsyncRestTemplate
org.springframework.boot
spring-boot-starter-parent
1.1.6.RELEASE
org.springframework.boot
spring-boot-starter-web
org.apache.httpcomponents
httpasyncclient
org.springframework.boot
spring-boot-starter-test
test
UTF-8
com.jayway.asyncservlet.Application
1.8
3.1.0
8.0.12
org.springframework.boot
spring-boot-maven-plugin
Considerations
- In this blog, I have showed how the
AsyncRestTemplate
to communicate with an external restful service. However, before using it in production I suggest that you configure the internals of theAsyncRestTemplate
such as connection timeout, read timeout, max number of connections, max connections per route etc. The Apache HttpAsyncClient examples, this blog post and this question on Stack Overflow all provide good advice. - The asynchronous service will tolerate a much higher load than a synchronous solution would. However, from a client perspective the latency may be an issue since it is first calling your service, which in turn has to call the remote service and wait for its response, before a new response can be created and returned. To mitigate this problem, different cache solutions come to mind. Consider caching the responses from the remote services, caching the Pojos generated by our service and / or using HTTP headers to implement appropriate caching of the responses generated by our service in the HTTP layer.
- Our asynchronous service is well prepared to handle big load, but is the remote service capable of handling the load you are delegating to it? The suggested cache solution may work if the remote service returns generic answers, but if the remote service returns unique responses for similar requests the cache solution will not do. And what happens if the remote service goes down? One way of handling such requirements, and to protect your server from cascading failures, is to wrap the remote call in a circuit breaker.
- If you can migrate to Spring 4.1 that was released last week you do no longer need to convert your result to
DeferredResult
. Simply return theListenableFuture
from your controller (see the list of improvements).
Result
The response as returned from GitHub directly:
$ curl https://api.github.com/search/repositories?q=spring+boot | jq .
{
"total_count": 883,
"incomplete_results": false,
"items": [
{
"id": 6296790,
"name": "spring boot",
"full_name": "spring-projects/spring-boot",
"owner": {
"login": "spring-projects",
"id": 317776,
"avatar_url": "https://avatars.githubusercontent.com/u/317776?v=2",
"gravatar_id": "6f8a529bd100f4272a9ff1b8cdfbd26e",
"url": "https://api.github.com/users/spring-projects",
"html_url": "https://github.com/spring-projects",
"followers_url": "https://api.github.com/users/spring-projects/followers",
"following_url": "https://api.github.com/users/spring-projects/following{/other_user}",
"gists_url": "https://api.github.com/users/spring-projects/gists{/gist_id}",
"starred_url": "https://api.github.com/users/spring-projects/starred{/owner}{/repo}",
"subscriptions_url": "https://api.github.com/users/spring-projects/subscriptions",
"organizations_url": "https://api.github.com/users/spring-projects/orgs",
"repos_url": "https://api.github.com/users/spring-projects/repos",
"events_url": "https://api.github.com/users/spring-projects/events{/privacy}",
"received_events_url": "https://api.github.com/users/spring-projects/received_events",
"type": "Organization",
"site_admin": false
},
"private": false,
"html_url": "https://github.com/spring-projects/spring-boot",
"description": "Spring Boot",
"fork": false,
"url": "https://api.github.com/repos/spring-projects/spring-boot",
"forks_url": "https://api.github.com/repos/spring-projects/spring-boot/forks",
"keys_url": "https://api.github.com/repos/spring-projects/spring-boot/keys{/key_id}",
"collaborators_url": "https://api.github.com/repos/spring-projects/spring-boot/collaborators{/collaborator}",
"teams_url": "https://api.github.com/repos/spring-projects/spring-boot/teams",
"hooks_url": "https://api.github.com/repos/spring-projects/spring-boot/hooks",
"issue_events_url": "https://api.github.com/repos/spring-projects/spring-boot/issues/events{/number}",
"events_url": "https://api.github.com/repos/spring-projects/spring-boot/events",
"assignees_url": "https://api.github.com/repos/spring-projects/spring-boot/assignees{/user}",
"branches_url": "https://api.github.com/repos/spring-projects/spring-boot/branches{/branch}",
"tags_url": "https://api.github.com/repos/spring-projects/spring-boot/tags",
"blobs_url": "https://api.github.com/repos/spring-projects/spring-boot/git/blobs{/sha}",
"git_tags_url": "https://api.github.com/repos/spring-projects/spring-boot/git/tags{/sha}",
"git_refs_url": "https://api.github.com/repos/spring-projects/spring-boot/git/refs{/sha}",
"trees_url": "https://api.github.com/repos/spring-projects/spring-boot/git/trees{/sha}",
"statuses_url": "https://api.github.com/repos/spring-projects/spring-boot/statuses/{sha}",
"languages_url": "https://api.github.com/repos/spring-projects/spring-boot/languages",
"stargazers_url": "https://api.github.com/repos/spring-projects/spring-boot/stargazers",
"contributors_url": "https://api.github.com/repos/spring-projects/spring-boot/contributors",
"subscribers_url": "https://api.github.com/repos/spring-projects/spring-boot/subscribers",
"subscription_url": "https://api.github.com/repos/spring-projects/spring-boot/subscription",
"commits_url": "https://api.github.com/repos/spring-projects/spring-boot/commits{/sha}",
"git_commits_url": "https://api.github.com/repos/spring-projects/spring-boot/git/commits{/sha}",
"comments_url": "https://api.github.com/repos/spring-projects/spring-boot/comments{/number}",
"issue_comment_url": "https://api.github.com/repos/spring-projects/spring-boot/issues/comments/{number}",
"contents_url": "https://api.github.com/repos/spring-projects/spring-boot/contents/{+path}",
"compare_url": "https://api.github.com/repos/spring-projects/spring-boot/compare/{base}...{head}",
"merges_url": "https://api.github.com/repos/spring-projects/spring-boot/merges",
"archive_url": "https://api.github.com/repos/spring-projects/spring-boot/{archive_format}{/ref}",
"downloads_url": "https://api.github.com/repos/spring-projects/spring-boot/downloads",
"issues_url": "https://api.github.com/repos/spring-projects/spring-boot/issues{/number}",
"pulls_url": "https://api.github.com/repos/spring-projects/spring-boot/pulls{/number}",
"milestones_url": "https://api.github.com/repos/spring-projects/spring-boot/milestones{/number}",
"notifications_url": "https://api.github.com/repos/spring-projects/spring-boot/notifications{?since,all,participating}",
"labels_url": "https://api.github.com/repos/spring-projects/spring-boot/labels{/name}",
"releases_url": "https://api.github.com/repos/spring-projects/spring-boot/releases{/id}",
"created_at": "2012-10-19T15:02:57Z",
"updated_at": "2014-09-05T09:37:27Z",
"pushed_at": "2014-09-05T02:22:04Z",
"git_url": "git://github.com/spring-projects/spring-boot.git",
"ssh_url": "git@github.com:spring-projects/spring-boot.git",
"clone_url": "https://github.com/spring-projects/spring-boot.git",
"svn_url": "https://github.com/spring-projects/spring-boot",
"homepage": "http://projects.spring.io/spring-boot",
"size": 46263,
"stargazers_count": 1028,
"watchers_count": 1028,
"language": "Java",
"has_issues": true,
"has_downloads": true,
"has_wiki": true,
"forks_count": 849,
"mirror_url": null,
"open_issues_count": 173,
"forks": 849,
"open_issues": 173,
"watchers": 1028,
"default_branch": "master",
"score": 78.31211
},
// more repositories...
]
}
The response that is returned by our service:
curl http://localhost:8080/async?q=spring+boot | jq .
{
"query": "spring boot",
"nbr_of_repositories": 860,
"repositories": [
{
"name": "spring-projects/spring-boot",
"url": "https://github.com/spring-projects/spring-boot",
"description": "Spring Boot",
"owner": "spring-projects",
"owner_url": "https://github.com/spring-projects",
"owner_avatar": "https://avatars.githubusercontent.com/u/317776?v=2"
},
// more repositories...
]
}
Side note, jq is a convenient tool for JSON processing that you can install.
References
- Spring Reference docs Asynchronous Request Processing.
- GitHub search repositories API.
- GitHub rate limiting.
Mattias, thank you for an interesting blog. In the Considerations section you say “However, from a client perspective the latency may be an issue since it is first calling your service, which in turn has to call the remote service and wait for its response, before a new response can be created and returned. ”
You really lost me there! I thought the whole purpose of the AsyncService was to avoid that. To my understanding the client who called the server via the controller would receive the DeferredResult and call getResult on it and only then block until the result is available.
@Haytham: Sorry for being unclear, let me elaborate. First of all, in this blog by “client” I mean “end user” or “consumer of the service”.
Now, consider a simplified example where a request from the client to the service takes 200ms and the request from the service to the remote service takes another 100ms during normal operation. If the service only receives a single request it does not matter if the service is single threaded and synchronous or multithreaded and asynchronous, the total response time for the client will be 300ms regardless of the service implementation. However, if we make 100 requests concurrently, the response time of all requests to the single threaded, synchronous service will be 10.2 seconds (200ms + 100 * 100ms) whereas for the asynchronous service it will still be 300ms. Consequently, the asynchronous implementation matters if your service has a high load.
Secondly, consider another example where the request from the client to the service takes 200ms, but this time the request from the service to the remote service takes 5 seconds. If a single request is being sent, the response time from the clients perspective is 5.2 seconds and the following request will also take 5.2 seconds. However, if the response to the second request can be generated from a cached value rather than from the remote service the response time for that (and any subsequent request with the same cache key) will only be 200ms since the call to the remote service is avoided. In other words, both the synchronous and the asynchronous service may get significantly decreased response times if cache is used. As a side effect, a cache will also decrease the load on the remote service.
These are just two staged and very simplified examples, but I hope they provide some more understanding to the problem.
I think the link you provided to one of your Colleague’s blogs answers my question:
http://blog.jayway.com/2014/05/16/async-servlets/
Returning a DeferredResult signals to Spring that the request should be treated asynchronously. After invoking setResult the response will be sent back to the client.
@Haytham: Quite right. Please read the Asynchronous Request Processing paragraph in the Spring Reference Docs for details. There is also a link to a blog post close to the end that you may find interesting.
From a testing perspective, does this work with MockMvc?
@Bob: Yes,
MockMvc
is supported, however you need to write some extra lines compared to a synchronous flow in order for it work. First, you start the async processing by calling themockMvc.perform(...)
as you would do normally, but make sure to keep a reference to the returnedMvcResult
. Next, you make an async dispatch by re-using theMvcResult
. The following lines are copied from the JavaDoc of the MockMvcRequestBuilders.asyncDispatch(MvcResult mvcResult) method:Also, since 4.1 you can use lambdas to set the callback methods (success and failure).
repositoryListDto.addCallback(r -> deferredResult.setResult(new ResponseEntity(r, HttpStatus.OK)), t -> deferredResult.setResult(new ResponseEntity(HttpStatus.SERVICE_UNAVAILABLE)));
@Gustavo: Yes, the implementation can be made more concise by using
ListenableFuture
‘s void addCallback(SuccessCallback super T> successCallback, FailureCallback failureCallback) method and the supporting SuccessCallback and FailureCallback interfaces that was added in Spring 4.1.You don’t really need the complex controller code to map the ListenableFuture into the DeferredResult manually. Why do you don’t return the ListenableFuture directly? Spring supports this as return type and is doing the rest for you, see http://docs.spring.io/spring/docs/current/spring-framework-reference/html/mvc.html#mvc-ann-return-types. The controller has nothing more to do than pass it through. That’s make the code easier to read and keep it clean. The result is exactly the same.
1. The Response type of the server and controller method has to be the same. Instead of returning a ResponseEntity of RepoListDto you could simply return a ListenableFuture of RepoListDto.
2. The exception handling can be implemented in a @ExceptionHandler annotated method.
CHANGES TO THE CONTROLLER
@RequestMapping(“/async”)
ListenableFuture async(@RequestParam(“q”) String query) {
return repoListService.search(query);
}
@ExceptionHandler(Exception.class)
public ResponseEntity handleException(Exception e) {
log.error(“Failed to fetch result from remote service”, e);
return new ResponseEntity(HttpStatus.SERVICE_UNAVAILABLE);
}
@Carl: Thank you for your comment. You are quite right, the implementation can be simplified in the ways that you suggest. The reason why I chose the implementation that I did was because I used Spring Boot version 1.1.6, i.e. Spring Framework version 4.0.7. Returning
ListanableFuture
from a controller or other service was not supported until Spring 4.1. I guess that I could have been more clear about which version was used in the blog post, but you can see the versions used in the associated project at GitHub. Moreover, I mentioned the possibility in one of considerations above as a suggestion when migrating to Spring 4.1.Does it work if the restTemplate’s getForEntity timeout?
@Sting: Since you ask, I guess it does not work, but I have not tried. Please also see the How do I set a timeout on Spring’s AsyncRestTemplate? question at Stack Overflow. It has been answered by Dave Syer and Rossen Stoyanchev, both Spring veterans employed by Pivotal.
hi,dose spring support Servlet3.1 Non-blocking IO ,do you have an example?thanks
@qeshine: Spring supports Servlet 3.1 as exemplified in this blog post, provided that you deploy your application in a servlet container that also supports Servlet 3.1.
Hello, you have a project using async non-blocking SOAP WSDL ?
@Juan: No, I am sorry. The last time I was working with SOAP was long before async came into fashion.