神刀安全网

Don’t Hate the HATEOAS Part Deux: Springtime for HATEOAS

In the much belated conclusion tomy 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 here: 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()) {    // Provide a link to an existing Order  resource.add(entityLinks.linkToSingleResource(retrieveExistingOrder()).withRel("addToCart"));   } else {    // Provide a link to create a new Order    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     }   } }

So 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 comments section below.

Additional Resources

http://docs.spring.io/spring-data/rest/docs/2.5.1.RELEASE/reference/html/

http://docs.spring.io/spring-hateoas/docs/0.19.0.RELEASE/reference/html/

转载本站任何文章请注明:转载至神刀安全网,谢谢神刀安全网 » Don’t Hate the HATEOAS Part Deux: Springtime for HATEOAS

分享到:更多 ()

评论 抢沙发

  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址