A simple, model-mapping, link-traversing Java client library for consuming a JSON+HAL REST API.
Overview
Bowman is a Java library for accessing a JSON+HAL REST API, supporting the mapping of a client-side model to HTTP resources with automatic link traversal into associated resources.
Philosophy
The original motivation for this library was to make it easier to write clients for Spring Data REST-exposed JPA repositories, supporting lazy-loading of associations in a similar style to JPA.
Bowman mandates the use of a client-side model — comprised of entities, though separate from the server-side model — which maps to an API’s JSON+HAL representation via annotations. It then enhances the model by creating proxies using Javassist, so that invoking an accessor or query method can transparently retrieve a linked remote resource and add it to the client-side object graph. This can make client code much easier to write and understand.
See the blog post Simpler Spring Data REST Clients with Bowman, for the thinking that led to the creation of this library. |
Getting Started
Add to Your Project
Add the Maven dependency:
<dependency>
<groupId>uk.co.blackpepper.bowman</groupId>
<artifactId>bowman-client</artifactId>
<version>0.9.0</version>
</dependency>
Usage Example
Given the following annotated model objects:
import uk.co.blackpepper.bowman.annotation.RemoteResource;
import uk.co.blackpepper.bowman.annotation.ResourceId;
@RemoteResource("/people")
public class Person {
private URI id;
private String name;
public Person() {}
public Person(String name) { this.name = name; }
@ResourceId public URI getId() { return id; }
public String getName() { return name; }
}
and
import uk.co.blackpepper.bowman.annotation.LinkedResource;
import uk.co.blackpepper.bowman.annotation.RemoteResource;
import uk.co.blackpepper.bowman.annotation.ResourceId;
@RemoteResource("/greetings")
public class Greeting {
private URI id;
private Person recipient;
private String message;
public Greeting() {}
public Greeting(String message, Person recipient)
{ this.message = message; this.recipient = recipient; }
@ResourceId public URI getId() { return id; }
@LinkedResource public Person getRecipient() { return recipient; }
public String getMessage() { return message; }
}
Client instances can be constructed and used as demonstrated below.
The HTTP requests/responses corresponding to each instruction are shown in a comment beneath. |
import uk.co.blackpepper.bowman.Client;
import uk.co.blackpepper.bowman.ClientFactory;
import uk.co.blackpepper.bowman.Configuration;
...
ClientFactory factory = Configuration.builder().setBaseUri("http://...").build()
.buildClientFactory();
Client<Person> people = factory.create(Person.class);
Client<Greeting> greetings = factory.create(Greeting.class);
URI id = people.post(new Person("Bob"));
// POST /people {"name": "Bob"}
// -> Location: http://.../people/1
Person recipient = people.get(id);
// GET /people/1
// -> {"name": "Bob", "_links": {"self": {"href": "http://.../people/1"}}}
assertThat(recipient.getName(), is("Bob"));
id = greetings.post(new Greeting("hello", recipient));
// POST /greetings {"message": "hello", "recipient": "http://.../people/1"}}
// -> Location: http://.../greetings/1
Greeting greeting = greetings.get(id);
// GET /greetings/1
// -> {"message": "hello", "_links": {"self": {"href": "http://.../greetings/1"},
// "recipient": {"href": "http://.../people/1"}}}
assertThat(greeting.getMessage(), is("hello"));
recipient = greeting.getRecipient();
// GET /people/1
// -> {"name": "Bob", "_links": {"self": {"href": {"http://.../people/1"}}}
assertThat(recipient.getName(), is("Bob"));
API Usage
Using Bowman requires:
-
An annotated client model
-
A configured
ClientFactory
-
A
Client
created from that factory
A Client
corresponds to a specific entity type; you will create one Client
for each entity type that requires direct (without following HAL links) retrieval or manipulation.
Factory Instantiation
ClientFactory
instances are created through Configuration.builder().getClientFactory()
.
Client
s are then created from the factory using ClientFactory.create(clazz)
.
Both ClientFactory and Client instances maintain no mutable state and may safely be shared across threads.
|
Factory Configuration
Base URI
Set the base URI for the API with setBaseUri
. Entities' base resources, specified by @RemoteResource
, will be resolved relative to this.
ClientFactory factory = Configuration.builder()
.setBaseUri("http://www.example.com/my-api-root")
.build();
This is often all the ClientFactory configuration you need.
Framework Components
Bowman uses Spring’s RestTemplate
and Jackson’s ObjectMapper
under the hood. It may often be necessary to customise these, to support custom serialisation, add authentication headers to the request etc.
ClientFactory factory = Configuration.builder()
.setBaseUri(...)
.setRestTemplateConfigurer(...) (1)
.setObjectMapperConfigurer(...) (2)
.setClientHttpRequestFactory(...) (3)
.build();
1 | Provide custom configuration of the RestTemplate via an implementation of uk.co.blackpepper.bowman.RestTemplateConfigurer |
2 | Provide custom configuration of the ObjectMapper via an implementation of uk.co.blackpepper.bowman.ObjectMapperConfigurer |
3 | Provide an implementation of org.springframework.http.client.ClientHttpRequestFactory . The default HttpComponentsClientHttpRequestFactory is often sufficient, but you may wish to provide a buffering request factory to allow logging of the response body in an interceptor prior to deserialisation. |
Provided implementations of ClientHttpRequestFactory must throw an org.springframework.web.client.HttpClientErrorException when an HTTP 404 is returned accessing the remote resource.
|
Client Instantiation
Then from your ClientFactory
you can create a Client
of the desired type. The base resource of the Client
's API is then determined by its @RemoteResource
annotation.
Client<Customer> customers = factory.create(Customer.class);
Client API Methods
Clients support:
-
get()
- GET the single entity from the base resource -
get(URI id)
- GET the entity with the given ID -
getAll()
- GET a collection of entities from the base resource -
getAll(URI location)
- GET a collection of entities from the given resource -
post(T object)
- POST the entity to the base resource -
put(T object)
- PUT the entity to its resource -
patch(URI id, P patch)
- PATCH the entity with the given ID with a set of changes -
delete(URI id)
- DELETE the entity with the given ID
PUT/PATCH are supported with caveats: there is currently a whole category of Spring Data REST limitations interacting via PUT/PATCH with JPA repositories due to attempts to replace persistent collections and state merge occurring outside of a transaction. |
The Client Model
Bowman requires you to define a client-side model, consisting of entities, and enhanced with annotations to define the remote resources these entities map to and the relationships between them.
Entity Types
Bowman supports abstract class and interface entity types as well as concrete classes.
Concrete Classes
Entity classes have a HAL representation containing JSON properties. Their corresponding classes are assumed to obey Java Bean conventions — i.e. have zero-arg constructors and private fields with public getters (get*
/is*
) and setters (set*
).
{
"property": "value",
"_links": {
"self": "http://www.example.com/entities/1"
}
}
class Entity {
private URI id;
private String property = "value";
@ResourceId URI getId() { return id; }
public String getProperty() { return property; }
public void setProperty(String property) { this.property = property; }
}
Abstract Classes
It may be more convenient to map read-only linked resources using abstract methods.
{
"property": "value",
"_links": {
"self": "http://www.example.com/entities/1",
"nextEntity": "http://www.example.com/entitites/1/next"
}
}
abstract class Entity {
private URI id;
private String property = "value";
@ResourceId URI getId() { return id; }
public String getProperty() { return property; }
public void setProperty(String property) { this.property = property; }
@LinkedResource public abstract Entity nextEntity();
}
Interfaces
If an entity’s HAL representation comprises read-only links only, it’s probably more convenient to map it using an interface type.
{
"_links": {
"findByName": "http://www.example.com/related/findByName{?name}"
}
}
interface Entity {
@LinkedResource Related findByName(String name);
}
Remote Resources
Annotate your entities with @RemoteResource(path)
, where path
is the location of the entity’s base resource, relative to the base URI set when building the ClientFactory
.
@RemoteResource("/things")
public class Thing { ... }
ID Property
Use @ResourceId
to mark a java.net.URI
accessor as the resource ID. This is the canonical URI for the resource - its 'self' link.
private URI id;
@ResourceId public URI getId() { return id; }
Linked Resources
Mark a resource as linked with @LinkedResource
on its accessor. Invoking this accessor will automatically query its associated linked remote resource to populate the model.
class Entity {
private Related related;
private Set<Related> relatedSet = new HashSet<>();
@LinkedResource public Related getRelated() { return related; }
@LinkedResource public Set<Related> getRelatedSet() { return relatedSet; }
}
interface Entity {
@LinkedResource public Related findByName(String name)
}
Links in an interface type are assumed to be templated with stringifiable query string parameters of the same names and in the same order as those in the interface method signatures.
The HAL rel
of a linked resource is assumed to be the same as the method name. You can customise this by setting the rel
attribute of the @LinkedResource
annotation:
@LinkedResource(rel = "my-rel") public Related getRelated() { ... }
By default, the rel
for each @LinkedResource
is required to be present in the consumed HAL’s _links
property. If you want to tolerate the link being absent completely, you can set the optionalLink
attribute:
@LinkedResource(optionalLink = true) public Related getRelated() { ... }
Inline Resources
Mark a resource as inline with the InlineAssociationDeserializer
Jackson deserializer. Invoking this accessor will create and return a proxy that is aware of the inline object’s links, and so is able to resolve nested linked resources.
private Related related;
private Set<Related> relatedSet = new HashSet<>();
@JsonDeserialize(using = InlineAssociationDeserializer.class)
public Related getRelated() { return related; }
@JsonDeserialize(contentUsing = InlineAssociationDeserializer.class)
public Set<Related> getRelatedSet() { return relatedSet; }
Polymorphism
Use @ResourceTypeInfo
to declare a type’s subtypes. On deserialization, the type of the resource will be determined using the self
link of the resource.
@ResourceTypeInfo(subtypes = {OneThing.class, AnotherThing.class})
class Thing { }
class OneThing extends Thing { }
class AnotherThing extends Thing { }
Alternatively you can register your own TypeResolver
to provide custom subtype resolution, perhaps using alternative resource links.
@ResourceTypeInfo(typeResolver = MyTypeResolver.class)
class Thing { }
class MyTypeResolver implements TypeResolver {
Class<?> resolveType(Class<?> declaredType, Links resourceLinks, Configuration configuration) {
// own type resolution code here...
}
}
FAQs
How can I access an API that requires authentication?
For example, you could define an interceptor to send the Authorization
header with your OAuth access token on each request:
ClientFactory clientFactory = Configuration.builder()
.setRestTemplateConfigurer(new RestTemplateConfigurer() {
public void configure(RestTemplate restTemplate) {
restTemplate.getInterceptors().add(new ClientHttpRequestInterceptor() {
public ClientHttpResponse intercept(HttpRequest request, byte[] body,
ClientHttpRequestExecution execution) throws IOException {
request.getHeaders().add("Authorization", "Bearer youraccesstoken");
return execution.execute(request, body);
}
});
}
})
.buildClientFactory();
You should be able to use Bowman or any other HTTP client library to first get the token from an OAuth access token response following authorisation.
Glossary
- Base resource
-
The remote resource identified as the entrypoint for an entity’s API, identified by the entity’s
@RemoteResource
annotation. An entity’s base resource may be single-valued or collection-valued. - Entity
-
An artifact within the client-side model. This may correspond to a full or embedded remote resource. Entity types may be concrete classes (Java Beans - when their representation contains data properties) or abstract classes or interfaces (useful when their representation contains read-only links).