目录

Life in Flow

知不知,尚矣;不知知,病矣。
不知不知,殆矣。

X

RESTful Web Service

REST

 REST 提供了一组架构约束(风格),当作为一个整体来应用时,强调组件交互的可伸缩性、接口的通用性、组件的独立部署、以及用来减少交互延迟、增强安全性、封装遗留系统的中间组件

Richardson 成熟度模型

Richardson 成熟度模型
 Richardson 服务成熟度模型是基于一个服务对于 URI,HTTP 和超媒体的支持,划分出服务成熟度的三个级别 + 零级(即:没有任何支持)。他以一种服务实现者容易理解的方式,将通用的架构模式映射到服务的设计。

如何实现 RESTFul Web Service

  • 识别资源
  • 选择合适的资源粒度
  • 设计 URI
  • 选择合适的 HTTP 方法和返回码
  • 设计资源的表述

识别资源

  • 找到领域名词:能用 CRUD 操作的名词
  • 将资源组织为集合(即集合资源)
  • 将资源合并为符合资源
  • 计算或处理函数

资源粒度

站在服务端的角度,要考虑

  • 网络效率
  • 表述的多少
  • 客户端的易用程度

站在客户端的角度,要考虑

  • 可缓存性
  • 修改频率
  • 可变性

构建更好的 URI

  • 使用域及子域对资源进行合理的分组或划分
  • 在 URI 的路径部分使用斜杠分隔符 ( / ) 来表示资源之间的层次关系
  • 在 URI 的路径部分使用逗号 ( , ) 和分号 ( ; ) 来表示非层次元素
  • 使用连字符 ( - ) 和下划线 ( _ ) 来改善长路径中名称的可读性
  • 在 URI 的查询部分使用“与”符号 ( & ) 来分隔参数
  • 在 URI 中避免出现文件扩展名 ( 例如 .php,.aspx 和 .jsp )

认识 HTTP 方法

认识 HTTP 方法

URI 与 HTTP 方法的组合
URI 与 HTTP 方法的组合

认识 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 示例
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 依赖

 1<?xml version="1.0" encoding="UTF-8"?>
 2<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
 3	xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
 4	<modelVersion>4.0.0</modelVersion>
 5	<parent>
 6		<groupId>org.springframework.boot</groupId>
 7		<artifactId>spring-boot-starter-parent</artifactId>
 8		<version>2.1.3.RELEASE</version>
 9		<relativePath/> <!-- lookup parent from repository -->
10	</parent>
11	<groupId>geektime.spring.springbucks</groupId>
12	<artifactId>waiter-service</artifactId>
13	<version>0.0.1-SNAPSHOT</version>
14	<name>waiter-service</name>
15	<description>Demo project for Spring Boot</description>
16
17	<properties>
18		<java.version>1.8</java.version>
19	</properties>
20
21	<dependencies>
22		<dependency>
23			<groupId>org.springframework.boot</groupId>
24			<artifactId>spring-boot-starter-data-jpa</artifactId>
25		</dependency>
26		<!-- 增加spring-boot-starter-data-rest -->
27		<dependency>
28			<groupId>org.springframework.boot</groupId>
29			<artifactId>spring-boot-starter-data-rest</artifactId>
30		</dependency>
31		<dependency>
32			<groupId>org.joda</groupId>
33			<artifactId>joda-money</artifactId>
34			<version>1.0.1</version>
35		</dependency>
36		<dependency>
37			<groupId>org.jadira.usertype</groupId>
38			<artifactId>usertype.core</artifactId>
39			<version>6.0.1.GA</version>
40		</dependency>
41		<!-- 增加Jackson的Hibernate类型支持 -->
42		<dependency>
43			<groupId>com.fasterxml.jackson.datatype</groupId>
44			<artifactId>jackson-datatype-hibernate5</artifactId>
45			<version>2.9.8</version>
46		</dependency>
47		<!-- 增加Jackson XML支持 -->
48		<dependency>
49			<groupId>com.fasterxml.jackson.dataformat</groupId>
50			<artifactId>jackson-dataformat-xml</artifactId>
51			<version>2.9.0</version>
52		</dependency>
53
54		<dependency>
55			<groupId>org.apache.commons</groupId>
56			<artifactId>commons-lang3</artifactId>
57		</dependency>
58
59		<dependency>
60			<groupId>com.h2database</groupId>
61			<artifactId>h2</artifactId>
62			<scope>runtime</scope>
63		</dependency>
64		<dependency>
65			<groupId>org.projectlombok</groupId>
66			<artifactId>lombok</artifactId>
67			<optional>true</optional>
68		</dependency>
69		<dependency>
70			<groupId>org.springframework.boot</groupId>
71			<artifactId>spring-boot-starter-test</artifactId>
72			<scope>test</scope>
73		</dependency>
74	</dependencies>
75
76	<build>
77		<plugins>
78			<plugin>
79				<groupId>org.springframework.boot</groupId>
80				<artifactId>spring-boot-maven-plugin</artifactId>
81			</plugin>
82		</plugins>
83	</build>
84</project>

