RESTful protobuf 定义的服务合同

RESTful service contracts for protobuf definitions

设计概述

问题

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