Level 7 - Milestone 7
Milestone 7 - Fetching Data with WebClient
Here we will use the WebClient class to make HTTP requests to another service. That is, we will
request data from another API, in order to provide the users of our application with information
relevant to their request.
Before completion of this milestone, students will:
- Add an instance of WebClient to their repository class
- Return the data receive from the request to our users as a String
Save the API Base URL as a Static Field
Soon we are going to need access to the base URL of the API from which we are requesting data. Go ahead and create a private static final baseUrl variable in the repository class, and initalize it with the base URL of the API.
Add the WebClient Dependency
In our repository class, we need to add a field and a constructor to support an instance of
WebClient.
This will be accomplished the exact same way we provided the controller and service classes
with instances of the classes they needed. There is one slight different however: instead of
taking the
WebClient as a parameter and having Spring instantiate it for us, we are going to do it ourself
inside of
the constructor. We can also take this opportunity
to set the base URL of the the API to which we expect to make requests. This makes it easier to
execute any request that we make within this class, as we then do not need to supply it for
every one.
The syntax for instantiation can be found below:
package org.jointheleague.api.cheetah.Cheetah_Search.repository; import org.springframework.stereotype.Repository; import org.springframework.web.reactive.function.client.WebClient; @Repository public class LocRepository { private final WebClient webClient; private static final String baseUrl = "https://www.loc.gov/books"; public LocRepository() { webClient = WebClient .builder() .baseUrl(baseUrl) .build(); } public String getResults(String query){ return "Searching for books related to " + query; } }
Requesting the Data
This step may look a slightly different depending on the specific API that we are requesting data from. Each API will require a somewhat specific set of data for a request to be successful. For example, an API may require that you specify the format in which you wish to receive that data, as is the case of the Library of Congress API. We will also specify that we don't want any extra information with the "at" parameter, and of course supply the topic that we want results relating to. Here is the completed code to make our desired request to Library of Congress for a books related to a topic:
public String getResults(String query) { return webClient.get() .uri(uriBuilder -> uriBuilder .queryParam("fo", "json") .queryParam("at", "results") .queryParam("q", query) .build() ) .retrieve() .bodyToMono(String.class) .block(); }
There is a lot of syntax there. While it is import that we understand how it works, this is a piece of code that could be copy and pasted without shame. In fact, this code was copy and pasted into these instructions from a project where it was copy and pasted into. Lets break this down line-by-line to try to understand what is going on here.
.get()
This represent the HTTP method that we want to use. Here we want to make a GET request to the API to simply retrieve some data..uri(...)
We are building upon the base URL that we specified in the constructor. Here we need to append a few URL parameters to the end of the String, exactly like how we accept the topic that our users are interested in with our API. You can see in this case we are adding three URL parameters. This chunk of the URL is also known as the "query string". If we viewed the entire request URL at this point, it would look like this, given that the value "cats" was passed into our method as a parameter: "https://www.loc.gov/books?fo=json&at=results&q=cats"..retrieve()
This means that we are preparing to declare how we want to extract the response. More specifically, this retrieve() method says that we are only interested in the body of the response. The method .extract() serves a similar role, but allows direct access to the headers and status code of the response..bodyToMono(String.class)
To truly understand this piece of code would require knowledge of a number of topics well beyond the scope of this course, but they get to the heart of why the WebClient class represents a big step forward in terms of java libraries designed to fetch data via HTTP requests. A simple explanation will be provided, as well as links to further reading if you are interested in attempting a deeper understanding.There are a number of class that we could use in place of WebClient here, two popular choices would be the HttpUrlConnection class and the RestTemplate class. Either of these would suffice for our application, but WebClient is actually the replacement for the RestTemplate class, which is now deprecated as of Spring 5. The major change that is implemented by WebClient: it is asynchronous and non-blocking! By now you are probably more than willing to accept the suggestion that a deep understanding of this is not necessary. In essence, WebClient being non-blocking means that our application doesn't need to sit idly by and wait for the response. It can come back when the result is ready, and in the meantime switch to another active task that uses the same underlying resource.
At the end of the day, we aren't even going to be taking advantage of the non-blocking nature of WebClient. However the reactive nature of this class is what leads us to the Mono class. Since WebClient is designed to be asynchronous and non-blocking, the request itself needs to return something that will, at a later point, contain the actual result. We can't simply return the result from the request, because we will not have it yet. The thing returned from the actual request is a "publisher", an idea that comes from the reactive streams initiative linked below. Project Reactor, the reactive streams implementation created for Spring, defines two publisher classes: Mono and Flux. Mono is meant to be used when expected 0 or 1 result from our request. Flux is meant to be used when expecting 0 to n results. Here we are only expecting one result, so the bodyToMono() is used.
The parameter to this method is really the only part that is important to us here. We are saying that we want to the result to be given to us as a String. While this isn't terribly exciting at the moment, we will later change this to allow our response to be given to us in the form of a custom Java class that represents the information we expect from the response. This will allow us to very easily pull out the specific pieces of information we are interested in.
.block()
This says that we actually want to wait for the response before the execution of our application continues. Essentially, this means we do not want to take advantage of the reactive stream related features of WebClient which were discussed previously. The request will be carried out with blocking, as it is when using RestTemplate or other classes.Another consequence of this method is that this long chain of methods will now return an object of type String, instead of type Mono<String>.
Additional Reading on Reactive Streams in Spring
- Intro to Reactive Programming - Project Reactor
- Understanding Reactive Types - Spring
- Reactive Streams Initiative
- Reactive Manifesto
Does the API Require Authentication?
Often times APIs will require you to prove that you have access to the service by having you provide
an API token as part of your requests. You will usually be issued the token after creating
an account on their website. There are two ways that an API would typically expect a token to
be provided: as a URL parameter, or as a header.
When supplying the token as a URL parameter, you will first need to find what the name of the header is
that they are expecting. Typically it would be something like "token" or "apiKey". Then you will supply this
when you make your request. You will likely also be supplying the query (i.e. search term) to the API as a
URL parameter, so it will just be a matter of supplying a second parameter. Depending on the name that the API
expects for your token and query, your request method will look something like this:
public ResultWrapper getResults(String query) { return webClient.get() .uri(uriBuilder -> uriBuilder .queryParam("query", query) .queryParam("apiKey", "YOUR API KEY GOES HERE") .build() ).retrieve() .bodyToMono(ResultWrapper.class) .block(); }
When supplying the token as a header, you will likewise need to find out what the name of the header is that the API is expecting. From that point there are two options. First, you could supply the header within the method that you have written to make the request:
public String getResults(String query) { return webClient.get() .uri(uriBuilder -> uriBuilder .queryParam("query", query) .build() ) .headers(httpHeaders -> httpHeaders.set("api-key", "YOUR API KEY GOES HERE")) .retrieve() .bodyToMono(String.class) .block(); }
You can use a multi-line lambda expression if you need to supply more than on header.
You could also supply your API token as a default header when you create your instance of WebClient within your repository class constructor. This is especially useful if you have multiple methods within your repository class that made different types of requests. Since you are likely to only have one method that makes requests within your repository class, this isn't strictly necessary in this case, but in general it could be considered a better approach. The code to add your token as a default header to your instance of WebClient is pretty similar, and would look something like this:
webClient = WebClient .builder() .baseUrl(baseUrl) .defaultHeader(httpHeaders -> { httpHeaders.set("api-key", "YOUR API KEY GOES HERE"); }) .build();
Important note: In this example, we are putting our API token in plain-text within our code. This is normally considered extraordinarily bad practice, since your token is visible to anyone with access to the source code. A better solution would be to use an environment variable to supply the value for your API token to your application. The actual value would be supplied within your IDE or operating system, and would be completely separate from your code and not appear in version control. The environment variable would likewise be supplied within your production environment, allowing it be used without exposing it to anyone with access to the code.
Test it Out!
After adding the code from the previous section, rerun the application and make a request using the Swagger UI. You should now see that you are being given back and giant chunk of JSON! The application is now returning real data to its users.
Summary of Code Changes for this Milestone
-
Cheetah-Search
- src
- main
- java
- org.jointheleague.level7.cheetah
- config
- ApiDocConfig.java
- presentation
- HomeController.java
- LocController.java
- service
- LocService.java
- repository
- resources
- application.yml
- build.gradle