认识 HAL
HAL
- Hypertext Application Language(全称)
- HAL 是一种简单的格式,为 API 中的资源提供简单一致的链接
是一种基于json的扩展
HAL 模型
- 链接的资源
- 内嵌资源
- 资源的状态
Spring Data REST
Spring Boot 依赖
- spring-boot-starter-data-rest
常用注解与类
- @RepositoryRestResource
对于URI做相关的定制 - Resource<T>
T类型的资源 - PagedResource<T>
有分页的资源
例子
目录结构如下:
<!-- 增加Jackson的Hibernate类型支持 -->
<dependency>
<groupId>com.fasterxml.jackson.datatype</groupId>
<artifactId>jackson-datatype-hibernate5</artifactId>
<version>2.9.8</version>
</dependency>
<!-- 增加Jackson XML支持 -->
<dependency>
<groupId>com.fasterxml.jackson.dataformat</groupId>
<artifactId>jackson-dataformat-xml</artifactId>
<version>2.9.0</version>
</dependency>
@SpringBootApplication
@EnableCaching
public class WaiterServiceApplication {
public static void main(String[] args) {
SpringApplication.run(WaiterServiceApplication.class, args);
}
@Bean
public Hibernate5Module hibernate5Module() {
return new Hibernate5Module();//做一个Hibernate5的支持
}
@Bean
public Jackson2ObjectMapperBuilderCustomizer jacksonBuilderCustomizer() {
return builder -> {
builder.indentOutput(true);//输出的json是有缩进的
builder.timeZone(TimeZone.getTimeZone("Asia/Shanghai"));//设置时区
};
}
}
@RepositoryRestResource(path = "/coffee")//定制URI
public interface CoffeeRepository extends JpaRepository<Coffee, Long> {
List<Coffee> findByNameInOrderById(List<String> list);
Coffee findByName(String name);
}
结果
首先,我们访问http://localhost:8080/
由上图我们可以看出,它生成一个类似于目录的页面,代码没有实现,这个是Spring Data REST
为我们提供的。
进入 http://localhost:8080/coffee/
我们可以看到 每个coffee 都有具体的链接信息(在link中) 还有 分页的信息 各种对于咖啡的操作
使用http://localhost:8080/coffee?page=0&size=3&sort=id,desc 做一个降序的分页查询 一页为3个 当前页为第0页
使用http://localhost:8080/coffee/search 列出 所有的查询列表
这两个就是当时 在 CoffeeRepository 中定义的两个方法 让我们来使用一下
使用http://localhost:8080/coffee/search/findByName?name=mocha,查询mocha
使用http://localhost:8080/coffee/search/findByNameInOrderById?list=mocha,latte,进行多项查询
使用客户端如何访问 HATEOAS 服务
配置 Jackson JSON
- 注册 HAL 支持
操作超链接
- 让客户端通过目录找到需要的 Link
- 通过webclient或者Repository访问超链接
客户端例子
目录结构如下:
CustomerServiceApplication代码:
@SpringBootApplication
@Slf4j
public class CustomerServiceApplication {
public static void main(String[] args) {
new SpringApplicationBuilder()
.sources(CustomerServiceApplication.class)
.bannerMode(Banner.Mode.OFF)
.web(WebApplicationType.NONE)
.run(args);
}
@Bean
public Jackson2HalModule jackson2HalModule() { //注册Hal
return new Jackson2HalModule();
}
@Bean
public HttpComponentsClientHttpRequestFactory requestFactory() {
PoolingHttpClientConnectionManager connectionManager =
new PoolingHttpClientConnectionManager(30, TimeUnit.SECONDS);//构造一个连接池的连接管理器 生命周期30s
connectionManager.setMaxTotal(200);//最大保持连接数
connectionManager.setDefaultMaxPerRoute(20);//设置每个路由的最大连接
CloseableHttpClient httpClient = HttpClients.custom()//定制HttpClients
.setConnectionManager(connectionManager) //设置连接管理器
.evictIdleConnections(30, TimeUnit.SECONDS)//空闲连接退出时间
.disableAutomaticRetries()//关闭自动重试 重试:请求处理的时候,系统进行了处理,但是返回响应的时候,没有把响应返回,客户端,会认为该操作没有成功,会再次尝试。这对于打款之类的敏感操作是有问题的
// 有 Keep-Alive 认里面的值,没有的话永久有效
//.setKeepAliveStrategy(DefaultConnectionKeepAliveStrategy.INSTANCE) 也可以使用官方提供的请求策略
// 换成自定义的
.setKeepAliveStrategy(new CustomConnectionKeepAliveStrategy())//设置请求策略
.build();
HttpComponentsClientHttpRequestFactory requestFactory =
new HttpComponentsClientHttpRequestFactory(httpClient);//使用httpClient 构造请求工厂
return requestFactory;
}
@Bean
public RestTemplate restTemplate(RestTemplateBuilder builder) {
return builder
.setConnectTimeout(Duration.ofMillis(100))
.setReadTimeout(Duration.ofMillis(500))
.requestFactory(this::requestFactory)
.build();
}
}
CustomerRunner代码;
@Component
@Slf4j
public class CustomerRunner implements ApplicationRunner {
private static final URI ROOT_URI = URI.create("http://localhost:8080/");
@Autowired
private RestTemplate restTemplate;
@Override
public void run(ApplicationArguments args) throws Exception {
Link coffeeLink = getLink(ROOT_URI,"coffees"); //从ROOT_URI根URI 获取有关coffee的link
readCoffeeMenu(coffeeLink);//获取所有coffee信息
Resource<Coffee> americano = addCoffee(coffeeLink);//添加一杯美式咖啡
Link orderLink = getLink(ROOT_URI, "coffeeOrders");
addOrder(orderLink, americano);//添加订单
queryOrders(orderLink);//取出订单
}
private Link getLink(URI uri, String rel) {
ResponseEntity<Resources<Link>> rootResp =
restTemplate.exchange(uri, HttpMethod.GET, null,
new ParameterizedTypeReference<Resources<Link>>() {});
Link link = rootResp.getBody().getLink(rel);
log.info("Link: {}", link);
return link;
}
private void readCoffeeMenu(Link coffeeLink) {
ResponseEntity<PagedResources<Resource<Coffee>>> coffeeResp = //超媒体分页Spring HATEOAS有一个PagedResources类, 他丰富了Page实体以及一些让用户更容易导航到资源的请求方式。Page转换到PagedResources是由一个实现了Spring HATEOASResourceAssembler接口的实现类:PagedResourcesAssembler提供转换的 使用Resource 做一个资源的缓存 转换 ResponseEntity可以定义返回的HttpStatus(状态码)和HttpHeaders(消息头:请求头和响应头)
restTemplate.exchange(coffeeLink.getTemplate().expand(),//取出这个http头
HttpMethod.GET, null, //get操作 无实体
new ParameterizedTypeReference<PagedResources<Resource<Coffee>>>() {});//new ParameterizedTypeReference() {} 就是通过定义一个匿名内部类的方式来获得泛型信息,从而进行反序列化的工作。 取出返回的所有咖啡放入coffeeResp中
log.info("Menu Response: {}", coffeeResp.getBody());
}
private Resource<Coffee> addCoffee(Link link) {
Coffee americano = Coffee.builder()
.name("americano")
.price(Money.of(CurrencyUnit.of("CNY"), 25.0))
.build();
RequestEntity<Coffee> req =
RequestEntity.post(link.getTemplate().expand()).body(americano); //将新coffee绑定到这个URI上
ResponseEntity<Resource<Coffee>> resp =
restTemplate.exchange(req,
new ParameterizedTypeReference<Resource<Coffee>>() {});
log.info("add Coffee Response: {}", resp);
return resp.getBody();
}
private void addOrder(Link link, Resource<Coffee> coffee) {
CoffeeOrder newOrder = CoffeeOrder.builder()
.customer("Li Lei")
.state(OrderState.INIT)
.build();
RequestEntity<?> req =
RequestEntity.post(link.getTemplate().expand()).body(newOrder); //post操作
ResponseEntity<Resource<CoffeeOrder>> resp =
restTemplate.exchange(req,
new ParameterizedTypeReference<Resource<CoffeeOrder>>() {});
log.info("add Order Response: {}", resp);
Resource<CoffeeOrder> order = resp.getBody();
Link items = order.getLink("items");
req = RequestEntity.post(items.getTemplate().expand()).body(Collections.singletonMap("_links", coffee.getLink("self"))); // 可以使用http链接 去代替加入的咖啡
ResponseEntity<String> itemResp = restTemplate.exchange(req, String.class);
log.info("add Order Items Response: {}", itemResp);
}
private void queryOrders(Link link) {
ResponseEntity<String> resp = restTemplate.getForEntity(link.getTemplate().expand(), String.class);
log.info("query Order Response: {}", resp);
}
}
分析
从ROOT_URI根URI 获取有关coffee的link时,rel的填写,只能填写服务端 方法的url名加个s 换成其他的(服务器修改为aab 客户端改为aabs) 都会报错 故 此处 写法固定。此处 问题 因为 rel在服务器就写了
所以 我们需要使用服务端的rel来
req = RequestEntity.post(items.getTemplate().expand()).body(Collections.singletonMap("_links", coffee.getLink("self"))); // 可以使用http链接 去代替加入的咖啡
的用法 用postman展示
在放入咖啡的时候,可以用此咖啡的地址 代替