使用spring cloud 和 zuul 进行服务路由

spring cloud 和 netfix zuul

  1. 网关中可以实现的横切关注点:
    1. 静态路由;
    2. 动态路由;
    3. 验证和授权:
    4. 度量数据收集和日志记录;
  2. 构建zuul:
    1. 导入依赖包;
      <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-starter-zuul</artifactId>
      </dependency>
    2. 添加注解,有两个注解可用:

      1. @EnableZuulProxy:会自动使用eureka来通过服务ID查询服务,其核心是反向代理,捕获客户端请求然后代表客户端访问远程资源,客户端不知道最终调用的服务;

      2. @EnableZuulServer:不会加载任何zuul反向代理过滤器,也不会使用eureka进行服务发现,当我们构建自己的路由服务而不使用任何zuul预置的功能时使用该注解,如与别的服务发现引擎(如consul)进行集成时使用该注解;

    3. 添加配置与eureka进行通信;

      server:
        port: 5555
      
      eureka:
        instance:
          preferIpAddress: true
        client:
          registerWithEureka: true
          fetchRegistry: true
          serviceUrl:
              defaultZone: http://localhost:8761/eureka/
      
      
    4. 为所有服务调用添加前缀;

      zuul:
        prefix:  /api
  3. 在zuul中配置路由,有几种zuul路由机制,可以通过http://ip:port/routes查看所有的路径;

    1. 通服务发现自动映射路由:不需要通过配置,zuul可以根据其服务ID自动路由请求,如果没有指定任何路由,zuul将根据eureka中的服务ID进行路由;

      1. http://ip:port/服务ID/具体的url请求:端口后面第一个被用于查找具体服务,后面的则是具体的路径;

      2. 关闭个别服务或全部服务的自动路径映射;

        
        zuul:
          #屏蔽全部服务的自动路由,则设置'*'
          ignored-services: '服务名1','服务名2'  
    2. 使用服务发现手动映射路由;

      zuul:
        routes:
          # 服务名: /路径/** 
          organizationservice: /organization/**
      

      自动映射和手动映射的区别:在使用自动映射路由时,zuul只会基于eureka服务ID来公开服务,如果服务实例没有运行,zuul将不会公开该服务的路由,而在没有使用eureka注册服务实例的情况下,手动映射路由仍然会将请求映射到服务ID。

    3. 使用静态URL手动映射路由,可以用了路由不受eureka管理的服务;

      1. 不使用负载均衡:

        zuul:
          routes:
            #服务名
            organizationservice:   #zuul用于在内部识别服务的关键字
              path: /organization/**  #静态路由
              url: http://ip:port   #服务的静态实例,将被直接调用,而不是zuul通过eureka调用

        此时只能有一条路径,而不能使用负载均衡路由到各个实例。

      2. 开启rabbon负载均衡,此时需要禁用掉ribbon和eureka的集成,并列出ribbon进行负载均衡的各个实例;

        #在ribbon中禁用掉eureka支持
        ribbon:
          eureka:
            enabled: false
        zuul:
          routes:
            organizationservice:   
              path: /organization/**  #静态路由
              serviceId: organizationservice  #定义一个服务ID,该服务ID用于在ribbon中查找服务
        organizationservice:  #与serviceId一致
          ribbon:
            listOfServices: http://ip:8081,http://ip:8082  #指定请求会路由到的服务列表

        注意:(1)静态路由并在ribbon中禁用eureka会造成一个问题,那就是禁用了对通过zuul网关运行的所有服务的ribbon支持,意味着eureka会承受跟多的负载,因为zuul无法使用ribbon来缓存服务的查找。

        (2)ribbon不会在每次发出调用时都去调用eureka,它会在本地缓存服务实例的位置,然后定期检查eureka是否有变化,缺少了ribbon,zuul每次需要解析服务的位置时都需要调用eureka来进行定位。
    4. zuul和服务超时,zuul使用netfix的hystrix和ribbon库来防止长时间运行的服务调用影响网关的性能;

      1. hystrix默认的超时间时间为1s,通过以下配置进行更改:

        hystrix:
          command:
            default:
              execution:
                isolation:
                  thread:
                    timeoutInMilliseconds: 2500
            服务ID:   #通过替代defalut来设置某个服务单独的hystrix超时时间
              execution:
                isolation:
                  thread:
                    timeoutInMilliseconds: 3000
      2. ribbon的默认超时时间为5s,可以通过以下配置进行修改:

        #修改某个服务的ribbon超时时间
        服务ID.ribbon.ReadTimeout: 7000  
  4. zuul的真正威力:过滤器;

    1. servlet过滤器和Spring Aspect被本地化为特定服务,而zuul过滤器允许为通过zuul路由的所有服务实现横切关注点;

    2. zuul支持三种类型的过滤器:

      1. 前置过滤器:将请求发送到实际目的地前调用,可以对http请求进行检查和修改,但是前置过滤器不能进行重定向到别的端点或服务;

      2. 后置过滤器:在调用目标服务后来自目标服务的响应会经过该过滤器,可用来检查和修改来自被调用服务的响应,通常用来记录从目标服务返回的响应、处理错误或审核对敏感信息的响应;

      3. 路由过滤器:可以将请求路由到其他服务,路由过滤器不能执行http重定向,而是会终止传入的http请求,然后创建新请求并代表原始调用者调用路由;

    3. 构建前置过滤器,通过扩展ZuulFilter实现一个从zuul流出的每个请求都有关联ID; 

      package com.thoughtmechanix.zuulsvr.filters;
      
      import com.netflix.zuul.ZuulFilter;
      import com.netflix.zuul.context.RequestContext;
      import org.springframework.stereotype.Component;
      
      /**
       * 所有zuul过滤器必须扩展ZuulFilter类
       */
      @Component
      public class TrackingFilter extends ZuulFilter {
      
          /**
           * 定义过滤器类型,前置、后置、路由
           */
          @Override
          public String filterType() {
              return "pre";
          }
      
          /**
           * 定义过滤器执行顺序
           */
          @Override
          public int filterOrder() {
              return 1;
          }
      
          /**
           * 是否要执行过滤器
           */
          public boolean shouldFilter() {
              return true;
          }
      
          /**
           * 每次服务通过过滤器时执行run方法
           */
          public Object run() {
              if (!isCorrelationIdPresent()) {
                  setCorrelationId(generateCorrelationId());
              }
              RequestContext ctx = RequestContext.getCurrentContext();
              return null;
          }
      
          private String getCorrelationId() {
              RequestContext ctx = RequestContext.getCurrentContext();
      
              if (ctx.getRequest().getHeader("tmx-correlation-id") != null) {
                  return ctx.getRequest().getHeader("tmx-correlation-id");
              } else {
                  return ctx.getZuulRequestHeaders().get("tmx-correlation-id");
              }
          }
      
          private void setCorrelationId(String correlationId) {
              RequestContext ctx = RequestContext.getCurrentContext();
              ctx.addZuulRequestHeader("tmx-correlation-id", correlationId);
          }
      
          private boolean isCorrelationIdPresent() {
              return getCorrelationId() != null;
          }
      
          private String generateCorrelationId() {
              return java.util.UUID.randomUUID().toString();
          }
      }

       注意:(1)在一般的spring mvc和spring boot服务中,RequestContext是org.springframework.web.servletsupport.RequestContext类型的,而zuul提供了一个专门的RequestContext(com.netflix.zuul.context.RequestContext),它具有额外几个方法拉访问zuul特定的值。

      (2)zuul不允许直接添加或修改传入请求中的HTTP头部,要向http头部添加请求头,应使用RequestContext 的addZuulRequestHeader()方法,该方法将维护一个单独的HTTP头部映射,当zuul服务器调用目标服务是,包含在ZuulRequestHeader映射中的数据将被合并。
    4. 当zuul调用目标服务时,在目标服务调用中使用前置过滤器添加的关联ID;

      1. 要求:被zuul调用的服务可以很容易访问到关联ID;被调用的服务在调用其他服务时也需要将关联ID传递下去。

      2. 定义servlet过滤器,拦截进入服务的请求,获取头部信息;

        package com.thoughtmechanix.licenses.utils;
        
        import org.springframework.stereotype.Component;
        
        import javax.servlet.Filter;
        import javax.servlet.FilterChain;
        import javax.servlet.FilterConfig;
        import javax.servlet.ServletException;
        import javax.servlet.ServletRequest;
        import javax.servlet.ServletResponse;
        import javax.servlet.http.HttpServletRequest;
        import java.io.IOException;
        
        @Component
        public class UserContextFilter implements Filter {
        
            @Override
            public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain)
                    throws IOException, ServletException {
                HttpServletRequest httpServletRequest = (HttpServletRequest) servletRequest;
        
                //获取http头部信息,并将其映射到UserContext类(pojo),报错存头部获取的信息,方便之后使用
                UserContextHolder.getContext().setCorrelationId(httpServletRequest.getHeader("tmx-correlation-id"));
                UserContextHolder.getContext().setUserId(httpServletRequest.getHeader("tmx-user-id"));
                UserContextHolder.getContext().setAuthToken(httpServletRequest.getHeader("tmx-auth-token"));
                UserContextHolder.getContext().setOrgId(httpServletRequest.getHeader("tmx-org-id"));
        
                filterChain.doFilter(httpServletRequest, servletResponse);
            }
        
            @Override
            public void init(FilterConfig filterConfig) throws ServletException {
            }
        
            @Override
            public void destroy() {
            }
        }

        UserContextHolder用户保存微服务处理的单个服务客户端请求的HTTP头部信息,通过保存在ThreadLocal中,确保用户请求的线程获取数据;

        package com.thoughtmechanix.licenses.utils;
        
        
        import org.springframework.util.Assert;
        
        public class UserContextHolder {
            private static final ThreadLocal<UserContext> userContext = new ThreadLocal<UserContext>();
        
            public static final UserContext getContext(){
                UserContext context = userContext.get();
        
                if (context == null) {
                    context = createEmptyContext();
                    userContext.set(context);
                }
                return userContext.get();
            }
        
            public static final void setContext(UserContext context) {
                Assert.notNull(context, "Only non-null UserContext instances are permitted");
                userContext.set(context);
            }
        
            public static final UserContext createEmptyContext(){
                return new UserContext();
            }
        }
        

        UserContext简单java对象;

        package com.thoughtmechanix.licenses.utils;
        
        import lombok.Data;
        import org.springframework.stereotype.Component;
        
        @Component
        @Data
        public class UserContext {
        
            private String correlationId= new String();
            private String authToken= new String();
            private String userId = new String();
            private String orgId = new String();
        
        }
      3. 自定义TestTemplate和ClientHttpRequestIntercept,确保关联ID被传播;

        1. 定义拦截器,添加HTTP头部;

          package com.thoughtmechanix.licenses.utils;
          
          import org.springframework.http.HttpHeaders;
          import org.springframework.http.HttpRequest;
          import org.springframework.http.client.ClientHttpRequestExecution;
          import org.springframework.http.client.ClientHttpRequestInterceptor;
          import org.springframework.http.client.ClientHttpResponse;
          
          import java.io.IOException;
          
          public class UserContextInterceptor implements ClientHttpRequestInterceptor {
              /**
               * 该方法在RestTemplate发生实际的HTTP服务调用之前被调用
               */
              @Override
              public ClientHttpResponse intercept(
                      HttpRequest request, byte[] body, ClientHttpRequestExecution execution)
                      throws IOException {
          
                  HttpHeaders headers = request.getHeaders();
                  headers.add("tmx-correlation-id", UserContextHolder.getContext().getCorrelationId());
                  headers.add("tmx-auth-token", UserContextHolder.getContext().getAuthToken());
          
                  return execution.execute(request, body);
              }
          }
        2. 定义RestTemplate  bean,将上述拦截器添加进去;

          package com.thoughtmechanix.licenses;
          
          import com.thoughtmechanix.licenses.utils.UserContextInterceptor;
          import org.springframework.boot.SpringApplication;
          import org.springframework.boot.autoconfigure.SpringBootApplication;
          import org.springframework.cloud.client.circuitbreaker.EnableCircuitBreaker;
          import org.springframework.cloud.client.loadbalancer.LoadBalanced;
          import org.springframework.cloud.netflix.eureka.EnableEurekaClient;
          import org.springframework.context.annotation.Bean;
          import org.springframework.web.client.RestTemplate;
          
          import java.util.Collections;
          import java.util.List;
          
          @SpringBootApplication
          @EnableEurekaClient
          @EnableCircuitBreaker
          public class Application {
          
              @LoadBalanced //该注解表明这个restTemplate要使用ribbon
              @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(Application.class, args);
              }
          }
          
    5. 定义后置过滤器,获取目标请求响应中的关联ID,并将其注入HTTP头部返回给服务调用者(zuul会结束目标http响应,并创建新的http响应返回给调用者,因此需要手动将目标响应中的头信息返回给服务调用者);

      package com.thoughtmechanix.zuulsvr.filters;
      
      
      import com.netflix.zuul.ZuulFilter;
      import com.netflix.zuul.context.RequestContext;
      import org.springframework.stereotype.Component;
      
      @Component
      public class ResponseFilter extends ZuulFilter {
      
          @Override
          public String filterType() {
              return "post";
          }
      
          @Override
          public int filterOrder() {
              return 1;
          }
      
          @Override
          public boolean shouldFilter() {
              return true;
          }
      
          @Override
          public Object run() {
              RequestContext ctx = RequestContext.getCurrentContext();
              //获取原始HTTP请求中传入的关联ID,并将它注入到响应中
              ctx.getResponse().addHeader("tmx-correlation-id", getCorrelationId());
              return null;
          }
      
          public String getCorrelationId() {
              RequestContext ctx = RequestContext.getCurrentContext();
              if (ctx.getRequest().getHeader("tmx-correlation-id") != null) {
                  return ctx.getRequest().getHeader("tmx-correlation-id");
              } else {
                  return ctx.getZuulRequestHeaders().get("tmx-correlation-id");
              }
          }
      }
      
    6. 构建动态路由过滤器,基本用不到,有点复杂,不看也罢,此处只做记录;

      package com.thoughtmechanix.zuulsvr.filters;
      
      
      import com.netflix.zuul.ZuulFilter;
      import com.netflix.zuul.context.RequestContext;
      import com.thoughtmechanix.zuulsvr.model.AbTestingRoute;
      import org.apache.http.Header;
      import org.apache.http.HttpHost;
      import org.apache.http.HttpRequest;
      import org.apache.http.HttpResponse;
      import org.apache.http.client.HttpClient;
      import org.apache.http.client.methods.HttpPatch;
      import org.apache.http.client.methods.HttpPost;
      import org.apache.http.client.methods.HttpPut;
      import org.apache.http.entity.ContentType;
      import org.apache.http.entity.InputStreamEntity;
      import org.apache.http.impl.client.CloseableHttpClient;
      import org.apache.http.impl.client.HttpClients;
      import org.apache.http.message.BasicHeader;
      import org.apache.http.message.BasicHttpRequest;
      import org.springframework.beans.factory.annotation.Autowired;
      import org.springframework.cloud.netflix.zuul.filters.ProxyRequestHelper;
      import org.springframework.http.HttpMethod;
      import org.springframework.http.HttpStatus;
      import org.springframework.http.ResponseEntity;
      import org.springframework.stereotype.Component;
      import org.springframework.util.LinkedMultiValueMap;
      import org.springframework.util.MultiValueMap;
      import org.springframework.web.client.HttpClientErrorException;
      import org.springframework.web.client.RestTemplate;
      
      
      import javax.servlet.http.HttpServletRequest;
      import javax.servlet.http.HttpServletResponse;
      import java.io.IOException;
      import java.io.InputStream;
      import java.net.URL;
      import java.util.ArrayList;
      import java.util.List;
      import java.util.Map;
      import java.util.Random;
      
      @Component
      public class SpecialRoutesFilter extends ZuulFilter {
      
          @Autowired
          RestTemplate restTemplate;
      
          //spring cloud 提供的类,带有用于代理服务请求的辅助方法
          private ProxyRequestHelper helper = new ProxyRequestHelper();
      
          @Override
          public Object run() {
              RequestContext ctx = RequestContext.getCurrentContext();
      
              boolean flag = new Random().nextBoolean();
      
              //根据逻辑条件(此处可根据具体情况进行替代)判断是否进行动态路由
              if (flag) {
                  String route = "路由到服务的完整路径";
                  forwardToSpecialRoute(route);
              }
              return null;
          }
      
          private void forwardToSpecialRoute(String route) {
              RequestContext context = RequestContext.getCurrentContext();
              HttpServletRequest request = context.getRequest();
      
              //创建将发送到服务的所有HTTP请求头部的副本
              MultiValueMap<String, String> headers = this.helper.buildZuulRequestHeaders(request);
              //创建所有HTTP请求参数的副本
              MultiValueMap<String, String> params = this.helper.buildZuulRequestQueryParams(request);
              String verb =  request.getMethod().toUpperCase();
              //创建将被转发到替代服务的HTTP主体的副本
              InputStream requestEntity =  null;
              try {
                  requestEntity = request.getInputStream();
              } catch (IOException ex) {
                  // no requestBody is ok.
              }
              if (request.getContentLength() < 0) {
                  context.setChunkedRequestBody();
              }
      
              this.helper.addIgnoredHeaders();
              CloseableHttpClient httpClient = null;
              HttpResponse response = null;
      
              try {
                  httpClient = HttpClients.createDefault();
                  //使用forward辅助方法调用替代服务
                  response = forward(httpClient, verb, route, request, headers,
                          params, requestEntity);
                  //通过setResponse辅助方法将服务调用的结果保存会回zuul服务器
                  setResponse(response);
              } catch (Exception ex) {
                  ex.printStackTrace();
      
              } finally {
                  try {
                      assert httpClient != null;
                      httpClient.close();
                  } catch (IOException ignored) {
                  }
              }
          }
      
          private HttpResponse forward(HttpClient httpclient, String verb, String uri,
                                       HttpServletRequest request, MultiValueMap<String, String> headers,
                                       MultiValueMap<String, String> params, InputStream requestEntity)
                  throws Exception {
              URL host = new URL(uri);
              HttpHost httpHost = new HttpHost(host.getHost(), host.getPort(), host.getProtocol());
      
              HttpRequest httpRequest;
              int contentLength = request.getContentLength();
              InputStreamEntity entity = new InputStreamEntity(requestEntity, contentLength,
                      request.getContentType() != null
                              ? ContentType.create(request.getContentType()) : null);
              switch (verb.toUpperCase()) {
                  case "POST":
                      HttpPost httpPost = new HttpPost(uri);
                      httpRequest = httpPost;
                      httpPost.setEntity(entity);
                      break;
                  case "PUT":
                      HttpPut httpPut = new HttpPut(uri);
                      httpRequest = httpPut;
                      httpPut.setEntity(entity);
                      break;
                  case "PATCH":
                      HttpPatch httpPatch = new HttpPatch(uri);
                      httpRequest = httpPatch;
                      httpPatch.setEntity(entity);
                      break;
                  default:
                      httpRequest = new BasicHttpRequest(verb, uri);
      
              }
              try {
                  httpRequest.setHeaders(convertHeaders(headers));
                  HttpResponse zuulResponse = forwardRequest(httpclient, httpHost, httpRequest);
      
                  return zuulResponse;
              } finally {
              }
          }
      
          private void setResponse(HttpResponse response) throws IOException {
              this.helper.setResponse(response.getStatusLine().getStatusCode(),
                      response.getEntity() == null ? null : response.getEntity().getContent(),
                      revertHeaders(response.getAllHeaders()));
          }
          
          @Override
          public String filterType() {
              return "route";
          }
      
          @Override
          public int filterOrder() {
              return 1;
          }
      
          @Override
          public boolean shouldFilter() {
              return true;
          }
      
          private Header[] convertHeaders(MultiValueMap<String, String> headers) {
              List<Header> list = new ArrayList<>();
              for (String name : headers.keySet()) {
                  for (String value : headers.get(name)) {
                      list.add(new BasicHeader(name, value));
                  }
              }
              return list.toArray(new BasicHeader[0]);
          }
      
          private HttpResponse forwardRequest(HttpClient httpclient, HttpHost httpHost,
                                              HttpRequest httpRequest) throws IOException {
              return httpclient.execute(httpHost, httpRequest);
          }
      
      
          private MultiValueMap<String, String> revertHeaders(Header[] headers) {
              MultiValueMap<String, String> map = new LinkedMultiValueMap<String, String>();
              for (Header header : headers) {
                  String name = header.getName();
                  if (!map.containsKey(name)) {
                      map.put(name, new ArrayList<String>());
                  }
                  map.get(name).add(header.getValue());
              }
              return map;
          }
      }
      

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值