目录

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

<?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);
    }
}

作者:Soulboy