使用 Spring Boot 2.0,Eureka 和 Spring Cloud 的微服务快速指南

原文作者:Piotr Mińkowski

原文地址:https://dzone.com/articles/quick-guide-to-microservices-with-spring-boot-20-e

在我的博客里面已经有了很多关于 Spring Boot 和 Spring Cloud 微服务开发的文章,而这篇文章的主要目的便是对这些微服务的开发框架的一些关键组件做一个简单的总结。本文会涉及到这些主题:

  • 使用 Spring Boot 2.0 在云原生环境里面进行开发
  • 使用 Spring Cloud Netflix Eureka 为所有微服务提供服务发现的功能
  • 使用 Spring Cloud Config 进行分布式的配置工作
  • 使用 Spring Cloud 内置的 Spring Cloud Gateway 项目实现 API 网关模式
  • 使用 Spring Cloud Sleuth 整合日志

在我们讨论源码之前,我们先看一看下面这张图。下图描绘了我们接下来所设计的一个示例系统的架构。在这个系统里有 3 个独立的微服务。它们会通过服务发现来完成服务的登记,并从配置服务器里面拉取相关属性,然后与其他微服务进行交互。

目前 Spring Cloud 的最新版本是 Finchley.M9spring-cloud-dependencies 的版本应该在依赖项目里面得到声明。

<?xml version="1.0" encoding="UTF-8"?>
<dependencyManagement>
   <dependencies>
      <dependency>
         <groupId>org.springframework.cloud</groupId>
         <artifactId>spring-cloud-dependencies</artifactId>
         <version>Finchley.M9</version>
         <type>pom</type>
         <scope>import</scope>
      </dependency>
   </dependencies>
</dependencyManagement>

现在,我们来进一步地考虑一下使用 Spring Cloud 来构建一个基于微服务的系统的步骤。我们首先从配置服务器开始。

本文设计的示例项目的源码可以在 GitHub 里面找到。

第 1 步 - 使用 Spring Cloud Config 构建配置服务器

为了能在项目里面使用 Spring Cloud Config 的功能,我们首先需要把 spring-cloud-config-server 加到依赖项目里面。

<?xml version="1.0" encoding="UTF-8"?>
<dependency>
   <groupId>org.springframework.cloud</groupId>
   <artifactId>spring-cloud-config-server</artifactId>
</dependency>

然后给应用加上 @EnableConfigServer 注解来让应用在启动的时候运行集成的配置服务器。

@SpringBootApplication
@EnableConfigServer
public class ConfigApplication {
    public static void main(String[] args) {
        new SpringApplicationBuilder(ConfigApplication.class).run(args);
    }
}

在默认情况下,Spring Cloud Config 服务器会在 Git 仓库里面保存配置信息的数据。这在生产环境里面是个不错的选择,不过在本文所设计的示例里面里面就有点过分了。毕竟我们可以把全部属性都写进 classpath 里面,而 Spring Cloud Config 会默认地在 classpath:/classpath:/configfile:./config 里面查找配置文件,这样构建一个配置服务器其实是非常简单的事情。

我们会把全部配置文件都放到 src/main/resources/config 里面。YAML 配置文件的文件名应该跟对应的服务的名字保持一致。比如 discovery-service 服务的 YAML 配置文件名路径就是 src/main/resources/config/discovery-service.yml

这里有两个重点。如果要基于一个文件系统作为后端来构建一个配置服务器,那就得启用 Spring Boot 的原生属性(在启动应用的时候加上 --spring.profiles.active=native 参数即可)。在这里我同时也把配置服务器的端口号从默认的 8888 改成了 8061(在 bootstrap.yml 配置文件里面设置 server.port 参数即可)。

第 2 步 - 使用 Spring Cloud Netflix Eureka 构建服务发现流程

在完成构建配置服务器之后,包括 discovery-service 在内的所有应用都要把 spring-cloud-starter-config 加进依赖项目里面来启动各自的针对配置服务器的客户端。除此之外,我们还应该加上 spring-cloud-starter-netflix-eureka-server 这个依赖。

<?xml version="1.0" encoding="UTF-8"?>
<dependency>
   <groupId>org.springframework.cloud</groupId>
   <artifactId>spring-cloud-starter-netflix-eureka-server</artifactId>
</dependency>

然后给 discovery-service 服务的对应应用加上 @EnableEurekaServer 来在应用启动的时候启动集成的服务发现服务器。

