Call on me, or Asynchronous REST
This article is a very simple example of a working asynchronous REST application, made with Spring Boot + Java 8. Spring Boot makes developing web applications almost ridiculously easy, but to simplify the task even more, I took an example from Spring repository called rest-service , forked it to my own repository and changed it for my purposes to create two applications: a client and a server.
Our server app will be a simple REST web service that will query GitHub to get some user data and return it. Our client app will also be a REST web service… that will query the first app!
The server code basically consists of the service and a controller. The service uses an asynchronous method with the @Async annotation and looks like this.
@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 CompletableFuture<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); // Artificial delay of 1s for demonstration purposes Thread.sleep(1000L); return CompletableFuture.completedFuture(results); } }
The server controller:
@RestController public class GitHubController { private final GitHubLookupService lookupService; @Autowired public GitHubController(GitHubLookupService lookupService) { this.lookupService = lookupService; } @RequestMapping("/user/{name}") public CompletableFuture<TimedResponse<User>> findUser(@PathVariable(value = "name") String name) throws InterruptedException, ExecutionException { long start = System.currentTimeMillis(); ServerResponse response = new ServerResponse(Thread.currentThread().getName()); return lookupService.findUser(name) .thenApply(user -> { response.setData(user); response.setTimeMs(System.currentTimeMillis() - start); response.setCompletingThread(Thread.currentThread().getName()); return response; }); } }
What we have here is a simple CompletableFuture from Java 8 which we transform into the format we need with the help of thenApply() which allows us to add some data about the current thread to make sure that the execution really happens asynchronously, that is, the thread that is finishing the work is not the thread that started the work. We can make sure of it, running the application and checking the result of the call:
marina@Marinas-MacBook-Pro:~$ http http://localhost:8080/user/mchernyavskaya HTTP/1.1 200 Content-Type: application/json;charset=UTF-8 Date: Mon, 02 Oct 2017 18:07:54 GMT Transfer-Encoding: chunked { "completingThread": "SimpleAsyncTaskExecutor-1", "data": { "avatar_url": "https://avatars2.githubusercontent.com/u/538843?v=4", "company": "OLX", "location": "Berlin, Germany", "name": "Maryna Cherniavska", "url": "https://api.github.com/users/mchernyavskaya" }, "error": false, "startingThread": "http-nio-8080-exec-1", "timeMs": 2002 }
Now we need to create a client app that will be calling the server app. There’s a very convenient class for consuming REST in Spring which is called RestTemplate. However, RestTemplate is synchronous and all our nice asynchronous processing that happens in the server application would be no help to the client application at all. The two applications are completely independent. All the client app knows is that it is going to handle a rather long-running call. Since the client app knows that and since it probably doesn’t want to hog the thread for the whole time the server app is queries, we are going to make it asynchronous as well. AsyncRestTemplate coming to the rescue!
Our client application will be even more simple and will mainly consist of the controller code. To run both applications on one local machine, we need to change ports for the server with the -Dserver.port=8082 parameter. So, our server is now on localhost:8080 and the client is on localhost:8082.
The client controller is mainly as follows.
@RestController public class GitHubController { private static final String BASE_URL = "http://localhost:8080/"; private final AsyncRestTemplate asyncRestTemplate = new AsyncRestTemplate(); @RequestMapping("/async/user/{name}") public ListenableFuture<ClientResponse> findUserAsync(@PathVariable(value = "name") String name) throws InterruptedException, ExecutionException { long start = System.currentTimeMillis(); ClientResponse clientResponse = new ClientResponse(Thread.currentThread().getName()); ListenableFuture<ResponseEntity<ServerResponse>> entity = asyncRestTemplate.getForEntity(BASE_URL + name, ServerResponse.class); entity.addCallback(new ListenableFutureCallback<ResponseEntity<ServerResponse>>() { @Override public void onFailure(Throwable ex) { clientResponse.setError(true); clientResponse.setCompletingThread(Thread.currentThread().getName()); clientResponse.setTimeMs(System.currentTimeMillis() - start); } @Override public void onSuccess(ResponseEntity<ServerResponse> result) { clientResponse.setData(result.getBody()); clientResponse.setCompletingThread(Thread.currentThread().getName()); clientResponse.setTimeMs(System.currentTimeMillis() - start); } }); } }
We are taking the server response and wrapping it into more data about the timing and current threads to better see what is going on. The AsyncRestTemplate gives us a ListenableFuture, but we make a CompletableFuture out of it because it allows us to manually control the moment when the future returns and also transform the output in the process.
When we call the client service, it returns the following data:
marina@Marinas-MacBook-Pro:~$ http http://localhost:8082/async/user/mchernyavskaya HTTP/1.1 200 Content-Type: application/json;charset=UTF-8 Date: Mon, 02 Oct 2017 18:28:36 GMT Transfer-Encoding: chunked { "completingThread": "SimpleAsyncTaskExecutor-1", "data": { "completingThread": "SimpleAsyncTaskExecutor-3", "data": { "avatar_url": "https://avatars2.githubusercontent.com/u/538843?v=4", "company": "OLX", "location": "Berlin, Germany", "name": "Maryna Cherniavska", "url": "https://api.github.com/users/mchernyavskaya" }, "error": false, "startingThread": "http-nio-8080-exec-7", "timeMs": 1403 }, "error": false, "startingThread": "http-nio-8082-exec-3", "timeMs": 1418 }
You can read more about asynchronous methods in Spring here but this simple example should help you understand how things work. The full code is in the repository. Hope it is of some use!
Published on Java Code Geeks with permission by Maryna Cherniavska, partner at our JCG program. See the original article here: Call on me, or Asynchronous REST Opinions expressed by Java Code Geeks contributors are their own. |
Very good and well written example, thanks!
The example works fine. How do we send an HTTP 202 response in this example to the client if we know that we have a long running process and the actual response will take time? Thanks
You would have to deal with ResponseEntity instead of returning the response data object directly. Example for the client code: https://gist.github.com/mchernyavskaya/89f505a5dc37559bf986fee588a3c439
If the starting thread & completing thread have different names is that enough to know the async worked asynchronously? It would be nice to see thread numbers higher than 1.
It’s not just the numbers. You can see that the completing thread is created by the SimpleAsyncTaskExecutor which is created by @EnableAsync by default (because I haven’t overridden the task executor for simplicity). The incoming request, however, is processed by the http-nio-xxx thread which comes from the web server thread pool. However, you can always clone the example and simulate some concurrent load with the Apache Benchmark (ab) or some other tool and log the output; see how many different numbers you get.
Yes, that was really my question. Is it possible to get asynchronous results without creating a task executor bean? I don’t think it is but I’ll clone and try tomorrow. Thanks for responding.
Yep as I said above, Spring Boot will create a default SimpleAsyncTaskExecutor when you use the @EnableAsync. It’s just not something you would use in prod because it doesn’t reuse the threads, so normally it would get overridden. But it is ok for an example.
I still don’t see the benefit of returning a CompletableFuture on the REST endpoint. Why not just increase the number of web server threads (Tomcat threads) to handle more concurrent events? With Asynchronous REST, we are just offloading work to the worker thread pool, and assuming the worker thread pool is a fixed size, we will end up being limited to the number of worker threads. Please explain the benefit to me..