不要讨厌HATEOAS Part Deux:HATEOAS的春天

在我关于HATEOAS的系列文章的最后结论中,我们将深入探讨如何使用Spring-Data-REST和Spring-HATEOAS实现HATEOAS。 HATEOAS的春天到了!

我整理了一个有效的项目,该项目将演示下面的代码示例以及其他一些功能。 该项目可以在这里找到: https : //github.com/in-the-keyhole/hateoas-demo-II 。 需要JDK 8和Maven,否则不需要外部依赖项即可运行项目。

服务资源

通过其资源与Web服务交互是REST的核心设计约束之一。 使用Spring-Data和Spring-MVC ,开始提供资源并不是很困难。 您需要为要提供服务的实体添加一个Repository ,并实现一个控制器来为其提供服务。 但是, Spring-Data-REST使此过程变得更加容易,并在此过程中提供了更丰富的资源(即添加超媒体标记)。

@RepositoryRestResource
public interface ItemRepo extends CrudRepository {
}

就这么简单。 如果您启动您的弹簧启动应用程序并导航到http://localhost:8080/items (并已做了一些其他必要的配置 为 好 ),你应该得到的回报JSON看起来是这样的:

{
  "_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"
    }
  }
}

除了易于演示的GET功能外,Spring-Data-REST还增加了PUT (出于某种原因决定将PUT用于创建和更新的原因,Spring-Data-REST)的功能,并DELETE资源以及检索资源的ID。 仅两行代码就具有很多功能!

分页和排序

资源通常会有很多记录。 通常,由于各个级别的资源成本很高,因此您不希望应要求返回所有这些记录。 分页是解决此问题的常用解决方案,而Spring-Data-REST使其易于实现。

另一个常见的需求是允许客户端对来自资源的返回进行排序的能力,在这里Spring-Data-REST还是可以解决的。 为了使用Item资源实现此功能,我们需要从将CrudRepository扩展为CrudRepositoryPagingAndSortingRepository所示:

@RepositoryRestResource
public interface ItemRepo extends PagingAndSortingRepository {
}

重新启动应用程序并返回http://localhost:8080/items ,返回的内容最初看起来是相同的,但是在页面底部附近,我们看到了一些新的JSON对象:

{
  ...    
  "_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提供了用于浏览资源返回页面的超媒体控件。 last,next,prev和first(如果适用)(注意:Spring-Data-REST使用基于0的数组进行分页)。 如果仔细观察,您还将注意到Spring-Data-REST如何允许客户端操纵每页的返回数( .../items?size=x )。 最后,还添加了排序,并可以使用URL参数: .../items?sort=name&name.dir=desc

搜索资源

因此,我们正在提供资源,对退货进行分页,并允许客户对这些退货进行排序。 这些都是非常有用的,但是客户端通常会希望搜索资源的特定子集。 这是Spring-Data-REST使极其简单的另一项任务。

@RepositoryRestResource
public interface ItemRepo extends PagingAndSortingRepository {

	List findByType(@Param("type") String type);

	@RestResource(path = "byMaxPrice")
	@Query("SELECT i FROM Item i WHERE i.price <= :maxPrice")
	List findItemsLessThan(@Param("maxPrice") double maxPrice);

	@RestResource(path = "byMaxPriceAndType")
	@Query("SELECT i FROM Item i WHERE i.price <= :maxPrice AND i.type = :type")
	List findItemsLessThanAndType(@Param("maxPrice") double maxPrice, @Param("type") String type);
}

上面是一些用户可能希望通过以下方式搜索商品的查询:商品的类型,商品的最高价格,然后将这两个参数组合在一起。 导航到http://localhost:8080/items/search ,Spring-Data-REST呈现所有可用的搜索选项以及如何与它们交互。 与搜索端点交互时,也会启用根资源端点处可用的分页和排序功能!

...
    "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
    },
...

改变资源的形状

有时候改变端点服务的实体的形状是有益的。 您可能需要展平对象树,隐藏字段或更改字段名称以维护合同。 Spring-Data-REST提供了使用投影来操纵形状的功能。

首先,我们需要创建一个接口并使用@Projection对其进行注释:

@Projection(name = "itemSummary", types = { Item.class })
public interface ItemSummary {
	String getName();
	String getPrice();
}

这将允许Spring-Data-REST根据请求以ItemSummary形状提供我们的Item实体: http://localhost:8080/api/items/1?projection=itemSummary 如果我们想使ItemSummary默认的形状,我们击中时返回/items端点可以通过添加来完成excerptProjectio n向@RepositoryRestResource上标注ItemRepo

@RepositoryRestResource(excerptProjection = ItemSummary.class)
public interface ItemRepo extends PagingAndSortingRepository {

现在,当我们点击../items ,我们的回报看起来像这样:

...
{
      "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
        }
      }
}
...

自定义资源的端点

