首页 > 解决方案 > Spring Boot 微服务中的客户端库

问题描述

三年前,我作为开发人员参与了我的第一个微服务项目。我对微服务概念一无所知。该项目正在构建为 Spring Boot 微服务。一般来说,没有什么特别的,但所有项目都采用了颇具争议的基于客户端库的微服务之间的集成方式。我认为那些客户端库是用天真的方式制作的。我会尝试给出他们的主要想法。

项目中有三个模块*-api*-client*-impl。这*-impl是一个成熟的 REST 服务,并且*-client是这个 REST 服务的客户端库。*-impl*-client模块依赖于(它们作为 Maven 依赖项*-api导入)。*-api反过来包含 Java 接口,这些*-api接口应该由模块中的@RestController*-impl和实现此 REST 服务的客户端库功能的类(通过RestTemplateFeignClient)实现。通常还包含Bean ValidationSwagger*-api可能涵盖的 DTO注释。在某些情况下,这些接口可能包含来自 Spring-MVC 的@RequestMapping注解。因此,@RestControllerFeignClient的实现同时继承了@RequestMapping

*-api

@ApiModel
class DTO {
  @NotNull
  private String field;
  // getters & setters
}

interface Api {
  @RequestMapping("/api")
  void method(DTO dto)
}

*-客户

@FeignClient("api")
interface Client extends Api {
  // void method(DTO) is inherited and implemented at runtime by Spring Cloud Feign
}

*-impl

@RestController
class ApiImpl implements Api {
  void method(@Validated DTO dto) {
    // implementation
  }
}

不难猜测其他一些微服务是否会拉取*-client依赖项,它可能会在其类路径中获得不可预测的传递依赖项。微服务之间也出现了紧密耦合。

我决定花一些时间研究这个问题并发现一些概念。首先,我从 Sam Newman 著名的《构建微服务》一书(“客户端库”一章)中了解了像这样的广泛观点。此外,我还了解了Consumer Driven Contracts及其实现 - PactSpring Cloud Contract。我决定是否要使用 Spring Boot 微服务开始一个新项目,我会尽量不制作客户端库和耦合微服务。因此我希望达到最小的耦合。Consumer Driven Contracts

在那个项目之后,我参与了另一个项目,它的构建方式几乎与第一个关于客户端库的项目相同。我试图与一个团队分享我的研究,但没有得到任何反馈,所有团队都继续制作客户端库。几个月后,我离开了项目。

最近,我成为了我的第三个微服务项目的开发人员,该项目也使用了 Spring Boot。而且我面临着与前两个项目一样的客户端库使用方式。在那里我也没有得到任何关于Consumer Driven Contracts使用的反馈。

我想知道社区的意见。您在项目中使用哪种方式?上述使用客户端库的方式是否合理?

附录1。

@JRichardsz 的问题:

  1. 你说的客户是什么意思?REST API 的客户端是 API 所有者提供的一种 sdk,允许客户端以简单的方式使用它,而不是 http 低级实现。
  2. 集成是什么意思?测试集成是您需要的吗?
  3. 我认为您的要求与如何在多个 api 之间组织源代码有关。这是对的吗?

答案:

  1. 这里我只考虑 Spring/Spring Cloud。如果我使用 Spring Boot 构建一个微服务,并且我想与另一个(微)服务交互/集成(这就是我所说的“集成”),我可以使用RestTemplate(它是一种客户端库,不是吗? )。如果我要使用 Spring Boot + Spring Cloud 构建微服务,我可以使用 Spring Cloud OpenFeign与另一个(微)服务进行交互(或集成)。我认为Spring Cloud OpenFeign也是一种客户端库,不是吗?在我的一般问题中,我谈到了由我工作的团队创建的自定义客户端库。例如有两个项目:microserviceA 和 microserviceB。这些项目中的每一个都包含三个 Maven 模块*-api*-client*-impl. 暗示*-clientmaven 模块包含*-apimaven 模块。maven 模块也*-api用作 maven 模块中的依赖*-impl项。当 microserviceA(microserviceA-implmaven 模块)想要与 microserviceB 交互时,它将导入microserviceB-clientmaven 模块。因此 microserviceA 和 microserviceB 是紧密耦合的。

  2. 我所说的集成是指微服务之间的交互。例如,microserviceA 与 microserviceB 交互/集成。

  3. 我的观点认为 microserviceA 和 microserviceB 不能有共同的源代码(通过客户端库)。这就是我问这些问题的原因:

