Spring微服务实战--注解版(第六章)

第6章 使用Zuul进行服务路由

在像微服务架构这样的分布式架构中,有时候需要做一些统一的动作,例如日志记录和追踪、记录接口调用的时间等,为了解决这个问题,需要将一些横切关注点抽象成一个独立的服务,这个服务会作为所有微服务的过滤器和路由器,这种横切关注点被成为服务网关。服务客户端不再直接调用微服务,取而代之的是,所有调用都通过服务网关进行路由,然后被路由到最终目的地。Zuul是开源的服务网关实现。
我们通过一个简单的图看下什么是服务网关。
在这里插入图片描述
服务网关充当的是客户端与服务端之间的中介,网关从客户端调用中分离出路径,此路径用于确定客户端真正想调用的服务,可以看到,其实网关还是所有微服务调用的入口,相当于守门人的角色,这样横切关注点就可以在网关实现,而不用每个服务都加上切面。服务网关中可以做的事情如下:

  • 路由—网关将所有服务调用放置在URL和API路由的后面,这样客户端调用只需要知道一个IP和Port即可。服务网关可以根据传入的url判断客户端真正请求的服务,执行智能路由
  • 验证和授权—由于所有服务调用都要经过网关进行路由,所以网关是检查服务调用是否已经进行了验证并被授权调用该服务
  • 度量数据收集和日志记录—可以做些基本的统计工作,如服务调用次数,服务响应时间,日志关联Id的记录。
    接下来介绍SpringCloud集成Zuul。Zuul提供了较多功能,我们具体介绍:
    1、将所有服务的路由映射到一个服务中,也就是提供服务的统一入口
    2、构建过滤器。
    下面介绍怎样建立一个Zuul项目。和以前一样,修改构建脚本、修改引导类、修改配置文件。
    首先构建脚本添加对于zuul依赖项:
    <dependency>
      <groupId>org.springframework.cloud</groupId>
      <artifactId>spring-cloud-starter-zuul</artifactId>
    </dependency>

这个依赖告诉SpringCloud框架,该服务将运行Zuul,并适当地初始化Zuul。
接下来修改引导类,为了使服务成为一个Zuul服务,需要添加一个@EnableZuulProxy的注解

@SpringBootApplication
//使服务成为一个Zuul服务
@EnableZuulProxy
public class ZuulServerApplication {

    @LoadBalanced
    @Bean
    public RestTemplate getRestTemplate(){
        RestTemplate template = new RestTemplate();
        List interceptors = template.getInterceptors();
        if (interceptors == null) {
            template.setInterceptors(Collections.singletonList(new UserContextInterceptor()));
        } else {
            interceptors.add(new UserContextInterceptor());
            template.setInterceptors(interceptors);
        }

        return template;
    }

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

还需要注意的一点是,Zuul也需要注册到Eureka上。其将使用Eureka来通过服务ID查找服务,然后使用Ribbon对来自Zuul的请求进行客户端负载均衡。
我们应该清楚,Zuul的核心是一个反向代理,反向代理负责捕获客户端的请求,然后代表客户端调用远程资源,Zuul必须知道如何将进来的请求映射到不同的服务中,主要有以下几种机制
通过服务发现自动映射路由
理论上将Zuul的所有映射都是在其application.yml中定义路由来完成的。但是Zuul可以根据其服务ID自动路由请求,例如如下的URL。
http://localhost:5555/organizationservice/v1/organizations/442adb6e-fa58-47f3-9ca2-ed1fecdfe86c。其中Zuul服务器通过http://localhost:5555进行访问。路径的第一部分organizationservice尝试调用在Eureka上注册的逻辑名称为organizationservice的服务。
使用服务发现手动映射路由
Zuul允许开发人员更细粒度地明确定义路由映射,而不是单纯地依赖服务的Eureka服务ID创建的自动路由,可以通过在Zuul服务上修改application.yml配置文件来手动定义路由映射。如果想要排除掉Eureka服务ID路由的自动映射,只提供自定义的组织服务路由,可以使用ignored-services参数,如果屏蔽掉所有的自动映射,属性值需设为‘*’。另外服务网关的一种常见模式是通过使用/api之类的前缀来为所有服务添加前缀,从而区别API路由和内容路由。如下:

zuul:
  prefix:  /api
  ignored-services: '*'
  routes:
      producer:
        strip-prefix: false
      organizationservice: /organization/**
      licensingservice: /licensing/**

虽然Zuul作为网关代理确实很灵巧,,但是Zuul的真正威力在于执行一组一致的应用程序,例如:安全性、日志记录、服务跟踪等。因为开发人员想将这些逻辑应用于所有微服务中,而不是在每个微服务中都要使用切面或者注解。那么此时Zuul的过滤器就大杀四方了。通过Zuul的过滤器,我们可以自定义自己的逻辑,Zuul的过滤器有三种:

  • 前置过滤器,在请求发送到实际目的地之前被调用,例如常用的判断Http的请求首部
  • 后置过滤器,将响应返回给客户端时被调用
  • 路由过滤器,简单来讲,就是通过路由过滤器的请求可以一部分被调用到A服务,一部分调用到B服务,服务升级时,可以使用,一小部分客户使用升级后的服务作为测试,大部分客户使用原服务。
    下面通过一个图,简单描述下:
    在这里插入图片描述
    下面,我们构建第一个过滤器
    前置过滤器
    功能需求:检查所有到网关的请求,并判断是否存在名为tmx-correlation-id的HTTP首部。这有点类似于日志追踪中的GUID,用于跨多个服务跟踪用户请求。前置过滤器的代码如下:
@Component
//所有Zuul过滤器必须扩展ZuulFilter,并覆盖如下四个方法
public class TrackingFilter extends ZuulFilter{
    private static final int      FILTER_ORDER =  1;
    private static final boolean  SHOULD_FILTER=true;
    private static final Logger logger = LoggerFactory.getLogger(TrackingFilter.class);
    
    //封装的过滤器类
    @Autowired
    FilterUtils filterUtils;

    /**
     * 告诉Zuul,这是前置过滤器
     * @return
     */
    @Override
    public String filterType() {
        return FilterUtils.PRE_FILTER_TYPE;
    }