@SpringBootApplication
@EnableEurekaServer
public class DiscoveryApplication {
    public static void main(String[] args) {
        new SpringApplicationBuilder(DiscoveryApplication.class).run(args);
    }
}

这一应用需要从配置服务器里面拉取配置信息。其实只需要在客户端的配置信息里面给出应用名称还有配置服务器的连接设置就可以了。

spring:
  application:
    name: discovery-service
  cloud:
    config:
      uri: http://localhost:8088

之前也提到过,discovery-service.yml 这一配置文件应该放在 config-service 模块里面。然而,我必须对下面的配置信息解释一下。首先,我们把 Eureka 的运行端口从默认值 8761 改成了 8061。其次,就一个独立出来的 Eureka 实例来说,我们应该禁用掉它的服务登记和发现功能。

server:
  port: 8061
eureka:
  instance:
    hostname: localhost
  client:
    registerWithEureka: false
    fetchRegistry: false
    serviceUrl:
      defaultZone: http://${eureka.instance.hostname}:${server.port}/eureka/

现在,在运行了带有集成的 Eureka 服务器的应用之后,我们应该能看到下面这些日志信息:

在成功运行这一应用之后,我们就可以通过访问 http://localhost:8061 来查看 Eureka 的配置界面了。

第 3 步 - 使用 Spring Boot 和 Spring Cloud 构建一个微服务

我们的微服务在启动的时候需要执行一些动作。它首先要从 config-service 里面拉取配置信息,然后通过 discovery-service 完成服务的登记,接着开放 HTTP API,并自动生成 API 文档。为了实现这若干步骤,我们需要在 pom.xml 里面加一些依赖项目。为了获取配置信息,我们应该加上 spring-cloud-starter-config 这一项目。为了启用服务登记和发现功能,我们还应该加上 spring-cloud-starter-netflix-eureka-client,然后给应用加上 @EnableDiscoveryClient 注解。为了让 Spring Boot 应用自动生成 API 文档,我们还应该加上 springfox-swagger2 项目,然后给应用加上 @EnableSwagger2 注解。

下面便是我们编写示例微服务所用到的完整依赖项目列表:

<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-config</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger2</artifactId>
<version>2.8.0</version>
</dependency>

下面便是加上了 Discovery Client 还有 Swagger2 的微服务的应用类。

@SpringBootApplication
@EnableDiscoveryClient
@EnableSwagger2
public class EmployeeApplication {
 public static void main(String[] args) {
  SpringApplication.run(EmployeeApplication.class, args);
 }
 @Bean
 public Docket swaggerApi() {
  return new Docket(DocumentationType.SWAGGER_2)
   .select()
   .apis(RequestHandlerSelectors.basePackage("pl.piomin.services.employee.controller"))
   .paths(PathSelectors.any())
   .build()
   .apiInfo(new ApiInfoBuilder().version("1.0").title("Employee API").description("Documentation Employee API v1.0").build());
 }
 ...
}

这一应用需要向一个远程服务器拉取配置信息,因此我们也应该给出一个 bootstrap.yml 文件来指定服务名还有服务器的 URL。其实这也是配置优先启动方法的一个例子,采用这个方法的应用会首先连接配置服务器,然后从远程配置服务器拉取服务发现服务器的地址。这里同样也有一种发现优先启动方法,采用此方法的应用会从服务发现服务器拉取配置服务器的地址。

spring:
  application:
    name: employee-service
  cloud:
    config:
      uri: http://localhost:8088

这一应用的配置信息其实不多。下面便是写在配置服务器里面的这一应用的配置信息,只包括了 HTTP API 的运行端口还有 Eureka 服务器的 URL。不过我还在配置服务器上放了一份 employee-service-instance2.yml 文件,为这一应用设置了一个不同的端口。这样,我们就可以运行同一服务的两个不同的实例,只需要在启动应用的时候加上 spring.profiles.active=instance2 参数即可,其中 employee-service 的第二个实例会运行在 9090 端口上。若按照默认设置,那么微服务会运行在 8090 端口上。

server:
  port: 9090
eureka:
  client:
    serviceUrl:
      defaultZone: http://localhost:8061/eureka/

下面就是服务的 REST 控制器类的实现。它对员工信息的添加和基于不同条件的查找提供了实现。

