spring微服务学习6

一、服务网关
使用Spring Cloud 和 Zuul进行服务路由,所有调用都通过服务网关进行路由, 然后被路由到最终目的地。
服务网关充当服务客户端和被调用的服务之间的中介。有了服务网关,服务客户端永远不会直接调用单个服务 URL ,而是将所有调用都放到服务网关上。

服务网关中实现的横切关注点包括以下几个:

静态路由
动态路由
验证和授权
度量数据收集和日志记录

构建服务阿关实现时,要注意以下几点:

1.在单独的服务组前面,负载均衡器仍然很有用。在这种情况下,将负载均衡器放到多个服务网关实例前面的是一个恰当的设计,它确保服务网关实现可以伸缩。
2.要保持为服务网关编写的代码是无状态的。不要在内存中为服务网关存储任何信息。
3.要保持为服务网关编写的代码是轻量的。

二、Spring Cloud和 Netflix Zuul
Zuul 是一个服务网关。Zuul 提供了许多功能,具体包括以下几个:

1.将应用程序 中的所有服务的路由映射到一个 URL。Zuul 不局限于一个 URL。Zuul 最常见的用例是构建一个单一的入口点,所有服务客户端调用将经过这个入口点。
2.构建可以对通过网关的请求进行检查和操作的过滤器。这些过滤器允许在代码中注入策略执行点,以一致的方式对所有服务调用执行大量操作。

使用Zuul,以下3步骤:

1、建立一个 Zuul Spring Boot 项目
pom.xml添加依赖:

<dependency>
     <groupId>org.springframework.cloud</groupId>
     <artifactId>spring-cloud-starter-netflix-zuul</artifactId>
     <version>2.1.2.RELEASE</version>
</dependency>

这个依赖项告诉 Spring Cloud 框架,该服务将运行 Zuul ,并适当地初始化 Zuul。

2、 Zuul 服务使用 Spring Cloud 注解
在引导类上添加@EnableZuulProxy注解,使服务成为一个Zuul服务器

3、配置Zuul 与Eureka 进行通信
application.yml

#配置Zuul服务器与Eureka通信
eureka:
  instance:
    preferIpAddress: true
  client:
    registerWithEureka: true
    fetchRegistry: true
    serviceUrl:
        defaultZone: http://localhost:8761/eureka/

三、在Zuul中配置路由
Zuul的核心是一个反向代理。反向代理是一个中间服务器,它位于尝试访问资源的客户端和资源本身之间。客户端甚至不知道它正与代理之外的服务器进行通信, 反向代理负责捕获客户端的请求,然后代表客户端调用远程资源。
在微服务架构的情况下, Zuul (反向代理)从客户端接收微服务调用并将其转发给下游服务。服务客户端认为它只与 Zuul 通信。
Zuul必须知道如何将进来的调用映射到下游路由。
Zuul 有几种机制包括:

1.通过服务发现自动映射路由。
2.使用服务发现手动映射路由。
3.使用静态 URL 手动映射路由。

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

Zuul 的所有路由映射都是通过在 项目如:zuulsvr/src/main/resources/application.yml 文件中定义路由来完成的。
Zuul 可以根据其服务 ID 自动路由请求,而不需要配置。 如果没有指定任何路由,Zuul 将自动使用正在调用的服务的 Eureka 服务 ID 并将其映射到下游服务实例。

如:调用 organizationservice 并通过 Zuul 使用自动路由,则可以使用以下 URL 作为端点

application.yml:

server:
  port: 5555

  #Setting logging levels
logging:
    level:
      com.netflix: WARN
      org.springframework.web: WARN
      com.example.zuulsvr: DEBUG

eureka:
  instance:
    preferIpAddress: true
  client:
    registerWithEureka: true
    fetchRegistry: true
    serviceUrl:
        defaultZone: http://localhost:8761/eureka/
http://localhost:5555/organizationservice/vl/organizations/xxxxx-xxxxxx

Zuul 服务器可通过 http: //localhost :55 55 进行访问。
该服务中的端点路径的第一部分表示正在尝试调用的服务(organizationservice)。

使用使用带有 Eureka uul 的优点在于,不仅可以拥有一个可以发出调用的单个端点,有了 Eureka ,还可以添加和删除服务的实例,而无须修改 Zuul。例如,可以向 Eureka加新服务, Zuul 将自动路由到该服务,因为 Zuul 会与 Eureka 进行通信,了解实际服务端点的位置。

通过Zuul服务器访问/routes,如以下地址,将返回服务中所有映射的列表:

http://localhost:5555/routes
{
	"/organizationservice/**": "organizationservice"
}
前面的key是Zuul 中的服务路由是基于Eureka 的服务ID 自动创建的。
后面那个value是路由所映射的Eureka服务ID。

2、使用服务发现手动映射路由
如果希望通过缩短组织名称来简化路由,而不是通过默认路由/organizationservice/vl/organizations/{organization -id 。在 Zuul 中访问服务 可以通过在项目 zuulsvr,src/main/resources/application.yml 中手动定义路由映射来做到,如下:
application.yml:

zuul:
  routes:
  	organizationservice: /organization/**

加上以上配置后,就可以通过访问/organization/vl/organizations/{organization-id 路由来访问服务了。

通过Zuul服务器访问/routes,可以看到做了映射。

{
	"/organization/**":"organizationservice",
	"/organizationservice/**": "organizationservice"
}

注:
在使用自动路由映射时, Zuul 只基于 Eureka 服务来公开服务,如果服务的实例没有在运行,Zuul 将不会公开该服务的路由。 然而,如果在没有使用 Eureka 注册服务实例的情况下 ,手动将路由映射到服务发现ID ,那么 Zuul 仍然会显示这条路由。 如果尝试为不存在的服务调用路由Zuul 将返回 500 错误。

如果想要排除 Eureka 服务 ID 路由的 自动映射,只提供自定义服务,可以向application.yml添加一个额外的Zuul参数ignored-services。
如:
application.yml:

zuul : 
  ignored-services : 'organizationservice'
  routes : 
		organizationservice : /organization/**

ignored-services属性允许想要从注册中排 Eureka 服务ID的列表,该列表以逗号分隔。

通过Zuul服务器访问/routes,可以看到做了映射。

{
	"/organizationservice/**": "organizationservice"
}

如果要排除所有基于 Eureka 的路由,可以将 ignored-services 性设置为"*"。
为所有的服务调用添加前缀。
application.yml:

zuul:
  prefix:  /api
  routes:
  	organizationservice: /organization/**

3、使用静态 URL 手动映射路由
Zuul 可以用来路由那些不受 Eureka 管理的服务 。在这种情况下,可以建立 Zuul 直接路由到一个静态定义的 URL。
1)将licenseservice服务映射到静态路由

zuul : 
  routes: 
	licensestatic: 		# 在内部识别服务的关键字
	  path: /licensestatic/** 
      url: http://licenseservice-static : 8081		 #已建立的服务的静态实例,它将被直接调用,而不是由Zuul通过Eureka调用

2)将licenseservice服务静态映射到多个路由
application.yml:

zuul:
  routes:
    licensestatic:
      path: /licensestatic/**
      serviceId: licensestatic
ribbon:
  eureka:
    enabled: false  #在Ribbon中禁用Eureka支持
licensestatic:
  ribbon:
    listOfServices: http://licenseservice-static1:8081  # 指定请求会路由到的服务器列表
      http://licenseservice-static2:8082

4、动态重新加载路由配置
可以使用 Spring Cloud Config 来外部化Zuul 路由

5、 Zuul 和服务超时

Zuul 使用 Netflix Hystrix Ribbon 库,来帮助防止长时间运行的服务调用影响服务网关的性能 。在默认情况下,对于任何需要用超过 1s的时间(这是 Hystrix 默认值)来处理请求的调用, Zuul 将终止井返回一个 HTTP 500 错误。
如:可以通过将默认的 Hystrix 超时设置为 2.5s,就可以在 Zuul Spring Cloud 置文件中使用以下配置:

hystrix.command.default.execution.isolation.thread.timeoutinMilliseconds: 2500

如果需要为特定服务设置 Hystrix 超时,可以使用需要覆盖超时的服务的 Eureka 服务ID名称来替换属性的default部分。

如: 如果想要将 licensingservice 的超时更改为 3s。并让其他服务使用默认的 Hystrix 超时

hystrix.command.default.licensingservice.execution.isolation thread.time utinMilliseconds: 3000

四、zuul过滤器
Zuul 支持以下3 种类型的过滤器:

过滤器描述
前置过滤器前置过滤器在 Zuul 将实际请求发送到目的地之前被调用。前置过滤器通常执行确保服务具有一致的消息格式
后置过滤器后置过滤器在目标服务被调用并将响应发送回客户端后被调用。通常后置过滤器会用来记录从目标服务返回的响应、处理错误或审核对敏感信息的响应
路由过滤器路由过滤器用于在调用目标服务之前拦截调用。通常使用路由过滤器来确定是否需要进行某些级别的动态路由

处理服务客户端请求时,前置过滤器、后置过滤器和路由过滤器组合,如:

1)在请求进入 Zuul 网关时, Zuul 调用所有在 Zuul 网关中定义的前置过滤器。
2)在针对 Zuul 传入请求执行前置过滤器之后, Zuul 将执行已定义的路由过滤器
3)路由过滤器可以将服务调用重定向到 Zuul 服务器被配置的发送路由以外的位置
4)如果路由过滤器没有动态、地将调用者重定向到新路由, Zuul 务器将发送到最初的目标
5)目标服务被调用后 Zuul 后置过滤器将被调用。

1、用于生成关联的ID的Zuul前置过滤器

TrackingFilter :

package com.example.zuulsvr.filters;

import com.netflix.zuul.ZuulFilter;
import com.netflix.zuul.context.RequestContext;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

@Component
public class TrackingFilter extends ZuulFilter{		// 所有 Zuul 过滤器必须扩展
    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;  // 在所有过滤器中使用的常用方法都封装在 filterUtil 类中

	// filterType()方法用于告诉 Zuul,该过滤器是前置过滤器、路由过滤器、还是后置过滤器
    @Override
    public String filterType() {	
        return FilterUtils.PRE_FILTER_TYPE;
    }

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

	// 指示该过滤器是否要执行
    public boolean shouldFilter() { 
        return SHOULD_FILTER;
    }

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

      return false;
    }

    // 检查correlation-id 是否存在 并且可以生成关联ID的GUID值
    private String generateCorrelationId(){
        return java.util.UUID.randomUUID().toString();
    }

    // run 方法是每次服务通过过滤器时执行的代码 run ()方法检查。
    // CorrelationId是否存在,如果不存在则生成一个关联值。
    // 并设置 HTTP CorrelationId
    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:

package com.example.zuulsvr.filters;

import com.netflix.zuul.context.RequestContext;
import org.springframework.stereotype.Component;

@Component
public class FilterUtils {

    public static final String CORRELATION_ID = "my-correlation-id";
    public static final String AUTH_TOKEN     = "my-auth-token";
    public static final String USER_ID        = "my-user-id";
    public static final String ORG_ID         = "my-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();
    }


}

2、在服务调用中使用关联 ID
正在被调用的微服务可以很容易访问关联 ID;
下游服务调用微服务时可能也会将关联ID 传播到下游调用中。
要实现这一点 需要为每个微服务构建一组 3个类。
从传入的 HTTP请求中读取关联 ID (以及稍后添加的其他信息),并将它映射到可以由应用程序中的业务逻辑轻松
访问和使用的类,然后确保关联 被传播到任何下游服务调用。
如:

1)当通过 Zuul 网关对licenseservice服务进行调用时, TrackingFilter 会为所有进入 Zuul调用在传入的 HTTP 首部中注入一个关联 ID。
2)UserContextFilter 类是一个自定义的 HTTP servlet 过滤器。它将关联 ID 映射到UserContext 类。UserContext 存储在本地线程存储中,以便稍后在调用中使用。
3)licenseservice服务业务逻辑需要执行对organization服务的调用。
4)RestTernplate 用于调用organization服务。RestTemplate 将使用自定义的 Spring 拦截器类(UserContextinterceptor )将关联 ID 作为 HTTP 首部注入出站调用。

1) UserContextFilter :拦截传入的 HTTP 请求
要构建一个类是 UserContextFilter 类是一个 HTTP servlet 滤器,它将拦截进入服务的所有传入 Hπp 请求,并将关联 ID(和其他 一些值) 请求映射 UserContext类。
UserContextFilter 用于将 HTTP 首部的值映射到 Java类 UserContext
如:

UserContextFilter :

package com.example.zuulsvr.utils;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
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 {	// 通过实现javax.servlet.Filter 接口来被spring注册和支持的。
    private static final Logger logger = LoggerFactory.getLogger(UserContextFilter.class);

    @Override
    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain)
            throws IOException, ServletException {
        HttpServletRequest httpServletRequest = (HttpServletRequest) servletRequest;
		
		// 过滤器从首部中检索关联ID,并将值设置在 UserContext
        UserContextHolder.getContext().setCorrelationId(  httpServletRequest.getHeader(UserContext.CORRELATION_ID) );
        UserContextHolder.getContext().setUserId(httpServletRequest.getHeader(UserContext.USER_ID));
        UserContextHolder.getContext().setAuthToken(httpServletRequest.getHeader(UserContext.AUTH_TOKEN));
        UserContextHolder.getContext().setOrgId(httpServletRequest.getHeader(UserContext.ORG_ID));

        logger.debug("Special Routes Service Incoming Correlation id: {}", UserContextHolder.getContext().getCorrelationId());

        filterChain.doFilter(httpServletRequest, servletResponse);
    }
	
    @Override
    public void init(FilterConfig filterConfig) throws ServletException {}

    @Override
    public void destroy() {}
}

2) UserContext :便服务易于访问 HTTP 首部

UserContext 类:将 HTTP首都值存储在 UserContext 类中

package com.example.zuulsvr.utils;

import org.springframework.stereotype.Component;

@Component
public class UserContext {
    public static final String CORRELATION_ID = "my-correlation-id";
    public static final String AUTH_TOKEN     = "my-auth-token";
    public static final String USER_ID        = "my-user-id";
    public static final String ORG_ID         = "my-org-id";

    private String correlationId= new String();
    private String authToken= new String();
    private String userId = new String();
    private String orgId = new String();

    public String getCorrelationId() { return correlationId;}
    public void setCorrelationId(String correlationId) {
        this.correlationId = correlationId;
    }

    public String getAuthToken() {
        return authToken;
    }

    public void setAuthToken(String authToken) {
        this.authToken = authToken;
    }

    public String getUserId() {
        return userId;
    }

    public void setUserId(String userId) {
        this.userId = userId;
    }

    public String getOrgId() {
        return orgId;
    }

    public void setOrgId(String orgId) {
        this.orgId = orgId;
    }

}

UserContextHolder :UserContextHolder 类将 UserContext 存储在 ThreadLocal 中

package com.example.zuulsvr.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();
    }
}

3)自定 RestTemplate 和UserContextInteceptor 确保关联 ID 被传播

UserContextInterceptor :所有传出的微服务调用都会注入关联ID

package com.example.zuulsvr.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 {
	// intercept()方法在RestTemplate发生在实际的 HTTP 服务调用之前被调用
    @Override
    public ClientHttpResponse intercept(
            HttpRequest request, byte[] body, ClientHttpRequestExecution execution)
            throws IOException {

        HttpHeaders headers = request.getHeaders();
        headers.add(UserContext.CORRELATION_ID, UserContextHolder.getContext().getCorrelationId());
        headers.add(UserContext.AUTH_TOKEN, UserContextHolder.getContext().getAuthToken());

        return execution.execute(request, body);
    }
}

ZuulServerApplication :将UserContextInterceptor 添加到RestTemplate类

package com.example.zuulsvr;


import com.example.zuulsvr.utils.UserContextInterceptor;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.loadbalancer.LoadBalanced;
import org.springframework.cloud.netflix.zuul.EnableZuulProxy;
import org.springframework.context.annotation.Bean;
import org.springframework.web.client.RestTemplate;

import java.util.Collections;
import java.util.List;


@SpringBootApplication
@EnableZuulProxy
public class ZuulServerApplication {

	// 每当使用 @Autowired 注解将 RestTemplate 注入一个类,就会使用 这里创建的RestTemplete ,附带UserContextInterceptor。
    @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);
    }
}

五、构建接收关联 ID 的后置过滤器

使用 Zuul 后置过滤器将关联 ID 注入 HTTP 响应首部中,该 HTTP 响应 部传回给服务调用者。这样,就可以将 ID 传回给调用者,而无需接触消息体。

ResponseFilter :

package com.example.zuulsvr.filters;


import com.netflix.zuul.ZuulFilter;
import com.netflix.zuul.context.RequestContext;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

@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() {	// 要构建一个后置过滤器,设置为POST_FILTER_TYPE
        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());
        // 获取原始HTTP请求中传入的关联ID,并将它注入响应中
        ctx.getResponse().addHeader(FilterUtils.CORRELATION_ID, filterUtils.getCorrelationId());

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

        return null;
    }
}

六、构建动态路由过滤器

在服务客户端调用 Zuul 的服务时, SpecialRoutesFilter 会执行以下流程:

1.SpecialRoutesFilter 检索被调用服务的服务 ID
2.SpecialRoutesFilter 调用 SpecialRoutes 服务
3.然后 SpecialRoutesFilter 生成一个随机数并将它与 SpecialRoutes 服务返回的权重进行比较。如果随机生成的数字大于替代端点权重的值,那么 SpecialRoutesFilter
4.如果 SpecialRoutesFilter 将请求发送到服务的新版本, Zuu 会维持最初的预定义管道,并通过已定义的后 过滤器将响应从替代服务端点发送回来

1、路由过滤器的骨架
创建一个SpecialRoutesFilter继承自 ZuulFilter

package com.example.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 {
    private static final int FILTER_ORDER =  1;
    private static final boolean SHOULD_FILTER =true;

    @Autowired
    FilterUtils filterUtils;

    @Autowired
    RestTemplate restTemplate;

    @Override
    public String filterType() {
        return filterUtils.ROUTE_FILTER_TYPE;
    }

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

    @Override
    public boolean shouldFilter() {
        return SHOULD_FILTER;
    }
	
	// helper 变量类是ProxyRequestHelper类型的一个实例变量。
    private ProxyRequestHelper helper = new ProxyRequestHelper();

    private AbTestingRoute getAbRoutingInfo(String serviceName){
        ResponseEntity<AbTestingRoute> restExchange = null;
        try {
    	    // 调用specialroutesservice端点
    	    // 调用 SpecialRouteservice 以查看路由记录是否存在
            restExchange = restTemplate.exchange(
                             "http://specialroutesservice/v1/route/abtesting/{serviceName}",
                             HttpMethod.GET,
                             null, AbTestingRoute.class, serviceName);
        }
        catch(HttpClientErrorException ex){	  // 如果路由服务没有找到记录(返回HTTP码404),该方法将返回空值。
            if (ex.getStatusCode()== HttpStatus.NOT_FOUND) return null;
            throw ex;
        }
        return restExchange.getBody();
    }

    private String buildRouteString(String oldEndpoint, String newEndpoint, String serviceName){
        int index = oldEndpoint.indexOf(serviceName);

        String strippedRoute = oldEndpoint.substring(index + serviceName.length());
        System.out.println("Target route: " + String.format("%s/%s", newEndpoint, strippedRoute));
        return String.format("%s/%s", newEndpoint, strippedRoute);
    }

    private String getVerb(HttpServletRequest request) {
        String sMethod = request.getMethod();
        return sMethod.toUpperCase();
    }

    private HttpHost getHttpHost(URL host) {
        HttpHost httpHost = new HttpHost(host.getHost(), host.getPort(),
                host.getProtocol());
        return httpHost;
    }

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

    private InputStream getRequestBody(HttpServletRequest request) {
        InputStream requestEntity = null;
        try {
            requestEntity = request.getInputStream();
        }
        catch (IOException ex) {
            // no requestBody is ok.
        }
        return requestEntity;
    }

    private void setResponse(HttpResponse response) throws IOException {
        this.helper.setResponse(response.getStatusLine().getStatusCode(),
                response.getEntity() == null ? null : response.getEntity().getContent(),
                revertHeaders(response.getAllHeaders()));
    }

    private HttpResponse forward(HttpClient httpclient, String verb, String uri,
                                 HttpServletRequest request, MultiValueMap<String, String> headers,
                                 MultiValueMap<String, String> params, InputStream requestEntity)
            throws Exception {
        Map<String, Object> info = this.helper.debug(verb, uri, headers, params,
                requestEntity);
        URL host = new URL( uri );
        HttpHost httpHost = getHttpHost(host);

        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 {
        }
    }



	// 决定是否使用替代服务路由
    public boolean useSpecialRoute(AbTestingRoute testRoute){
        Random random = new Random();
		
		// 检查路由是否为活跃状态
        if (testRoute.getActive().equals("N")) return false;
		
		// 确定是否应该使用替代服务路由
        int value = random.nextInt((10 - 1) + 1) + 1;

        if (testRoute.getWeight()<value) return true;

        return false;
    }

    @Override
    public Object run() {
        RequestContext ctx = RequestContext.getCurrentContext();
		
		// 执行对 SpecialRoutes 服务的调用,以确定该服务ID是否有路由记录
        AbTestingRoute abTestRoute = getAbRoutingInfo( filterUtils.getServiceId() );

		// useSpecialRoute() 方法将会接受路径的权重,生成一个随机数,并确定是否将请求转发到替代服务
        if (abTestRoute!=null && useSpecialRoute(abTestRoute)) {	
            String route = buildRouteString(ctx.getRequest().getRequestURI(),
                    abTestRoute.getEndpoint(),
                    ctx.get("serviceId").toString());
            forwardToSpecialRoute(route);	// 如果有路由记录,则将完整地URL(包含路径)构建到SpecialRoutes服务指定的服务位置
        }

        return null;
    }

    private void forwardToSpecialRoute(String route) {
        RequestContext context = RequestContext.getCurrentContext();
        HttpServletRequest request = context.getRequest();
		
		// 创建将发送到服务的所有Http 请求头header
        MultiValueMap<String, String> headers = this.helper
                .buildZuulRequestHeaders(request);
        // 创建所有Http请求参数的副本
        MultiValueMap<String, String> params = this.helper
                .buildZuulRequestQueryParams(request);
        String verb = getVerb(request);
        // 创建将被转发到替代服务的Http主体
        InputStream requestEntity = getRequestBody(request);
        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);
            // 将服务调用的结果保存回Zuul服务器
            setResponse(response);
        }
        catch (Exception ex ) {
            ex.printStackTrace();

        }
        finally{
            try {
                httpClient.close();
            }
            catch(IOException ex){}
        }
    }
}

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值