实体的名称可能不一定总是作为资源端点的名称。 它可能不符合遗留需求,您可能需要在资源的终结点之前加上前缀,或者只是想要一个不同的名称。 Spring-Data-REST提供了满足所有这些需求的钩子。

更改资源名称:

@RepositoryRestResource(collectionResourceRel = "merchandise", path = "merchandise")
public interface ItemRepo extends PagingAndSortingRepository {
}

并添加基本路径:

@Configuration
public class RestConfiguration extends RepositoryRestConfigurerAdapter {

	@Override
	public void configureRepositoryRestConfiguration(RepositoryRestConfiguration config) {
		config.setBasePath("api");
	}

}

现在,将不再是通过../api/merchandise来提供Item实体,而是通过../items来提供../api/merchandise

保护资源

安全是一个非常重要和复杂的主题。 即使是整个谈话也几乎没有触及表面。 因此,将此部分视为对这个主题的轻微磨损。

隐藏场

如上一节所述,投影是隐藏字段的一种方法。 另一种更安全的方法是在如下所示的字段上使用@JsonIgnore以防止其返回:

public class Item implements Serializable, Identifiable {
	@JsonIgnore
	@Column(name = "secret_field")
	private String secretField;
}

限制通过HTTP的访问

在某些情况下,无论您是谁,都根本无法通过HTTP访问功能。 这可以通过@RestResource(exported = false)来实现,它告诉Spring-Data-REST完全不将该资源或资源的一部分发布到Web上。 可以在“类型”和“方法”级别上进行设置。 如果要广泛拒绝,然后显式定义应访问的内容,则在方法级别也可以覆盖类型级别。

方法级别:

public interface OrderRepo extends CrudRepository {

	@Override
	@RestResource(exported = false)
	 S save(S entity);
}

类型级别,具有方法级别覆盖:

@RestResource(exported = false)
public interface OrderRepo extends CrudRepository {

	@Override
	@RestResource(exported = true)
	 S save(S entity);
}

另一种方法(如果您愿意)是扩展Repository接口,而仅定义您希望客户端可以访问的方法。

public interface PaymentRepo extends Repository {
	Payment findOne(Long id);

	 S save(S entity);
}

通过角色限制访问

您可能还希望将功能限制为仅某些类型的用户。

@RepositoryRestResource(collectionResourceRel = "merchandise", path = "merchandise")
public interface ItemRepo extends PagingAndSortingRepository {
	@PreAuthorize("hasRole('ADMIN')")
	 S save(S entity);

	@PreAuthorize("hasRole('ADMIN')")
	 Iterable save(Iterable entities);
}

虽然我认为并不是严格要求的,但是由于可能与Spring-MVC过滤器进行了一些时髦的交互,因此需要一些其他的URL配置才能使基于角色的安全性发挥作用。 (我花了很多时间研究这个问题。)但是,无论如何,实现多层安全性通常是一个好习惯,因此,这也不一定是错误的:

@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();
	}
}

@RestResource一样, @PreAuthorize也可以放在类型级别,并在方法级别覆盖。

@PreAuthorize("hasRole('USER')")
public interface OrderRepo extends CrudRepository {
}

Spring-HATEOAS的附加定制

到目前为止,我已经演示了Spring-Data-REST的所有功能,以及如何使HATEOAS服务的实现变得轻而易举。 las,使用Spring-Data-REST可以做的事情有局限性。 幸运的是,还有另一个Spring项目Spring-HATEOAS可以从那里开始。

Spring-HATEOAS简化了向资源添加超媒体标记的过程,对于处理资源之间的自定义交互非常有用。 例如,将商品添加到订单中:

@RequestMapping("/{id}")
public ResponseEntity> viewItem(@PathVariable String id) {
  Item item = itemRepo.findOne(Long.valueOf(id));
  
  Resource resource = new Resource(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);
}

这样,我们就覆盖了Spring-Data-REST提供的默认/merchandise/(id)功能,现在将返回以下结果:

{
  "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
    }
  }
}

因此,我们的客户代码现在可以呈现一个链接,使用户可以轻松地将商品添加到他们的购物车或创建新的购物车并向其中添加商品。

结论

HATEOAS是REST规范中经常被忽略的部分,主要是因为实现和维护它会非常耗时。 Spring-Data-REST和Spring-HATEOAS大大减少了实现时间和维护时间,这使得HATEOAS在RESTful服务中实现更加实用。

我只能接触到Spring-Data-REST和Spring-HATEOAS必须提供的某些功能。 有关它们各自功能集的完整说明,建议您查看下面链接的参考文档。 如果您有任何疑问或需要进一步说明,请随时在下面的评论部分中提问。

其他资源

  • 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/

翻译自: https://www.javacodegeeks.com/2016/05/dont-hate-hateoas-part-deux-springtime-hateoas.html

你可能感兴趣的:(不要讨厌HATEOAS Part Deux:HATEOAS的春天)