Overview of This Lesson

Refresh this page because I am probably still making changes to it.

You've learned how to create web services, the next step is to learn to consume web services. Consuming web services is how we use web services in an application.

Pre-Requisites

Before doing this lesson, be sure you've gone through the Web Services Introduction.

RestTemplate and ResponseEntity

In the previous lesson we created web services in a @RestController and tested them using Postman. In this lesson, we'll learn how to consume web services in a Spring application.

To consume a web service, you need to make a request for the web service method using the appropriate HTTP verb (GET, POST, PUT, DELETE, etc.) We can invoke ReST web service methods in our Java Spring applications by using the RestTemplate class. This class contains methods that allow you to make requests to a ReST service using standard HTTP verbs:

Notice that you have 2 options for GET and POST requests:

Additionally, all of these different methods are overloaded so that they can be invoked with different information:

All of the examples above also apply to postForObject() and postForEntity()

The getForEntity() and postForEntity() methods return a ResponseEntity object instead of just the response body. ResponseEntity models/contains the entire response, which includes the response headers and the response status code.

ResponseEntity is a child of a class called HttpEntity. HttpEntity is actually the parent of both ResponseEntity and RequestEntity, which models the entire request that was sent. So if you feel adventurous, have a look at the documentation for RequestEntity: you will see that you can add/change the request headers and request body.

Using the methods that return a ResponseEntity means you can access different elements of the response object using ResponseEntity methods, such as:

To perform PUT and DELETE requests, you can use the put() and delete() methods, respectively. They are also overloaded in a similar manner.

PUT methods:

DELETE methods:

Demonstration

Let's try consuming our web services from the previous lesson with a demonstration.

If you wish, make a copy of your previous project where we created a Rest Controller for operating on our collection of Container objects.

Let's start off with a fun demonstration. In the previous lesson you tried the Dog API web service: it generated a random picture of a dog.

This service is a ReSTful service and it returns JSON data. The easiest way to be able to use the JSON data is to just create a bean that models the response. If you look at the Dog API page, you'll see the mention that a sample response is in the format:

{
    "message" : "the URL to the image",
    "status" : "success" 
}

The status value "success" is sent when the request is successful. We are unable to test for an unsuccessful response, but we can probably assume that a failed request will not include the value "success" as the status.

Therefore, we can make a quick DogResponse bean using Lombok:

@Data
@AllArgsConstructor
@NoArgsConstructor
public class DogResponse {

    private String message;
    private String status;
}

Go ahead and add that to the .beans package of your project.

Next, add a @Controller to your project.

Let's add a handler method that performs a request to the Dog API service:

@GetMapping("/dog")
public String getDog(Model model, RestTemplate rest) {
    ResponseEntity<DogResponse> responseEntity
        = rest.getForEntity("https://dog.ceo/api/breeds/image/random", DogResponse.class);

    // testing
    System.out.println(responseEntity.getStatusCode());
    responseEntity.getHeaders().forEach((key, value) ->
    {
        System.out.println(key + ": " + value);
    });

    model.addAttribute("dogResponse", responseEntity.getBody());
    return "dog.html";
}

In this handler method, we're adding both a Model parameter and a RestTemplate parameter. Recall that RestTemplate contains methods that allow you to create requests to web services. It's automatically injected by Spring from the inversion of control container.

Inside the handler, we invoke the getForEntity() method: we pass it the web service endpoint URL, and the type for our DogResponse bean (DogResponse.class returns a class instance for the DogResponse bean). This tells getForEntity() that we want to take the JSON response and convert or parse it into a DogResponse object. And this will happen automatically as long as you used the right names for the data members inside DogResponse!

The getForEntity() method returns the JSON response, parses it into a DogResponse object, and puts that object into the response body of a ResponseEntity instance.

For testing purposes (and for our own interest), we print the response status and also the collection of response headers to the console. To print the collection of headers I'm using a forEach() lambda (you may or may not have learned this somewhere in Java 2, but even if you didn't, it should be self-explanatory at this point).