@RestController
public class EmployeeController {
 private static final Logger LOGGER = LoggerFactory.getLogger(EmployeeController.class);
 @Autowired
 EmployeeRepository repository;
 @PostMapping
 public Employee add(@RequestBody Employee employee) {
  LOGGER.info("Employee add: {}", employee);
  return repository.add(employee);
 }
 @GetMapping("/{id}")
 public Employee findById(@PathVariable("id") Long id) {
  LOGGER.info("Employee find: id={}", id);
  return repository.findById(id);
 }
 @GetMapping
 public List findAll() {
  LOGGER.info("Employee find");
  return repository.findAll();
 }
 @GetMapping("/department/{departmentId}")
 public List findByDepartment(@PathVariable("departmentId") Long departmentId) {
  LOGGER.info("Employee find: departmentId={}", departmentId);
  return repository.findByDepartment(departmentId);
 }
 @GetMapping("/organization/{organizationId}")
 public List findByOrganization(@PathVariable("organizationId") Long organizationId) {
  LOGGER.info("Employee find: organizationId={}", organizationId);
  return repository.findByOrganization(organizationId);
 }
}

第 4 步 - 使用 Spring Cloud Open Feign 实现微服务间的交互

我们已经构建并启动了一个微服务了。现在我们便加上另外一些微服务来产生一些交互。下图便展示了我们所编写的 3 个微服务(organization-servicedepartment-serviceemployee-ervice)间的交互流模式。organization-service 微服务会通过 department-service 的端点(endpoint)获取带有下属员工列表的部门列表(GET /organization/{organizationId}/with-employees)和不带员工列表的部门列表(GET /organization/{organizationId}),还会直接通过 employee-service 获取没有用部门来进行划分的员工列表。department-service 微服务则能够获取特定部门下的员工列表。

在上述情形里面,orgranization-service 还有 department-service 应该持有别的微服务的知识,并与其他微服务进行交互。对此,我们需要加上另外一个依赖项目 spring-cloud-starter-openfeign。Spring Cloud Open Feign 是一个声明式的 REST 客户端,会使用 Ribbon 客户端的负载均衡器来和其他微服务进行交互。

<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>

Open Feign 有个替代方案是 Spring 框架里面加上了 @LoadBalaced 注解的 RestTemplete 。不过,Feign 对定义 REST 客户端给出了一种更加优雅的方法,因此我也更倾向于 Feign。在加入 Feign 这一依赖项目之后,我们就可以在应用变量加上 @EnableFeignClients 注解来启用 Feign 的客户端功能了。

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

现在我们需要定义客户端的接口。鉴于 organization-service 跟其余 2 个微服务都有交互,我们应该构建 2 个接口,一个接口对应一个微服务。每个客户端接口都要加上 @FeignClient 注解,且要往注解里面填入 name 字段。这一字段应该和这一接口所对应的已登记的微服务名称保持一致。下面就是调用 employee-service 开放的端点 GET /organization/{organizationId} 的客户端的一个接口。

@FeignClient(name = "employee-service")
public interface EmployeeClient {
    @GetMapping("/organization/{organizationId}")
    List findByOrganization(@PathVariable("organizationId") Long organizationId);
}

第二个 organization-service 内置的客户端接口调用了 department-service 的两个端点。一个是 GET /organization/{organizationId} ,只会返回现存的部门列表;另一个是 GET /organization/{organizationId}/with-employees,会返回部门列表以及每个部门下属的员工列表。

@FeignClient(name = "department-service")
public interface DepartmentClient {
    @GetMapping("/organization/{organizationId}")
    public List findByOrganization(@PathVariable("organizationId") Long organizationId);
    @GetMapping("/organization/{organizationId}/with-employees")
    public List findByOrganizationWithEmployees(@PathVariable("organizationId") Long organizationId);
}

最后便可以将这两个 Feign 客户端作为 Bean 注入到 REST 控制器类里面。然后我们便能通过调用 DepartmentClient 还有 EmployeeClient 里面的方法来调用相应的 REST 端点。

