Chapter 18. rest-ws

18.1. rest-ws: Building RESTful applications with Spring MVC

18.2. Introduction

In this lab you’ll use some of the features of Spring 3.0 that support RESTful web services. Note that there’s more than we can cover in this lab, please refer back to the presentation for a good overview.

What you will learn

  1. Working with RESTful URLs that expose resources
  2. Mapping request- and response-bodies using HTTP message converters
  3. Use Spring MVC to implement server-side REST
  4. Writing a programmatic HTTP client to consume RESTful web services

Specific subjects you will gain experience with:

  1. Processing URI Templates using @PathVariable
  2. Using @RequestBody and @ResponseBody
  3. Using the RestTemplate

Estimated time to complete: 50 minutes

18.3. Quick Instructions

Quick instructions for this exercise have been embedded within the lab materials in the form of TODO comments. To display them, open the Tasks view (Window >> Show View >> Tasks (not Task List)).

Occasionally, TODO’S defined within XML files may fail to appear in the Tasks view (i.e. gaps in the number sequence). To correct this, go to Preferences >> General >> Editors >> Structured Text Editor >> Task Tags pane. Check Enable searching for Task Tags and click Clean and Redetect Tasks. On the Filters tab, ensure XML content type is checked.

18.4. Detailed Instructions

The instructions for this lab are organized into sections. In the first section you’ll add support for retrieving a JSON-representation of accounts and their beneficiaries and test that using the RestTemplate. In the second section you’ll add support for making changes by adding an account and adding and removing a beneficiary. The optional bonus section will let you map an existing exception to a specific HTTP status code.

18.4.1. Exposing accounts and beneficiaries as RESTful resources

In this section you’ll expose accounts and beneficiaries as RESTful resources using Spring’s URI template support, HTTP Message Converters and the RestTemplate.

18.4.1.1. Inspect the current application

(TODO-01) First open the RestWsApplication class in the accounts package to see how the application is bootstrapped: it imports the AppConfig configuration class, which contains an accountManager bean that provides transactional data access operations to manage Account instances. As RestWsApplication uses the SpringBootApplication annotation, it will use component scanning and will define a bean for the AccountController class.

Under the src/test/java source folder you’ll find an AccountClientTests JUnit test case: this is what you’ll use to interact with the RESTful web services on the server.

Finally, run this Spring Boot application (right click >> Run As >> Spring Boot App) and verify that the application deployed successfully by accessing http://localhost:8080 from a browser. When you see the welcome page, the application was started successfully. If you have a problem starting the application, the most likely cause is that you already have an application from a previous lab still running, so be sure to stop it.

18.4.1.2. Expose the list of accounts

(TODO-02) Open the AccountController. Notice that it offers several methods to deal with various requests to access certain resources. Add the necessary annotations to the accountSummary method to make it respond to GET requests to /accounts.

[Tip] Tip

You need one annotation to map the method to the correct URL and HTTP Method, and another one to ensure that the result will be written to the HTTP response by an HTTP Message Converter (instead of an MVC View).

When you’ve done that, save all work and restart the application. Now try to access http://localhost:8080/accounts from that same browser. Depending on the browser used, you may see the response inline or you may see a popup asking you what to do with the response: save it to a local file and open that in a local text editor (Notepad is available on every Windows machine). You’ll see that you’ve just received a response using a JSON representation (JavaScript Object Notation). How is that possible?

The reason is that the project includes the Jackson library on its classpath - check your Maven Dependencies folder for your project (in the Package Explorer).

If this is the case, an HTTP Message Converter that uses Jackson will be active by default. The library mostly 'just works' with our classes without further configuration: if you’re interested you can have a look at the MonetaryAmount and Percentage classes and search for the Json annotations to see the additional configuration.

18.4.1.3. Retrieve the list of accounts using a RestTemplate

