03. Spring Cloud--服务路由

3.1 简介

在向微服务框架这样的分布式架构中,需要确保跨多个服务调用的关键行文的正常运行,如安全、日志记录和用户跟踪。要实现此功能,开发人员需要在所有服务中始终如一地强制这些特性,而不需要每个开发团队都构建自己的解决方案。虽然可以使用公共库或扩家来帮助在单个服务中直接构建这些功能,但这样做会造成3个影响。

  1. 在构建的每个服务中很难始终实现这些功能。开发人员专注于交付功能,在每日的快速开发工作中,他们很容易忘记实现服务日志记录或跟踪。
  2. 很难正确地实现这些功能。对每个正在开发的服务进行诸如微服务安全的建立与配置可能是很痛苦的。将实现横切关注点(cross-cutting concern,如安全问题)的责任推给各个开发团队,大大增加了开发人员没有正确实现或忘记实现这些功能的可能性。
  3. 这会在所有服务中创建多个强依赖。开发人员将本该写在通用代码中的功能,分别写在各个服务中的次数越多,更新迭代的时候就越困难。

为了解决这个问题,需要将这些横切关注点抽象成一个独立且作为应用程序中所有微服务调用的过滤器和路由器的服务。这种横切关注点被称为服务网关(service gateway)。服务客户端不再直接调用服务。取而代之的是,服务网关作为单个策略执行点,所有调用都通过服务网关进行路由,然后被路由到最终目的地。

使用服务网关时的注意事项:
在实现服务网关功能时,我们通常将服务网关置于所有服务实例的前面,但是这样做有一定的危险性,因为它会是服务网关成为瓶颈。
为了尽量避免瓶颈的出现,我们要保持为服务网关编写的代码是无状态的。不要在内存中为服务网关存储任何信息。如果不小心,就有可能限制网关的伸缩性,导致不得不确保数据在所有服务网关实例中被复制。

3.2 Netflix Zuul

Spring Cloud集成了Netflix开源项目Zuul。Zuul是一个服务网关,它非常容易通过Spring Cloud注解进行创建和使用。Zuul提供了许多功能,具体包括以下几个:

  • 将应用程序中的所有服务的路由映射到一个URL – 在Zuul中,开发人员可以定义多个路由条目,使路由映射非常细粒度(每个服务端点都有自己的路由映射)。而Zuul最常见的用力是构建一个单一的入口点,所有服务客户端调用都将经过这个入口点。
  • 构建可以对通过网关的请求进行检查和操作的过滤器 – 这些过滤器允许开发人员在代码中诸如策略执行点,以一致的方式对所有服务调用执行大量操作。

3.2.1 构建Zull服务器

在pom.xml中导入依赖:

<!--实现Zuul服务器的主要依赖-->
<dependency>
  <groupId>org.springframework.cloud</groupId>
  <artifactId>spring-cloud-starter-netflix-zuul</artifactId>
</dependency>
<!--将zuul服务器注册到Eureka Server上-->
<dependency>
  <groupId>org.springframework.cloud</groupId>
  <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
<!--通过Spring Config Server实现动态路由-->
<dependency>
  <groupId>org.springframework.cloud</groupId>
  <artifactId>spring-cloud-starter-config</artifactId>
</dependency>
<!--通过Spring Config Server实现动态路由-->
<dependency>
  <groupId>org.springframework.cloud</groupId>
  <artifactId>spring-cloud-starter-bus-amqp</artifactId>
</dependency>
<!--用于获取Zuul的路由映射表-->
<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-actuator</artifactId>
</dependency>

在bootstrap.properties中加入配置:

spring.application.name=zuul-server
server.port=9002
eureka.client.service-url.defaultZone=http://localhost:9000/eureka/

spring.rabbitmq.host=192.168.3.12
spring.rabbitmq.port=5672
spring.rabbitmq.username=guest
spring.rabbitmq.password=guest

spring.cloud.config.discovery.enabled=true
spring.cloud.config.discovery.service-id=config-server
spring.cloud.config.profile=dev

# 开放一个API接口,用于获取路由表
management.endpoints.web.exposure.include=routes

至此,我们就搭建好了Zuul服务器。

怎样获取路由表:
使用GET方法,访问以下URL:http://<zuul服务器IP地址>:<zuul服务器端口号>/actuator/routes,可以获取到路由表。

3.2.2 通过服务发现自动映射路由

Zuul的所有路由映射都是通过在properties或者yml文件中定义路由来完成的。但是,Zuul可以根据其服务ID自动路由请求,而不需要配置,比如有一个服务的ID的order-eureka,那么路由映射中就会自动多出一行"/order-eureka/**": "order-eureka"

使用自动映射路由有一个好处,当我们想Eureka服务器中添加新服务的时候,Zuul可以将新服务添加到路由映射表中,而无需修改Zuul。

3.2.3 使用服务发现手动映射路由

Zuul允许开发人员更细粒度地明月定义路由映射,而不是单纯依赖服务的Eureka服务ID创建的自动路由。假设开发人员希望通过缩短服务名称来简化路由,而不是通过默认路由在Zuul中访问服务,做法如下:

# 将order-eureka服务的路由映射为/order/**
zuul.routes.order.service-id=order-rureka
zuul.routes.order.path=/order/**

如果想要排除Eureka服务ID路由的自动映射,只提供自定义的服务路由,可以这样来做:

# 排除order-rureka的自动路由映射
zuul.ignored-services=order-rureka

如果要排除所有基于Eureka的路由,可以将ignored-services属性设置为*

3.2.4 使用静态URL手动映射路由

Zuul可以用来路由那些不受Eureka管理的服务。在这种情况下,可以建立Zuul直接路由到一个静态定义的URL。做法如下:

zuul.routes.angular.path=/angular/**
zuul.routes.angular.url=http://<serverIp>:<port>

3.3 过滤器

虽然通过Zuul网关代理所有请求确实可以简化服务调用,但是在想要编写应用于所有流经网关的服务调用的自定义逻辑时,Zuul的真正威力才发挥出来,在大多数情况下,这种自定义逻辑用语强制执行一组一致的应用程序策略,如安全性、日志记录和对所有服务的追踪。

这些应用程序策略被认为是横切关注点,因为开发人员希望将它们应用于应用程序中的所有服务,而无须修改每个服务来实现它们。通过这种方式,Zuul 过滤器可以按照与J2EE servlet 过滤器或Spring Aspect 类似的方式来使用。这种方式可以拦截大量行为,并且在原始编码人员意识不到变化的情况下,对调用的行为进行装饰或更改。servlet 过滤器或Spring Aspect 被本地化为特定的服务,而使用Zuul 和Zuul 过滤器允许开发人员为通过Zuul 路由的所有服务实现横切关注点。

Zuul允许开发人员使用Zuul网关内的过滤器构建自定义逻辑。过滤器可用于实现每个服务请求在执行时都会经过的业务逻辑。Zuul支持以下3中类型的过滤器。

  1. 前置过滤器 – 前置过滤器在Zuul将实际请求发送到目的地之前被调用。前置过滤器通常执行确保服务具有一致的消息格式(如,关键的HTTP Header是否设置妥当)的任务,或者充当看门人,确保调用该服务的用户已经通过验证和授权。
  2. 后置过滤器 – 后置过滤器在目标服务被调用并将响应发送给客户端后被调用。通常后置过滤器会用来记录从目标服务返回的响应、处理错误或审核对敏感信息的响应。
  3. 路由过滤器 – 路由过滤器用于在调用目标服务之前拦截调用。通常使用路由过滤器来确定是否需要进行某些级别的动态路由。例如,本章的后面讲使用路由级别的过滤器,该过滤器将在同一服务的两个不同版本之间进行路由,以便将一小部分服务调用路由到服务的新版本,而不是路由到现有的服务。这样就能够在不让每个人都使用新服务的情况下,让少量的用户体验新功能。

3.3.1 过滤器调用流程

流程:

  1. 在请求进入Zuul网关时,Zuul调用所有在Zuul网关中定义的前置过滤器。前置过滤器可以在HTTP请求到达实际服务之前对HTTP请求进行检查和修改。前置过滤器不能讲用户重定向到不同的端点或服务。
  2. 在针对Zuul的传入请求执行前置过滤器之后,Zuul将执行已定义的路由过滤器。路由过滤器可以更改服务所指向的目的地。
  3. 路由过滤器可以将服务调用重定向到Zuul服务器被配置的发送路由意外的位置。但Zuul路由过滤器不会执行HTTP重定向,而是会终止传入的HTTP请求,然后代表原始调用者调用路由。这意味着路由过滤器必须完全负责动态路由的调用,并且不能执行HTTP重定向。
  4. 如果路由过滤器没有动态地将调用者重定向到新路由,Zuul服务器将发送到最初的目标路由。
  5. 目标路由被调用之后,Zuul后置过滤器将被调用。后置过滤器可以检查和修改来自被调用服务的响应。

3.3.2 前置过滤器实例

我们要做一个前置过滤器,用来检测URL参数里面有没有token,当存在token时不做任何处理,当不存在token时,返回一个401异常。
代码如下:

@Component
public class TokenFilter extends ZuulFilter {
    /**
     * filterType()方法用于告诉Zuul,该过滤器是前置过滤器、后置过滤器还是路由过滤器
     * @return
     */
    @Override
    public String filterType() {
        return FilterConstants.PRE_TYPE;
    }

    /**
     * filterOrder()方法返回一个整数值,只是不同类型的过滤器的执行顺序
     * 数值越小,越优先执行
     * @return
     */
    @Override
    public int filterOrder() {
        return FilterConstants.PRE_DECORATION_FILTER_ORDER - 1;
    }

    /**
     * shouldFilter()方法返回一个布尔值来指示该过滤器是否要执行
     * @return
     */
    @Override
    public boolean shouldFilter() {
        return true;
    }

    @Override
    public Object run() throws ZuulException {
        RequestContext requestContext = RequestContext.getCurrentContext();
         HttpServletRequest request = requestContext.getRequest();
         String token = request.getParameter("token");
         if (StringUtils.isBlank(token)) {
             requestContext.setSendZuulResponse(false);
         	 requestContext.setResponseStatusCode(HttpStatus.UNAUTHORIZED.value());
         }
        return null;
    }
}

3.3.3 后置过滤器实例

我们要做一个后置过滤器,当请求结果返回给客户端的时候,往HTTP Header中添加数据。
代码如下:

@Component
public class AddHeaderFilter extends ZuulFilter {
    @Override
    public String filterType() {
        return FilterConstants.POST_TYPE;
    }

    @Override
    public int filterOrder() {
        return FilterConstants.SEND_RESPONSE_FILTER_ORDER - 1;
    }

    @Override
    public boolean shouldFilter() {
        return true;
    }

    @Override
    public Object run() throws ZuulException {
        RequestContext requestContext = RequestContext.getCurrentContext();
        HttpServletResponse response = requestContext.getResponse();
        response.setHeader("token", "this is token");
        return null;
    }
}

猜你喜欢

转载自blog.csdn.net/hushukang/article/details/83767888
03.