CoffeeOrderRepository

1package geektime.spring.springbucks.waiter.repository;
2
3import geektime.spring.springbucks.waiter.model.CoffeeOrder;
4import org.springframework.data.jpa.repository.JpaRepository;
5
6public interface CoffeeOrderRepository extends JpaRepository<CoffeeOrder, Long> {
7}

CoffeeRepository

 1package geektime.spring.springbucks.waiter.repository;
 2
 3import geektime.spring.springbucks.waiter.model.Coffee;
 4import org.springframework.data.jpa.repository.JpaRepository;
 5import org.springframework.data.rest.core.annotation.RepositoryRestResource;
 6
 7import java.util.List;
 8
 9@RepositoryRestResource(path = "/coffee")
10public interface CoffeeRepository extends JpaRepository<Coffee, Long> {
11    List<Coffee> findByNameInOrderById(List<String> list);
12    Coffee findByName(String name);
13}

MoneySerializer

 1package geektime.spring.springbucks.waiter.support;
 2
 3import com.fasterxml.jackson.core.JsonGenerator;
 4import com.fasterxml.jackson.databind.SerializerProvider;
 5import com.fasterxml.jackson.databind.ser.std.StdSerializer;
 6import org.joda.money.Money;
 7import org.springframework.boot.jackson.JsonComponent;
 8
 9import java.io.IOException;
10
11@JsonComponent
12public class MoneySerializer extends StdSerializer<Money> {
13    protected MoneySerializer() {
14        super(Money.class);
15    }
16
17    @Override
18    public void serialize(Money money, JsonGenerator jsonGenerator, SerializerProvider serializerProvider) throws IOException {
19        jsonGenerator.writeNumber(money.getAmount());
20    }
21}

MoneyDeserializer

 1package geektime.spring.springbucks.waiter.support;
 2
 3import com.fasterxml.jackson.core.JsonParser;
 4import com.fasterxml.jackson.core.JsonProcessingException;
 5import com.fasterxml.jackson.databind.DeserializationContext;
 6import com.fasterxml.jackson.databind.deser.std.StdDeserializer;
 7import org.joda.money.CurrencyUnit;
 8import org.joda.money.Money;
 9import org.springframework.boot.jackson.JsonComponent;
10
11import java.io.IOException;
12
13@JsonComponent
14public class MoneyDeserializer extends StdDeserializer<Money> {
15    protected MoneyDeserializer() {
16        super(Money.class);
17    }
18
19    @Override
20    public Money deserialize(JsonParser p, DeserializationContext ctxt) throws IOException, JsonProcessingException {
21        return Money.of(CurrencyUnit.of("CNY"), p.getDecimalValue());
22    }
23}