(TODO 03) A client can process this JSON response anyway it sees fit. In our case, we’ll rely on the same HTTP Message Converter to deserialize the JSON contents back into Account objects. Open the AccountClientTests class under the src/test/java source folder in the accounts.client package. This class uses a plain RestTemplate to connect to the server. Use the supplied template to retrieve the list of accounts from the server, from the same URL that you used in your browser.

[Tip] Tip

You can use the BASE_URL variable to come up with the full URL to use.

[Note] Note

We cannot assign to a List<Account> here, since Jackson won’t be able to determine the generic type to deserialize to in that case: therefore we use an Account[] instead.

When you’ve completed this TODO, run the test and make sure that the listAccounts test succeeds. You’ll make the other test methods pass in the following steps.

18.4.1.4. Expose a single account

(TODO 04) To expose a single account, we’ll use the same /accounts URL followed by the entityId of the Account, for example /accounts/1. Switch back to the AccountController and complete the accountDetails method.

[Tip] Tip

Since the {accountId} part of the URL is variable, use the @PathVariable annotation to extract its value from the URI template that you use to map the method to GET requests to the given URL.

Save your work and restart your application. To test your code, just try to access http://localhost:8080/accounts/0 to verify the result.

(TODO 05) When you’re done with the controller, complete the AccountClientTests by retrieving the account with id 0.

[Tip] Tip

The RestTemplate also supports URI templates, so use one and pass 0 as a the value for the urlVariables varargs parameter.

Run the test and ensure that the getAccount test now succeeds as well.

If the time allocated for the lab is almost used up, this is a good place to stop.

18.4.2. Modifying data using POST and DELETE

In this section we will create and remove data.

18.4.2.1. Create a new account

So far we’ve only exposed resources by responding to GET methods. Now you’ll add support for creating a new account as a new resource.

(TODO 06) Update the createAccount method by adding a mapping to it from POSTs to /accounts. The body of the POST will contain a JSON representation of an Account, just like the representation that our client received in the previous step. Make sure to annotate the account parameter appropriately to let the request’s body be unmarshaled into an account object. When the method completes successfully, the client should receive a 201 Created instead of 200 OK, so annotate the method to make that happen as well. We will write a test for this method in an upcoming step.

(TODO 07) RESTful clients that receive a 201 Created response will expect a Location header in the response containing the URL of the newly created resource. Complete the TODO by implementing entityWithLocation(). Then save all work and restart the application.

[Tip] Tip

To help you generate the full URL on which the new account can be accessed, you will need to use a ServletUriComponentsBuilder - refer back to the example in the slides to see how. This approach means you can avoid hard-coding the server name and servlet mapping in your controller code! You should use a ResponseEntity to generate the response.

(TODO 08 - 09) When you’re done, complete the test method by POSTing the given Account to the /accounts URL. The RestTemplate has two methods for this. Use the one that returns the location of the newly created resource and assign that to a variable. Then complete TODO 09 by retrieving the new account on the given location. The returned Account will be equal to the one you POSTed, but will also have been given an entityId when it was saved to the database.

Run the tests again and see if the createAccount test runs successfully. Regardless of whether this is the case or not, proceed with the next step!

18.4.2.2. Seeing what happens at the HTTP level

If your create account test worked, and you are running short of time, you may skip this section and move on to "Create and delete a beneficiary". However the HTTP monitor discussed in this section is a useful debugging tool to know.

(TODO 10) If your test did not work, you may be wondering what caused an error. Because of all the help that you get from Spring, it’s actually not that easy to see what’s happening at the HTTP transport level in terms of requests and responses when you exercise the application. For debugging or monitoring HTTP traffic, Eclipse ships with a built-in tool that can be of great value: the TCP/IP Monitor. To open this tool, which is just an Eclipse View, press CTRL+3 (COMMAND+3 on MacOS) and type 'tcp' in the resulting popup window; then press Enter to open the TCP/IP Monitor View. Click the small arrow pointing downwards (on the top right) and choose "properties".

monitor properties

Figure 18.1. The "properties" menu entry of the TCP/IP Monitor view



