Simpler Spring Data REST Clients with Bowman
Spring Data REST, when combined with JPA, is a tool allowing for some pretty awesome productivity in building web services backed by a relational database. Just write some annotated entity classes and a bare repository subinterface, and you’re off! – full CRUD functionality on your database schema exposed as JSON endpoints.
What is less widely discussed, however, is how to write a client capable of actually invoking this API. The Spring HATEOAS project offers a useful first step. But the great strength of JPA (or, some would argue, its great weakness…) is its ability to lazily retrieve an entity’s associations, without requiring any knowledge of the structure of the underlying database schema. How can this paradigm be extended to the hypermedia-based interactions that comprise a REST conversation?
In this article, I briefly give an overview of Spring Data REST, and describe a simple web service built using it. I then explain how this can be accessed using a web client built on existing Spring libraries, and outline some of the pitfalls and limitations you may encounter when attempting this. Finally, I introduce Black Pepper’s Bowman, a library we’ve developed to greatly simplify Spring Data REST web client code by supporting, among other things, transparent traversal into associated resources.
Introduction to Spring Data REST
Spring Data REST provides a Spring MVC controller that delegates to Spring Data repositories via the HTTP verbs and URIs you would expect of a RESTful interface, for example:
GET /customers
– return all customersGET /customers/1
– return the customer with database identifier “1”POST /customers
– persist a new customer
It produces and consumes the JSON+HAL content type, as described by the HAL specification. HAL simply defines the means of embedding links to associated resources within a JSON payload. This is important because discussion of web clients in this article is therefore not specific to Spring Data REST, but is in fact applicable to any JSON+HAL service.
Our Banking System
As is traditional, I’ll use a banking system of customers and accounts as the example for our application. In our database we have account types and customers as aggregate roots. A customer can have one or more accounts, each of which comprises an account type and a credit limit.
We have a single customer in our database, “Jeremy Corbyn”, who has an account of type “Aardvantage” with credit limit £2000, and a “PlatyPlus” account with limit £1000. So our database schema looks like the following:
CUSTOMER
ID |
NAME |
---|---|
1 | Jeremy Corbyn |
ACCOUNT_TYPE
ID |
NAME |
---|---|
1 | Aardvantage |
2 | PlatyPlus |
ACCOUNT
ID |
CREDIT_LIMIT |
CUSTOMER_ID |
TYPE_ID |
---|---|---|---|
1 | 2000.00 | 1 | 1 |
2 | 1000.00 | 1 | 2 |
Our model:
@Entity
public class AccountType {
@Id private int id;
private String name;
// getters go here
}
@Entity
public class Customer {
@Id private int id;
private String name;
@OneToMany(mappedBy = "customer", cascade = CascadeType.ALL)
private Set<Account> accounts = new LinkedHashSet<>();
// getters go here
}
@Entity
public class Account {
@Id private int id;
@ManyToOne private Customer customer;
@ManyToOne private AccountType type;
private BigDecimal creditLimit;
// getters go here
}
And our two repositories:
public interface AccountTypeRepository
extends CrudRepository<AccountType, Integer> {}
public interface CustomerRepository
extends CrudRepository<Customer, Integer> {}
Then all that is required is to access http://localhost:8080/customers/1 to retrieve the JSON+HAL representation of that customer:
{
"name" : "Jeremy Corbyn",
"accounts" : [ {
"creditLimit" : 1000.00,
"_links" : {
"customer" : {
"href" : "http://localhost:8080/customers/1"
},
"type" : {
"href" : "http://localhost:8080/accountTypes/2"
}
}
}, {
"creditLimit" : 2000.00,
"_links" : {
"customer" : {
"href" : "http://localhost:8080/customers/1"
},
"type" : {
"href" : "http://localhost:8080/accountTypes/1"
}
}
} ],
"_links" : {
"self" : {
"href" : "http://localhost:8080/customers/1"
},
"customer" : {
"href" : "http://localhost:8080/customers/1"
}
}
}
There’s two distinct types of associations visible in this resource:
Customer.accounts
is what we call an inline association – each account is inlined into the customer resource together with its links.Account.type
rather is a linked association – no corresponding JSON property is added. Instead a link is added to the HAL_links
section with a rel oftype
.
The reason for the difference is that, for associations, Spring Data REST always marshals links to entities that have their own exposed repositories. Otherwise it inlines them.
Retrieving a Customer’s Accounts
So, let’s say we’re conducting an internal credit check on Jeremy Corbyn and we would like to know the types of accounts he holds, together with these accounts’ credit limits.
We will use Spring’s RestTemplate
to query the service. As the service returns a JSON+HAL response, we configure our RestTemplate
with the Jackson2HalModule
from the Spring HATEOAS project. This allows us to use the Spring HATEOAS Resource
class in our client-side model so we can be aware of any HAL links in the response.
Creating our RestTemplate
is pretty straightforward:
ObjectMapper objectMapper = Jackson2ObjectMapperBuilder.json()
.modules(new Jackson2HalModule())
.build();
RestTemplate restTemplate = new RestTemplate(Arrays.asList(
new MappingJackson2HttpMessageConverter(objectMapper)));
Creating the Client Model
We create a model specifically for use in our client, avoiding reuse of the server model to decouple the client from the server. The objects returned from our RestTemplate
invocations will be of these types.
public class AccountType {
private String name;
// getter & setter go here
}
public class Customer {
private String name;
private List<Resource<Account>> accounts;
// getters & setters go here
}
public class Account {
private BigDecimal creditLimit;
// getter & setter go here
}
There’s a couple of things to notice here:
Customer.accounts
must be declared as aList<Resource<Account>>
as this is an inline association. If it were declared as aList<Account>
there would be no way to retrieve the HAL links for each account.- There is no
Account.type
as this is a linked association. The type can only be retrieved by following the HAL link for each entry inCustomer.accounts
.
Retrieving the Account Details
So now we can invoke methods on our RestTemplate
in order to get all the information we need:
Customer customer = restTemplate.getForObject(
"http://localhost:8080/customers/1", Customer.class);
for (Resource<Account> accountResource : customer.getAccounts()) {
Account account = accountResource.getContent();
Link accountTypeLink = accountResource.getLink("type");
AccountType accountType = restTemplate.getForObject(
accountTypeLink.getHref(), AccountType.class);
System.out.format("%s: credit limit £%.2f%n", accountType.getName(),
account.getCreditLimit());
}
All things going well, we should get the output:
Aardvantage: credit limit £2000.00
PlatyPlus: credit limit £1000.00
Ugh
It works, but this sort of client code is fraught with problems:
- It’s brittle. We have to get links by their string names, and client code is tightly coupled to the structure of the JSON – something that can completely change just due to e.g. removing a Spring Data repository for an entity.
- It’s hard to write and understand. Knowing whether to map fields as
List<Thing>
orList<Resource<Thing>>
can determine whether we have access to the data we require or not, and there are completely different patterns for retrieving data from linked vs. inline associations. - It’s verbose. Having to continually invoke methods on the
RestTemplate
to load associations, and retrieve content and links from theResource
separately, makes for some very ugly code as the object graph grows in complexity.
A particular difficulty we’ve encountered in using client code of this pattern is the writing of a Hamcrest Matcher
to compare a retrieved instance using its associations. This is difficult to achieve without hideously parameterising your matchers with the RestTemplate
, or copying everything you need to compare into another, fully initialised model up front.
Surely there is a better way?
A Better Way with Bowman
Enter Bowman. Our client is a wrapper around Spring HATEOAS and Jackson with a heavy JPA influence. Retrieving an object from the remote returns a proxy of the returned object, instrumented using Javassist, whose accessors can transparently conduct further remote service invocations in accordance with its HAL links. This allows linked and inline associations to both be defined in the same way in the client model and greatly simplifies client code.
Constructing a Client
is easy. Configuration
, ClientFactory
and Client
instances are immutable and so may be used safely by multiple threads.
ClientFactory clientFactory =
Configuration.build().buildClientFactory();
Client<Customer> client = clientFactory.create(Customer.class);
Our New Model
We need to update our Customer and Account classes for use with Bowman.
public class Customer {
private String name;
private List<Account> accounts;
@JsonDeserialize(contentUsing = InlineAssociationDeserializer.class)
public List<Account> getAccounts() { return accounts; }
// all other getters & setters
}
public class Account {
private AccountType type;
private BigDecimal creditLimit;
@LinkedResource public AccountType getType() { return type; }
// all other getters & setters
}
Things to note here:
- Spring HATEOAS
Resource
types are no longer required. - Inline associations must specify Jackson deserialization with the
InlineAssociationDeserializer
. This tells Jackson to create a further proxy of the deserialized object, allowing nested linked associations to be resolved. - Linked associations must specify the
@LinkedResource
annotation. This tells Bowman proxies that the property must be populated by the result of a HAL link traversal on access.
Simplified Client Code
So what does our client code look like now?
Customer customer = client.get(URI.create(
"http://localhost:8080/customers/1"));
for (Account account : customer.getAccounts()) {
System.out.format("%s: credit limit £%.2f%n",
account.getType().getName(), account.getCreditLimit());
}
Again giving:
Aardvantage: credit limit £2000.00
PlatyPlus: credit limit £1000.00
In this example, the invocation account.getType().getName()
will automatically retrieve the resource identified by the type
HAL link, returning the name
property of that resource. Nice and tidy!
Just Scratching the Surface
There are other ways Bowman can help you write succinct, expressive client code for HAL APIs. For example, it will marshal inline links to linked associations when POSTing to a resource, and it supports unmarshalling a collection resource to its constituent entities via the HAL _embedded
construct. However, I will leave these topics to a purely theoretical future article.
But I’d be fascinated to hear people’s views, whether in the comments below-the-line, or even just by starring the project on Github. Thoughts on the approach, the API, feature enhancements or anything else would be most welcome.
The source code outlined here is all available as a demo project again on Github. Thanks for reading!
This article was updated on 11 June 2017 to reflect the library rename from HAL Client to Bowman.