Lastly, we take the body of the response (responseEntity.getBody()) and add it to the Model as the attribute "dogResponse" before we load the view "dog.html".

Add the dog.html file to your project templates (make sure you include the thymeleaf namespace) and add the code that displays the dog image, if the request was successful:

<div th:if="${dogResponse.status == 'success'}">
  <img style="width:50%;" th:src="${dogResponse.message}">
</div>

Now save and run your application and browse to http://localhost:8080/dog

If all goes well, you should see a random image of a dog!

Using the Rest Controller as a Service Layer

In an earlier lesson we referred briefly to an application's Service Layer. Other parts of your application, such as the model, view, controller, and database access components, often share different interactions with each other. In order to maintain modularity and adhere to SOLID principles, the service layer is created to act as the coordinator of communications between other components and layers. This ensures that when one component is modified, it doesn't affect any of the other layers. For example, you wrote an application earlier where the controller and the user details service both needed to use the database access layer. If you changed the database access logic significantly, you might have to also update every other class that used it. Instead, we use the service layer to access the data layer and pass data/information on to the other components that need it. That way, only the service layer needs to worry about changes to the data layer.

This is where a Rest Controller comes in handy: it's a set of web service methods that act as the service layer of our application between the database access layer and everything else.

Let's modify our program by consuming our container web service methods.

Consuming GET Services

In your controller, let's add two handler methods that consume both our GET web service methods (remember there are two: one to get all the containers and one to get a single container by the container name). All of these handler methods will use @GetMapping, unless stated otherwise.