您在项目中使用哪种方式?上述使用客户端库的方式是否合理?

附录 2。

我将尝试详细解释并举例说明。

介绍。

当我参与构建为微服务的项目时,他们使用相同的方式来实现微服务之间的交互,即“客户端库”。它们不是封装低级 http 交互、将 http 主体(等等)序列化/反序列化为 or 的客户端RestTemplateFeighClient。它们是自定义客户端库,其唯一目的是与唯一的微服务进行交互(请求/响应)。例如,有一些microservice-b提供一些microservice-b-client.jar(它是一个自定义客户端库)并且microservice-a应该使用它jar来与microservice-b. 它与RPC实现非常相似。

例子。

微服务-b项目

微服务-b-api maven 模块

pom.xml:

<artifactId>microservice-b-api</artifactId>

<dependencies>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    <dependency>
        <groupId>io.springfox</groupId>
        <artifactId>springfox-swagger2</artifactId>
        <version>2.9.2</version>
    </dependency>
    <dependency>
        <groupId>javax.validation</groupId>
        <artifactId>validation-api</artifactId>
    </dependency>
    <dependency>
        <groupId>org.projectlombok</groupId>
        <artifactId>lombok</artifactId>
        <optional>true</optional>
    </dependency>
</dependencies>

HelloController 接口:

@Api("Hello API")
@RequestMapping("/hello")
public interface HelloController {
    @PostMapping
    HelloResponse hello(@RequestBody HelloRequest request);
}

HelloRequest dto:

@Getter
@Setter
@ApiModel("request model")
public class HelloRequest {
    @NotNull
    @ApiModelProperty("name property")
    private String name;
}

HelloResponse dto:

@Getter
@Setter
@ApiModel("response model")
public class HelloResponse {
    @ApiModelProperty("greeting property")
    private String greeting;
}

microservice-b-client maven 模块

pom.xml:

<artifactId>microservice-b-client</artifactId>

<dependencies>
    <dependency>
        <groupId>my.rinat</groupId>
        <artifactId>microservice-b-api</artifactId>
        <version>0.0</version>
    </dependency>
    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-starter-openfeign</artifactId>
    </dependency>
</dependencies>

HelloClient 接口:

@FeignClient(value = "hello", url = "http://localhost:8181")
public interface HelloClient extends HelloController {
}

microservice-b-impl maven 模块

pom.xml:

<artifactId>microservice-b-impl</artifactId>

<dependencies>
    <dependency>
        <groupId>my.rinat</groupId>
        <artifactId>microservice-b-client</artifactId>
        <version>0.0</version>
    </dependency>
</dependencies>

微服务B类:

@EnableFeignClients
@EnableSwagger2
@SpringBootApplication
public class MicroserviceB {
    public static void main(String[] args) {
        SpringApplication.run(MicroserviceB.class, args);
    }
}

HelloControllerImpl 类:

@RestController
public class HelloControllerImpl implements HelloController {
    @Override
    public HelloResponse hello(HelloRequest request) {
        var hello = new HelloResponse();
        hello.setGreeting("Hello " + request.getName());
        return hello;
    }
}

应用程序.yml:

server:
  port: 8181

微服务-一个项目

pom.xml:

<artifactId>microservice-a</artifactId>

<dependencies>
    <dependency>
        <groupId>my.rinat</groupId>
        <artifactId>microservice-b-client</artifactId>
        <version>0.0</version>
    </dependency>
    <dependency>
        <groupId>org.projectlombok</groupId>
        <artifactId>lombok</artifactId>
    </dependency>
</dependencies>

微服务类:

@Slf4j
@EnableFeignClients(basePackageClasses = HelloClient.class)
@SpringBootApplication
public class MicroserviceA {

