In the much belated conclusion to my series on HATEOAS, we will be diving into how to implement HATEOAS using Spring-Data-REST and Spring-HATEOAS. It is springtime for HATEOAS!
I put together a functioning project that will demonstrate the code examples I have below as well as a few other features. The project can be found at https://github.com/in-the-keyhole/hateoas-demo-II. JDK 8 and Maven are required, but otherwise no external dependencies are needed to run the project.
Serving a Resource
Interacting with a web service via its resources is one of the core design constraints of REST. Using Spring-Data and Spring-MVC, it is not too difficult to start serving up a resource. You’ll need to add a Repository
for the entity you want to serve and implement a controller to serve it. Spring-Data-REST however, makes this process even easier and provides a richer resource in the process (i.e., adding hypermedia markup).
@RepositoryRestResource
public interface ItemRepo extends CrudRepository<Item, Long> {
}
And it is as simple as that. If you boot up your Spring-Boot app and navigate to http://localhost:8080/items (and have done some of the other necessary configurations as well), you should get JSON returns that looks something like this:
{
"_embedded" : {
"items" : [ {
"name" : "Independence Day",
"description" : "Best. Movie. Speech. Ever!",
"price" : 10.0,
"type" : "Movies",
"_links" : {
"self" : {
"href" : "http://localhost:8080/api/items/21"
},
"item" : {
"href" : "http://localhost:8080/api/items/21"
}
}
},
...
]
},
"_links" : {
"self" : {
"href" : "http://localhost:8080/items/"
},
"profile" : {
"href" : "http://localhost:8080/profile/items"
},
"search" : {
"href" : "http://localhost:8080/items/search"
}
}
}
Along with the easy-to-demonstrate GET
functionality, Spring-Data-REST also adds the ability to PUT
(Spring-Data-REST for some reason decided to use PUT
for both create and update) and DELETE
a resource, as well as retrieve a resource by its ID. This is a lot of functionality for just two lines of code!
Pagination and Sorting
Resources will often have a lot of records. Typically, you would not want to return all of those records on request due to the high resource cost at all levels. Pagination is a frequently used solution to address this issue and Spring-Data-REST makes it extremely easy to implement.
Another common need is the ability to allow clients to sort the returns from a resource, and here again Spring-Data-REST is to the rescue. To implement this functionality with the Item
resource, we need to change from extending a CrudRepository
to a PagingAndSortingRepository
like so:
@RepositoryRestResource
public interface ItemRepo extends PagingAndSortingRepository<Item, Long> {
}
When restarting the application and returning to http://localhost:8080/items, our returns initially look the same, but near the bottom of the page, we see some new JSON objects:
{
...
"_links" : {
"first" : {
"href" : "http://localhost:8080/items?page=0&size=20"
},
"self" : {
"href" : "http://localhost:8080/items"
},
"next" : {
"href" : "http://localhost:8080/items?page=1&size=20"
},
"last" : {
"href" : "http://localhost:8080/items?page=1&size=20"
},
"profile" : {
"href" : "http://localhost:8080/profile/items"
},
"search" : {
"href" : "http://localhost:8080/items/search"
}
},
"page" : {
"size" : 20,
"totalElements" : 23,
"totalPages" : 2,
"number" : 0
}
}
Spring-Data-REST renders hypermedia controls for navigating through the pages of the returns for a resource; last, next, prev, and first when applicable (note: Spring-Data-REST is using a 0-based array for pagination). If you look closely, you will also notice how Spring-Data-REST allows the client to manipulate the number of returns per page (.../items?size=x). Finally, sorting has also been added and can be accomplished with URL parameters: .../items?sort=name&name.dir=desc.
Searching a Resource
So we are serving a resource, paginating the returns, and allowing clients to sort those returns. These are all very useful, but often clients will want to search a specific subset of a resource. This is another task Spring-Data-REST makes extremely simple.
@RepositoryRestResource
public interface ItemRepo extends PagingAndSortingRepository<Item, Long> {
List<Item> findByType(@Param("type") String type);
@RestResource(path = "byMaxPrice")
@Query("SELECT i FROM Item i WHERE i.price <= :maxPrice")
List<Item> findItemsLessThan(@Param("maxPrice") double maxPrice);
@RestResource(path = "byMaxPriceAndType")
@Query("SELECT i FROM Item i WHERE i.price <= :maxPrice AND i.type = :type")
List<Item> findItemsLessThanAndType(@Param("maxPrice")
double maxPrice, @Param("type") String type);
}
Above are a few queries that users may want to search items by: an item’s type, the max price of an item, and then those two parameters combined. Navigating to http://localhost:8080/items/search, Spring-Data-REST renders all the search options available as well as how to interact with them. The pagination and sorting functionality available at the root resource endpoint is enabled when interacting with the search endpoints as well!
...
"findItemsLessThan" : {
"href" : "http://localhost:8080/items/search/byMaxPrice{?maxPrice}",
"templated" : true
},
"findByType" : {
"href" : "http://localhost:8080/items/search/findByType{?type}",
"templated" : true
},
"findItemsLessThanAndType" : {
"href" : "http://localhost:8080/items/search/byMaxPriceAndType{?maxPrice,type}",
"templated" : true
},
...
Changing the Shape of a Resource
There will be times when it is beneficial to change the shape of an entity an endpoint serves; you may want to flatten out an object tree, hide fields, or change the name of fields to maintain a contract. Spring-Data-REST offers the functionality to manipulate the shape with projections.
First, we will need to create an interface and annotate it with @Projection
:
@Projection(name = "itemSummary", types = { Item.class })
public interface ItemSummary {
String getName();
String getPrice();
}
This will allow Spring-Data-REST to serve our Item entity in the ItemSummary
shape upon request: http://localhost:8080/api/items/1?projection=itemSummary. If we want to make ItemSummary
the default shape, we return when hitting the /items endpoint that can be accomplished by adding the excerptProjectio
n to the @RepositoryRestResource
annotation on ItemRepo
.
@RepositoryRestResource(excerptProjection = ItemSummary.class)
public interface ItemRepo extends PagingAndSortingRepository<Item, Long> {
Now when we hit ../items, our returns look like this:
...
{
"name" : "Sony 55 TV",
"price" : "1350.0",
"_links" : {
"self" : {
"href" : "http://localhost:8080/api/items/2"
},
"item" : {
"href" : "http://localhost:8080/api/items/2{?projection}",
"templated" : true
}
}
}
...
Customizing a Resource’s Endpoint
The name of an entity may not always be desirable as the name of a resource’s endpoint; it may not conform to legacy needs, you may need to prefix a resource’s endpoint, or simply a different name is wanted. Spring-Data-REST offers hooks for all of these needs.
For changing a resource’s name:
@RepositoryRestResource(collectionResourceRel = "merchandise", path = "merchandise")
public interface ItemRepo extends PagingAndSortingRepository<Item, Long> {
}
And adding a base path:
@Configuration
public class RestConfiguration extends RepositoryRestConfigurerAdapter {
@Override
public void configureRepositoryRestConfiguration(RepositoryRestConfiguration config) {
config.setBasePath("api");
}
}
Now instead of Item entities being served at ../items, they will be served from ../api/merchandise.
Securing a Resource
Security is a very important and complex topic. Even entire talks barely scratch the surface. So consider this portion a minor abrasion on the subject.
Hiding Fields
As mentioned in the previous section, projections are one way of hiding fields. Another, more secure, way is to use @JsonIgnore
on a field like below to prevent it from being returned:
public class Item implements Serializable, Identifiable<Long> {
@JsonIgnore
@Column(name = "secret_field")
private String secretField;
}
Restricting Access over HTTP
There might be cases where functionality should not be accessible over HTTP at all, no matter who you are. That can be accomplished with @RestResource(exported = false)
, which tells Spring-Data-REST not to publish that resource or portion of resource at all to the web. This can be set at both the Type and Method level. The Type level can also be overridden at the Method level if you want to broadly deny but then explicitly define what should be accessible.
Method level:
public interface OrderRepo extends CrudRepository<Order, Long> {
@Override
@RestResource(exported = false)
<S extends Order> S save(S entity);
}
Type level, with method level override:
@RestResource(exported = false)
public interface OrderRepo extends CrudRepository<Order, Long> {
@Override
@RestResource(exported = true)
<S extends Order> S save(S entity);
}
An alternative method (if you so desire) is to instead extend the Repository interface and only define the methods you want clients to have access to.
public interface PaymentRepo extends Repository<Payment, Long> {
Payment findOne(Long id);
<S extends Payment> S save(S entity);
}
Restricting Access by Role
You might also want to limit functionality to only certain types of users.
@RepositoryRestResource(collectionResourceRel = "merchandise", path = "merchandise")
public interface ItemRepo extends PagingAndSortingRepository<Item, Long> {
@PreAuthorize("hasRole('ADMIN')")
<S extends Item> S save(S entity);
@PreAuthorize("hasRole('ADMIN')")
<S extends Item> Iterable<S> save(Iterable<S> entities);
}
While I don’t think it is strictly required, due to some funky interaction possibly with Spring-MVC filters, some additional URL configuration is required to get role-based security working. (I spent many hours researching this issue.) However, implementing multiple layers of security is generally a good practice anyways, so this isn’t necessarily wrong either:
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SpringSecurityConfiguration extends WebSecurityConfigurerAdapter {
@Override
@Autowired
public void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.inMemoryAuthentication().withUser("admin").password("admin").roles("ADMIN")
.and().withUser("user").password("password").roles("USER");
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http.csrf().disable()
.antMatcher("/merchandise").authorizeRequests().antMatchers
(HttpMethod.POST).hasAnyRole("ADMIN")
.and().antMatcher("/merchandise").authorizeRequests().antMatchers
(HttpMethod.PUT).hasAnyRole("ADMIN")
.and().antMatcher("/**").authorizeRequests().antMatchers
(HttpMethod.DELETE).denyAll()
.and().antMatcher("/merchandise").authorizeRequests().antMatchers
(HttpMethod.GET).permitAll()
.and().antMatcher("/**").authorizeRequests().anyRequest().authenticated()
.and().httpBasic();
}
}
Like @RestResource
, @PreAuthorize
can also be placed at the Type level and overridden at the Method level.
@PreAuthorize("hasRole('USER')")
public interface OrderRepo extends CrudRepository<Order, Long> {
}
Additional Customization with Spring-HATEOAS
Up to this point, I have demonstrated all the features of Spring-Data-REST and how it makes implementing a HATEOAS service a breeze. Alas, there are limits to what you can do with Spring-Data-REST. Luckily, there is another Spring project, Spring-HATEOAS, to take up the ground from there.
Spring-HATEOAS eases the process of adding hypermedia markup to a resource and is useful for handling custom interactions between resources. For example, adding an item to an order:
@RequestMapping("/{id}")
public ResponseEntity<Resource<Item>> viewItem(@PathVariable String id) {
Item item = itemRepo.findOne(Long.valueOf(id));
Resource<Item> resource = new Resource<Item>(item);
if (hasExistingOrder()) {
resource.add(entityLinks.linkToSingleResource(retrieveExistingOrder()).withRel("addToCart"));
} else {
resource.add(entityLinks.linkFor(Order.class).withRel("addToCart"));
}
resource.add(entityLinks.linkToSingleResource(item).withSelfRel());
return ResponseEntity.ok(resource);
}
With this, we have overwritten the default /merchandise/(id)
functionality that Spring-Data-REST provides and will now return this result:
{
"name" : "Samsung 55 TV",
"description" : "Samsung 55 LCD HD TV",
"price" : 1500.0,
"type" : "Electronics",
"_links" : {
"addToCart" : {
"href" : "http://localhost:8080/api/orders"
},
"self" : {
"href" : "http://localhost:8080/api/merchandise/1{?projection}",
"templated" : true
}
}
}
CodeProjectSo our client code can now render a link allowing a user to easily add an item to their cart or create a new cart and add an item to it.
Conclusions
HATEOAS is an often overlooked portion of the REST specification, mostly because it can be quite time consuming to implement and maintain. Spring-Data-REST and Spring-HATEOAS greatly reduce both the time to implement and time to maintain, making HATEOAS much more practical to implement in your RESTful service.
I was only able to touch on some of the features Spring-Data-REST and Spring-HATEOAS have to offer. For a full description of their respective feature set, I recommend checking out the reference docs linked below. If you have any questions or need further explanation, please feel free to ask to in the comments section below.
Additional Resources