RESTful protobuf 定义的服务合同
RESTful service contracts for protobuf definitions
设计概述
- 请求和响应对象在 protobuf 中建模。
- 类 使用协议在 Python 和 Java 中生成。
- 用户在 Python 中创建请求对象并将其发送到 RESTful Java Spring 引导微服务。
- JavaScript React Web 应用程序和节点服务器也调用 RESTful 端点。
- 请求和响应被序列化为 Json。
- 1+ Java 微服务可能使用相同的 request/response 对象。例如。 aggregator/API 网关微服务会将请求传递给实际的微服务,提供相关服务。
问题
Protobuf 对 request/response 对象强制执行某种级别的类型检查和一种契约。但是,我如何开发、维护和执行 RESTful 合同(HTTP 动词 + 路径 + 请求 + 响应)?
这是要走的路吗?
在 Spring Cloud Contract 中开发合同并自动生成集成合同测试。
您可以在此处 https://github.com/spring-cloud-samples/spring-cloud-contract-samples/tree/master/producer_proto and on the consumer side here https://github.com/spring-cloud-samples/spring-cloud-contract-samples/tree/master/consumer_proto
查看在生产者端使用带有 spring 云合同的原型缓冲区的示例
这个想法就是将内容视为二进制文件。假设我将请求和响应以二进制格式存储在 .bin 文件中。然后我可以创建以下合约
package contracts.beer.rest
import org.springframework.cloud.contract.spec.Contract
Contract.make {
description("""
Represents a successful scenario of getting a beer
```
given:
client is old enough
when:
he applies for a beer
then:
we'll grant him the beer
```
""")
request {
method 'POST'
url '/check'
body(fileAsBytes("PersonToCheck_old_enough.bin"))
headers {
contentType("application/x-protobuf")
}
}
response {
status 200
body(fileAsBytes("Response_old_enough.bin"))
headers {
contentType("application/x-protobuf")
}
}
}
拥有这样的控制器
@RestController
public class ProducerController {
private final PersonCheckingService personCheckingService;
public ProducerController(PersonCheckingService personCheckingService) {
this.personCheckingService = personCheckingService;
}
@RequestMapping(value = "/check",
method=RequestMethod.POST,
consumes="application/x-protobuf",
produces="application/x-protobuf")
public Beer.Response check(@RequestBody Beer.PersonToCheck personToCheck) {
//remove::start[]
if (this.personCheckingService.shouldGetBeer(personToCheck)) {
return Beer.Response.newBuilder().setStatus(Beer.Response.BeerCheckStatus.OK).build();
}
return Beer.Response.newBuilder().setStatus(Beer.Response.BeerCheckStatus.NOT_OK).build();
//remove::end[return]
}
}
interface PersonCheckingService {
Boolean shouldGetBeer(Beer.PersonToCheck personToCheck);
}
以及生成测试的基础 class(我假设您已经设置了合约插件)
package com.example;
//remove::start[]
import io.restassured.module.mockmvc.RestAssuredMockMvc;
//remove::end[]
import org.junit.Before;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
import org.springframework.test.context.junit4.SpringRunner;
import org.springframework.web.context.WebApplicationContext;
@RunWith(SpringRunner.class)
@SpringBootTest(classes = BeerRestBase.Config.class)
public abstract class BeerRestBase {
@Autowired
WebApplicationContext context;
//remove::start[]
@Before
public void setup() {
RestAssuredMockMvc.webAppContextSetup(this.context);
}
// remove::end[]
@Configuration
@EnableAutoConfiguration
@Import({ ProtoConfiguration.class, ProducerController.class })
static class Config {
@Bean
PersonCheckingService personCheckingService() {
return argument -> argument.getAge() >= 20;
}
}
}
将生成正确的测试和存根。查看上述示例以了解具体的实施细节。
在消费者方面,您可以获取存根并运行针对它们进行测试
package com.example;
import org.assertj.core.api.BDDAssertions;
import org.junit.Assume;
import org.junit.Before;
import org.junit.BeforeClass;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.context.SpringBootTest.WebEnvironment;
import org.springframework.cloud.contract.stubrunner.junit.StubRunnerRule;
import org.springframework.cloud.contract.stubrunner.spring.StubRunnerProperties;
import org.springframework.test.context.junit4.SpringRunner;
import org.springframework.util.StringUtils;
import org.springframework.web.client.RestTemplate;
/**
* @author Marcin Grzejszczak
*/
@RunWith(SpringRunner.class)
@SpringBootTest(webEnvironment = WebEnvironment.NONE)
public class ProtoTest {
@Autowired
RestTemplate restTemplate;
int port;
@Rule
public StubRunnerRule rule = new StubRunnerRule()
.downloadStub("com.example", "beer-api-producer-proto")
.stubsMode(StubRunnerProperties.StubsMode.LOCAL);
@Before
public void setupPort() {
this.port = this.rule.findStubUrl("beer-api-producer-proto").getPort();
}
@Test
public void should_give_me_a_beer_when_im_old_enough() throws Exception {
Beer.Response response = this.restTemplate.postForObject(
"http://localhost:" + this.port + "/check",
Beer.PersonToCheck.newBuilder().setAge(23).build(), Beer.Response.class);
BDDAssertions.then(response.getStatus()).isEqualTo(Beer.Response.BeerCheckStatus.OK);
}
@Test
public void should_reject_a_beer_when_im_too_young() throws Exception {
Beer.Response response = this.restTemplate.postForObject(
"http://localhost:" + this.port + "/check",
Beer.PersonToCheck.newBuilder().setAge(17).build(), Beer.Response.class);
response = response == null ? Beer.Response.newBuilder().build() : response;
BDDAssertions.then(response.getStatus()).isEqualTo(Beer.Response.BeerCheckStatus.NOT_OK);
}
}
同样,请检查具体示例以了解实施细节。
这是一个很好的问题,因为它代表了一种使用 Protobuf 合同管理 APIs 的现代方法。
我稍后会详细讨论 API 使用 Protobuf 进行管理,但直接回答您的问题 - 在尝试定义 REST 契约时,您将需要使用 Openapi 注释,然后生成 Openapi 定义文件。
service UserService {
rpc AddUser(AddUserRequest) returns (User) {
option (google.api.http) = {
// Route to this method from POST requests to /api/v1/users
post: "/api/v1/users"
body: "*"
};
option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = {
summary: "Add a user"
description: "Add a user to the server."
tags: "Users"
};
}
生成打开API 定义
protoc \
-I "$PROTO_ROOT" \
-I "$ROOT"/protos/thirdparty/grpc-gateway/ \
-I "$ROOT"/protos/thirdparty/googleapis \
--openapiv2_out="$ROOT/gen/swagger" \
"$proto"
在尝试创建专业的 API 管理时,您应该采取以下步骤:
- 使用 Protobuf IDL 语言创建服务定义
- 生成 Openapi 定义文件
- 从 Openapi 定义生成 HTTP 客户端
- 根据您的 Openapi 定义生成 Swagger UI 客户端
- 从 Protobuf 生成 gRPC 存根(如果需要 gRPC 支持)
- 添加 back-compatibility 检查新更改
- 添加样式代码检查以保证一致性
你可以在这个项目中看到上面提到的所有内容https://github.com/apssouza22/modern-api-management
设计概述
- 请求和响应对象在 protobuf 中建模。
- 类 使用协议在 Python 和 Java 中生成。
- 用户在 Python 中创建请求对象并将其发送到 RESTful Java Spring 引导微服务。
- JavaScript React Web 应用程序和节点服务器也调用 RESTful 端点。
- 请求和响应被序列化为 Json。
- 1+ Java 微服务可能使用相同的 request/response 对象。例如。 aggregator/API 网关微服务会将请求传递给实际的微服务,提供相关服务。
问题
Protobuf 对 request/response 对象强制执行某种级别的类型检查和一种契约。但是,我如何开发、维护和执行 RESTful 合同(HTTP 动词 + 路径 + 请求 + 响应)?
这是要走的路吗?
在 Spring Cloud Contract 中开发合同并自动生成集成合同测试。
您可以在此处 https://github.com/spring-cloud-samples/spring-cloud-contract-samples/tree/master/producer_proto and on the consumer side here https://github.com/spring-cloud-samples/spring-cloud-contract-samples/tree/master/consumer_proto
查看在生产者端使用带有 spring 云合同的原型缓冲区的示例这个想法就是将内容视为二进制文件。假设我将请求和响应以二进制格式存储在 .bin 文件中。然后我可以创建以下合约
package contracts.beer.rest
import org.springframework.cloud.contract.spec.Contract
Contract.make {
description("""
Represents a successful scenario of getting a beer
```
given:
client is old enough
when:
he applies for a beer
then:
we'll grant him the beer
```
""")
request {
method 'POST'
url '/check'
body(fileAsBytes("PersonToCheck_old_enough.bin"))
headers {
contentType("application/x-protobuf")
}
}
response {
status 200
body(fileAsBytes("Response_old_enough.bin"))
headers {
contentType("application/x-protobuf")
}
}
}
拥有这样的控制器
@RestController
public class ProducerController {
private final PersonCheckingService personCheckingService;
public ProducerController(PersonCheckingService personCheckingService) {
this.personCheckingService = personCheckingService;
}
@RequestMapping(value = "/check",
method=RequestMethod.POST,
consumes="application/x-protobuf",
produces="application/x-protobuf")
public Beer.Response check(@RequestBody Beer.PersonToCheck personToCheck) {
//remove::start[]
if (this.personCheckingService.shouldGetBeer(personToCheck)) {
return Beer.Response.newBuilder().setStatus(Beer.Response.BeerCheckStatus.OK).build();
}
return Beer.Response.newBuilder().setStatus(Beer.Response.BeerCheckStatus.NOT_OK).build();
//remove::end[return]
}
}
interface PersonCheckingService {
Boolean shouldGetBeer(Beer.PersonToCheck personToCheck);
}
以及生成测试的基础 class(我假设您已经设置了合约插件)
package com.example;
//remove::start[]
import io.restassured.module.mockmvc.RestAssuredMockMvc;
//remove::end[]
import org.junit.Before;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
import org.springframework.test.context.junit4.SpringRunner;
import org.springframework.web.context.WebApplicationContext;
@RunWith(SpringRunner.class)
@SpringBootTest(classes = BeerRestBase.Config.class)
public abstract class BeerRestBase {
@Autowired
WebApplicationContext context;
//remove::start[]
@Before
public void setup() {
RestAssuredMockMvc.webAppContextSetup(this.context);
}
// remove::end[]
@Configuration
@EnableAutoConfiguration
@Import({ ProtoConfiguration.class, ProducerController.class })
static class Config {
@Bean
PersonCheckingService personCheckingService() {
return argument -> argument.getAge() >= 20;
}
}
}
将生成正确的测试和存根。查看上述示例以了解具体的实施细节。
在消费者方面,您可以获取存根并运行针对它们进行测试
package com.example;
import org.assertj.core.api.BDDAssertions;
import org.junit.Assume;
import org.junit.Before;
import org.junit.BeforeClass;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.context.SpringBootTest.WebEnvironment;
import org.springframework.cloud.contract.stubrunner.junit.StubRunnerRule;
import org.springframework.cloud.contract.stubrunner.spring.StubRunnerProperties;
import org.springframework.test.context.junit4.SpringRunner;
import org.springframework.util.StringUtils;
import org.springframework.web.client.RestTemplate;
/**
* @author Marcin Grzejszczak
*/
@RunWith(SpringRunner.class)
@SpringBootTest(webEnvironment = WebEnvironment.NONE)
public class ProtoTest {
@Autowired
RestTemplate restTemplate;
int port;
@Rule
public StubRunnerRule rule = new StubRunnerRule()
.downloadStub("com.example", "beer-api-producer-proto")
.stubsMode(StubRunnerProperties.StubsMode.LOCAL);
@Before
public void setupPort() {
this.port = this.rule.findStubUrl("beer-api-producer-proto").getPort();
}
@Test
public void should_give_me_a_beer_when_im_old_enough() throws Exception {
Beer.Response response = this.restTemplate.postForObject(
"http://localhost:" + this.port + "/check",
Beer.PersonToCheck.newBuilder().setAge(23).build(), Beer.Response.class);
BDDAssertions.then(response.getStatus()).isEqualTo(Beer.Response.BeerCheckStatus.OK);
}
@Test
public void should_reject_a_beer_when_im_too_young() throws Exception {
Beer.Response response = this.restTemplate.postForObject(
"http://localhost:" + this.port + "/check",
Beer.PersonToCheck.newBuilder().setAge(17).build(), Beer.Response.class);
response = response == null ? Beer.Response.newBuilder().build() : response;
BDDAssertions.then(response.getStatus()).isEqualTo(Beer.Response.BeerCheckStatus.NOT_OK);
}
}
同样,请检查具体示例以了解实施细节。
这是一个很好的问题,因为它代表了一种使用 Protobuf 合同管理 APIs 的现代方法。
我稍后会详细讨论 API 使用 Protobuf 进行管理,但直接回答您的问题 - 在尝试定义 REST 契约时,您将需要使用 Openapi 注释,然后生成 Openapi 定义文件。
service UserService {
rpc AddUser(AddUserRequest) returns (User) {
option (google.api.http) = {
// Route to this method from POST requests to /api/v1/users
post: "/api/v1/users"
body: "*"
};
option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = {
summary: "Add a user"
description: "Add a user to the server."
tags: "Users"
};
}
生成打开API 定义
protoc \
-I "$PROTO_ROOT" \
-I "$ROOT"/protos/thirdparty/grpc-gateway/ \
-I "$ROOT"/protos/thirdparty/googleapis \
--openapiv2_out="$ROOT/gen/swagger" \
"$proto"
在尝试创建专业的 API 管理时,您应该采取以下步骤:
- 使用 Protobuf IDL 语言创建服务定义
- 生成 Openapi 定义文件
- 从 Openapi 定义生成 HTTP 客户端
- 根据您的 Openapi 定义生成 Swagger UI 客户端
- 从 Protobuf 生成 gRPC 存根(如果需要 gRPC 支持)
- 添加 back-compatibility 检查新更改
- 添加样式代码检查以保证一致性
你可以在这个项目中看到上面提到的所有内容https://github.com/apssouza22/modern-api-management