    /**
     * 不同类型过滤器的执行顺序
     * @return
     */
    @Override
    public int filterOrder() {
        return FILTER_ORDER;
    }

    /**
     * 是否执行该过滤器
     * @return
     */
    public boolean shouldFilter() {
        return SHOULD_FILTER;
    }

    private boolean isCorrelationIdPresent(){
      if (filterUtils.getCorrelationId() !=null){
          return true;
      }

      return false;
    }

    private String generateCorrelationId(){
        return java.util.UUID.randomUUID().toString();
    }

    /**
     * 每次服务调用通过过滤器时,都会调用。检查首部是否存在,如果不存在,则设置一个
     * @return
     */
    public Object run() {

        if (isCorrelationIdPresent()) {
           logger.debug("tmx-correlation-id found in tracking filter: {}. ", filterUtils.getCorrelationId());
        }
        else{
            filterUtils.setCorrelationId(generateCorrelationId());
            logger.debug("tmx-correlation-id generated in tracking filter: {}.", filterUtils.getCorrelationId());
        }

        RequestContext ctx = RequestContext.getCurrentContext();
        logger.debug("Processing incoming request for {}.",  ctx.getRequest().getRequestURI());
        return null;
    }
}

FilterUtils类用于封装所有过滤器使用的常用功能,如下:

@Component
public class FilterUtils {

    public static final String CORRELATION_ID = "tmx-correlation-id";
    public static final String AUTH_TOKEN     = "tmx-auth-token";
    public static final String USER_ID        = "tmx-user-id";
    public static final String ORG_ID         = "tmx-org-id";
    public static final String PRE_FILTER_TYPE = "pre";
    public static final String POST_FILTER_TYPE = "post";
    public static final String ROUTE_FILTER_TYPE = "route";

    public String getCorrelationId(){
        RequestContext ctx = RequestContext.getCurrentContext();

        if (ctx.getRequest().getHeader(CORRELATION_ID) !=null) {
            return ctx.getRequest().getHeader(CORRELATION_ID);
        }
        else{
            return  ctx.getZuulRequestHeaders().get(CORRELATION_ID);
        }
    }

    public void setCorrelationId(String correlationId){
        RequestContext ctx = RequestContext.getCurrentContext();
        ctx.addZuulRequestHeader(CORRELATION_ID, correlationId);
    }

    public  final String getOrgId(){
        RequestContext ctx = RequestContext.getCurrentContext();
        if (ctx.getRequest().getHeader(ORG_ID) !=null) {
            return ctx.getRequest().getHeader(ORG_ID);
        }
        else{
            return  ctx.getZuulRequestHeaders().get(ORG_ID);
        }
    }

    public void setOrgId(String orgId){
        RequestContext ctx = RequestContext.getCurrentContext();
        ctx.addZuulRequestHeader(ORG_ID,  orgId);
    }

    public final String getUserId(){
        RequestContext ctx = RequestContext.getCurrentContext();
        if (ctx.getRequest().getHeader(USER_ID) !=null) {
            return ctx.getRequest().getHeader(USER_ID);
        }
        else{
            return  ctx.getZuulRequestHeaders().get(USER_ID);
        }
    }

    public void setUserId(String userId){
        RequestContext ctx = RequestContext.getCurrentContext();
        ctx.addZuulRequestHeader(USER_ID,  userId);
    }

    public final String getAuthToken(){
        RequestContext ctx = RequestContext.getCurrentContext();
        return ctx.getRequest().getHeader(AUTH_TOKEN);
    }

    public String getServiceId(){
        RequestContext ctx = RequestContext.getCurrentContext();

        //We might not have a service id if we are using a static, non-eureka route.
        if (ctx.get("serviceId")==null) return "";
        return ctx.get("serviceId").toString();
    }
}

现在我们启动网关服务和组织服务,调用下:
在这里插入图片描述

后置过滤器
后置过滤器是收集指标并完成与用户相关联的日志记录的理想场所,下面实现将传递给微服务的关联ID注入回用户。使用后置过滤器将关联ID注入HTTP响应首部中。后置过滤器的代码如下:

@Component
public class ResponseFilter extends ZuulFilter{
    private static final int  FILTER_ORDER=1;
    private static final boolean  SHOULD_FILTER=true;
    private static final Logger logger = LoggerFactory.getLogger(ResponseFilter.class);
    
    @Autowired
    FilterUtils filterUtils;

    @Override
    public String filterType() {
        return FilterUtils.POST_FILTER_TYPE;
    }

    @Override
    public int filterOrder() {
        return FILTER_ORDER;
    }

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

    @Override
    public Object run() {
        RequestContext ctx = RequestContext.getCurrentContext();

        logger.debug("Adding the correlation id to the outbound headers. {}", filterUtils.getCorrelationId());
        ctx.getResponse().addHeader(FilterUtils.CORRELATION_ID, filterUtils.getCorrelationId());

        logger.debug("Completing outgoing request for {}.", ctx.getRequest().getRequestURI());

        return null;
    }
}

我们调用下,看下响应中是否存在对应的首部。
在这里插入图片描述

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值