Choose "Add…​" to add a new monitor. As local monitoring port, enter 8081 since this port is probably unused. As host name, enter "localhost" and as port enter 8080 since this is the port your application is running on. Press OK and then press "Start" to start the newly defined monitor.

[Tip] Tip

Don’t forget to start the monitor after adding it!

Now switch to the AccountClientTests and change the port number in the BASE_URL to 8081 so all requests pass through the monitor.

[Note] Note

This assumes that you’ve used that variable to construct all your URLs: if that’s not the case, then make sure to update the other places in your code that contain the port number as well!

Now run the tests again and switch back to the TCP/IP Monitor View (double-click on the tab’s title to maximize it if it’s too small). You’ll see your requests and corresponding responses. Click on the small menu arrow again and now choose 'Show Header': this will also show you the HTTP headers, including the Location header you speficied for the response to the POST that created a new account.

[Note] Note

Actually, there’s one request missing: the request to retrieve the new account. This is because the monitor rewrites the request to use port 8080, which means the Location header will include that port number instead of the 8081 the original request was made to. We won’t try to fix that in this lab, but it wouldn’t be too hard to come up with some interceptor that changes the port number to make all requests pass through the filter.

If your createAccount test method didn’t work yet, then use the monitor to debug it. Proceed to the next step when the test runs successfully.

18.4.2.3. Create and delete a beneficiary

(TODO 11) Complete the addBeneficiary method in the AccountController. This is similar to what you did in the previous step, but now you also have to use a URI template to parse the accountId. Make sure to return a 201 Created status again. This time, the response’s body will only contain the name of the beneficiary: an HTTP Message Converter that will convert this to a String is enabled by default, so simply annotate the method parameter again to obtain the name.

(TODO 12) Create a ResponseEntity containing the location of the newly created beneficiary the new beneficiary. Save all work and restart the application.

[Note] Note

As you can see in the getBeneficiary method, the name of the beneficiary is used to identify it in the URL. You will need to use entityWithLocation() again.

(TODO 13) Complete the removeBeneficiary method. This time, return a 204 No Content status.

(TODO 14 - 17) To test your work, switch to the AccountClientTests and complete the TODOs. When you’re done, run the test and verify that this time all test methods run successfully. If this is the case, you’ve completed the lab!

BONUS (Optional): return a 409 Conflict when creating an account with an existing number ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^^

(TODO 18) The current test ensures that we always create a new account using a unique number. Let’s change that and see what happens. Edit the createAccount method in the test case to use a fixed account number, like "123123123". Run the test: the first time it should succeed. Run the test again: this time it should fail. When you look at the exception in the JUnit View or at the response in the TCP/IP monitor, you’ll see that the server returned a 500 Internal Server Error. If you look in the Console View for the server, you’ll see what caused this: a DataIntegrityViolationConstraint, ultimately caused by a SQLException indicating that the number is violating a unique constraint.

This isn’t really a server error: this is caused by the client providing us with conflicting data when attempting to create a new account. To properly indicate that to the client, we should return a 409 Conflict rather than the 500 Internal Server Error that’s returned by default for uncaught exceptions. To make it so, add a new exception handling method that returns the correct code in case of a DataIntegrityViolationConstraint.

[Tip] Tip

Have a look at the existing handleNotFound method or in the Advanced notes in the slides for a way to do this.

When you’re done, run the test again (do it twice as the database will re-initialize on redeploy) and check that you now receive the correct status code. Optionally you can even restore the test method and create a new test method that verifies the new behavior.

18.4.2.4. BONUS (Optional): Testing with a Mock HTTP Request

The AccountController.createAccount() method uses a ServletUriComponentsBuilder to generate the location URL of the new Account. It does this by initializing itself with the URL that invoked the createAccount() method. This is possible because Spring MVC has put the current request information in the current thread. This doesn’t happen when running tests.

Follow the instructions in (TODO-20) to see how to mock up the necessary request for testing.