From 8538be9492365f7a7e18eff75ea8f7fb2f115afd Mon Sep 17 00:00:00 2001 From: p_chwucai Date: Fri, 13 Oct 2023 19:25:44 +0800 Subject: [PATCH 1/5] add gray predicate support. --- .../config/GatewayAutoConfiguration.java | 7 + .../OpenGrayRoutePredicateFactory.java | 206 ++++++++++++++++++ 2 files changed, 213 insertions(+) create mode 100644 spring-cloud-gateway-server/src/main/java/org/springframework/cloud/gateway/handler/predicate/OpenGrayRoutePredicateFactory.java diff --git a/spring-cloud-gateway-server/src/main/java/org/springframework/cloud/gateway/config/GatewayAutoConfiguration.java b/spring-cloud-gateway-server/src/main/java/org/springframework/cloud/gateway/config/GatewayAutoConfiguration.java index 534277df36..5436c88d3c 100644 --- a/spring-cloud-gateway-server/src/main/java/org/springframework/cloud/gateway/config/GatewayAutoConfiguration.java +++ b/spring-cloud-gateway-server/src/main/java/org/springframework/cloud/gateway/config/GatewayAutoConfiguration.java @@ -27,6 +27,7 @@ import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; +import org.springframework.cloud.gateway.handler.predicate.OpenGrayRoutePredicateFactory; import reactor.core.publisher.Flux; import reactor.netty.http.client.HttpClient; import reactor.netty.http.client.WebsocketClientSpec; @@ -494,6 +495,12 @@ public CloudFoundryRouteServiceRoutePredicateFactory cloudFoundryRouteServiceRou return new CloudFoundryRouteServiceRoutePredicateFactory(); } + @Bean + @ConditionalOnEnabledPredicate + public OpenGrayRoutePredicateFactory openGrayRoutePredicateFactory() { + return new OpenGrayRoutePredicateFactory(); + } + // GatewayFilter Factory beans @Bean diff --git a/spring-cloud-gateway-server/src/main/java/org/springframework/cloud/gateway/handler/predicate/OpenGrayRoutePredicateFactory.java b/spring-cloud-gateway-server/src/main/java/org/springframework/cloud/gateway/handler/predicate/OpenGrayRoutePredicateFactory.java new file mode 100644 index 0000000000..8e2b8cc0e1 --- /dev/null +++ b/spring-cloud-gateway-server/src/main/java/org/springframework/cloud/gateway/handler/predicate/OpenGrayRoutePredicateFactory.java @@ -0,0 +1,206 @@ +/* + * Copyright 2013-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.gateway.handler.predicate; + + +import com.google.common.hash.HashFunction; +import com.google.common.hash.Hashing; +import java.nio.charset.StandardCharsets; +import java.util.Arrays; +import java.util.List; +import java.util.Objects; +import java.util.Optional; +import java.util.function.Predicate; + + + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.springframework.http.HttpCookie; +import org.springframework.util.ObjectUtils; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.server.ServerWebExchange; + +/** + * other than WeightRoutePredicateFactory route traffic random, + * this predicate route traffic base on specific parameter + */ + +public class OpenGrayRoutePredicateFactory extends AbstractRoutePredicateFactory { + private static final Log log = LogFactory.getLog(OpenGrayRoutePredicateFactory.class); + private final String REQUEST_QUERY_PATTERN = "request.query."; + private final String REQUEST_HEADER_PATTERN = "request.header."; + private final String REQUEST_COOKIE_PATTERN = "request.cookie."; + private final String REQUEST_PATH_PATTERN = "request.path"; + + /** + * available range is [0, 1000) + */ + public static final int MAX_WEIGHT_RANGE = 1000; + + public static final String FILED_KEY = "fieldPattern"; + /** + */ + public static final String START_KEY = "start"; + public static final String END_KEY = "end"; + + public static final String MATCH_EMPTY_FIELD_KEY = "matchEmptyField"; + + + public OpenGrayRoutePredicateFactory() { + super(Config.class); + } + + @Override + public List shortcutFieldOrder() { + return Arrays.asList(FILED_KEY, START_KEY, END_KEY, MATCH_EMPTY_FIELD_KEY); + } + + @Override + public Predicate apply(Config config) { + return new GatewayPredicate() { + @Override + public boolean test(ServerWebExchange exchange) { + // get gray value + String patternValue = patternParamResolve(exchange, config.getFieldPattern()); + if (ObjectUtils.isEmpty(patternValue)) { + return config.isMatchEmptyField(); + } + + + long weightStart = Math.max(0, config.getStart()); + long weightEnd = Math.min(MAX_WEIGHT_RANGE, config.getEnd()); + + if (weightEnd < weightStart) { + // invalid match range + return false; + } + + + long hash = Math.abs(getMuHashString(patternValue)) % MAX_WEIGHT_RANGE; + + return hash >= weightStart && hash < weightEnd; + } + + @Override + public Object getConfig() { + return config; + } + + @Override + public String toString() { + return String.format("field: %s ,start=%s ,end=%s", config.getFieldPattern(), config.getStart(), config.getEnd()); + } + }; + } + + private String patternParamResolve(ServerWebExchange exchange, String singlePattern){ + if (ObjectUtils.isEmpty(singlePattern)) { + return ""; + } + if (singlePattern.startsWith(REQUEST_QUERY_PATTERN)) { + String fieldName = singlePattern.substring(REQUEST_QUERY_PATTERN.length()); + return Optional.ofNullable(exchange.getRequest().getQueryParams().getFirst(fieldName)).orElse(""); + } else if (singlePattern.startsWith(REQUEST_HEADER_PATTERN)) { + String headerName = singlePattern.substring(REQUEST_HEADER_PATTERN.length()); + return Optional.ofNullable(exchange.getRequest().getHeaders().getFirst(headerName)).orElse(""); + }else if (singlePattern.startsWith(REQUEST_COOKIE_PATTERN)) { + String cookieName = singlePattern.substring(REQUEST_COOKIE_PATTERN.length()); + return Optional.ofNullable(exchange.getRequest().getCookies().getFirst(cookieName)) + .map(HttpCookie::getValue) + .orElse(""); + }else if (Objects.equals(singlePattern, REQUEST_PATH_PATTERN)) { + return Optional.ofNullable(exchange.getRequest().getURI().getPath()).orElse(""); + }else { + // unsupported + return ""; + } + } + + + private Long getMuHashString(String str) { + HashFunction hashFunction = Hashing.murmur3_128(); + return hashFunction.hashString(str, StandardCharsets.UTF_8).asLong(); + } + + + /** + * match range [start, end) range. [0, 1000) full match range + */ + @Validated + public static class Config { + + /** + * field pattern to specify which field weight calculate base on + *

+ * request.header.xx + * request.query.xx + * request.cookie.xx + * request.path + *

+ */ + + private String fieldPattern; + + /** + * how to handle empty field value. false miss-match, true always match + */ + private boolean matchEmptyField = false; + + /** + * weight range include start, valid input [0, 999] + */ + private int start; + /** + * weight range exclude end, valid input [1, 1000] + */ + private int end; + + public String getFieldPattern() { + return fieldPattern; + } + + public void setFieldPattern(String fieldPattern) { + this.fieldPattern = fieldPattern; + } + + public int getStart() { + return start; + } + + public void setStart(int start) { + this.start = start; + } + + public int getEnd() { + return end; + } + + public void setEnd(int end) { + this.end = end; + } + + public boolean isMatchEmptyField() { + return matchEmptyField; + } + + public void setMatchEmptyField(boolean matchEmptyField) { + this.matchEmptyField = matchEmptyField; + } + } + +} From cb47b41d7be081f9f30252f2336aa451053bdc02 Mon Sep 17 00:00:00 2001 From: p_chwucai Date: Mon, 11 Dec 2023 12:16:34 +0800 Subject: [PATCH 2/5] change hash function impls. --- spring-cloud-gateway-server/pom.xml | 8 +++++++- .../predicate/OpenGrayRoutePredicateFactory.java | 15 ++++----------- 2 files changed, 11 insertions(+), 12 deletions(-) diff --git a/spring-cloud-gateway-server/pom.xml b/spring-cloud-gateway-server/pom.xml index 6417e9361d..4b1220a5f0 100644 --- a/spring-cloud-gateway-server/pom.xml +++ b/spring-cloud-gateway-server/pom.xml @@ -235,7 +235,13 @@ spring-core-test test - + + commons-codec + commons-codec + 1.16.0 + compile + + diff --git a/spring-cloud-gateway-server/src/main/java/org/springframework/cloud/gateway/handler/predicate/OpenGrayRoutePredicateFactory.java b/spring-cloud-gateway-server/src/main/java/org/springframework/cloud/gateway/handler/predicate/OpenGrayRoutePredicateFactory.java index 8e2b8cc0e1..29512f8711 100644 --- a/spring-cloud-gateway-server/src/main/java/org/springframework/cloud/gateway/handler/predicate/OpenGrayRoutePredicateFactory.java +++ b/spring-cloud-gateway-server/src/main/java/org/springframework/cloud/gateway/handler/predicate/OpenGrayRoutePredicateFactory.java @@ -27,7 +27,7 @@ import java.util.function.Predicate; - +import org.apache.commons.codec.digest.MurmurHash3; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.springframework.http.HttpCookie; @@ -82,8 +82,8 @@ public boolean test(ServerWebExchange exchange) { } - long weightStart = Math.max(0, config.getStart()); - long weightEnd = Math.min(MAX_WEIGHT_RANGE, config.getEnd()); + int weightStart = Math.max(0, config.getStart()); + int weightEnd = Math.min(MAX_WEIGHT_RANGE, config.getEnd()); if (weightEnd < weightStart) { // invalid match range @@ -91,7 +91,7 @@ public boolean test(ServerWebExchange exchange) { } - long hash = Math.abs(getMuHashString(patternValue)) % MAX_WEIGHT_RANGE; + int hash = Math.abs(MurmurHash3.hash32x86(patternValue.getBytes(StandardCharsets.UTF_8))) % MAX_WEIGHT_RANGE; return hash >= weightStart && hash < weightEnd; } @@ -131,13 +131,6 @@ private String patternParamResolve(ServerWebExchange exchange, String singlePatt } } - - private Long getMuHashString(String str) { - HashFunction hashFunction = Hashing.murmur3_128(); - return hashFunction.hashString(str, StandardCharsets.UTF_8).asLong(); - } - - /** * match range [start, end) range. [0, 1000) full match range */ From cace3d002be1f7d93b7fd8702c111c8bb66eac9f Mon Sep 17 00:00:00 2001 From: p_chwucai Date: Mon, 11 Dec 2023 14:26:22 +0800 Subject: [PATCH 3/5] remove useless imports. --- .../handler/predicate/OpenGrayRoutePredicateFactory.java | 4 ---- 1 file changed, 4 deletions(-) diff --git a/spring-cloud-gateway-server/src/main/java/org/springframework/cloud/gateway/handler/predicate/OpenGrayRoutePredicateFactory.java b/spring-cloud-gateway-server/src/main/java/org/springframework/cloud/gateway/handler/predicate/OpenGrayRoutePredicateFactory.java index 29512f8711..b555a0e9c4 100644 --- a/spring-cloud-gateway-server/src/main/java/org/springframework/cloud/gateway/handler/predicate/OpenGrayRoutePredicateFactory.java +++ b/spring-cloud-gateway-server/src/main/java/org/springframework/cloud/gateway/handler/predicate/OpenGrayRoutePredicateFactory.java @@ -17,16 +17,12 @@ package org.springframework.cloud.gateway.handler.predicate; -import com.google.common.hash.HashFunction; -import com.google.common.hash.Hashing; import java.nio.charset.StandardCharsets; import java.util.Arrays; import java.util.List; import java.util.Objects; import java.util.Optional; import java.util.function.Predicate; - - import org.apache.commons.codec.digest.MurmurHash3; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; From a33faa23497b6d629fb30cd4ddc19534c48e0d50 Mon Sep 17 00:00:00 2001 From: caichengwu Date: Mon, 15 Jan 2024 10:52:14 +0800 Subject: [PATCH 4/5] fix bugs in org.springframework.cloud.gateway.handler.predicate.OpenGrayRoutePredicateFactory when hash value equals int.min_value. --- .../OpenGrayRoutePredicateFactory.java | 297 +++++++++--------- 1 file changed, 150 insertions(+), 147 deletions(-) diff --git a/spring-cloud-gateway-server/src/main/java/org/springframework/cloud/gateway/handler/predicate/OpenGrayRoutePredicateFactory.java b/spring-cloud-gateway-server/src/main/java/org/springframework/cloud/gateway/handler/predicate/OpenGrayRoutePredicateFactory.java index b555a0e9c4..0f22bd9ff5 100644 --- a/spring-cloud-gateway-server/src/main/java/org/springframework/cloud/gateway/handler/predicate/OpenGrayRoutePredicateFactory.java +++ b/spring-cloud-gateway-server/src/main/java/org/springframework/cloud/gateway/handler/predicate/OpenGrayRoutePredicateFactory.java @@ -23,6 +23,7 @@ import java.util.Objects; import java.util.Optional; import java.util.function.Predicate; + import org.apache.commons.codec.digest.MurmurHash3; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; @@ -38,158 +39,160 @@ public class OpenGrayRoutePredicateFactory extends AbstractRoutePredicateFactory { private static final Log log = LogFactory.getLog(OpenGrayRoutePredicateFactory.class); - private final String REQUEST_QUERY_PATTERN = "request.query."; - private final String REQUEST_HEADER_PATTERN = "request.header."; - private final String REQUEST_COOKIE_PATTERN = "request.cookie."; - private final String REQUEST_PATH_PATTERN = "request.path"; - - /** - * available range is [0, 1000) - */ - public static final int MAX_WEIGHT_RANGE = 1000; - - public static final String FILED_KEY = "fieldPattern"; - /** - */ - public static final String START_KEY = "start"; - public static final String END_KEY = "end"; - - public static final String MATCH_EMPTY_FIELD_KEY = "matchEmptyField"; - - - public OpenGrayRoutePredicateFactory() { - super(Config.class); - } - - @Override - public List shortcutFieldOrder() { - return Arrays.asList(FILED_KEY, START_KEY, END_KEY, MATCH_EMPTY_FIELD_KEY); - } - - @Override - public Predicate apply(Config config) { - return new GatewayPredicate() { - @Override - public boolean test(ServerWebExchange exchange) { - // get gray value - String patternValue = patternParamResolve(exchange, config.getFieldPattern()); - if (ObjectUtils.isEmpty(patternValue)) { - return config.isMatchEmptyField(); - } - - - int weightStart = Math.max(0, config.getStart()); + private final String REQUEST_QUERY_PATTERN = "request.query."; + private final String REQUEST_HEADER_PATTERN = "request.header."; + private final String REQUEST_COOKIE_PATTERN = "request.cookie."; + private final String REQUEST_PATH_PATTERN = "request.path"; + + /** + * available range is [0, 1000) + */ + public static final int MAX_WEIGHT_RANGE = 1000; + + public static final String FILED_KEY = "fieldPattern"; + /** + * + */ + public static final String START_KEY = "start"; + public static final String END_KEY = "end"; + + public static final String MATCH_EMPTY_FIELD_KEY = "matchEmptyField"; + + + public OpenGrayRoutePredicateFactory() { + super(Config.class); + } + + @Override + public List shortcutFieldOrder() { + return Arrays.asList(FILED_KEY, START_KEY, END_KEY, MATCH_EMPTY_FIELD_KEY); + } + + @Override + public Predicate apply(Config config) { + return new GatewayPredicate() { + @Override + public boolean test(ServerWebExchange exchange) { + // get gray value + String patternValue = patternParamResolve(exchange, config.getFieldPattern()); + if (ObjectUtils.isEmpty(patternValue)) { + return config.isMatchEmptyField(); + } + + + int weightStart = Math.max(0, config.getStart()); int weightEnd = Math.min(MAX_WEIGHT_RANGE, config.getEnd()); - if (weightEnd < weightStart) { - // invalid match range - return false; - } + if (weightEnd < weightStart) { + // invalid match range + return false; + } + int val = MurmurHash3.hash32x86(patternValue.getBytes(StandardCharsets.UTF_8)); - int hash = Math.abs(MurmurHash3.hash32x86(patternValue.getBytes(StandardCharsets.UTF_8))) % MAX_WEIGHT_RANGE; + int hash = Math.abs(val == Integer.MIN_VALUE ? val++ : val) % MAX_WEIGHT_RANGE; return hash >= weightStart && hash < weightEnd; - } - - @Override - public Object getConfig() { - return config; - } - - @Override - public String toString() { - return String.format("field: %s ,start=%s ,end=%s", config.getFieldPattern(), config.getStart(), config.getEnd()); - } - }; - } - - private String patternParamResolve(ServerWebExchange exchange, String singlePattern){ - if (ObjectUtils.isEmpty(singlePattern)) { - return ""; - } - if (singlePattern.startsWith(REQUEST_QUERY_PATTERN)) { - String fieldName = singlePattern.substring(REQUEST_QUERY_PATTERN.length()); - return Optional.ofNullable(exchange.getRequest().getQueryParams().getFirst(fieldName)).orElse(""); - } else if (singlePattern.startsWith(REQUEST_HEADER_PATTERN)) { - String headerName = singlePattern.substring(REQUEST_HEADER_PATTERN.length()); - return Optional.ofNullable(exchange.getRequest().getHeaders().getFirst(headerName)).orElse(""); - }else if (singlePattern.startsWith(REQUEST_COOKIE_PATTERN)) { - String cookieName = singlePattern.substring(REQUEST_COOKIE_PATTERN.length()); - return Optional.ofNullable(exchange.getRequest().getCookies().getFirst(cookieName)) - .map(HttpCookie::getValue) - .orElse(""); - }else if (Objects.equals(singlePattern, REQUEST_PATH_PATTERN)) { - return Optional.ofNullable(exchange.getRequest().getURI().getPath()).orElse(""); - }else { - // unsupported - return ""; - } - } - - /** - * match range [start, end) range. [0, 1000) full match range - */ - @Validated - public static class Config { - - /** - * field pattern to specify which field weight calculate base on - *

- * request.header.xx - * request.query.xx - * request.cookie.xx - * request.path - *

- */ - - private String fieldPattern; - - /** - * how to handle empty field value. false miss-match, true always match - */ - private boolean matchEmptyField = false; - - /** - * weight range include start, valid input [0, 999] - */ - private int start; - /** - * weight range exclude end, valid input [1, 1000] - */ - private int end; - - public String getFieldPattern() { - return fieldPattern; - } - - public void setFieldPattern(String fieldPattern) { - this.fieldPattern = fieldPattern; - } - - public int getStart() { - return start; - } - - public void setStart(int start) { - this.start = start; - } - - public int getEnd() { - return end; - } - - public void setEnd(int end) { - this.end = end; - } - - public boolean isMatchEmptyField() { - return matchEmptyField; - } - - public void setMatchEmptyField(boolean matchEmptyField) { - this.matchEmptyField = matchEmptyField; - } - } + } + + @Override + public Object getConfig() { + return config; + } + + @Override + public String toString() { + return String.format("field: %s ,start=%s ,end=%s", config.getFieldPattern(), config.getStart(), config.getEnd()); + } + }; + } + + private String patternParamResolve(ServerWebExchange exchange, String singlePattern) { + if (ObjectUtils.isEmpty(singlePattern)) { + return ""; + } + if (singlePattern.startsWith(REQUEST_QUERY_PATTERN)) { + String fieldName = singlePattern.substring(REQUEST_QUERY_PATTERN.length()); + return Optional.ofNullable(exchange.getRequest().getQueryParams().getFirst(fieldName)).orElse(""); + } else if (singlePattern.startsWith(REQUEST_HEADER_PATTERN)) { + String headerName = singlePattern.substring(REQUEST_HEADER_PATTERN.length()); + return Optional.ofNullable(exchange.getRequest().getHeaders().getFirst(headerName)).orElse(""); + } else if (singlePattern.startsWith(REQUEST_COOKIE_PATTERN)) { + String cookieName = singlePattern.substring(REQUEST_COOKIE_PATTERN.length()); + return Optional.ofNullable(exchange.getRequest().getCookies().getFirst(cookieName)) + .map(HttpCookie::getValue) + .orElse(""); + } else if (Objects.equals(singlePattern, REQUEST_PATH_PATTERN)) { + return Optional.ofNullable(exchange.getRequest().getURI().getPath()).orElse(""); + } else { + // unsupported + return ""; + } + } + + /** + * match range [start, end) range. [0, 1000) full match range + */ + @Validated + public static class Config { + + /** + * field pattern to specify which field weight calculate base on + *

+ * request.header.xx + * request.query.xx + * request.cookie.xx + * request.path + *

+ */ + + private String fieldPattern; + + /** + * how to handle empty field value. false miss-match, true always match + */ + private boolean matchEmptyField = false; + + /** + * weight range include start, valid input [0, 999] + */ + private int start; + /** + * weight range exclude end, valid input [1, 1000] + */ + private int end; + + public String getFieldPattern() { + return fieldPattern; + } + + public void setFieldPattern(String fieldPattern) { + this.fieldPattern = fieldPattern; + } + + public int getStart() { + return start; + } + + public void setStart(int start) { + this.start = start; + } + + public int getEnd() { + return end; + } + + public void setEnd(int end) { + this.end = end; + } + + public boolean isMatchEmptyField() { + return matchEmptyField; + } + + public void setMatchEmptyField(boolean matchEmptyField) { + this.matchEmptyField = matchEmptyField; + } + } } From f9936a677fbbaa30128ebbe00060f77920e6cf86 Mon Sep 17 00:00:00 2001 From: caichengwu Date: Mon, 15 Jan 2024 10:59:18 +0800 Subject: [PATCH 5/5] fix bugs in org.springframework.cloud.gateway.handler.predicate.OpenGrayRoutePredicateFactory when hash value equals int.min_value. --- .../OpenGrayRoutePredicateFactory.java | 297 +++++++++--------- 1 file changed, 147 insertions(+), 150 deletions(-) diff --git a/spring-cloud-gateway-server/src/main/java/org/springframework/cloud/gateway/handler/predicate/OpenGrayRoutePredicateFactory.java b/spring-cloud-gateway-server/src/main/java/org/springframework/cloud/gateway/handler/predicate/OpenGrayRoutePredicateFactory.java index 0f22bd9ff5..bd8dfb8f1c 100644 --- a/spring-cloud-gateway-server/src/main/java/org/springframework/cloud/gateway/handler/predicate/OpenGrayRoutePredicateFactory.java +++ b/spring-cloud-gateway-server/src/main/java/org/springframework/cloud/gateway/handler/predicate/OpenGrayRoutePredicateFactory.java @@ -23,7 +23,6 @@ import java.util.Objects; import java.util.Optional; import java.util.function.Predicate; - import org.apache.commons.codec.digest.MurmurHash3; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; @@ -39,160 +38,158 @@ public class OpenGrayRoutePredicateFactory extends AbstractRoutePredicateFactory { private static final Log log = LogFactory.getLog(OpenGrayRoutePredicateFactory.class); - private final String REQUEST_QUERY_PATTERN = "request.query."; - private final String REQUEST_HEADER_PATTERN = "request.header."; - private final String REQUEST_COOKIE_PATTERN = "request.cookie."; - private final String REQUEST_PATH_PATTERN = "request.path"; - - /** - * available range is [0, 1000) - */ - public static final int MAX_WEIGHT_RANGE = 1000; - - public static final String FILED_KEY = "fieldPattern"; - /** - * - */ - public static final String START_KEY = "start"; - public static final String END_KEY = "end"; - - public static final String MATCH_EMPTY_FIELD_KEY = "matchEmptyField"; - - - public OpenGrayRoutePredicateFactory() { - super(Config.class); - } - - @Override - public List shortcutFieldOrder() { - return Arrays.asList(FILED_KEY, START_KEY, END_KEY, MATCH_EMPTY_FIELD_KEY); - } - - @Override - public Predicate apply(Config config) { - return new GatewayPredicate() { - @Override - public boolean test(ServerWebExchange exchange) { - // get gray value - String patternValue = patternParamResolve(exchange, config.getFieldPattern()); - if (ObjectUtils.isEmpty(patternValue)) { - return config.isMatchEmptyField(); - } - - - int weightStart = Math.max(0, config.getStart()); + private final String REQUEST_QUERY_PATTERN = "request.query."; + private final String REQUEST_HEADER_PATTERN = "request.header."; + private final String REQUEST_COOKIE_PATTERN = "request.cookie."; + private final String REQUEST_PATH_PATTERN = "request.path"; + + /** + * available range is [0, 1000) + */ + public static final int MAX_WEIGHT_RANGE = 1000; + + public static final String FILED_KEY = "fieldPattern"; + /** + */ + public static final String START_KEY = "start"; + public static final String END_KEY = "end"; + + public static final String MATCH_EMPTY_FIELD_KEY = "matchEmptyField"; + + + public OpenGrayRoutePredicateFactory() { + super(Config.class); + } + + @Override + public List shortcutFieldOrder() { + return Arrays.asList(FILED_KEY, START_KEY, END_KEY, MATCH_EMPTY_FIELD_KEY); + } + + @Override + public Predicate apply(Config config) { + return new GatewayPredicate() { + @Override + public boolean test(ServerWebExchange exchange) { + // get gray value + String patternValue = patternParamResolve(exchange, config.getFieldPattern()); + if (ObjectUtils.isEmpty(patternValue)) { + return config.isMatchEmptyField(); + } + + + int weightStart = Math.max(0, config.getStart()); int weightEnd = Math.min(MAX_WEIGHT_RANGE, config.getEnd()); - if (weightEnd < weightStart) { - // invalid match range - return false; - } + if (weightEnd < weightStart) { + // invalid match range + return false; + } - int val = MurmurHash3.hash32x86(patternValue.getBytes(StandardCharsets.UTF_8)); - int hash = Math.abs(val == Integer.MIN_VALUE ? val++ : val) % MAX_WEIGHT_RANGE; + int hash = Math.abs(MurmurHash3.hash32x86(patternValue.getBytes(StandardCharsets.UTF_8))% MAX_WEIGHT_RANGE) ; return hash >= weightStart && hash < weightEnd; - } - - @Override - public Object getConfig() { - return config; - } - - @Override - public String toString() { - return String.format("field: %s ,start=%s ,end=%s", config.getFieldPattern(), config.getStart(), config.getEnd()); - } - }; - } - - private String patternParamResolve(ServerWebExchange exchange, String singlePattern) { - if (ObjectUtils.isEmpty(singlePattern)) { - return ""; - } - if (singlePattern.startsWith(REQUEST_QUERY_PATTERN)) { - String fieldName = singlePattern.substring(REQUEST_QUERY_PATTERN.length()); - return Optional.ofNullable(exchange.getRequest().getQueryParams().getFirst(fieldName)).orElse(""); - } else if (singlePattern.startsWith(REQUEST_HEADER_PATTERN)) { - String headerName = singlePattern.substring(REQUEST_HEADER_PATTERN.length()); - return Optional.ofNullable(exchange.getRequest().getHeaders().getFirst(headerName)).orElse(""); - } else if (singlePattern.startsWith(REQUEST_COOKIE_PATTERN)) { - String cookieName = singlePattern.substring(REQUEST_COOKIE_PATTERN.length()); - return Optional.ofNullable(exchange.getRequest().getCookies().getFirst(cookieName)) - .map(HttpCookie::getValue) - .orElse(""); - } else if (Objects.equals(singlePattern, REQUEST_PATH_PATTERN)) { - return Optional.ofNullable(exchange.getRequest().getURI().getPath()).orElse(""); - } else { - // unsupported - return ""; - } - } - - /** - * match range [start, end) range. [0, 1000) full match range - */ - @Validated - public static class Config { - - /** - * field pattern to specify which field weight calculate base on - *

- * request.header.xx - * request.query.xx - * request.cookie.xx - * request.path - *

- */ - - private String fieldPattern; - - /** - * how to handle empty field value. false miss-match, true always match - */ - private boolean matchEmptyField = false; - - /** - * weight range include start, valid input [0, 999] - */ - private int start; - /** - * weight range exclude end, valid input [1, 1000] - */ - private int end; - - public String getFieldPattern() { - return fieldPattern; - } - - public void setFieldPattern(String fieldPattern) { - this.fieldPattern = fieldPattern; - } - - public int getStart() { - return start; - } - - public void setStart(int start) { - this.start = start; - } - - public int getEnd() { - return end; - } - - public void setEnd(int end) { - this.end = end; - } - - public boolean isMatchEmptyField() { - return matchEmptyField; - } - - public void setMatchEmptyField(boolean matchEmptyField) { - this.matchEmptyField = matchEmptyField; - } - } + } + + @Override + public Object getConfig() { + return config; + } + + @Override + public String toString() { + return String.format("field: %s ,start=%s ,end=%s", config.getFieldPattern(), config.getStart(), config.getEnd()); + } + }; + } + + private String patternParamResolve(ServerWebExchange exchange, String singlePattern){ + if (ObjectUtils.isEmpty(singlePattern)) { + return ""; + } + if (singlePattern.startsWith(REQUEST_QUERY_PATTERN)) { + String fieldName = singlePattern.substring(REQUEST_QUERY_PATTERN.length()); + return Optional.ofNullable(exchange.getRequest().getQueryParams().getFirst(fieldName)).orElse(""); + } else if (singlePattern.startsWith(REQUEST_HEADER_PATTERN)) { + String headerName = singlePattern.substring(REQUEST_HEADER_PATTERN.length()); + return Optional.ofNullable(exchange.getRequest().getHeaders().getFirst(headerName)).orElse(""); + }else if (singlePattern.startsWith(REQUEST_COOKIE_PATTERN)) { + String cookieName = singlePattern.substring(REQUEST_COOKIE_PATTERN.length()); + return Optional.ofNullable(exchange.getRequest().getCookies().getFirst(cookieName)) + .map(HttpCookie::getValue) + .orElse(""); + }else if (Objects.equals(singlePattern, REQUEST_PATH_PATTERN)) { + return Optional.ofNullable(exchange.getRequest().getURI().getPath()).orElse(""); + }else { + // unsupported + return ""; + } + } + + /** + * match range [start, end) range. [0, 1000) full match range + */ + @Validated + public static class Config { + + /** + * field pattern to specify which field weight calculate base on + *

+ * request.header.xx + * request.query.xx + * request.cookie.xx + * request.path + *

+ */ + + private String fieldPattern; + + /** + * how to handle empty field value. false miss-match, true always match + */ + private boolean matchEmptyField = false; + + /** + * weight range include start, valid input [0, 999] + */ + private int start; + /** + * weight range exclude end, valid input [1, 1000] + */ + private int end; + + public String getFieldPattern() { + return fieldPattern; + } + + public void setFieldPattern(String fieldPattern) { + this.fieldPattern = fieldPattern; + } + + public int getStart() { + return start; + } + + public void setStart(int start) { + this.start = start; + } + + public int getEnd() { + return end; + } + + public void setEnd(int end) { + this.end = end; + } + + public boolean isMatchEmptyField() { + return matchEmptyField; + } + + public void setMatchEmptyField(boolean matchEmptyField) { + this.matchEmptyField = matchEmptyField; + } + } }