This is an automated email from the ASF dual-hosted git repository.
Aias00 pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/shenyu.git
The following commit(s) were added to refs/heads/master by this push:
new 0f4487a7de perf(plugin): select matching candidates in one pass (#7264)
0f4487a7de is described below
commit 0f4487a7de4f0e4324a58a533dcc218714e5b5f6
Author: Liming Deng <[email protected]>
AuthorDate: Wed Sep 30 11:36:07 2026 +0800
perf(plugin): select matching candidates in one pass (#7264)
* perf(plugin): select matching candidates in one pass
* test(plugin): pin matching precedence with real conditions
---
.../shenyu/plugin/base/AbstractShenyuPlugin.java | 90 +++++++---------
.../shenyu/plugin/base/SinglePassMatchingTest.java | 117 +++++++++++++++++++++
2 files changed, 155 insertions(+), 52 deletions(-)
diff --git
a/shenyu-plugin/shenyu-plugin-base/src/main/java/org/apache/shenyu/plugin/base/AbstractShenyuPlugin.java
b/shenyu-plugin/shenyu-plugin-base/src/main/java/org/apache/shenyu/plugin/base/AbstractShenyuPlugin.java
index 87b4214c05..981ea6437a 100644
---
a/shenyu-plugin/shenyu-plugin-base/src/main/java/org/apache/shenyu/plugin/base/AbstractShenyuPlugin.java
+++
b/shenyu-plugin/shenyu-plugin-base/src/main/java/org/apache/shenyu/plugin/base/AbstractShenyuPlugin.java
@@ -41,12 +41,8 @@ import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;
import java.util.Collection;
-import java.util.Collections;
-import java.util.Comparator;
import java.util.List;
-import java.util.Map;
import java.util.Objects;
-import java.util.stream.Collectors;
/**
* abstract shenyu plugin please extends.
@@ -231,32 +227,26 @@ public abstract class AbstractShenyuPlugin implements
ShenyuPlugin {
}
private Pair<Boolean, SelectorData> matchSelector(final ServerWebExchange
exchange, final Collection<SelectorData> selectors) {
- List<SelectorData> filterCollectors = selectors.stream()
- .filter(selector -> selector.getEnabled() &&
filterSelector(selector, exchange))
- .distinct()
- .collect(Collectors.toList());
- if (filterCollectors.size() > 1) {
- return Pair.of(Boolean.FALSE, manyMatchSelector(filterCollectors));
- } else {
- return Pair.of(Boolean.TRUE,
filterCollectors.stream().findFirst().orElse(null));
+ SelectorData best = null;
+ int bestSpecificity = -1;
+ boolean unique = true;
+ for (SelectorData selector : selectors) {
+ if (!selector.getEnabled() || !filterSelector(selector, exchange))
{
+ continue;
+ }
+ if (Objects.nonNull(best)) {
+ if (best.equals(selector)) {
+ continue;
+ }
+ unique = false;
+ }
+ int specificity = MatchModeEnum.match(selector.getMatchMode(),
MatchModeEnum.AND) ? CollectionUtils.size(selector.getConditionList()) : 0;
+ if (Objects.isNull(best) || specificity > bestSpecificity ||
specificity == bestSpecificity && selector.getSort() < best.getSort()) {
+ best = selector;
+ bestSpecificity = specificity;
+ }
}
- }
-
- private SelectorData manyMatchSelector(final List<SelectorData>
filterCollectors) {
- //What needs to be dealt with here is the and condition. If the number
of and conditions is the same and is matched at the same time,
- // it will be sorted by the sort field.
- Map<Integer, List<Pair<Integer, SelectorData>>> collect =
- filterCollectors.stream().map(selector -> {
- boolean match =
MatchModeEnum.match(selector.getMatchMode(), MatchModeEnum.AND);
- int sort = 0;
- if (match) {
- sort = selector.getConditionList().size();
- }
- return Pair.of(sort, selector);
- }).collect(Collectors.groupingBy(Pair::getLeft));
- Integer max = Collections.max(collect.keySet());
- List<Pair<Integer, SelectorData>> pairs = collect.get(max);
- return
pairs.stream().map(Pair::getRight).min(Comparator.comparing(SelectorData::getSort)).orElse(null);
+ return Pair.of(unique, best);
}
private Boolean filterSelector(final SelectorData selector, final
ServerWebExchange exchange) {
@@ -270,30 +260,26 @@ public abstract class AbstractShenyuPlugin implements
ShenyuPlugin {
}
private Pair<Boolean, RuleData> matchRule(final ServerWebExchange
exchange, final Collection<RuleData> rules) {
- List<RuleData> filterRuleData = rules.stream()
- .filter(rule -> filterRule(rule, exchange))
- .distinct()
- .collect(Collectors.toList());
- if (filterRuleData.size() > 1) {
- return Pair.of(Boolean.FALSE, manyMatchRule(filterRuleData));
- } else {
- return Pair.of(Boolean.TRUE,
filterRuleData.stream().findFirst().orElse(null));
+ RuleData best = null;
+ int bestSpecificity = -1;
+ boolean unique = true;
+ for (RuleData rule : rules) {
+ if (!filterRule(rule, exchange)) {
+ continue;
+ }
+ if (Objects.nonNull(best)) {
+ if (best.equals(rule)) {
+ continue;
+ }
+ unique = false;
+ }
+ int specificity = MatchModeEnum.match(rule.getMatchMode(),
MatchModeEnum.AND) ? CollectionUtils.size(rule.getConditionDataList()) : 0;
+ if (Objects.isNull(best) || specificity > bestSpecificity ||
specificity == bestSpecificity && rule.getSort() < best.getSort()) {
+ best = rule;
+ bestSpecificity = specificity;
+ }
}
- }
-
- private RuleData manyMatchRule(final List<RuleData> filterRuleData) {
- Map<Integer, List<Pair<Integer, RuleData>>> collect =
- filterRuleData.stream().map(rule -> {
- boolean match = MatchModeEnum.match(rule.getMatchMode(),
MatchModeEnum.AND);
- int sort = 0;
- if (match) {
- sort = rule.getConditionDataList().size();
- }
- return Pair.of(sort, rule);
- }).collect(Collectors.groupingBy(Pair::getLeft));
- Integer max = Collections.max(collect.keySet());
- List<Pair<Integer, RuleData>> pairs = collect.get(max);
- return
pairs.stream().map(Pair::getRight).min(Comparator.comparing(RuleData::getSort)).orElse(null);
+ return Pair.of(unique, best);
}
private Boolean filterRule(final RuleData ruleData, final
ServerWebExchange exchange) {
diff --git
a/shenyu-plugin/shenyu-plugin-base/src/test/java/org/apache/shenyu/plugin/base/SinglePassMatchingTest.java
b/shenyu-plugin/shenyu-plugin-base/src/test/java/org/apache/shenyu/plugin/base/SinglePassMatchingTest.java
new file mode 100644
index 0000000000..ed755923a5
--- /dev/null
+++
b/shenyu-plugin/shenyu-plugin-base/src/test/java/org/apache/shenyu/plugin/base/SinglePassMatchingTest.java
@@ -0,0 +1,117 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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
+ *
+ * http://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.apache.shenyu.plugin.base;
+
+import org.apache.commons.lang3.tuple.Pair;
+import org.apache.shenyu.common.dto.ConditionData;
+import org.apache.shenyu.common.dto.RuleData;
+import org.apache.shenyu.common.dto.SelectorData;
+import org.apache.shenyu.common.enums.SelectorTypeEnum;
+import org.apache.shenyu.plugin.base.condition.strategy.MatchStrategyFactory;
+import org.junit.jupiter.api.Test;
+import org.mockito.MockedStatic;
+import org.springframework.mock.http.server.reactive.MockServerHttpRequest;
+import org.springframework.mock.web.server.MockServerWebExchange;
+import org.springframework.test.util.ReflectionTestUtils;
+import org.springframework.web.server.ServerWebExchange;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.List;
+import java.util.Random;
+import java.util.stream.Collectors;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertSame;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.Mockito.CALLS_REAL_METHODS;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.mockStatic;
+
+class SinglePassMatchingTest {
+
+ @Test
+ void specificityThenSortThenEncounterOrderDetermineTheWinner() {
+ final AbstractShenyuPlugin plugin = mock(AbstractShenyuPlugin.class,
CALLS_REAL_METHODS);
+ final ServerWebExchange exchange =
MockServerWebExchange.from(MockServerHttpRequest.get("/test"));
+ ConditionData condition = new ConditionData();
+ condition.setParamType("uri");
+ condition.setOperator("=");
+ condition.setParamValue("/test");
+ List<SelectorData> selectors = new ArrayList<>();
+ List<RuleData> rules = new ArrayList<>();
+ // Candidate 1 wins over a less-specific lower sort, a higher sort,
and an equally ranked later entry.
+ for (int i = 0; i < 4; i++) {
+ List<ConditionData> conditions = Collections.nCopies(i == 0 ? 1 :
2, condition);
+ int sort = i == 0 ? 0 : i == 2 ? 10 : 5;
+
selectors.add(SelectorData.builder().id(String.valueOf(i)).enabled(true).matchMode(0).sort(sort)
+
.type(SelectorTypeEnum.CUSTOM_FLOW.getCode()).conditionList(conditions).build());
+
rules.add(RuleData.builder().id(String.valueOf(i)).enabled(true).matchMode(0).sort(sort).conditionDataList(conditions).build());
+ }
+ Pair<Boolean, SelectorData> selectorResult =
ReflectionTestUtils.invokeMethod(plugin, "matchSelector", exchange, selectors);
+ Pair<Boolean, RuleData> ruleResult =
ReflectionTestUtils.invokeMethod(plugin, "matchRule", exchange, rules);
+ assertSame(selectors.get(1), selectorResult.getRight());
+ assertSame(rules.get(1), ruleResult.getRight());
+ assertEquals(Boolean.FALSE, selectorResult.getLeft());
+ assertEquals(Boolean.FALSE, ruleResult.getLeft());
+ }
+
+ @Test
+ void preservesSelectionAndCacheEligibilityAcrossCandidateOrderings() {
+ AbstractShenyuPlugin plugin = mock(AbstractShenyuPlugin.class,
CALLS_REAL_METHODS);
+ ServerWebExchange exchange =
MockServerWebExchange.from(MockServerHttpRequest.get("/test"));
+ Random random = new Random(6579);
+ try (MockedStatic<MatchStrategyFactory> matches =
mockStatic(MatchStrategyFactory.class)) {
+ matches.when(() -> MatchStrategyFactory.match(anyInt(), any(),
any())).thenReturn(true);
+ for (int round = 0; round < 100; round++) {
+ List<SelectorData> selectors = new ArrayList<>();
+ List<RuleData> rules = new ArrayList<>();
+ for (int i = 0; i < round % 10; i++) {
+ int mode = random.nextInt(2);
+ int sort = random.nextInt(4);
+ boolean enabled = random.nextBoolean();
+ List<ConditionData> conditions =
Collections.nCopies(random.nextInt(4), new ConditionData());
+
selectors.add(SelectorData.builder().id(String.valueOf(i)).enabled(enabled).matchMode(mode).sort(sort)
+
.type(SelectorTypeEnum.FULL_FLOW.getCode()).conditionList(conditions).build());
+
rules.add(RuleData.builder().id(String.valueOf(i)).enabled(enabled).matchMode(mode).sort(sort).conditionDataList(conditions).build());
+ }
+ selectors.addAll(new ArrayList<>(selectors));
+ rules.addAll(new ArrayList<>(rules));
+ Collections.shuffle(selectors, random);
+ Collections.shuffle(rules, random);
+ List<SelectorData> expectedSelectors =
selectors.stream().filter(SelectorData::getEnabled).distinct().collect(Collectors.toList());
+ SelectorData expectedSelector =
expectedSelectors.stream().min(Comparator
+ .comparingInt((SelectorData selector) ->
selector.getMatchMode() == 0 ? -selector.getConditionList().size() : 0)
+ .thenComparing(SelectorData::getSort)).orElse(null);
+ Pair<Boolean, SelectorData> selectorResult =
ReflectionTestUtils.invokeMethod(plugin, "matchSelector", exchange, selectors);
+ assertSame(expectedSelector, selectorResult.getRight());
+ assertEquals(expectedSelectors.size() <= 1,
selectorResult.getLeft());
+ List<RuleData> expectedRules =
rules.stream().filter(RuleData::getEnabled).distinct().collect(Collectors.toList());
+ RuleData expectedRule = expectedRules.stream().min(Comparator
+ .comparingInt((RuleData rule) -> rule.getMatchMode()
== 0 ? -rule.getConditionDataList().size() : 0)
+ .thenComparing(RuleData::getSort)).orElse(null);
+ Pair<Boolean, RuleData> ruleResult =
ReflectionTestUtils.invokeMethod(plugin, "matchRule", exchange, rules);
+ assertSame(expectedRule, ruleResult.getRight());
+ assertEquals(expectedRules.size() <= 1, ruleResult.getLeft());
+ }
+ }
+ }
+}