    public static void main(String[] args) {
        SpringApplication.run(MicroserviceA.class, args);
    }

    @Bean
    CommandLineRunner hello(HelloClient client) {
        return args -> {
            var request = new HelloRequest();
            request.setName("StackOverflow");
            var response = client.hello(request);
            log.info(response.getGreeting());
        };
    }
}

MicroserviceA 运行的结果:

2020-01-02 10:06:20.623  INFO 22288 --- [           main] com.example.microservicea.MicroserviceA  : Hello StackOverflow

在这里你可以看到完整的例子

问题。

我认为这种微服务之间的集成方式(通过自定义客户端库)是一种错误的方式。首先,微服务变得紧密耦合。其次 - 客户端库带来了不良的依赖关系。尽管有这些情况,我工作的团队还是使用了这种奇怪的方式来实现微服务之间的集成。我想知道这种方式是否可以使微服务的集成合理(正确)?在微服务之间进行集成的最佳实践是什么?

PS 在我看来,Spring Boot 微服务应该通过Consumer Driven ContractsSpring Cloud ContractPact)耦合,仅此而已。你觉得怎么走是对的?

标签: spring-bootmicroservicesspring-cloudclean-architectureclient-library

解决方案


这是构建数十个 API 并对其进行测试的策略。这很有效,我在工作中使用了它。

假设我在 acme.org 工作,需要开发两个 api:employee-api 和 customer-api。您可以使用-microservice代替-api后缀。

家长和图书馆

如果我要和我的团队一起开发几个 api 和应用程序,我们需要在我们的开发中重用代码,所以开始开发之前的第一个任务是创建我们的公共库和它们之间的关系

对于这项任务,我将向您推荐:

  • 使用 Maven 父母
  • 不断审查世界级库的 Java 代码,如:spring、mule esb、pentaho、apache、google/amazon sdks 等。他们有很好的方法来命名他们的类、库和关系船。例如这个策略:spring boot 的启动器

