Spring Cloud Gateway(六):路由謂詞工廠 RoutePredicateFactory


本文基於 spring cloud gateway 2.0.1

1、簡介

Spring Cloud Gateway 創建 Route 對象時, 使用 RoutePredicateFactory 創建 Predicate 對象,Predicate 對象可以賦值給 Route。 Spring Cloud Gateway 包含許多內置的Route Predicate Factories。所有這些謂詞都匹配HTTP請求的不同屬性。多種謂詞工廠可以組合,並通過邏輯and。

路由選擇是通過Predicate函數式接口進行判斷當前路由是否滿足給定條件。

image

路由謂詞工廠 RoutePredicateFactory 包含的主要實現類如圖所示,可以看到該接口有多個實現類。抽象類 AbstractRoutePredicateFactory 實現了路由謂詞工廠,但是沒有實際的方法,具體的實現類都是繼承自抽象類 AbstractRoutePredicateFactory 包括 Datetime、 請求的遠端地址、 路由權重、 請求頭、 Host 地址、 請求方法、 請求路徑 和 請求參數等類型的路由斷言。

2、路由謂詞工廠 RoutePredicateFactory

按照功能對路由謂詞工廠進行划分,可以划分為以下幾種,如圖所示:

路由謂詞工廠功能划分

@FunctionalInterface
public interface RoutePredicateFactory<C> extends ShortcutConfigurable, Configurable<C> {
	String PATTERN_KEY = "pattern";

	// useful for javadsl
	default Predicate<ServerWebExchange> apply(Consumer<C> consumer) {
		C config = newConfig();
		consumer.accept(config);
		beforeApply(config);
		return apply(config);
	}

	default AsyncPredicate<ServerWebExchange> applyAsync(Consumer<C> consumer) {
		C config = newConfig();
		consumer.accept(config);
		beforeApply(config);
		return applyAsync(config);
	}

	default Class<C> getConfigClass() {
		throw new UnsupportedOperationException("getConfigClass() not implemented");
	}

	@Override
	default C newConfig() {
		throw new UnsupportedOperationException("newConfig() not implemented");
	}

	default void beforeApply(C config) {}

	Predicate<ServerWebExchange> apply(C config);

	default AsyncPredicate<ServerWebExchange> applyAsync(C config) {
		return toAsyncPredicate(apply(config));
	}

	default String name() {
		return NameUtils.normalizeRoutePredicateName(getClass());
	}

}

RoutePredicateFactory 接口繼承自 ShortcutConfigurable 接口,ShortcutConfigurable 接口在多個實現類中都有出現,根據傳入的具體 RouteDefinitionLocator 獲取路由定義對象時,就用到了該接口中的默認方法。路由謂詞的種類很多,不同的謂詞需要的配置參數不一樣,所以每種 路由謂詞(斷言)和過濾器的實現都會實現 ShortcutConfigurable 接口,來指定自身參數的個數和順序。

2.1、ShortcutConfigurable

ShortcutConfigurable 接口提供的默認方法,主要用於對過濾器和斷言參數進行標准化處理,將表達式和生成的鍵進行轉換。

public interface ShortcutConfigurable {

	enum ShortcutType {
		DEFAULT {
			@Override
			public Map<String, Object> normalize(Map<String, String> args, ShortcutConfigurable shortcutConf, SpelExpressionParser parser, BeanFactory beanFactory) {
				Map<String, Object> map = new HashMap<>();
				int entryIdx = 0;
				for (Map.Entry<String, String> entry : args.entrySet()) {
					String key = normalizeKey(entry.getKey(), entryIdx, shortcutConf, args);
					Object value = getValue(parser, beanFactory, entry.getValue());

					map.put(key, value);
					entryIdx++;
				}
				return map;
			}
		},

		GATHER_LIST {
			@Override
			public Map<String, Object> normalize(Map<String, String> args, ShortcutConfigurable shortcutConf, SpelExpressionParser parser, BeanFactory beanFactory) {
				Map<String, Object> map = new HashMap<>();
				// field order should be of size 1
				List<String> fieldOrder = shortcutConf.shortcutFieldOrder();
				Assert.isTrue(fieldOrder != null
								&& fieldOrder.size() == 1,
						"Shortcut Configuration Type GATHER_LIST must have shortcutFieldOrder of size 1");
				String fieldName = fieldOrder.get(0);
				map.put(fieldName, args.values().stream()
						.map(value -> getValue(parser, beanFactory, value))
						.collect(Collectors.toList()));
				return map;
			}
		};

		public abstract Map<String, Object> normalize(Map<String, String> args, ShortcutConfigurable shortcutConf,
													  SpelExpressionParser parser, BeanFactory beanFactory);
	}

	static String normalizeKey(String key, int entryIdx, ShortcutConfigurable argHints, Map<String, String> args) {
	    /*************************(1)*********************************/
		// RoutePredicateFactory has name hints and this has a fake key name
		// replace with the matching key hint
		if (key.startsWith(NameUtils.GENERATED_NAME_PREFIX) && !argHints.shortcutFieldOrder().isEmpty()
				&& entryIdx < args.size() && entryIdx < argHints.shortcutFieldOrder().size()) {
			key = argHints.shortcutFieldOrder().get(entryIdx);
		}
		return key;
	}