First, create the handler method viewContainers mapped to "/viewAll" that performs a GET request to http://localhost:8080/container and retrieves the response as a collection of Container objects:

  1. The method needs Model and RestTemplate parameters.
  2. Invoke the rest template method getForObject() (we don't need any other part of the response for now, but feel free to use getForEntity() instead so you can try other things!) and get the result as a Container collection, e.g. Container[]. Notice this is an array, not a List<Container>: we can't say "List<Container>.class" so this is easier. Also, make sure you use the URL pattern you set up in your Rest Controller in the previous lesson.
  3. Add the containers collection (or request body if you used ResponseEntity) to the model.
  4. Load the view containers.html

Now create the second handler method getContainer() that maps to "/view":

  1. The method needs a Model parameter, RestTemplate parameter, and a request parameter for a form input field called "name".
  2. Invoke the rest template method getForObject() and get the result as a single Container object. The request URL needs the request parameter variable! Make sure you use the URL pattern you set up in your Rest Controller in the previous lesson.
  3. Add the container returned to the model and load the view containers.html

Now add the containers.html page to your templates, and make sure you add the thymeleaf namespace to it:

  1. If there's a container object in the model, display it in a DIV.
  2. If there's a container collection in the model, display the containers in a table with a column for the names and a column for the volumes.

Lastly, add an index.html page with links to /viewAll and a form with an input field where the user can enter a container name to search for. The form only needs a text input field called "name" where the user types the container name.

<p><a href="#" th:href="@{/viewAll}">View All</a></p>

<form action="#" th:action="@{/view}">

  <p><label for="name">Enter Container Name:
    <input type="text" name="name" id="name"></label>
  </p>
  <p><input type="submit" value="Find Container"></p>
</form>

Save all the changes and run your program: Load your index page and try the View All link, then go back to the index page and try finding a container by the container name!

message wendi if you use a screen reader
Solution: Handler methods

Consuming a POST Service

Now let's try consuming the POST web service - remember we only had the one - that added a new container to the existing set of containers. But there's an issue we have to solve: when we use a handler in our main controller to use the POST request web service method, it's unable to handle the container's default ID of 0 because it uses the All-Args Container constructor. This constructor requires that the ID be greater than 0. We can fix this with one of two solutions:

Edit the Container bean and go to the mutator method for the container ID. Modify the if statement to test if the id is greater than or equal to 0. This way, the Container all-args constructor will permit a 0 ID.

However, this may not be appropriate for all programs: perhaps you don't want to allow a 0-value ID! In this case, a second solution is available:

Set the ID of the container to a "dummy" value before you send it to the web service. The value won't be added to this container's database record, because your database access method only creates the new container record using the name and volume; the ID value is an auto-increment field.

Either of these solutions will work, you can do whatever is appropriate for your program. For this one, we'll use the second solution.

Now we can try a POST request:

  1. Add a form page new.html with a form that allows a user to add a new container. The form is bound to a container object:
    <form method="post" action="#" th:action="@{/post}" th:object="${container}">
        <p><label for="name">Name:
        <input type="text" name="name" id="name" th:field="*{name}">
        </label><br>
        <label for="volume">Volume:
        <input type="text" name="volume" id="volume" th:field="*{volume}">
        </label>
      </p>
      <p><input type="submit"></p>
    </form>
  2. Add a handler method to your main controller that loads the new.html form page. Make sure you add an empty container to the model so that it can be bound to the form.
  3. Add a handler method to your main controller mapped to "/post" (the form's action attribute value).
  4. The handler method needs the model parameter, a container model attribute, and the rest template parameter.
  5. Set the container's ID to whatever valid value you want, it doesn't matter since it won't be used.
  6. Add a statement to invoke a POST request to the appropriate URL. Remember that the POST service in our rest controller returns the new record's ID as an integer (check your code to see if you used Integer or Long)! Store the returned record ID in the model so we can display it on the containers page.
  7. Remember that after we do an insert (or update or delete) we should refresh the containers list. Add the code to invoke the appropriate web service method to refresh the list of containers (hint: you can copy and paste this from one of the methods you wrote earlier!)
  8. Load the containers.html page.

Modify the containers page: add a DIV that displays the record number (if there is one in the model).

<div th:if="${newId}" th:text="|New Record #${newId} added.|">new record added</div>

Now give it a try: restart the application and load the new.html page via its handler method. The form will be filled with the default container data, so don't panic. Just replace it with new data (remember that container name has to be unique). Then submit the form - you should see the new container, the new container's record number, and the list of containers (which includes your new one)!

message wendi if you use a screen reader
Solution

Consuming PUT Services

Let's try one more: let's try the PUT service that replaces the entire list of containers.

In your main controller, add a handler method that tests our PUT service method that replaces the entire collection. Note that this handler uses a @GetMapping because it's handling a Get-Request to a URL e.g. http://localhost:8080/putAll.

I'm not going to bother with an input form, that will be way too much effort at this point. We'll just hard-code a List of container objects to replace the current containers with:

  1. Create the handler method, map it to "/putAll" and make sure it has the Model and RestTemplate parameters.
  2. Create a new List of containers and add 3 containers to it (note that the IDs aren't necessarily going to be the primary key values since the database auto-increments the primary key field):
    List<Container> list = new ArrayList<Container>();
    list.add(new Container(1, "small shipping container", 1200));
    list.add(new Container(2, "medium shipping container", 1750));
    list.add(new Container(3, "large shipping container", 2400));
  3. Use the rest template's put() method to request the appropriate URL. Pass it the list that you just hard-coded.
  4. Add the statements to get a fresh list of containers and add it to the model.
  5. Load the containers.html page.

You can test your handler method by going to http://localhost:8080/putAll. You should see the 3 containers that you hard-coded: you replaced the original set of containers with these 3.

message wendi if you use a screen reader
Solution: Handler method

At this point you should be comfortable consuming the web services you wrote in the previous lesson. The rest are going to be left as exercises.

Next, we'll learn how to consume these web services using JavaScript and AJAX!

Exercises

Add the handler methods that consume your two DELETE web service methods:

Delete One Item By ID:

  1. In your containers.html page, add a column to the containers table with a DELETE link for each container item.
  2. Create a handler method that is invoked when a DELETE link is clicked:
    • Request the appropriate URL with the ID of the record to be deleted.
    • Refresh the list of containers.
    • Load the containers.html page.

Delete All Containers:

  1. Add the handler method to request the appropriate URL to delete all the containers.
  2. Refresh the list of containers (ideally, it's going to be empty, but we will try anyway just to make sure).
  3. Load the containers.html page.