Spring Boot 微服务中的客户端库

Posted

技术标签:

【中文标题】Spring Boot 微服务中的客户端库【英文标题】:Client libraries in Spring Boot microservices 【发布时间】:2020-04-16 15:24:13 【问题描述】:

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

项目中有三个模块:*-api*-client*-impl*-impl 是一个成熟的 REST 服务,*-client 是这个 REST 服务的客户端库。 *-impl*-client 模块依赖于 *-api(它们将 *-api 作为 maven 依赖项导入)。 *-api 又包含 Java 接口,这些接口应该由 *-impl 模块中的 @RestController 类和实现此 REST 服务的客户端库功能的类(通过 RestTemplate 或 FeignClient)实现。此外,*-api 通常包含 DTO,Bean Validation 和 Swagger 注释可能会覆盖这些 DTO。在某些情况下,这些接口可能包含来自 Spring-MVC 的 @RequestMapping 注释。因此@RestController 和FeignClient 的实现同时继承了@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 依赖项,它可能会在其类路径中获得不可预测的传递依赖项。微服务之间也出现了紧密耦合。

我决定花一些时间研究这个问题并发现一些概念。首先,我了解了诸如this one 或Sam Newman 著名的Building Microservices book(“客户端库”一章)之类的广泛意见。我也知道Consumer Driven Contracts 和他们的实现——Pact 和Spring Cloud Contract。我决定是否要使用 Spring Boot 微服务开始一个新项目,我会尽量不制作客户端库并仅通过 Consumer Driven Contracts 耦合微服务。因此我希望达到最小的耦合。

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

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

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

附录 1.

@JRichardsz 的问题:

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

答案:

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

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

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

您在项目中使用哪种方式?是上面提到的方式与 客户端库合理吗?

附录 2。

我会尽量详细并举例说明。

简介。

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

示例。

微服务-b 项目

microservice-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;
    

application.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("***");
            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 ***

Here you can see full example

问题。

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

附注在我看来,Spring Boot 微服务应该通过 Consumer Driven Contracts(Spring Cloud Contract 或 Pact)耦合,仅此而已。你认为这是正确的方式吗?

【问题讨论】:

你可能想看看这个问题:***.com/questions/52033686/… 您的问题是否与 @OlgaMaciaszek 所说的使用库(服务、dto 等)或合同测试模块化 Spring Boot 代码(几个 api)有关? @JRichardsz 是关于如何在 spring-boot 微服务中组织客户端库。我们真的需要使用它们吗?我认为我在问题中提到的示例确实为微服务带来了紧密耦合,但大多数项目确实使用它们。为什么?在微服务之间创建集成的最佳做法是什么? #1 客户是什么意思? REST API 的客户端是 API 所有者提供的一种 sdk,允许客户端以简单的方式使用它,而不是 http 低级实现。 #2 集成是什么意思?测试集成是您需要的吗? #3 我认为您的要求与如何在多个 api 之间组织源代码有关。对吗? @JRichardsz 我已经在“附录 1”部分回答了您的问题。感谢您的帮助。 【参考方案1】:

这里是构建数十个 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) 和只是库(.jar):

acme 基础 所有应用程序的 java 版本为 &lt;artifactId&gt;maven-compiler-plugin&lt;/artifactId&gt; 的父项目和其他通用属性,如 project.build.sourceEncoding 等 acme.org 中的任何 java 项目都必须使用此父项。用几个 java 版本管理几个 api 会很痛苦:s acme-base-spring 具有主要 Spring 类和版本的父项目:spring-web、spring-core、spring-context 并非 acme.org 中的所有开发都是 api 休息。可能是库、时间表或演示等。所以如果他们需要一些 spring 库,他们必须使用这个父类 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-starter-tomcat acme-base-api 此父项目必须使用 acme-base-spring-boot-api 作为父项目。

有了这些父母,你的 employee-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 库。 员工持续 使用 employee-model 作为依赖项的 java 库。 可以存储 daos 或 jpa 存储库

有了这些父母和依赖,你的 employee-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 的代码都适用于它们,所以需要一个新的库

acme-common 此项目必须在 customer-api、employee-api 和 acme.org 中的任何其他 api 中用作 依赖项

这里列出了一些跨多个 rest api 的常用库:

acme-api-log 使用记录器和mdc在每个日志条目中添加元数据 使用google stack driver、gray log 等将日志外部化 acme-api-审计 在每个请求中添加unique identifier 以便于跟踪或错误支持。 acme-api-错误 异常、controller advice、异常处理程序等 acme-api-健康 必须为任何 api 发布的 /health /status 等端点。

使用 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 作为依赖项。而不是 http 低级实现来消耗客户 api,只需要:
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。可用于任何需要在 customer-api 中使用某些端点的 api 这个 customer-api-sdk 可用于模拟,例如为 Spring Cloud Contract 生成的 jars sdk 和 passport 的想法取自:google 和 amazon sdks(向其平台执行 http 请求)、为axis、jaxws 生成的soap 客户端等

【讨论】:

很好的例子,谢谢!你对父项目的方法给了我一个很大的澄清。如果有可能与团队就这种方法进行谈判,它可能会解决有关客户端库、共享库、API 版本等的许多问题。 但是@Filippo Possenti 已经提到这个问题有很多取舍,这就是为什么我还没有接受这个答案作为解决方案。 这是一个非常结构化的例子。我在我现在的公司做的事情非常相似,以前的公司也试图以类似的方式构建他们的模块。关键始终是它是否解决了您遇到的问题或您还没有(并且可能永远不会遇到)的问题。如前所述,我也在使用它,它确实解决了一些问题……但在尝试解决这些问题之前,请确保你确实遇到了这些问题。除此之外,我通常会推荐这是一个很好的结构。【参考方案2】:

免责声明: 我认为您的问题没有一个明确的答案。更具体地说,我认为最佳解决方案会随着项目的发展而变化,因此以下内容在很大程度上可以被认为是“个人意见”。我希望人们会像我一样表达他们的意见,而不是对我的答案投票,无论是向上还是向下投票。


微服务的目的之一确实是“简化”软件产品的集成和发展,这实际上引发了将客户端锁定到通用 API 库的好处的问题。

由于甚至有公司从微服务迁移回单体架构的故事,我从来不敢说一种方法绝对错误或绝对正确。

在某些情况下,使用客户端库可能不是一个坏主意,因为额外的负担可能会迫使不守纪律的开发人员协调并保证更新的客户端始终与实际服务一起开发。尽管如此,除非我有特定需求,否则它不会是我的首选,这可能更多地与公司内不同开发团队使用的技能和方法水平的差异有关。

我个人认为,最简单的方法 (customer contracts) 适用于具有少量客户/客户(另一个微服务是客户/客户)的应用程序,并且可以立即投入生产,这有助于减少市场/发布,同时支持公司的启动阶段。

随着公司的发展,由于维护成本增加和与customer contracts 相关的挫败感,需要更多结构并需要重新审查选择,此时有关业务和相关需求的可用信息极大地有助于选择“下一个”要走的路,可能是 customer-driven contracts,因为它们是封闭和完整的,出于多种原因,这是可取的,而且只有在了解了对客户而言重要的事情之后才能实现。

痛苦的经历可能会导致选择依赖客户端库,但我认为这种情况并不常见,而且更有可能发生在“全新”项目中,因为技术主管在之前的项目中留下了“未处理的创伤”,而该项目已逾期customer contracts 模式应该迁移到 customer-driven contracts

对我来说,关键是在一开始就做出一个关键的选择,为未来几乎完全改变想法的可能性腾出空间,从而支持未来的增长,而不必立即“拔掉”老客户作为业务连续性只是不允许这样的举动。一种方法是为 API 提供一个包含在 URL 中的代号,从而允许将未来的版本巧妙地分开,从而为消费者提供升级的宽限期。代号实际上是贵公司销售的产品的名称。


明确尝试回答您的问题:

您在项目中使用哪种方式?这真的取决于谁将使用我的微服务。如果它是我公司内的特定参与者并且也使用 Java,我更愿意在向他们公开微服务时提供一个成熟的客户端(带有相关源代码),并要求他们在向他们公开微服务时也这样做我。这可以避免至少一些问题,比如他们指责我,因为“它不起作用”,而问题在于他们的客户端,同时防止对给定微服务的内部工作产生误解,并推动参与者开发微服务相当稳定(即:人们将避免不断更改“签名”以避免不得不重建相应的客户端......基本上,我利用了我们与生俱来的懒惰)。他们显然不需要使用我的客户端,我也不需要使用他们的客户端:这更像是微服务工作的“声明”,旨在让各方通过检查彼此的客户端代码来找出真正的问题出在哪里.这还可以让您了解他们的编码方式,从而深入了解微服务代码的预期质量,从而了解预期的稳健性和可预测性。

上面提到的客户端库的方式合理吗?这种方式使微服务的集成合理(正确)吗? 有时是,有时不是。老实说,我不会使用客户端库,但对于简单的架构,它可以工作并且可以确保每个人都在同一个页面上,所以这不一定是一件坏事。

在微服务之间进行集成的最佳实践是什么?我相信它会随着时间的推移而变化,具体取决于项目。我会开始让消费者自生自灭,以加快上市时间,我很清楚我一定会从consumer contracts 开始(尽管我会尝试在某种程度上使架构适应未来)并让体验和增长将架构固化为consumer-driven contracts 之一。

你认为这是正确的方式吗?会阻碍未来的增长。老实说,这不是一个很好的答案,但事实是你的问题很难回答,而且很大程度上取决于项目的范围。关键是正确的方式很可能会随着时间的推移而改变,因此您应该着眼于选择一个您认为可以实现 3-5 年增长的解决方案,同时提供一种意外情况,让您可以优雅地迁移到一个将支持未来 8-10 年的增长。这也意味着“正确的方式”不仅仅是一个技术问题,也是一种业务管理方法,特别是一种允许有条不紊地规划未来的方法。

【讨论】:

感谢您的回答!如果客户端库具有干净的架构,我并不反对它们,但我的示例具有严重的缺点(无规则的紧密耦合、不良依赖关系),我不想在我的项目中应用这种方法。我希望看到可以在实际项目中应用的“干净”示例。 另一个答案中提供的示例是拥有一组整合 API 的公司最终会走向何方。所以这构成了你最结构化的例子。另一方面,从一开始,您的示例看起来更像您可以在教程中找到的示例。它可能看起来“不专业”,但简单却有美感,它可以让您专注于功能。

以上是关于Spring Boot 微服务中的客户端库的主要内容,如果未能解决你的问题,请参考以下文章

Spring Boot + Spring Cloud 构建微服务系统:API服务网关(Zuul)

Spring Boot 微服务授权

spring boot - 假装客户端发送基本授权标头|将 jwt 令牌从一个微服务传递到另一个微服务

Spring Boot + Spring Cloud 构建微服务系统:客户端负载均衡(Ribbon)

spring boot 微服务如何实现编排? [关闭]

没有 Spring-boot 的 Eureka 服务发现