	static Object getValue(SpelExpressionParser parser, BeanFactory beanFactory, String entryValue) {
		/*************************(2)*********************************/

		Object value;
		String rawValue = entryValue;
		if (rawValue != null) {
			rawValue = rawValue.trim();
		}
		if (rawValue != null && rawValue.startsWith("#{") && entryValue.endsWith("}")) {
		/*************************(3)*********************************/

			// assume it's spel
			StandardEvaluationContext context = new StandardEvaluationContext();
			context.setBeanResolver(new BeanFactoryResolver(beanFactory));
			Expression expression = parser.parseExpression(entryValue, new TemplateParserContext());
			value = expression.getValue(context);
		} else {
			value = entryValue;
		}
		return value;
	}

	default ShortcutType shortcutType() {
		return ShortcutType.DEFAULT;
	}

	/**
	 * Returns hints about the number of args and the order for shortcut parsing.
	 * @return
	 */
	default List<String> shortcutFieldOrder() {
	/*************************(4)*********************************/

		return Collections.emptyList();
	}

	default String shortcutFieldPrefix() {
		return "";
	}

}

以上代碼主要做了以下工作:

1)對鍵進行標准化處理,因為鍵有可能是自動生成,當鍵以_ genkey_ 開頭時, 表明是自動生成的。

2)獲取真實值,需要傳入 Spring EL 解析器、Bean工廠等工具類。

3)對傳入的 entryValue 是一個表達式的情況進行處理,這里默認是 Spring EL 表達式。

4)返回有關參數數量和解析順序的提示。

3、Datetime 類型的路由謂詞工廠

Datetime 類型的路由謂詞工廠有三種,分別為:

  • AfterRoutePredicateFactory: 接收一個日期參數,判斷請求日期是否晚於指定日志

  • BeforeRoutePredicateFactory: 接收一個日期參數,判斷請求日期是否早於指定日志

  • BetweenRoutePredicateFactory: 接收兩個日期參數,判斷請求日期是否在指定時間段內

以 AfterRoutePredicateFactory 為例,介紹 Datetime 類型的斷言工廠的應用:

spring:
    cloud: 
        gateway:
            routes: 
            - id: after_route_id
            uri: http://www.baidu.com
            predicates: 
            - After= 2018-12-30T23:59:59.789+08:00[Asia/Shanghai]

上面的配置文件指定了路由的斷言,謂詞關鍵字是 After ,表示請求時間必須晚於上海時間 2018年12月30日 23:59:59 才可用。

4、基於遠程地址(RemoteAttr)的路由謂詞工廠

RemoteAddrRoutePredicateFactory 屬於根據請求IP進行路由類型,接收 CIDR表示 法(IPv4或IPv6)的字符串列表(列表最小長度為1)作為參數, 例如 192.168.0.1/ 16, 其中 192. 168. 0. 1 是 IP 地址, 16 是 子 網 掩 碼。

spring: 
    cloud:
        gateway: 
            routes:
            -id: remoteaddr_ route_id
            uri: http://www.baidu.com
            predicates:
            -RemoteAddr=192.168.1.1/24

以上配置表示如果請求的遠程地址是 192.168.1.10,將會匹配該路由。

// RemoteAddrRoutePredicateFactory.java

@Override
	public Predicate<ServerWebExchange> apply(Config config) {
        List<IpSubnetFilterRule> sources = convert(config.sources);

		return exchange -> {
		    //獲取請求中的遠程地址
			InetSocketAddress remoteAddress = config.remoteAddressResolver.resolve(exchange);
			if (remoteAddress != null) {
				String hostAddress = remoteAddress.getAddress().getHostAddress();
				String host = exchange.getRequest().getURI().getHost();

				if (log.isDebugEnabled() && !hostAddress.equals(host)) {
					log.debug("Remote addresses didn't match " + hostAddress + " != " + host);
				}
                //遍歷配置好的 RemoteAddr 列表, 如果遠程地址在列表中則匹配成功
				for (IpSubnetFilterRule source : sources) {
					if (source.matches(remoteAddress)) {
						return true;
					}
				}
			}

			return false;
		};
	}

	private void addSource(List<IpSubnetFilterRule> sources, String source) {
		if (!source.contains("/")) { 
		//當 RemoteAddr 沒有子網掩碼時,默認為/32
			source = source + "/32";
		}

		String[] ipAddressCidrPrefix = source.split("/",2);
		String ipAddress = ipAddressCidrPrefix[0];
		int cidrPrefix = Integer.parseInt(ipAddressCidrPrefix[1]);
        //根據 ip 和 子網,確定 RemoteAddr 的范圍,並加入到 sources 中
		sources.add(new IpSubnetFilterRule(ipAddress, cidrPrefix, IpFilterRuleType.ACCEPT));
	}

基於遠程地址匹配的路由斷言實現,首先獲取配置文件中的 RemoteAttr 列表,然后將配置的 RemoteAttr 列表轉換成 sources 列表,主要是根據IP地址和子網掩碼確定地址范圍,最后判斷請求的遠程地址是否在設置的 IP 列表中。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM