Spring Cloud Zuul----詳解一

過濾器

在這裏插入圖片描述

pre過濾器

過濾器 描述 解釋
ServletDetectionFilter 它的執行順序爲-3,是最先被執行的過濾器。該過濾器總是會被執行,主要用來檢測當前請求是通過Spring的DispatcherServlet處理運行,還是通過ZuulServlet來處理運行的。它的檢測結果會以布爾類型保存在當前請求上下文的isDispatcherServletRequest參數中,這樣在後續的過濾器中,我們就可以通過RequestUtils.isDispatcherServletRequest()和RequestUtils.isZuulServletRequest()方法判斷它以實現做不同的處理。 如果不是以/zuul/**開頭,則先走DispatcherServlet,獲取具體的Controller,在Controller調用ZuulServlet.service()處理;當請求中以/zuul/**開頭,請求直接走ZuulServlet,不走DispatcherServlet。主要用來應對處理大文件上傳的情況。另外,對於ZuulServlet的訪問路徑/zuul/,我們可以通過zuul.servletPath參數來進行修改。
Servlet30WrapperFilter 它的執行順序爲-2,是第二個執行的過濾器。目前的實現會對所有請求生效,主要爲了將原始的HttpServletRequest包裝成Servlet30RequestWrapper對象。 當請求是/zuul時,不會被包裝成Servlet30RequestWrapper對象,一遍FormBodyWrapperFilter使用
FormBodyWrapperFilter 它的執行順序爲-1,是第三個執行的過濾器。 該過濾器僅對兩種類請求生效,第一類是Content-Type爲application/x-www-form-urlencoded的請求,第二類是Content-Type爲multipart/form-data並且是由Spring的DispatcherServlet處理的請求(用到了ServletDetectionFilter的處理結果)。而該過濾器的主要目的是將符合要求的請求體包裝成FormBodyRequestWrapper對象。
DebugFilter 它的執行順序爲1,是第四個執行的過濾器。 該過濾器會根據配置參數zuul.debug.request和請求中的debug參數來決定是否執行過濾器中的操作。而它的具體操作內容則是將當前的請求上下文中的debugRouting和debugRequest參數設置爲true。由於在同一個請求的不同生命週期中,都可以訪問到這兩個值,所以我們在後續的各個過濾器中可以利用這兩值來定義一些debug信息,這樣當線上環境出現問題的時候,可以通過請求參數的方式來激活這些debug信息以幫助分析問題,另外,對於請求參數中的debug參數,我們也可以通過zuul.debug.parameter來進行自定義。
PreDecorationFilter 它的執行順序爲5,是pre階段最後被執行的過濾器。該過濾器會判斷當前請求上下文中是否存在forward.to和serviceId參數,如果都不存在,那麼它就會執行具體過濾器的操作(如果有一個存在的話,說明當前請求已經被處理過了,因爲這兩個信息就是根據當前請求的路由信息加載進來的)。而它的具體操作內容就是爲當前請求做一些預處理,比如:進行路由規則的匹配、在請求上下文中設置該請求的基本信息以及將路由匹配結果等一些設置信息等,這些信息將是後續過濾器進行處理的重要依據,我們可以通過RequestContext.getCurrentContext()來訪問這些信息。另外,我們還可以在該實現中找到一些對HTTP頭請求進行處理的邏輯,其中包含了一些耳熟能詳的頭域,比如:X-Forwarded-Host、X-Forwarded-Port。另外,對於這些頭域的記錄是通過zuul.addProxyHeaders參數進行控制的,而這個參數默認值爲true,所以Zuul在請求跳轉時默認地會爲請求增加X-Forwarded-*頭域,包括:X-Forwarded-Host、X-Forwarded-Port、X-Forwarded-For、X-Forwarded-Prefix、X-Forwarded-Proto。我們也可以通過設置zuul.addProxyHeaders=false關閉對這些頭域的添加動作 1、請求信息;2、敏感頭;3、判斷是否是用註冊中心還是http;4、代理頭

route過濾器

過濾器 描述 解釋
RibbonRoutingFilter 它的執行順序爲10,是route階段第一個執行的過濾器。該過濾器只對請求上下文中存在serviceId參數的請求進行處理,即只對通過serviceId配置路由規則的請求生效。而該過濾器的執行邏輯就是面向服務路由的核心,它通過使用Ribbon和Hystrix來向服務實例發起請求,並將服務實例的請求結果返回。
SimpleHostRoutingFilter 它的執行順序爲100,是route階段第二個執行的過濾器。該過濾器只對請求上下文中存在routeHost參數的請求進行處理,即只對通過url配置路由規則的請求生效。而該過濾器的執行邏輯就是直接向routeHost參數的物理地址發起請求,從源碼中我們可以知道該請求是直接通過httpclient包實現的,而沒有使用Hystrix命令進行包裝,所以這類請求並沒有線程隔離和斷路器的保護。
SendForwardFilter 它的執行順序爲500,是route階段第三個執行的過濾器。該過濾器只對請求上下文中存在forward.to參數的請求進行處理,即用來處理路由規則中的forward本地跳轉配置。

類結構

項目啓動時會加載RibbonCommandFactoryConfiguration和SpringClientFactory類到Spring容器中,在調用接口時,會在RibbonRoutingFilter中利用SpringClientFactory從Spring中加載RibbonClientConfiguration獲取RibbonLoadBalancingHttpClient和ILoadBalancer
在這裏插入圖片描述
RibbonClientConfiguration,如果項目中配置ribbon.eureka.enabled=false,則會走RibbonClientConfiguration的IPing和ServerList,不會走Eureka註冊中心,如果沒有配置或配置爲true則會走EurekaRibbonClientConfiguration,在加載Eureka註冊中心
在這裏插入圖片描述
CommonClientConfigKey:核心配置類

1、通過ZuulProxyAutoConfiguration加載pre過濾器route過濾器(RibbonRoutingFilter)
2、通過ZuulServerAutoConfiguration加載ZuulHandlerMappingZuulServlet

如果請求不是以/zuul/**開頭,則先通過DispatcherServlet,通過ZuulHandlerMapping獲取具體的Controller,在Controller調用ZuulServlet.service()處理;當請求中以/zuul/**開頭,請求直接走ZuulServlet,不走DispatcherServlet。

3、在請求到PreDecorationFilter過濾器時,通過RouteLocator加載Zuul配置信息,通過RouteLocator獲取配置文件中的Route配置信息,

  1. 判斷url是否以http開頭,是則設置;
  2. 判斷url是否以判斷url是否以forward:開頭,設置爲轉發
  3. 剩下的爲服務註冊
@Override
	public Object run() {
		RequestContext ctx = RequestContext.getCurrentContext();
		final String requestURI = this.urlPathHelper
				.getPathWithinApplication(ctx.getRequest());
		// 加載配置信息
		Route route = this.routeLocator.getMatchingRoute(requestURI);
		if (route != null) {
			// 獲取url
			String location = route.getLocation();
			if (location != null) {
				ctx.put(REQUEST_URI_KEY, route.getPath());
				ctx.put(PROXY_KEY, route.getId());
				if (!route.isCustomSensitiveHeaders()) {
					this.proxyRequestHelper.addIgnoredHeaders(
							this.properties.getSensitiveHeaders().toArray(new String[0]));
				}
				else {
					this.proxyRequestHelper.addIgnoredHeaders(
							route.getSensitiveHeaders().toArray(new String[0]));
				}

				if (route.getRetryable() != null) {
					ctx.put(RETRYABLE_KEY, route.getRetryable());
				}

				// url是否以http開頭
				if (location.startsWith(HTTP_SCHEME + ":")
						|| location.startsWith(HTTPS_SCHEME + ":")) {
					ctx.setRouteHost(getUrl(location));
					ctx.addOriginResponseHeader(SERVICE_HEADER, location);
				}
				// 判斷url是否以forward:開頭
				else if (location.startsWith(FORWARD_LOCATION_PREFIX)) {
					ctx.set(FORWARD_TO_KEY,
							StringUtils.cleanPath(
									location.substring(FORWARD_LOCATION_PREFIX.length())
											+ route.getPath()));
					ctx.setRouteHost(null);
					return null;
				}
				else {
					// set serviceId for use in filters.route.RibbonRequest
					ctx.set(SERVICE_ID_KEY, location);
					ctx.setRouteHost(null);
					ctx.addOriginResponseHeader(SERVICE_ID_HEADER, location);
				}
				if (this.properties.isAddProxyHeaders()) {
					addProxyHeaders(ctx, route);
					String xforwardedfor = ctx.getRequest()
							.getHeader(X_FORWARDED_FOR_HEADER);
					String remoteAddr = ctx.getRequest().getRemoteAddr();
					if (xforwardedfor == null) {
						xforwardedfor = remoteAddr;
					}
					else if (!xforwardedfor.contains(remoteAddr)) { // Prevent duplicates
						xforwardedfor += ", " + remoteAddr;
					}
					ctx.addZuulRequestHeader(X_FORWARDED_FOR_HEADER, xforwardedfor);
				}
				if (this.properties.isAddHostHeader()) {
					ctx.addZuulRequestHeader(HttpHeaders.HOST,
							toHostHeader(ctx.getRequest()));
				}
			}
		}
		else {
			log.warn("No route found for uri: " + requestURI);
			String forwardURI = getForwardUri(requestURI);

			ctx.set(FORWARD_TO_KEY, forwardURI);
		}
		return null;
	}

4、當請求發送過來時,在RibbonRoutingFilter過濾器中,通過HttpClientRibbonCommandFactory工廠類,通過SpringClientFactory創建RibbonLoadBalancingHttpClient、IClientConfig 及ILoadBalancer,SpringClientFactory會爲每個服務創建一個Spring上下文

注:RibbonLoadBalancingHttpClient、IClientConfig 及ILoadBalancer是通過RibbonClientConfiguration加載;IClientConfig 主要保存Ribbon配置信息,在創建RibbonLoadBalancingHttpClient會一次加載這個配置信息;IClientConfig 配置如下圖所示

在這裏插入圖片描述

public class RibbonRoutingFilter extends ZuulFilter {
	@Override
	public Object run() {
		RequestContext context = RequestContext.getCurrentContext();
		this.helper.addIgnoredHeaders();
		try {
			RibbonCommandContext commandContext = buildCommandContext(context);
			// 通過HttpClientRibbonCommandFactory工廠類,通過SpringClientFactory創建RibbonLoadBalancingHttpClient 及ILoadBalancer
			ClientHttpResponse response = forward(commandContext);
			setResponse(response);
			return response;
		}
		catch (ZuulException ex) {
			throw new ZuulRuntimeException(ex);
		}
		catch (Exception ex) {
			throw new ZuulRuntimeException(ex);
		}
	}
	protected ClientHttpResponse forward(RibbonCommandContext context) throws Exception {
		Map<String, Object> info = this.helper.debug(context.getMethod(),
				context.getUri(), context.getHeaders(), context.getParams(),
				context.getRequestEntity());

	// 通過HttpClientRibbonCommandFactory工廠類,通過SpringClientFactory創建RibbonLoadBalancingHttpClient 及ILoadBalancer
		RibbonCommand command = this.ribbonCommandFactory.create(context);
		try {
			ClientHttpResponse response = command.execute();
			this.helper.appendDebug(info, response.getRawStatusCode(),
					response.getHeaders());
			return response;
		}
		catch (HystrixRuntimeException ex) {
			return handleException(info, ex);
		}

	}
}
public class HttpClientRibbonCommandFactory extends AbstractRibbonCommandFactory {
	
	private final SpringClientFactory clientFactory;

	private final ZuulProperties zuulProperties;

	public HttpClientRibbonCommandFactory(SpringClientFactory clientFactory,
			ZuulProperties zuulProperties, Set<FallbackProvider> fallbackProviders) {
		super(fallbackProviders);
		this.clientFactory = clientFactory;
		this.zuulProperties = zuulProperties;
	}
	@Override
	public HttpClientRibbonCommand create(final RibbonCommandContext context) {
		FallbackProvider zuulFallbackProvider = getFallbackProvider(
				context.getServiceId());
		final String serviceId = context.getServiceId();
		// 通過SpringClientFactory創建RibbonLoadBalancingHttpClient 及ILoadBalancer
		final RibbonLoadBalancingHttpClient client = this.clientFactory
				.getClient(serviceId, RibbonLoadBalancingHttpClient.class);
		client.setLoadBalancer(this.clientFactory.getLoadBalancer(serviceId));

		return new HttpClientRibbonCommand(serviceId, client, context, zuulProperties,
				zuulFallbackProvider, clientFactory.getClientConfig(serviceId));
	}
}

4、通過SpringClientFactory創建RibbonLoadBalancingHttpClient 及ILoadBalancer,SpringClientFactory會爲每個服務創建一個Spring上下文。

public class SpringClientFactory extends NamedContextFactory<RibbonClientSpecification> {
	public SpringClientFactory() {
		super(RibbonClientConfiguration.class, NAMESPACE, "ribbon.client.name");
	}
	// 加載ILoadBalancer
	public ILoadBalancer getLoadBalancer(String name) {
		return getInstance(name, ILoadBalancer.class);
	}
	@Override
	public <C> C getInstance(String name, Class<C> type) {
		// 調用子類加載ILoadBalancer
		C instance = super.getInstance(name, type);
		if (instance != null) {
			return instance;
		}
		IClientConfig config = getInstance(name, IClientConfig.class);
		return instantiateWithConfig(getContext(name), type, config);
	}
}
public abstract class NamedContextFactory<C extends NamedContextFactory.Specification>
		implements DisposableBean, ApplicationContextAware {
		
		// 加載ILoadBalancer
		public <T> T getInstance(String name, Class<T> type) {
			AnnotationConfigApplicationContext context = getContext(name);
			if (BeanFactoryUtils.beanNamesForTypeIncludingAncestors(context,
					type).length > 0) {
				return context.getBean(type);
			}
			return null;
		}
		protected AnnotationConfigApplicationContext getContext(String name) {
		 if (!this.contexts.containsKey(name)) {
			synchronized (this.contexts) {
				if (!this.contexts.containsKey(name)) {
					// 創建上下文
					this.contexts.put(name, createContext(name));
				}
			}
		}
		return this.contexts.get(name);
	}
	protected AnnotationConfigApplicationContext createContext(String name) {
		AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext();
		if (this.configurations.containsKey(name)) {
		// 其中configurations的值爲default.org.springframework.cloud.netflix.ribbon.RibbonAutoConfiguration,具體在RibbonAutoConfiguration類中,加載SpringClientFactory的時候
			for (Class<?> configuration : this.configurations.get(name)
					.getConfiguration()) {
				context.register(configuration);
			}
		}
		for (Map.Entry<String, C> entry : this.configurations.entrySet()) {
			if (entry.getKey().startsWith("default.")) {
				for (Class<?> configuration : entry.getValue().getConfiguration()) {
					context.register(configuration);
				}
			}
		}
		//defaultConfigType爲RibbonClientConfiguration,具體在SpringClientFactory 構造函數中加載
		context.register(PropertyPlaceholderAutoConfiguration.class,
				this.defaultConfigType);
		context.getEnvironment().getPropertySources().addFirst(new MapPropertySource(
				this.propertySourceName,
				Collections.<String, Object>singletonMap(this.propertyName, name)));
		if (this.parent != null) {
			// Uses Environment from parent as well as beans
			context.setParent(this.parent);
			// jdk11 issue
			// https://github.com/spring-cloud/spring-cloud-netflix/issues/3101
			context.setClassLoader(this.parent.getClassLoader());
		}
		context.setDisplayName(generateDisplayName(name));
		context.refresh();
		return context;
	}
}

用法

註冊中心配置

zuul:
  routes:
    tmp-test1:
      path: /redis/**
      url: tmp  ## 和serviceId效果一樣(PreDecorationFilter)
      stripPrefix: false   ## 默認爲true,如果爲false,表示ip:port/redis/hello將請求到ip:port/redis/hello;如果爲true,表示ip:port/redis/hello將請求到ip:port/hello(PreDecorationFilter)
  ignored-services: '*'   ## zuul有默認的映射機制,‘*’表示忽略所有的映射機制
  add-proxy-headers: false  ## 是否在頭中增加代理信息(PreDecorationFilter)
  sensitive-headers:  ## 請求發送到網關後,需要移除的頭信息
ribbon:  
  ReadTimeout: 20000 ## 針對所有服務,與下邊tmp.ribbon.ReadTimeout作用一樣
  ConnectTimeout: 30000
#tmp:  ### 服務級,優先級比ribbon.ReadTimeout高
  #ribbon:
      #ReadTimeout: 20000
      #ConnectTimeout: 30000

http請求配置(單實例)

走SimpleHostRoutingFilter過濾器

zuul:
  routes:
    tmp-test1:
      path: /redis/**
      url: http://localhost:8088/ ## 和serviceId效果一樣(PreDecorationFilter)
      stripPrefix: false   ## 默認爲true,如果爲false,表示ip:port/redis/hello將請求到ip:port/redis/hello;如果爲true,表示ip:port/redis/hello將請求到ip:port/hello(PreDecorationFilter)
    test:
      path: /test/**
      url: forward:/hello ## 轉發,需要再網關層定義controller處理,例如請求/test/test,則最終會跳轉到/hello/test中,具體請看PreDecorationFilter類中run方法
  ignored-services: '*'   ## zuul有默認的映射機制,‘*’表示忽略所有的映射機制
  add-proxy-headers: false  ## 是否在頭中增加代理信息(PreDecorationFilter)
  sensitive-headers:  ## 請求發送到網關後,需要移除的頭信息
  host:
    connect-timeout-millis: 2000  ## 當使用http時,創建httpClient,請求連接時間(SimpleHostRoutingFilter.initialize())
    socket-timeout-millis: 6000  ## 當使用http時,創建httpClient,請求連接時間(SimpleHostRoutingFilter.initialize())

http請求配置(多實例,負載均衡)

走RibbonRoutingFilter 過濾器

zuul:
  routes:
    tmp-test1:
      path: /redis/**
      url: tmp  ## 和serviceId效果一樣(PreDecorationFilter)
      stripPrefix: false   ## 默認爲true,如果爲false,表示ip:port/redis/hello將請求到ip:port/redis/hello;如果爲true,表示ip:port/redis/hello將請求到ip:port/hello(PreDecorationFilter)
  ignored-services: '*'   ## zuul有默認的映射機制,‘*’表示忽略所有的映射機制
  add-proxy-headers: false  ## 是否在頭中增加代理信息(PreDecorationFilter)
  sensitive-headers:  ## 請求發送到網關後,需要移除的頭信息
ribbon:
  ReadTimeout: 20000  ## 針對所有服務,與下邊tmp.ribbon.ReadTimeout作用一樣
  ConnectTimeout: 30000
  eureka:
    enabled: false ## 如果引入了eureka包,想使用負載均衡,zuul.routes.*.url配置的不是http,此時需要將該值設置成false
tmp:  ### 服務級,優先級比ribbon.ReadTimeout高
  ribbon:
    #ReadTimeout: 20000
    #ConnectTimeout: 30000
    listOfServers: http://localhost:8088/,http://localhost:8081  ## ribbon負載均衡加載的url

超時重試

1、 在pom.xml中增加依賴項

    <dependency>
            <groupId>org.springframework.retry</groupId>
            <artifactId>spring-retry</artifactId>
       </dependency>

2、配置文件

zuul:
  ignored-services: '*'
  routes:
    task:
      path: /task/**
      serviceId: tmp
      customSensitiveHeaders: true
  retryable: true  ## *****開啓超時重試開關(必須增加否則不起作用)*****
ribbon:
  ReadTimeout: 10000   ## 請求處理的超時時間
  ConnectTimeout: 5000  ## 請求連接的超時時間
  MaxAutoRetries: 1  ## 對當前實例的重試次數(此處爲1+1,重試2次,默認0)
  MaxAutoRetriesNextServer: 2 ## 切換實例的重試次數(2*2及(MaxAutoRetries+1)*MaxAutoRetriesNextServer,默認1)
  retryableStatusCodes: 500 ## *****處理錯誤狀態碼*****
  OkToRetryOnAllOperations: false ## *****對所有操作請求都進行重試(默認GET請求)*****
hystrix:
  command:
    default:
      execution:
        isolation:
          thread:
            timeoutInMilliseconds: 151000
tmp:
  ribbon:  ### 服務級,優先級比ribbon.ReadTimeout高
    #ReadTimeout: 20000
    #ConnectTimeout: 30000
    listOfServers: http://localhost:8080/,http://localhost:8081/

熔斷機制(Hystrix)

@Component
public class UserServiceFallbackProvider implements FallbackProvider {
    @Override
    public String getRoute() {
        return "tmp";
    }

    @Override
    public ClientHttpResponse fallbackResponse(String route, Throwable cause) {
        return new ClientHttpResponse() {
            @Override
            public HttpStatus getStatusCode() throws IOException {
                return HttpStatus.OK;
            }

            @Override
            public int getRawStatusCode() throws IOException {
                return 200;
            }

            @Override
            public String getStatusText() throws IOException {
                return "OK";
            }

            @Override
            public void close() {

            }

            @Override
            public InputStream getBody() throws IOException {
                return new ByteArrayInputStream("fallback".getBytes());
            }

            @Override
            public HttpHeaders getHeaders() {
                HttpHeaders headers = new HttpHeaders();
                headers.setContentType(MediaType.APPLICATION_JSON);
                return headers;
            }
        };
    }
}

退避策略(Ribbon)

參考資料

@Configuration
public class MyConfiguration {
    @Bean
    LoadBalancedRetryFactory retryFactory() {
        return new LoadBalancedRetryFactory() {
            @Override
            public BackOffPolicy createBackOffPolicy(String service) {
                return new ExponentialBackOffPolicy();
            }
        };
    }
}

Filter工作原理

Zuul中的Filter

Zuul是圍繞一系列Filter展開的,這些Filter在整個HTTP請求過程中執行一連串的操作。
Zuul Filter有以下幾個特徵:

  • Type:用以表示路由過程中的階段(內置包含PRE、ROUTING、POST和ERROR)
  • Execution Order:表示相同Type的Filter的執行順序
  • Criteria:執行條件
  • Action:執行體

Filter Types

以下提供四種標準的Filter類型及其在請求生命週期中所處的位置:

  • PRE Filter:在請求路由到目標之前執行。一般用於請求認證、負載均衡和日誌記錄。
  • ROUTING Filter:處理目標請求。這裏使用Apache HttpClient或Netflix Ribbon構造對目標的HTTP請求。
  • POST Filter:在目標請求返回後執行。一般會在此步驟添加響應頭、收集統計和性能數據等。
  • ERROR Filter:整個流程某塊出錯時執行。

自定義一個Filter

@Component
public class PreLogFilter extends ZuulFilter{
 
    @Override
    public String filterType() {
        return PRE_TYPE;
    }
 
    @Override
    public int filterOrder() {
        return 0;
    }
 
    @Override
    public boolean shouldFilter() {
        return true;
    }
 
    @Override
    public Object run() {
        RequestContext currentContext = RequestContext.getCurrentContext();
        HttpServletRequest request = currentContext.getRequest();
        System.out.print(String.format("send %s request to %s",request.getMethod(),request.getRequestURL()));
        return null;
    }

Filter的啓用與禁用

我們自己寫的Filter可以通過修改shouldFilter()啓用或禁用。如果第三方的Filter怎樣控制其啓用及禁用呢?

很簡單,通過配置文件就可以做到:

zuul:
  SendErrorFilter: #自定義Filter類名
    error: #Type
      disable: true
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章