RESTful Web Service
REST
REST 提供了一组架构约束(风格),当作为一个整体来应用时,强调组件交互的可伸缩性、接口的通用性、组件的独立部署、以及用来减少交互延迟、增强安全性、封装遗留系统的中间组件。
Richardson 成熟度模型
Richardson 服务成熟度模型是基于一个服务对于 URI,HTTP 和超媒体的支持,划分出服务成熟度的三个级别 + 零级(即:没有任何支持)。他以一种服务实现者容易理解的方式,将通用的架构模式映射到服务的设计。
如何实现 RESTFul Web Service
- 识别资源
- 选择合适的资源粒度
- 设计 URI
- 选择合适的 HTTP 方法和返回码
- 设计资源的表述
识别资源
- 找到领域名词:能用 CRUD 操作的名词
- 将资源组织为集合(即集合资源)
- 将资源合并为符合资源
- 计算或处理函数
资源粒度
站在服务端的角度,要考虑
- 网络效率
- 表述的多少
- 客户端的易用程度
站在客户端的角度,要考虑
- 可缓存性
- 修改频率
- 可变性
构建更好的 URI
- 使用域及子域对资源进行合理的分组或划分
- 在 URI 的路径部分使用斜杠分隔符 ( / ) 来表示资源之间的层次关系
- 在 URI 的路径部分使用逗号 ( , ) 和分号 ( ; ) 来表示非层次元素
- 使用连字符 ( - ) 和下划线 ( _ ) 来改善长路径中名称的可读性
- 在 URI 的查询部分使用“与”符号 ( & ) 来分隔参数
- 在 URI 中避免出现文件扩展名 ( 例如 .php,.aspx 和 .jsp )
认识 HTTP 方法
URI 与 HTTP 方法的组合
认识 HTTP 状态码
选择合适的表述
JSON
- MappingJackson2HttpMessageConverter
- GsonHttpMessageConverter
- JsonbHttpMessageConverter
XML
- MappingJackson2XmlHttpMessageConverter
- Jaxb2RootElementHttpMessageConverter
HTML
ProtoBuf
- ProtobufHttpMessageConverter
HATEOAS
Richardson 成熟度模型中的:Level 3 - Hypermedia Controls
- Hybermedia As The Engine Of Application State:把超媒体视为应用程序状态的引擎
- REST 统一接口的必要组成部分
HATEOAS v.s. WSDL
HATEOAS
- 表述中的超链接会提供服务所需的各种 REST 接口信息
- 无需事先约定如何访问服务
传统的服务契约
- 必须事先约定服务的地址与格式
HATEOAS 示例
常用的超链接类型
HAL
- Hypertext Application Language
- HAL 是 ⼀种简单的格式,为 API 中的资源提供简单 ⼀致的链接
HAL 模型
- 链接
- 内嵌资源
- 状态
使用 Spring Data REST 实现简单的超媒体服务
Spring Boot 依赖
- spring-boot-starter-data-rest
常用注解与类
- @RepositoryRestResource
- Resource
- PagedResource
示例 hateoas-waiter-service
pom 依赖
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.1.3.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>geektime.spring.springbucks</groupId>
<artifactId>waiter-service</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>waiter-service</name>
<description>Demo project for Spring Boot</description>
<properties>
<java.version>1.8</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<!-- 增加spring-boot-starter-data-rest -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-rest</artifactId>
</dependency>
<dependency>
<groupId>org.joda</groupId>
<artifactId>joda-money</artifactId>
<version>1.0.1</version>
</dependency>
<dependency>
<groupId>org.jadira.usertype</groupId>
<artifactId>usertype.core</artifactId>
<version>6.0.1.GA</version>
</dependency>
<!-- 增加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>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
</dependency>
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
CoffeeOrderRepository
package geektime.spring.springbucks.waiter.repository;
import geektime.spring.springbucks.waiter.model.CoffeeOrder;
import org.springframework.data.jpa.repository.JpaRepository;
public interface CoffeeOrderRepository extends JpaRepository<CoffeeOrder, Long> {
}
CoffeeRepository
package geektime.spring.springbucks.waiter.repository;
import geektime.spring.springbucks.waiter.model.Coffee;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.rest.core.annotation.RepositoryRestResource;
import java.util.List;
@RepositoryRestResource(path = "/coffee")
public interface CoffeeRepository extends JpaRepository<Coffee, Long> {
List<Coffee> findByNameInOrderById(List<String> list);
Coffee findByName(String name);
}
MoneySerializer
package geektime.spring.springbucks.waiter.support;
import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.databind.SerializerProvider;
import com.fasterxml.jackson.databind.ser.std.StdSerializer;
import org.joda.money.Money;
import org.springframework.boot.jackson.JsonComponent;
import java.io.IOException;
@JsonComponent
public class MoneySerializer extends StdSerializer<Money> {
protected MoneySerializer() {
super(Money.class);
}
@Override
public void serialize(Money money, JsonGenerator jsonGenerator, SerializerProvider serializerProvider) throws IOException {
jsonGenerator.writeNumber(money.getAmount());
}
}
MoneyDeserializer
package geektime.spring.springbucks.waiter.support;
import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.DeserializationContext;
import com.fasterxml.jackson.databind.deser.std.StdDeserializer;
import org.joda.money.CurrencyUnit;
import org.joda.money.Money;
import org.springframework.boot.jackson.JsonComponent;
import java.io.IOException;
@JsonComponent
public class MoneyDeserializer extends StdDeserializer<Money> {
protected MoneyDeserializer() {
super(Money.class);
}
@Override
public Money deserialize(JsonParser p, DeserializationContext ctxt) throws IOException, JsonProcessingException {
return Money.of(CurrencyUnit.of("CNY"), p.getDecimalValue());
}
}
启动类 WaiterServiceApplication
package geektime.spring.springbucks.waiter;
import com.fasterxml.jackson.datatype.hibernate5.Hibernate5Module;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.autoconfigure.jackson.Jackson2ObjectMapperBuilderCustomizer;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.context.annotation.Bean;
import java.util.TimeZone;
@SpringBootApplication
@EnableCaching
public class WaiterServiceApplication {
public static void main(String[] args) {
SpringApplication.run(WaiterServiceApplication.class, args);
}
@Bean
public Hibernate5Module hibernate5Module() {
return new Hibernate5Module();
}
@Bean
public Jackson2ObjectMapperBuilderCustomizer jacksonBuilderCustomizer() {
return builder -> {
builder.indentOutput(true);
builder.timeZone(TimeZone.getTimeZone("Asia/Shanghai"));
};
}
}
测试 (根据超媒体目录做访问和搜索)
// http://localhost:8080/
{
"_links": {
"coffeeOrders": {
"href": "http://localhost:8080/coffeeOrders{?page,size,sort}",
"templated": true
},
"coffees": {
"href": "http://localhost:8080/coffee{?page,size,sort}",
"templated": true
},
"profile": {
"href": "http://localhost:8080/profile"
}
}
}
// http://localhost:8080/coffee
{
"_embedded": {
"coffees": [
{
"createTime": "2020-01-28T13:53:46.415+0800",
"updateTime": "2020-01-28T13:53:46.415+0800",
"name": "espresso",
"price": 20.00,
"_links": {
"self": {
"href": "http://localhost:8080/coffee/1"
},
"coffee": {
"href": "http://localhost:8080/coffee/1"
}
}
},
{
"createTime": "2020-01-28T13:53:46.417+0800",
"updateTime": "2020-01-28T13:53:46.417+0800",
"name": "latte",
"price": 25.00,
"_links": {
"self": {
"href": "http://localhost:8080/coffee/2"
},
"coffee": {
"href": "http://localhost:8080/coffee/2"
}
}
},
{
"createTime": "2020-01-28T13:53:46.417+0800",
"updateTime": "2020-01-28T13:53:46.417+0800",
"name": "capuccino",
"price": 25.00,
"_links": {
"self": {
"href": "http://localhost:8080/coffee/3"
},
"coffee": {
"href": "http://localhost:8080/coffee/3"
}
}
},
{
"createTime": "2020-01-28T13:53:46.418+0800",
"updateTime": "2020-01-28T13:53:46.418+0800",
"name": "mocha",
"price": 30.00,
"_links": {
"self": {
"href": "http://localhost:8080/coffee/4"
},
"coffee": {
"href": "http://localhost:8080/coffee/4"
}
}
},
{
"createTime": "2020-01-28T13:53:46.418+0800",
"updateTime": "2020-01-28T13:53:46.418+0800",
"name": "macchiato",
"price": 30.00,
"_links": {
"self": {
"href": "http://localhost:8080/coffee/5"
},
"coffee": {
"href": "http://localhost:8080/coffee/5"
}
}
}
]
},
"_links": {
"self": {
"href": "http://localhost:8080/coffee{?page,size,sort}",
"templated": true
},
"profile": {
"href": "http://localhost:8080/profile/coffee"
},
"search": {
"href": "http://localhost:8080/coffee/search"
}
},
"page": {
"size": 20,
"totalElements": 5,
"totalPages": 1,
"number": 0
}
}
// http://localhost:8080/coffee?page=0&size=3&sort=id,desc
{
"_embedded": {
"coffees": [
{
"createTime": "2020-01-28T13:53:46.418+0800",
"updateTime": "2020-01-28T13:53:46.418+0800",
"name": "macchiato",
"price": 30.00,
"_links": {
"self": {
"href": "http://localhost:8080/coffee/5"
},
"coffee": {
"href": "http://localhost:8080/coffee/5"
}
}
},
{
"createTime": "2020-01-28T13:53:46.418+0800",
"updateTime": "2020-01-28T13:53:46.418+0800",
"name": "mocha",
"price": 30.00,
"_links": {
"self": {
"href": "http://localhost:8080/coffee/4"
},
"coffee": {
"href": "http://localhost:8080/coffee/4"
}
}
},
{
"createTime": "2020-01-28T13:53:46.417+0800",
"updateTime": "2020-01-28T13:53:46.417+0800",
"name": "capuccino",
"price": 25.00,
"_links": {
"self": {
"href": "http://localhost:8080/coffee/3"
},
"coffee": {
"href": "http://localhost:8080/coffee/3"
}
}
}
]
},
"_links": {
"first": {
"href": "http://localhost:8080/coffee?page=0&size=3&sort=id,desc"
},
"self": {
"href": "http://localhost:8080/coffee"
},
"next": {
"href": "http://localhost:8080/coffee?page=1&size=3&sort=id,desc"
},
"last": {
"href": "http://localhost:8080/coffee?page=1&size=3&sort=id,desc"
},
"profile": {
"href": "http://localhost:8080/profile/coffee"
},
"search": {
"href": "http://localhost:8080/coffee/search"
}
},
"page": {
"size": 3,
"totalElements": 5,
"totalPages": 2,
"number": 0
}
}
// http://localhost:8080/coffee/search
{
"_links": {
"findByNameInOrderById": {
"href": "http://localhost:8080/coffee/search/findByNameInOrderById{?list}",
"templated": true
},
"findByName": {
"href": "http://localhost:8080/coffee/search/findByName{?name}",
"templated": true
},
"self": {
"href": "http://localhost:8080/coffee/search"
}
}
}
// http://localhost:8080/coffee?page=0&size=3&sort=id,desc
{
"_embedded": {
"coffees": [
{
"createTime": "2020-01-28T13:53:46.418+0800",
"updateTime": "2020-01-28T13:53:46.418+0800",
"name": "macchiato",
"price": 30.00,
"_links": {
"self": {
"href": "http://localhost:8080/coffee/5"
},
"coffee": {
"href": "http://localhost:8080/coffee/5"
}
}
},
{
"createTime": "2020-01-28T13:53:46.418+0800",
"updateTime": "2020-01-28T13:53:46.418+0800",
"name": "mocha",
"price": 30.00,
"_links": {
"self": {
"href": "http://localhost:8080/coffee/4"
},
"coffee": {
"href": "http://localhost:8080/coffee/4"
}
}
},
{
"createTime": "2020-01-28T13:53:46.417+0800",
"updateTime": "2020-01-28T13:53:46.417+0800",
"name": "capuccino",
"price": 25.00,
"_links": {
"self": {
"href": "http://localhost:8080/coffee/3"
},
"coffee": {
"href": "http://localhost:8080/coffee/3"
}
}
}
]
},
"_links": {
"first": {
"href": "http://localhost:8080/coffee?page=0&size=3&sort=id,desc"
},
"self": {
"href": "http://localhost:8080/coffee"
},
"next": {
"href": "http://localhost:8080/coffee?page=1&size=3&sort=id,desc"
},
"last": {
"href": "http://localhost:8080/coffee?page=1&size=3&sort=id,desc"
},
"profile": {
"href": "http://localhost:8080/profile/coffee"
},
"search": {
"href": "http://localhost:8080/coffee/search"
}
},
"page": {
"size": 3,
"totalElements": 5,
"totalPages": 2,
"number": 0
}
}
如何访问 HATEOAS 服务
配置 Jackson JSON
- 注册 HAL 支持
操作超链接
- 找到需要的 Link
- 访问超链接
编程方式(客户端访问 Rest 服务资源)
引入依赖
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.1.3.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>geektime.spring.springbucks</groupId>
<artifactId>customer-service</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>customer-service</name>
<description>Demo project for Spring Boot</description>
<properties>
<java.version>1.8</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-rest</artifactId>
</dependency>
<dependency>
<groupId>org.joda</groupId>
<artifactId>joda-money</artifactId>
<version>1.0.1</version>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
</dependency>
<dependency>
<groupId>org.apache.httpcomponents</groupId>
<artifactId>httpclient</artifactId>
<version>4.5.7</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
CustomerServiceApplication
package geektime.spring.springbucks.customer;
import geektime.spring.springbucks.customer.support.CustomConnectionKeepAliveStrategy;
import lombok.extern.slf4j.Slf4j;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClients;
import org.apache.http.impl.conn.PoolingHttpClientConnectionManager;
import org.springframework.boot.Banner;
import org.springframework.boot.WebApplicationType;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.builder.SpringApplicationBuilder;
import org.springframework.boot.web.client.RestTemplateBuilder;
import org.springframework.context.annotation.Bean;
import org.springframework.hateoas.hal.Jackson2HalModule;
import org.springframework.http.client.HttpComponentsClientHttpRequestFactory;
import org.springframework.web.client.RestTemplate;
import java.time.Duration;
import java.util.concurrent.TimeUnit;
@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() {
return new Jackson2HalModule();
}
@Bean
public HttpComponentsClientHttpRequestFactory requestFactory() {
PoolingHttpClientConnectionManager connectionManager =
new PoolingHttpClientConnectionManager(30, TimeUnit.SECONDS);
connectionManager.setMaxTotal(200);
connectionManager.setDefaultMaxPerRoute(20);
CloseableHttpClient httpClient = HttpClients.custom()
.setConnectionManager(connectionManager)
.evictIdleConnections(30, TimeUnit.SECONDS)
.disableAutomaticRetries()
// 有 Keep-Alive 认里面的值,没有的话永久有效
//.setKeepAliveStrategy(DefaultConnectionKeepAliveStrategy.INSTANCE)
// 换成自定义的
.setKeepAliveStrategy(new CustomConnectionKeepAliveStrategy())
.build();
HttpComponentsClientHttpRequestFactory requestFactory =
new HttpComponentsClientHttpRequestFactory(httpClient);
return requestFactory;
}
@Bean
public RestTemplate restTemplate(RestTemplateBuilder builder) {
return builder
.setConnectTimeout(Duration.ofMillis(100))
.setReadTimeout(Duration.ofMillis(500))
.requestFactory(this::requestFactory)
.build();
}
}
CustomerRunner
package geektime.spring.springbucks.customer;
import geektime.spring.springbucks.customer.model.Coffee;
import geektime.spring.springbucks.customer.model.CoffeeOrder;
import geektime.spring.springbucks.customer.model.OrderState;
import lombok.extern.slf4j.Slf4j;
import org.joda.money.CurrencyUnit;
import org.joda.money.Money;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.ApplicationArguments;
import org.springframework.boot.ApplicationRunner;
import org.springframework.core.ParameterizedTypeReference;
import org.springframework.hateoas.Link;
import org.springframework.hateoas.PagedResources;
import org.springframework.hateoas.Resource;
import org.springframework.hateoas.Resources;
import org.springframework.http.HttpMethod;
import org.springframework.http.RequestEntity;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Component;
import org.springframework.web.client.RestTemplate;
import java.net.URI;
import java.util.Collections;
@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");
//Link: <http://localhost:8080/coffee{?page,size,sort}>;rel="coffees"
readCoffeeMenu(coffeeLink);
//Menu Response: PagedResource { content: [Resource { content: Coffee(name=espresso, price=CNY 20.00, createTime=Tue Jan 28 14:59:39 CST 2020, updateTime=Tue Jan 28 14:59:39 CST 2020), links: [<http://localhost:8080/coffee/1>;rel="self", <http://localhost:8080/coffee/1>;rel="coffee"] }, Resource { content: Coffee(name=latte, price=CNY 25.00, createTime=Tue Jan 28 14:59:39 CST 2020, updateTime=Tue Jan 28 14:59:39 CST 2020), links: [<http://localhost:8080/coffee/2>;rel="self", <http://localhost:8080/coffee/2>;rel="coffee"] }, Resource { content: Coffee(name=capuccino, price=CNY 25.00, createTime=Tue Jan 28 14:59:39 CST 2020, updateTime=Tue Jan 28 14:59:39 CST 2020), links: [<http://localhost:8080/coffee/3>;rel="self", <http://localhost:8080/coffee/3>;rel="coffee"] }, Resource { content: Coffee(name=mocha, price=CNY 30.00, createTime=Tue Jan 28 14:59:39 CST 2020, updateTime=Tue Jan 28 14:59:39 CST 2020), links: [<http://localhost:8080/coffee/4>;rel="self", <http://localhost:8080/coffee/4>;rel="coffee"] }, Resource { content: Coffee(name=macchiato, price=CNY 30.00, createTime=Tue Jan 28 14:59:39 CST 2020, updateTime=Tue Jan 28 14:59:39 CST 2020), links: [<http://localhost:8080/coffee/5>;rel="self", <http://localhost:8080/coffee/5>;rel="coffee"] }], metadata: Metadata { number: 0, total pages: 1, total elements: 5, size: 20 }, links: [<http://localhost:8080/coffee{?page,size,sort}>;rel="self", <http://localhost:8080/profile/coffee>;rel="profile", <http://localhost:8080/coffee/search>;rel="search"] }
Resource<Coffee> americano = addCoffee(coffeeLink);
//add Coffee Response: <201,Resource { content: Coffee(name=americano, price=CNY 25.00, createTime=Tue Jan 28 15:38:51 CST 2020, updateTime=Tue Jan 28 15:38:51 CST 2020), links: [<http://localhost:8080/coffee/6>;rel="self", <http://localhost:8080/coffee/6>;rel="coffee"] },[Location:"http://localhost:8080/coffee/6", Content-Type:"application/json;charset=UTF-8", Transfer-Encoding:"chunked", Date:"Tue, 28 Jan 2020 07:38:50 GMT"]>
Link orderLink = getLink(ROOT_URI, "coffeeOrders");
//Link: <http://localhost:8080/coffeeOrders{?page,size,sort}>;rel="coffeeOrders"
addOrder(orderLink, americano);
//add Order Response: <201,Resource { content: CoffeeOrder(id=null, customer=Li Lei, state=INIT, createTime=Tue Jan 28 15:38:51 CST 2020, updateTime=Tue Jan 28 15:38:51 CST 2020), links: [<http://localhost:8080/coffeeOrders/1>;rel="self", <http://localhost:8080/coffeeOrders/1>;rel="coffeeOrder", <http://localhost:8080/coffeeOrders/1/items>;rel="items"] },[Location:"http://localhost:8080/coffeeOrders/1", Content-Type:"application/json;charset=UTF-8", Transfer-Encoding:"chunked", Date:"Tue, 28 Jan 2020 07:38:50 GMT"]>
//add Order Items Response: <204,[Date:"Tue, 28 Jan 2020 07:38:50 GMT"]>
queryOrders(orderLink);
//query Order Response: <200,{....}
}
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 =
restTemplate.exchange(coffeeLink.getTemplate().expand(),
HttpMethod.GET, null,
new ParameterizedTypeReference<PagedResources<Resource<Coffee>>>() {});
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);
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);
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")));
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);
}
}