启动类 WaiterServiceApplication

 1package geektime.spring.springbucks.waiter;
 2
 3import com.fasterxml.jackson.datatype.hibernate5.Hibernate5Module;
 4import org.springframework.boot.SpringApplication;
 5import org.springframework.boot.autoconfigure.SpringBootApplication;
 6import org.springframework.boot.autoconfigure.jackson.Jackson2ObjectMapperBuilderCustomizer;
 7import org.springframework.cache.annotation.EnableCaching;
 8import org.springframework.context.annotation.Bean;
 9
10import java.util.TimeZone;
11
12@SpringBootApplication
13@EnableCaching
14public class WaiterServiceApplication {
15
16	public static void main(String[] args) {
17		SpringApplication.run(WaiterServiceApplication.class, args);
18	}
19	@Bean
20	public Hibernate5Module hibernate5Module() {
21		return new Hibernate5Module();
22	}
23
24	@Bean
25	public Jackson2ObjectMapperBuilderCustomizer jacksonBuilderCustomizer() {
26		return builder -> {
27			builder.indentOutput(true);
28			builder.timeZone(TimeZone.getTimeZone("Asia/Shanghai"));
29		};
30	}
31}

测试 (根据超媒体目录做访问和搜索)

  1// http://localhost:8080/
  2
  3{
  4  "_links": {
  5    "coffeeOrders": {
  6      "href": "http://localhost:8080/coffeeOrders{?page,size,sort}",
  7      "templated": true
  8    },
  9    "coffees": {
 10      "href": "http://localhost:8080/coffee{?page,size,sort}",
 11      "templated": true
 12    },
 13    "profile": {
 14      "href": "http://localhost:8080/profile"
 15    }
 16  }
 17}
 18
 19
 20// http://localhost:8080/coffee
 21
 22{
 23  "_embedded": {
 24    "coffees": [
 25      {
 26        "createTime": "2020-01-28T13:53:46.415+0800",
 27        "updateTime": "2020-01-28T13:53:46.415+0800",
 28        "name": "espresso",
 29        "price": 20.00,
 30        "_links": {
 31          "self": {
 32            "href": "http://localhost:8080/coffee/1"
 33          },
 34          "coffee": {
 35            "href": "http://localhost:8080/coffee/1"
 36          }
 37        }
 38      },
 39      {
 40        "createTime": "2020-01-28T13:53:46.417+0800",
 41        "updateTime": "2020-01-28T13:53:46.417+0800",
 42        "name": "latte",
 43        "price": 25.00,
 44        "_links": {
 45          "self": {
 46            "href": "http://localhost:8080/coffee/2"
 47          },
 48          "coffee": {
 49            "href": "http://localhost:8080/coffee/2"
 50          }
 51        }
 52      },
 53      {
 54        "createTime": "2020-01-28T13:53:46.417+0800",
 55        "updateTime": "2020-01-28T13:53:46.417+0800",
 56        "name": "capuccino",
 57        "price": 25.00,
 58        "_links": {
 59          "self": {
 60            "href": "http://localhost:8080/coffee/3"
 61          },
 62          "coffee": {
 63            "href": "http://localhost:8080/coffee/3"
 64          }
 65        }
 66      },
 67      {
 68        "createTime": "2020-01-28T13:53:46.418+0800",
 69        "updateTime": "2020-01-28T13:53:46.418+0800",
 70        "name": "mocha",
 71        "price": 30.00,
 72        "_links": {
 73          "self": {
 74            "href": "http://localhost:8080/coffee/4"
 75          },
 76          "coffee": {
 77            "href": "http://localhost:8080/coffee/4"
 78          }
 79        }
 80      },
 81      {
 82        "createTime": "2020-01-28T13:53:46.418+0800",
 83        "updateTime": "2020-01-28T13:53:46.418+0800",
 84        "name": "macchiato",
 85        "price": 30.00,
 86        "_links": {
 87          "self": {
 88            "href": "http://localhost:8080/coffee/5"
 89          },
 90          "coffee": {
 91            "href": "http://localhost:8080/coffee/5"
 92          }
 93        }
 94      }
 95    ]
 96  },
 97  "_links": {
 98    "self": {
 99      "href": "http://localhost:8080/coffee{?page,size,sort}",
100      "templated": true
101    },
102    "profile": {
103      "href": "http://localhost:8080/profile/coffee"
104    },
105    "search": {
106      "href": "http://localhost:8080/coffee/search"
107    }
108  },
109  "page": {
110    "size": 20,
111    "totalElements": 5,
112    "totalPages": 1,
113    "number": 0
114  }
115}
116
117// http://localhost:8080/coffee?page=0&size=3&sort=id,desc
118
119{
120  "_embedded": {
121    "coffees": [
122      {
123        "createTime": "2020-01-28T13:53:46.418+0800",
124        "updateTime": "2020-01-28T13:53:46.418+0800",
125        "name": "macchiato",
126        "price": 30.00,
127        "_links": {
128          "self": {
129            "href": "http://localhost:8080/coffee/5"
130          },
131          "coffee": {
132            "href": "http://localhost:8080/coffee/5"
133          }
134        }
135      },
136      {
137        "createTime": "2020-01-28T13:53:46.418+0800",
138        "updateTime": "2020-01-28T13:53:46.418+0800",
139        "name": "mocha",
140        "price": 30.00,
141        "_links": {
142          "self": {
143            "href": "http://localhost:8080/coffee/4"
144          },
145          "coffee": {
146            "href": "http://localhost:8080/coffee/4"
147          }
148        }
149      },
150      {
151        "createTime": "2020-01-28T13:53:46.417+0800",
152        "updateTime": "2020-01-28T13:53:46.417+0800",
153        "name": "capuccino",
154        "price": 25.00,
155        "_links": {
156          "self": {
157            "href": "http://localhost:8080/coffee/3"
158          },
159          "coffee": {
160            "href": "http://localhost:8080/coffee/3"
161          }
162        }
163      }
164    ]
165  },
166  "_links": {
167    "first": {
168      "href": "http://localhost:8080/coffee?page=0&size=3&sort=id,desc"
169    },
170    "self": {
171      "href": "http://localhost:8080/coffee"
172    },
173    "next": {
174      "href": "http://localhost:8080/coffee?page=1&size=3&sort=id,desc"
175    },
176    "last": {
177      "href": "http://localhost:8080/coffee?page=1&size=3&sort=id,desc"
178    },
179    "profile": {
180      "href": "http://localhost:8080/profile/coffee"
181    },
182    "search": {
183      "href": "http://localhost:8080/coffee/search"
184    }
185  },
186  "page": {
187    "size": 3,
188    "totalElements": 5,
189    "totalPages": 2,
190    "number": 0
191  }
192}
193
194
195// http://localhost:8080/coffee/search
196
197{
198  "_links": {
199    "findByNameInOrderById": {
200      "href": "http://localhost:8080/coffee/search/findByNameInOrderById{?list}",
201      "templated": true
202    },
203    "findByName": {
204      "href": "http://localhost:8080/coffee/search/findByName{?name}",
205      "templated": true
206    },
207    "self": {
208      "href": "http://localhost:8080/coffee/search"
209    }
210  }
211}
212
213// http://localhost:8080/coffee?page=0&size=3&sort=id,desc
214
215{
216  "_embedded": {
217    "coffees": [
218      {
219        "createTime": "2020-01-28T13:53:46.418+0800",
220        "updateTime": "2020-01-28T13:53:46.418+0800",
221        "name": "macchiato",
222        "price": 30.00,
223        "_links": {
224          "self": {
225            "href": "http://localhost:8080/coffee/5"
226          },
227          "coffee": {
228            "href": "http://localhost:8080/coffee/5"
229          }
230        }
231      },
232      {
233        "createTime": "2020-01-28T13:53:46.418+0800",
234        "updateTime": "2020-01-28T13:53:46.418+0800",
235        "name": "mocha",
236        "price": 30.00,
237        "_links": {
238          "self": {
239            "href": "http://localhost:8080/coffee/4"
240          },
241          "coffee": {
242            "href": "http://localhost:8080/coffee/4"
243          }
244        }
245      },
246      {
247        "createTime": "2020-01-28T13:53:46.417+0800",
248        "updateTime": "2020-01-28T13:53:46.417+0800",
249        "name": "capuccino",
250        "price": 25.00,
251        "_links": {
252          "self": {
253            "href": "http://localhost:8080/coffee/3"
254          },
255          "coffee": {
256            "href": "http://localhost:8080/coffee/3"
257          }
258        }
259      }
260    ]
261  },
262  "_links": {
263    "first": {
264      "href": "http://localhost:8080/coffee?page=0&size=3&sort=id,desc"
265    },
266    "self": {
267      "href": "http://localhost:8080/coffee"
268    },
269    "next": {
270      "href": "http://localhost:8080/coffee?page=1&size=3&sort=id,desc"
271    },
272    "last": {
273      "href": "http://localhost:8080/coffee?page=1&size=3&sort=id,desc"
274    },
275    "profile": {
276      "href": "http://localhost:8080/profile/coffee"
277    },
278    "search": {
279      "href": "http://localhost:8080/coffee/search"
280    }
281  },
282  "page": {
283    "size": 3,
284    "totalElements": 5,
285    "totalPages": 2,
286    "number": 0
287  }
288}

如何访问 HATEOAS 服务

配置 Jackson JSON

  • 注册 HAL 支持

操作超链接

  • 找到需要的 Link
  • 访问超链接

编程方式(客户端访问 Rest 服务资源)
引入依赖

 1<?xml version="1.0" encoding="UTF-8"?>
 2<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
 3	xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
 4	<modelVersion>4.0.0</modelVersion>
 5	<parent>
 6		<groupId>org.springframework.boot</groupId>
 7		<artifactId>spring-boot-starter-parent</artifactId>
 8		<version>2.1.3.RELEASE</version>
 9		<relativePath/> <!-- lookup parent from repository -->
10	</parent>
11	<groupId>geektime.spring.springbucks</groupId>
12	<artifactId>customer-service</artifactId>
13	<version>0.0.1-SNAPSHOT</version>
14	<name>customer-service</name>
15	<description>Demo project for Spring Boot</description>
16
17	<properties>
18		<java.version>1.8</java.version>
19	</properties>
20
21	<dependencies>
22		<dependency>
23			<groupId>org.springframework.boot</groupId>
24			<artifactId>spring-boot-starter-web</artifactId>
25		</dependency>
26		<dependency>
27			<groupId>org.springframework.boot</groupId>
28			<artifactId>spring-boot-starter-data-rest</artifactId>
29		</dependency>
30
31		<dependency>
32			<groupId>org.joda</groupId>
33			<artifactId>joda-money</artifactId>
34			<version>1.0.1</version>
35		</dependency>
36
37		<dependency>
38			<groupId>org.apache.commons</groupId>
39			<artifactId>commons-lang3</artifactId>
40		</dependency>
41
42		<dependency>
43			<groupId>org.apache.httpcomponents</groupId>
44			<artifactId>httpclient</artifactId>
45			<version>4.5.7</version>
46		</dependency>
47
48		<dependency>
49			<groupId>org.projectlombok</groupId>
50			<artifactId>lombok</artifactId>
51			<optional>true</optional>
52		</dependency>
53		<dependency>
54			<groupId>org.springframework.boot</groupId>
55			<artifactId>spring-boot-starter-test</artifactId>
56			<scope>test</scope>
57		</dependency>
58	</dependencies>
59
60	<build>
61		<plugins>
62			<plugin>
63				<groupId>org.springframework.boot</groupId>
64				<artifactId>spring-boot-maven-plugin</artifactId>
65			</plugin>
66		</plugins>
67	</build>
68</project>

CustomerServiceApplication

 1package geektime.spring.springbucks.customer;
 2
 3import geektime.spring.springbucks.customer.support.CustomConnectionKeepAliveStrategy;
 4import lombok.extern.slf4j.Slf4j;
 5import org.apache.http.impl.client.CloseableHttpClient;
 6import org.apache.http.impl.client.HttpClients;
 7import org.apache.http.impl.conn.PoolingHttpClientConnectionManager;
 8import org.springframework.boot.Banner;
 9import org.springframework.boot.WebApplicationType;
10import org.springframework.boot.autoconfigure.SpringBootApplication;
11import org.springframework.boot.builder.SpringApplicationBuilder;
12import org.springframework.boot.web.client.RestTemplateBuilder;
13import org.springframework.context.annotation.Bean;
14import org.springframework.hateoas.hal.Jackson2HalModule;
15import org.springframework.http.client.HttpComponentsClientHttpRequestFactory;
16import org.springframework.web.client.RestTemplate;
17
18import java.time.Duration;
19import java.util.concurrent.TimeUnit;
20
21@SpringBootApplication
22@Slf4j
23public class CustomerServiceApplication {
24
25	public static void main(String[] args) {
26		new SpringApplicationBuilder()
27				.sources(CustomerServiceApplication.class)
28				.bannerMode(Banner.Mode.OFF)
29				.web(WebApplicationType.NONE)
30				.run(args);
31	}
32
33	@Bean
34	public Jackson2HalModule jackson2HalModule() {
35		return new Jackson2HalModule();
36	}
37
38	@Bean
39	public HttpComponentsClientHttpRequestFactory requestFactory() {
40		PoolingHttpClientConnectionManager connectionManager =
41				new PoolingHttpClientConnectionManager(30, TimeUnit.SECONDS);
42		connectionManager.setMaxTotal(200);
43		connectionManager.setDefaultMaxPerRoute(20);
44
45		CloseableHttpClient httpClient = HttpClients.custom()
46				.setConnectionManager(connectionManager)
47				.evictIdleConnections(30, TimeUnit.SECONDS)
48				.disableAutomaticRetries()
49				// 有 Keep-Alive 认里面的值,没有的话永久有效
50				//.setKeepAliveStrategy(DefaultConnectionKeepAliveStrategy.INSTANCE)
51				// 换成自定义的
52				.setKeepAliveStrategy(new CustomConnectionKeepAliveStrategy())
53				.build();
54
55		HttpComponentsClientHttpRequestFactory requestFactory =
56				new HttpComponentsClientHttpRequestFactory(httpClient);
57
58		return requestFactory;
59	}
60
61	@Bean
62	public RestTemplate restTemplate(RestTemplateBuilder builder) {
63		return builder
64				.setConnectTimeout(Duration.ofMillis(100))
65				.setReadTimeout(Duration.ofMillis(500))
66				.requestFactory(this::requestFactory)
67				.build();
68	}
69}

CustomerRunner

  1package geektime.spring.springbucks.customer;
  2
  3import geektime.spring.springbucks.customer.model.Coffee;
  4import geektime.spring.springbucks.customer.model.CoffeeOrder;
  5import geektime.spring.springbucks.customer.model.OrderState;
  6import lombok.extern.slf4j.Slf4j;
  7import org.joda.money.CurrencyUnit;
  8import org.joda.money.Money;
  9import org.springframework.beans.factory.annotation.Autowired;
 10import org.springframework.boot.ApplicationArguments;
 11import org.springframework.boot.ApplicationRunner;
 12import org.springframework.core.ParameterizedTypeReference;
 13import org.springframework.hateoas.Link;
 14import org.springframework.hateoas.PagedResources;
 15import org.springframework.hateoas.Resource;
 16import org.springframework.hateoas.Resources;
 17import org.springframework.http.HttpMethod;
 18import org.springframework.http.RequestEntity;
 19import org.springframework.http.ResponseEntity;
 20import org.springframework.stereotype.Component;
 21import org.springframework.web.client.RestTemplate;
 22
 23import java.net.URI;
 24import java.util.Collections;
 25
 26@Component
 27@Slf4j
 28public class CustomerRunner implements ApplicationRunner {
 29    private static final URI ROOT_URI = URI.create("http://localhost:8080/");
 30    @Autowired
 31    private RestTemplate restTemplate;
 32
 33    @Override
 34    public void run(ApplicationArguments args) throws Exception {
 35        Link coffeeLink = getLink(ROOT_URI, "coffees");
 36        //Link: <http://localhost:8080/coffee{?page,size,sort}>;rel="coffees"
 37
 38        readCoffeeMenu(coffeeLink);
 39        //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"] }
 40
 41        Resource<Coffee> americano = addCoffee(coffeeLink);
 42        //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"]>
 43
 44        Link orderLink = getLink(ROOT_URI, "coffeeOrders");
 45        //Link: <http://localhost:8080/coffeeOrders{?page,size,sort}>;rel="coffeeOrders"
 46
 47        addOrder(orderLink, americano);
 48        //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"]>
 49        //add Order Items Response: <204,[Date:"Tue, 28 Jan 2020 07:38:50 GMT"]>
 50        
 51        queryOrders(orderLink);
 52        //query Order Response: <200,{....}
 53    }
 54
 55    private Link getLink(URI uri, String rel) {
 56        ResponseEntity<Resources<Link>> rootResp =
 57                restTemplate.exchange(uri, HttpMethod.GET, null,
 58                        new ParameterizedTypeReference<Resources<Link>>() {});
 59        Link link = rootResp.getBody().getLink(rel);
 60        log.info("Link: {}", link);
 61        return link;
 62    }
 63
 64    private void readCoffeeMenu(Link coffeeLink) {
 65        ResponseEntity<PagedResources<Resource<Coffee>>> coffeeResp =
 66                restTemplate.exchange(coffeeLink.getTemplate().expand(),
 67                        HttpMethod.GET, null,
 68                        new ParameterizedTypeReference<PagedResources<Resource<Coffee>>>() {});
 69        log.info("Menu Response: {}", coffeeResp.getBody());
 70    }
 71
 72    private Resource<Coffee> addCoffee(Link link) {
 73        Coffee americano = Coffee.builder()
 74                .name("americano")
 75                .price(Money.of(CurrencyUnit.of("CNY"), 25.0))
 76                .build();
 77        RequestEntity<Coffee> req =
 78                RequestEntity.post(link.getTemplate().expand()).body(americano);
 79        ResponseEntity<Resource<Coffee>> resp =
 80                restTemplate.exchange(req,
 81                        new ParameterizedTypeReference<Resource<Coffee>>() {});
 82        log.info("add Coffee Response: {}", resp);
 83        return resp.getBody();
 84    }
 85
 86    private void addOrder(Link link, Resource<Coffee> coffee) {
 87        CoffeeOrder newOrder = CoffeeOrder.builder()
 88                .customer("Li Lei")
 89                .state(OrderState.INIT)
 90                .build();
 91        RequestEntity<?> req =
 92                RequestEntity.post(link.getTemplate().expand()).body(newOrder);
 93        ResponseEntity<Resource<CoffeeOrder>> resp =
 94                restTemplate.exchange(req,
 95                        new ParameterizedTypeReference<Resource<CoffeeOrder>>() {});
 96        log.info("add Order Response: {}", resp);
 97
 98        Resource<CoffeeOrder> order = resp.getBody();
 99        Link items = order.getLink("items");
100        req = RequestEntity.post(items.getTemplate().expand()).body(Collections.singletonMap("_links", coffee.getLink("self")));
101        ResponseEntity<String> itemResp = restTemplate.exchange(req, String.class);
102        log.info("add Order Items Response: {}", itemResp);
103    }
104
105    private void queryOrders(Link link) {
106        ResponseEntity<String> resp = restTemplate.getForEntity(link.getTemplate().expand(), String.class);
107        log.info("query Order Response: {}", resp);
108    }
109}

作者:Soulboy