这里我的一些库是 maven 项目(maven parents(.pom) 和只是 libraries(.jar) :

  • 极致基础
    • 具有所有应用程序的 java 版本<artifactId>maven-compiler-plugin</artifactId>和其他一般属性(如project.build.sourceEncoding等)的父项目
    • acme.org 中的任何 java 项目都必须使用这个父项。用几个 java 版本管理几个 api 会很痛苦:s
  • 极致基础弹簧
    • 具有主要 spring 类和版本的父项目:spring-web、spring-core、spring-context
    • 并非 acme.org 中的所有开发都将成为 api 休息。可能是库,时间表或演示等。所以如果他们需要一些弹簧库,他们必须使用这个父
  • acme-base-spring-boot-api
    • 具有一般 Spring Boot 配置和启动器的父项目。
    • spring boot,全部减少,但如果您有多个应用程序,我们可以进一步减少它们。
    • 这个项目必须spring-boot-starter-parent作为父级和acme-base-spring作为超级 pom。
    • 这个项目有这个构建配置和依赖 spring-boot-maven-plugin、spring-boot-starter-actuator、spring-boot-starter-test、spring-boot-devtools、spring-boot-starter-web 和 spring-boot-启动器-tomcat
  • 极致基础 api
    • 这个父项目必须使用acme-base-spring-boot-api作为父项目。

有了这些父母,你的员工 api可以有一个最小的 pom,比如:

<project>
    <modelVersion>4.0.0</modelVersion>
    <groupId>org.acme.api</groupId>
    <artifactId>employees-api</artifactId>
    <version>1.0.0</version>
    <packaging>jar</packaging>

    <parent>
        <groupId>org.acme.base</groupId>
        <artifactId>acme-base-api</artifactId>
        <version>1.0.0</version>
    </parent>

</project>
  • 员工模型
    • 这个项目不是父项目。只是一个库(.jar)
    • 该项目的目标是将所有实体存储在 acme.org
    • 必须使用acme-base作为父级。
    • 如果您将使用 jpa 注释,请在此处添加 jpa 库。
  • 员工坚持
    • 使用员工模型作为依赖项的 java 库。
    • 可以存储 daos 或 jpa 存储库

有了这些父项和依赖项,您的员工 API可以有一个最小的 pom,例如:

<project>
    <modelVersion>4.0.0</modelVersion>
    <groupId>org.acme.api</groupId>
    <artifactId>employees-api</artifactId>
    <version>1.0.0</version>
    <packaging>jar</packaging>

    <parent>
        <groupId>org.acme.base</groupId>
        <artifactId>acme-base-api</artifactId>
        <version>1.0.0</version>
    </parent>

  <dependencies>
        <dependency>
            <groupId>org.acme.api.employee</groupId>
            <artifactId>employee-model</artifactId>
        </dependency>
        <dependency>
            <groupId>org.acme.api.employee</groupId>
            <artifactId>employee-persistent</artifactId>
        </dependency>
    </dependencies>  

</project>

然后customer-api 和employee-api 的代码都适用于它们,因此需要一个新库

  • 极致常见
    • 此项目必须用作customer-api、employee-api 和 acme.org 中的任何其他 api 中的依赖

这里列出了跨多个 REST API 的一些常用库:

  • 极致 API 日志
  • acme-api-审计
    • 在每个请求中添加唯一标识符,以便于跟踪或错误支持。
  • 极致 API 错误
  • 极致 API 健康
    • /health /status 等端点必须为任何 api 发布。

使用 Spring Cloud Contract 进行测试

如今,应用程序都经过了彻底的测试——无论是单元测试、集成测试还是端到端测试。在微服务架构中,一个服务(消费者)与另一个服务(生产者)通信以完成请求是很常见的。

为了测试它们,我们有两个选择:

  • #1 使用 Selenium 之类的库部署所有微服务并执行端到端测试:需要更多基础设施
  • #2 通过模拟对其他服务的调用来编写集成测试:模拟不会反映生产 api 中的变化

在 #2 方法中,我们的集成测试用例仍然可以正常工作,因为其他 api 被模拟了这个问题可能会在登台或生产环境中被注意到,而不是复杂的测试用例。

Spring Cloud Contract为我们提供了针对这些情况的 Spring Cloud Contract Verifier。它从生产者 (api) 创建一个存根 (.jar),消费者服务可以使用它来模拟调用。

因此,我们可以从生产者(api)下载存根来创建更多真实的模拟,而不是制作我们的本地模拟

推荐阅读:https ://stackabuse.com/spring-cloud-contract/


sdk 或 rest-client

正如前一点所说:在微服务架构中,一个服务(消费者)与另一个服务(生产者)通信以完成请求是很常见的。

这通常使用RestTemplate实现。

我还有一个策略:开发一种任何api rest提供的sdk。这个 sdk 包含使用 RestTemplate 的低级或复杂的 http 调用:http 方法、json 绑定、错误等

示例:如果employee-api 需要使用customer-api 的某个端点(/verify-existence),我们需要:

  • customer-api-sdk 或 customer-api-rest-client。该库具有使用 customer-api 所需的源代码。
  • employee-api 添加customer-api-sdk作为依赖项。而不是使用customer-api的http低级实现,只需要:
CustomerApiPassport passport = new CustomerApiPassport();
passport.setBaseUrl("http://customer-api.com");
passport.etc();

CustomerApiSecurity security = new CustomerApiSecurity();
security.setToken("");
security.setBasicAuthentication("user", "password");
security.etc();

CustomerApiSdk customerSdk = new CustomerApiSdk();
customerSdk.setPassport(passport);
customerSdk.setSecurity(security);

VerifyCustomerExistenceRequest request = new VerifyCustomerExistenceRequest();
request.setCustomerPersonId("215456");

//consume /verify-existence endpoint
VerifyCustomerExistenceResponse response = customerSdk.verifyCustomerExistence(request);
response.exist();
  • customer-api-sdk不仅适用于员工 API。可用于任何需要在客户 api 中消耗某些端点的 api
  • customer-api-sdk可用于模拟为Spring Cloud Contract生成的 jars
  • sdk 和 passport 的想法来自:google 和 amazon sdks(向其平台执行 http 请求)、为轴生成的 soap 客户端、jaxws 等

推荐阅读