@RestController
public class OrganizationController {
    private static final Logger LOGGER = LoggerFactory.getLogger(OrganizationController.class);
    @Autowired
    OrganizationRepository repository;
    @Autowired
    DepartmentClient departmentClient;
    @Autowired
    EmployeeClient employeeClient;
    // ...
    @GetMapping("/{id}")
    public Organization findById(@PathVariable("id") Long id) {
        LOGGER.info("Organization find: id={}", id);
        return repository.findById(id);
    }
    @GetMapping("/{id}/with-departments")
    public Organization findByIdWithDepartments(@PathVariable("id") Long id) {
        LOGGER.info("Organization find: id={}", id);
        Organization organization = repository.findById(id);
        organization.setDepartments(departmentClient.findByOrganization(organization.getId()));
        return organization;
    }
    @GetMapping("/{id}/with-departments-and-employees")
    public Organization findByIdWithDepartmentsAndEmployees(@PathVariable("id") Long id) {
        LOGGER.info("Organization find: id={}", id);
        Organization organization = repository.findById(id);
        organization.setDepartments(departmentClient.findByOrganizationWithEmployees(organization.getId()));
        return organization;
    }
    @GetMapping("/{id}/with-employees")
    public Organization findByIdWithEmployees(@PathVariable("id") Long id) {
        LOGGER.info("Organization find: id={}", id);
        Organization organization = repository.findById(id);
        organization.setEmployees(employeeClient.findByOrganization(organization.getId()));
        return organization;
    }
}

第 5 步 - 使用 Spring Cloud Gateway 构建 API 网关

Spring Cloud Gateway 算是一个比较新的 Spring Cloud 项目。它是基于 Spring Framework 5、Project Reactor 还有 Spring Boot 2.0 而构建的,需要依赖 Spring Boot 还有 Spring Webflux 所提供的 Netty 运行库来运行。过去为 Spring Cloud 项目里的微服务提供 API 网关功能的一直是 Spring Cloud Netflix Zuul,而现在 Spring Cloud Gateway 则成为了一个很不错的替代方案。

示例项目里的 gateway-service 模块给出了 API 网关的实现。为了实现 API 网关,我们首先还是要把 cloud-starter-gateway 加到依赖项目里面:

<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-gateway</artifactId>
</dependency>

我们还需要在这一模块启用服务发现功能,毕竟 gateway-service 得把 Eureka 整合进来才能发现下游的服务并为它们提供路由功能。我们所设计的微服务开放的各个端点的 API 文档也会在网关这里的到开放。这也是引入 Swagger2 的一个好处。

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

Spring Cloud Gateway 有 3 个基本配置组件:路由、转发条件,和过滤器。路由是网关的基础组件,有一个目标 URI 地址列表,转发条件列表还有过滤器列表。转发条件则负责匹配传入 HTTP 请求的内容,比如首部或参数。过滤器则会修改某个请求,并在将其转发给下游服务之前和之后作出响应。这些组件都可以用配置属性来设置。我们会把针对我们的微服务的路由的配置属性放在配置服务器的 gateway-service.yml 里面。

不过在这之前,我们应该设置 spring.cloud.gateway.discovery.locator.enabled=true 来确保服务发现功能跟网关的路由功能整合到了一起。然后我们便可以开始定义路由规则了。这里我们会用 Path Route Predicate Factory 来匹配传入请求,用 RewritePath GatewayFilter Factory 来修改请求目标的路径,使后者能适配为下游服务的格式,其中 URI 参数所表示的是目标服务在服务发现服务器上的登记名称。

接下来我们便来看一看下面的路由配置定义。举个例子,为了能让 organization-service 能通过网关的 /orgranization/** 路径来调用,我们就应该定义转发条件为 Path=/organization/**,然后从请求路径里除掉前缀 /organization ,毕竟目标服务是在 /** 路径下开放的。另外,我们可以用 lb://organization-serivce 这一 URI 值来表示从 Eureka 服务器拉取来的目标服务的地址。

spring:
  cloud:
    gateway:
      discovery:
        locator:
          enabled: true
      routes:
      - id: employee-service
        uri: lb://employee-service
        predicates:
        - Path=/employee/**
        filters:
        - RewritePath=/employee/(?<path>.*), /$\{path}
      - id: department-service
        uri: lb://department-service
        predicates:
        - Path=/department/**
        filters:
        - RewritePath=/department/(?<path>.*), /$\{path}
      - id: organization-service
        uri: lb://organization-service
        predicates:
        - Path=/organization/**
        filters:
        - RewritePath=/organization/(?<path>.*), /$\{path}

第 6 步 - 在网关上使用 Swagger2 来整合 API 文档

所有被 @EnableSwagger2 注解过的 Spring Boot 微服务都会在 /v2/api-docs 这一路径下开放它们的 Swagger API 文档。不过,我们也时常会希望能将各个微服务的 API 文档整合到一起 - 其实也就是整合到 API 网关上。为了达成这一目的,我们需要在 gateway-service 模块给出一个 SwaggerResourcesProvider 接口的实现类,然后将其封装成一个 Bean 以便注入到其他类里面。这一实现类会承担定义 Swagger 文档资源位置的列表的任务,而这一列表就是应用所应呈现的内容。下面会给出 SwaggerResourecesProvider 的一个实现。它会基于 Spring Cloud Gateway 的配置属性去使用服务发现功能来获取所需要的资源的位置。

然而,SpringFox Swagger 并没有为 Spring WebFlux 提供支持。也就是说,若要把 SpringFox Swagger 加到这一网关项目的依赖项目里,那应用可就启动不了了。为了把 Swagger2 集成进项目里面,我们也只能先运行一个基于 Spring Cloud Netflix Zuul 的网关了,希望 Swagger 将来能支持 WebFlux。

我编写了一个 proxy-service 模块来运行一个基于 Netflix Zuul 的网关来替代 gateway-service。下面便是内置进网关的,封装成 Bean 的 SwaggerResourcesProvider 的实现类。它用了 ZuulProperties 这一 Bean 来动态地将路由定义加载到一个列表里面。

@Configuration
public class ProxyApi {
    @Autowired
    ZuulProperties properties;
    @Primary
    @Bean
    public SwaggerResourcesProvider swaggerResourcesProvider() {
        return () -> {
            List resources = new ArrayList();
            properties.getRoutes().values().stream()
                      .forEach(route -> resources.add(createResource(route.getServiceId(), route.getId(), "2.0")));
            return resources;
  };
 }
 private SwaggerResource createResource(String name, String location, String version) {
     SwaggerResource swaggerResource = new SwaggerResource();
     swaggerResource.setName(name);
     swaggerResource.setLocation("/" + location + "/v2/api-docs");
     swaggerResource.setSwaggerVersion(version);
     return swaggerResource;
 }
}

下图是示例项目的 Swagger 界面,可以通过地址 http://localhost:8060/swagger-ui.html 来访问。

第 7 步 - 运行应用

我们再来看看整个系统的架构,如下图所示。我们从 organization-service 的视角看起。在系统启动之后,organization-service 会通过地址 localhost:8088 连接 config-service (1)。然后,利用从远程配置服务器获取的设置信息,它就能在 Eureka 服务器进行服务的登记 (2)。 在 organization-service 的端点被外部客户端通过网关(地址为 localhost:8060)调用时 (3),请求会被转发到 organization-service 的其中一个已登记的实例 (4)。然后,在要调用 department-service 的端点的情况下,organization-service 会在 Eureka 里面查找 department-service 的地址 (5),并调用对应的端点 (6)。最终,department-service 又会调用 employee-service 的端点。这一请求带来的负载会通过 Ribbon 均匀分摊到两个 employee-service(7)

我们再来访问 http://localhost:8061 来看看 Eureka 的主界面。主界面应该登记着 4 个微服务的实例,分别有 1 个 organization-servicedepartment-service 还有 2 个 employee-service

现在我们试试调用 http://localhost:8060/organization/1/with-departments-and-employees 这个端点。其返回的结果应该跟下图所示的差不多:

第 8 步 - 使用 Spring Cloud Sleuth 整合微服务器的日志记录

这一步非常简单。其实我们只要把 spring-cloud-starter-sleuth 这一依赖项目加到每个微服务还有网关的项目里面就可以了。

<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-sleuth</artifactId>
</dependency>

具体而言,我们会把默认的日志格式改成 %d{yyyy-MM-dd HH:mm:ss} ${LOG_LEVEL_PATTERN:-%5p} %m%n。下图就展示了我们的三种微服务所各自生成的日志。Spring Cloud Stream 会在每行输出包含在一个中括号 [] 里面的四个条目。其中对我们而言最重要的是第二个条目,它表示的是 traceId 字段。每当系统接收到一个传入 HTTP 请求的时候,系统就会给它设置一个 traceId

本文的版权归 Tnecesoc 所有,如需转载请联系作者。

发表于

扫描关注云+社区

云计算

148 篇文章94 人订阅

专注于提供全球领先的专业云计算资讯。

我来说两句

0 条评论
登录 后参与评论

相关文章

扫描关注云+社区