【问题标题】:RESTful service contracts for protobuf definitions用于 protobuf 定义的 RESTful 服务契约
【发布时间】:2020-10-16 19:29:25
【问题描述】:

设计概述

  • 请求和响应对象在 protobuf 中建模。
  • 类是在 Python 和 Java 中使用 protoc 生成的。
  • 用户在 Python 中创建请求对象并将其发送到 RESTful Java Spring Boot 微服务。
  • JavaScript React Web 应用程序和 Node 服务器也调用 RESTful 端点。
  • 请求和响应被序列化为 Json。
  • 1+ Java 微服务可能使用相同的请求/响应对象。例如。聚合器/API 网关微服务会将请求传递给实际的微服务,提供相关服务。

问题

Protobuf 对请求/响应对象强制执行某种级别的类型检查和一种契约。但是,如何开发、维护和执行 RESTful 合约(HTTP 动词 + 路径 + 请求 + 响应)?

这是要走的路吗?

Spring Cloud Contract 中开发合同并自动生成集成合同测试。

【问题讨论】:

    标签: rest protocol-buffers spring-cloud-contract


    【解决方案1】:

    您可以在此处https://github.com/spring-cloud-samples/spring-cloud-contract-samples/tree/master/producer_proto 的生产者端和在消费者端https://github.com/spring-cloud-samples/spring-cloud-contract-samples/tree/master/consumer_proto 中查看使用带有 Spring Cloud 合约的 protobuffers 的示例

    这个想法是将内容视为二进制。假设我将请求和响应以二进制格式存储在 .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);
    }
    

    还有这样一个用于生成测试的基类(我假设你已经设置了合约插件)

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

    再次,请查看具体示例以了解实施细节。

    【讨论】:

    • 感谢您的快速响应,Marcin!您如何看待使用 protobuf 对请求和响应进行建模?我们的团队在这个技术堆栈方面没有太多经验,所以我想小心添加新东西而不增加太多价值。
    • 如果您没有经验,并且这项技术不能为您解决任何问题,请不要使用它
    • 能够用Python和Java生成代码对我们来说是一个重要的要求。我们也尝试了 Avro 和 Json Schema,但 Protobuf 在我们的用例中感觉更自然。这是标准方法吗?
    • 没有标准方法。你应该使用任何对你有用的东西来解决你的问题
    【解决方案2】:

    这是一个很好的问题,因为它代表了一种使用带有 Protobuf 的合同来管理 API 的现代方式。

    稍后我将更多地讨论使用 Protobuf 进行 API 管理,但直接针对您的问题 - 在尝试定义 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"
        };
      }
    

    生成 OpenAPI 定义

    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 支持)
    • 为新更改添加向后兼容性检查
    • 添加样式代码检查以保证一致性

    你可以在这个项目https://github.com/apssouza22/modern-api-management看到上面提到的所有内容

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2016-09-21
      • 1970-01-01
      • 2019-07-12
      • 2012-12-29
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多