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 依赖
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}