diff --git a/shenyu-common/src/main/java/org/apache/shenyu/common/dto/PluginData.java b/shenyu-common/src/main/java/org/apache/shenyu/common/dto/PluginData.java index a93e39e3f6b9..99f83720c550 100644 --- a/shenyu-common/src/main/java/org/apache/shenyu/common/dto/PluginData.java +++ b/shenyu-common/src/main/java/org/apache/shenyu/common/dto/PluginData.java @@ -180,24 +180,14 @@ public int hashCode() { return Objects.hash(getId(), getName(), config, role, getEnabled(), getSort(), pluginJar, getNamespaceId()); } - public static final class Builder { - - private String id; - - private String name; + public static final class Builder extends BaseData { private String config; private String role; - private Boolean enabled; - - private Integer sort; - private String pluginJar; - private String namespaceId; - /** * no args constructor. */ @@ -220,7 +210,7 @@ public static Builder builder() { * @return this */ public Builder id(final String id) { - this.id = id; + setId(id); return this; } @@ -231,7 +221,7 @@ public Builder id(final String id) { * @return this */ public Builder name(final String name) { - this.name = name; + setName(name); return this; } @@ -264,7 +254,7 @@ public Builder role(final String role) { * @return this */ public Builder enabled(final Boolean enabled) { - this.enabled = enabled; + setEnabled(enabled); return this; } @@ -275,7 +265,7 @@ public Builder enabled(final Boolean enabled) { * @return this */ public Builder sort(final Integer sort) { - this.sort = sort; + setSort(sort); return this; } @@ -297,7 +287,7 @@ public Builder pluginJar(final String pluginJar) { * @return this */ public Builder namespaceId(final String namespaceId) { - this.namespaceId = namespaceId; + setNamespaceId(namespaceId); return this; } @@ -308,14 +298,14 @@ public Builder namespaceId(final String namespaceId) { */ public PluginData build() { PluginData pluginData = new PluginData(); - pluginData.setId(id); - pluginData.setName(name); + pluginData.setId(getId()); + pluginData.setName(getName()); pluginData.setConfig(config); pluginData.setRole(role); - pluginData.setEnabled(enabled); - pluginData.setSort(sort); + pluginData.setEnabled(getEnabled()); + pluginData.setSort(getSort()); pluginData.setPluginJar(pluginJar); - pluginData.setNamespaceId(namespaceId); + pluginData.setNamespaceId(getNamespaceId()); return pluginData; } } diff --git a/shenyu-common/src/main/java/org/apache/shenyu/common/dto/RuleData.java b/shenyu-common/src/main/java/org/apache/shenyu/common/dto/RuleData.java index ec3fef65f616..9a6c0b64d997 100644 --- a/shenyu-common/src/main/java/org/apache/shenyu/common/dto/RuleData.java +++ b/shenyu-common/src/main/java/org/apache/shenyu/common/dto/RuleData.java @@ -64,19 +64,19 @@ public RuleData() { * @param builder builder */ private RuleData(final Builder builder) { - this.setId(builder.id); - this.setName(builder.name); + this.setId(builder.getId()); + this.setName(builder.getName()); this.pluginName = builder.pluginName; this.selectorId = builder.selectorId; this.matchMode = builder.matchMode; - this.setSort(builder.sort); - this.setEnabled(builder.enabled); + this.setSort(builder.getSort()); + this.setEnabled(builder.getEnabled()); this.loged = builder.loged; this.handle = builder.handle; this.conditionDataList = builder.conditionDataList; this.beforeConditionDataList = builder.beforeConditionDataList; this.matchRestful = builder.matchRestful; - this.setNamespaceId(builder.namespaceId); + this.setNamespaceId(builder.getNamespaceId()); } /** @@ -298,7 +298,7 @@ public Boolean getMatchRestful() { public void setMatchRestful(final Boolean matchRestful) { this.matchRestful = matchRestful; } - + @Override public boolean equals(final Object o) { if (this == o) { @@ -365,11 +365,7 @@ public String toString() { /** * class builder. */ - public static final class Builder { - - private String id; - - private String name; + public static final class Builder extends BaseData { private String pluginName; @@ -377,10 +373,6 @@ public static final class Builder { private Integer matchMode; - private Integer sort; - - private Boolean enabled; - private Boolean loged; private String handle; @@ -388,10 +380,8 @@ public static final class Builder { private List conditionDataList; private List beforeConditionDataList; - + private Boolean matchRestful; - - private String namespaceId; /** * no args constructor. @@ -415,7 +405,7 @@ public RuleData build() { * @return this */ public Builder id(final String id) { - this.id = id; + setId(id); return this; } @@ -426,7 +416,7 @@ public Builder id(final String id) { * @return this */ public Builder name(final String name) { - this.name = name; + setName(name); return this; } @@ -470,7 +460,7 @@ public Builder matchMode(final Integer matchMode) { * @return this */ public Builder sort(final Integer sort) { - this.sort = sort; + setSort(sort); return this; } @@ -481,7 +471,7 @@ public Builder sort(final Integer sort) { * @return this */ public Builder enabled(final Boolean enabled) { - this.enabled = enabled; + setEnabled(enabled); return this; } @@ -528,7 +518,7 @@ public Builder beforeConditionDataList(final List beforeCondition this.beforeConditionDataList = beforeConditionDataList; return this; } - + /** * build match restful. * @@ -539,7 +529,7 @@ public Builder matchRestful(final Boolean matchRestful) { this.matchRestful = matchRestful; return this; } - + /** * build namespaceId. * @@ -547,7 +537,7 @@ public Builder matchRestful(final Boolean matchRestful) { * @return this */ public Builder namespaceId(final String namespaceId) { - this.namespaceId = namespaceId; + setNamespaceId(namespaceId); return this; } } diff --git a/shenyu-common/src/main/java/org/apache/shenyu/common/dto/SelectorData.java b/shenyu-common/src/main/java/org/apache/shenyu/common/dto/SelectorData.java index c6aec37add82..71750e8afb8e 100644 --- a/shenyu-common/src/main/java/org/apache/shenyu/common/dto/SelectorData.java +++ b/shenyu-common/src/main/java/org/apache/shenyu/common/dto/SelectorData.java @@ -71,21 +71,21 @@ public SelectorData() { * @param builder builder */ private SelectorData(final Builder builder) { - this.setId(builder.id); + this.setId(builder.getId()); this.pluginId = builder.pluginId; this.pluginName = builder.pluginName; - this.setName(builder.name); + this.setName(builder.getName()); this.matchMode = builder.matchMode; this.type = builder.type; - this.setSort(builder.sort); - this.setEnabled(builder.enabled); + this.setSort(builder.getSort()); + this.setEnabled(builder.getEnabled()); this.logged = builder.logged; this.continued = builder.continued; this.handle = builder.handle; this.conditionList = builder.conditionList; this.matchRestful = builder.matchRestful; this.beforeConditionList = builder.beforeConditionList; - this.setNamespaceId(builder.namespaceId); + this.setNamespaceId(builder.getNamespaceId()); } /** @@ -345,11 +345,7 @@ public String toString() { /** * class builder. */ - public static final class Builder { - - private String id; - - private String name; + public static final class Builder extends BaseData { private String pluginId; @@ -359,10 +355,6 @@ public static final class Builder { private Integer type; - private Integer sort; - - private Boolean enabled; - private Boolean logged; private Boolean continued; @@ -375,8 +367,6 @@ public static final class Builder { private List beforeConditionList; - private String namespaceId; - /** * no args constructor. */ @@ -399,7 +389,7 @@ public SelectorData build() { * @return this */ public Builder id(final String id) { - this.id = id; + setId(id); return this; } @@ -432,7 +422,7 @@ public Builder pluginName(final String pluginName) { * @return this */ public Builder name(final String name) { - this.name = name; + setName(name); return this; } @@ -465,7 +455,7 @@ public Builder type(final Integer type) { * @return this */ public Builder sort(final Integer sort) { - this.sort = sort; + setSort(sort); return this; } @@ -476,7 +466,7 @@ public Builder sort(final Integer sort) { * @return this */ public Builder enabled(final Boolean enabled) { - this.enabled = enabled; + setEnabled(enabled); return this; } @@ -553,7 +543,7 @@ public Builder beforeConditionList(final List beforeConditionList * @return this */ public Builder namespaceId(final String namespaceId) { - this.namespaceId = namespaceId; + setNamespaceId(namespaceId); return this; } } 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 46d019790507..139d66349bc6 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 @@ -18,39 +18,22 @@ package org.apache.shenyu.plugin.base; import org.apache.commons.collections4.CollectionUtils; -import org.apache.commons.lang3.StringUtils; -import org.apache.commons.lang3.tuple.Pair; -import org.apache.shenyu.common.config.ShenyuConfig; import org.apache.shenyu.common.constant.Constants; -import org.apache.shenyu.common.dto.ConditionData; import org.apache.shenyu.common.dto.PluginData; import org.apache.shenyu.common.dto.RuleData; import org.apache.shenyu.common.dto.SelectorData; -import org.apache.shenyu.common.enums.MatchModeEnum; import org.apache.shenyu.common.enums.SelectorTypeEnum; -import org.apache.shenyu.common.enums.TrieCacheTypeEnum; -import org.apache.shenyu.common.utils.ListUtil; -import org.apache.shenyu.common.utils.LogUtils; import org.apache.shenyu.plugin.api.ShenyuPlugin; import org.apache.shenyu.plugin.api.ShenyuPluginChain; -import org.apache.shenyu.plugin.api.utils.SpringBeanUtils; -import org.apache.shenyu.plugin.base.cache.BaseDataCache; -import org.apache.shenyu.plugin.base.cache.MatchDataCache; -import org.apache.shenyu.plugin.base.condition.strategy.MatchStrategyFactory; -import org.apache.shenyu.plugin.base.trie.ShenyuTrie; -import org.apache.shenyu.plugin.base.trie.ShenyuTrieNode; +import org.apache.shenyu.plugin.base.maker.PluginDataDecisionMaker; +import org.apache.shenyu.plugin.base.maker.RuleDataDecisionMaker; +import org.apache.shenyu.plugin.base.maker.SelectorDataDecisionMaker; import org.slf4j.Logger; import org.slf4j.LoggerFactory; 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. @@ -59,15 +42,11 @@ public abstract class AbstractShenyuPlugin implements ShenyuPlugin { private static final Logger LOG = LoggerFactory.getLogger(AbstractShenyuPlugin.class); - private static final String URI_CONDITION_TYPE = "uri"; - - private ShenyuTrie selectorTrie; - - private ShenyuTrie ruleTrie; - - private ShenyuConfig.SelectorMatchCache selectorMatchConfig; - - private ShenyuConfig.RuleMatchCache ruleMatchConfig; + private SelectorDataDecisionMaker selectorDataDecisionMaker = new SelectorDataDecisionMaker(); + + private RuleDataDecisionMaker ruleDataDecisionMaker = new RuleDataDecisionMaker(); + + private PluginDataDecisionMaker pluginDataDecisionMaker = new PluginDataDecisionMaker(); /** * this is Template Method child has implements your own logic. @@ -90,133 +69,53 @@ public abstract class AbstractShenyuPlugin implements ShenyuPlugin { */ @Override public Mono execute(final ServerWebExchange exchange, final ShenyuPluginChain chain) { - initCacheConfig(); final String pluginName = named(); - PluginData pluginData = BaseDataCache.getInstance().obtainPluginData(pluginName); - // early exit - if (Objects.isNull(pluginData) || !pluginData.getEnabled()) { - return chain.execute(exchange); - } final String path = getRawPath(exchange); - List selectors = BaseDataCache.getInstance().obtainSelectorData(pluginName); - if (CollectionUtils.isEmpty(selectors)) { - return handleSelectorIfNull(pluginName, exchange, chain); + + List pluginDataList = pluginDataDecisionMaker.getData(pluginName); + if (CollectionUtils.isEmpty(pluginDataList) || !pluginDataDecisionMaker.shouldContinue(pluginDataList.get(0))) { + return pluginDataDecisionMaker.handleEmpty(pluginName, exchange, chain); } - SelectorData selectorData = obtainSelectorDataCacheIfEnabled(path); - // handle Selector - if (Objects.nonNull(selectorData) && StringUtils.isBlank(selectorData.getId())) { - return handleSelectorIfNull(pluginName, exchange, chain); + + List selectorDataList = selectorDataDecisionMaker.getData(pluginName); + if (CollectionUtils.isEmpty(selectorDataList)) { + return selectorDataDecisionMaker.handleEmpty(pluginName, exchange, chain); } + + SelectorData selectorData = selectorDataDecisionMaker.matchData(exchange, pluginName, selectorDataList, path, null); if (Objects.isNull(selectorData)) { - selectorData = trieMatchSelector(exchange, pluginName, path); - if (Objects.isNull(selectorData)) { - selectorData = defaultMatchSelector(exchange, selectors, path); - if (Objects.isNull(selectorData)) { - return handleSelectorIfNull(pluginName, exchange, chain); - } - } + return selectorDataDecisionMaker.handleEmpty(pluginName, exchange, chain); } + printLog(selectorData, pluginName); - if (!selectorData.getContinued()) { - // if continued, not match rules + if (!selectorDataDecisionMaker.shouldContinue(selectorData)) { return doExecute(exchange, chain, selectorData, defaultRuleData(selectorData)); } - List rules = BaseDataCache.getInstance().obtainRuleData(selectorData.getId()); - if (CollectionUtils.isEmpty(rules)) { - return handleRuleIfNull(pluginName, exchange, chain); + + List ruleDataList = ruleDataDecisionMaker.getData(selectorData.getId()); + if (CollectionUtils.isEmpty(ruleDataList)) { + return ruleDataDecisionMaker.handleEmpty(pluginName, exchange, chain); } + if (selectorData.getType() == SelectorTypeEnum.FULL_FLOW.getCode()) { - //get last - RuleData rule = rules.get(rules.size() - 1); + RuleData rule = ruleDataList.get(ruleDataList.size() - 1); printLog(rule, pluginName); return doExecute(exchange, chain, selectorData, rule); } - // lru map as L1 cache,the cache is enabled by default. - // if the L1 cache fails to hit, using L2 cache based on trie cache. - // if the L2 cache fails to hit, execute default strategy. - RuleData ruleData = obtainRuleDataCacheIfEnabled(path); - if (Objects.nonNull(ruleData) && Objects.isNull(ruleData.getId())) { - return handleRuleIfNull(pluginName, exchange, chain); - } + + RuleData ruleData = ruleDataDecisionMaker.matchData(exchange, named(), ruleDataList, path, selectorData); if (Objects.isNull(ruleData)) { - // L1 cache not exist data, try to get data through trie cache - ruleData = trieMatchRule(exchange, selectorData, path); - // trie cache fails to hit, execute default strategy - if (Objects.isNull(ruleData)) { - ruleData = defaultMatchRule(exchange, rules, path); - if (Objects.isNull(ruleData)) { - return handleRuleIfNull(pluginName, exchange, chain); - } - } + return ruleDataDecisionMaker.handleEmpty(pluginName, exchange, chain); } + printLog(ruleData, pluginName); return doExecute(exchange, chain, selectorData, ruleData); } - + protected String getRawPath(final ServerWebExchange exchange) { return exchange.getRequest().getURI().getRawPath(); } - - private void initCacheConfig() { - if (Objects.isNull(selectorMatchConfig) || Objects.isNull(ruleMatchConfig)) { - ShenyuConfig shenyuConfig = SpringBeanUtils.getInstance().getBean(ShenyuConfig.class); - selectorMatchConfig = shenyuConfig.getSelectorMatchCache(); - ruleMatchConfig = shenyuConfig.getRuleMatchCache(); - } - if (Objects.isNull(selectorTrie) || Objects.isNull(ruleTrie)) { - selectorTrie = SpringBeanUtils.getInstance().getBean(TrieCacheTypeEnum.SELECTOR.getTrieType()); - ruleTrie = SpringBeanUtils.getInstance().getBean(TrieCacheTypeEnum.RULE.getTrieType()); - } - } - - private SelectorData obtainSelectorDataCacheIfEnabled(final String path) { - return selectorMatchConfig.getCache().getEnabled() ? MatchDataCache.getInstance().obtainSelectorData(named(), path) : null; - } - - private RuleData obtainRuleDataCacheIfEnabled(final String path) { - return ruleMatchConfig.getCache().getEnabled() ? MatchDataCache.getInstance().obtainRuleData(named(), path) : null; - } - private void cacheSelectorData(final String path, final SelectorData selectorData) { - if (Boolean.FALSE.equals(selectorMatchConfig.getCache().getEnabled()) || Objects.isNull(selectorData) - || Boolean.TRUE.equals(selectorData.getMatchRestful())) { - return; - } - int initialCapacity = selectorMatchConfig.getCache().getInitialCapacity(); - long maximumSize = selectorMatchConfig.getCache().getMaximumSize(); - if (StringUtils.isBlank(selectorData.getId())) { - MatchDataCache.getInstance().cacheSelectorData(path, selectorData, initialCapacity, maximumSize); - return; - } - List conditionList = selectorData.getConditionList(); - if (CollectionUtils.isNotEmpty(conditionList)) { - boolean isUriCondition = conditionList.stream().allMatch(v -> URI_CONDITION_TYPE.equals(v.getParamType())); - if (isUriCondition) { - MatchDataCache.getInstance().cacheSelectorData(path, selectorData, initialCapacity, maximumSize); - } - } - } - - private void cacheRuleData(final String path, final RuleData ruleData) { - // if the ruleCache is disabled or rule data is null, not cache rule data. - if (Boolean.FALSE.equals(ruleMatchConfig.getCache().getEnabled()) || Objects.isNull(ruleData) - || Boolean.TRUE.equals(ruleData.getMatchRestful())) { - return; - } - int initialCapacity = ruleMatchConfig.getCache().getInitialCapacity(); - long maximumSize = ruleMatchConfig.getCache().getMaximumSize(); - if (StringUtils.isBlank(ruleData.getId())) { - MatchDataCache.getInstance().cacheRuleData(path, ruleData, initialCapacity, maximumSize); - return; - } - List conditionList = ruleData.getConditionDataList(); - if (CollectionUtils.isNotEmpty(conditionList)) { - boolean isUriCondition = conditionList.stream().allMatch(v -> URI_CONDITION_TYPE.equals(v.getParamType())); - if (isUriCondition) { - MatchDataCache.getInstance().cacheRuleData(path, ruleData, initialCapacity, maximumSize); - } - } - } private RuleData defaultRuleData(final SelectorData selectorData) { RuleData ruleData = new RuleData(); @@ -225,7 +124,7 @@ private RuleData defaultRuleData(final SelectorData selectorData) { ruleData.setId(Constants.DEFAULT_RULE); return ruleData; } - + /** * Handle selector if null mono. * @@ -237,7 +136,7 @@ private RuleData defaultRuleData(final SelectorData selectorData) { protected Mono handleSelectorIfNull(final String pluginName, final ServerWebExchange exchange, final ShenyuPluginChain chain) { return chain.execute(exchange); } - + /** * Handle rule if null mono. * @@ -250,173 +149,6 @@ protected Mono handleRuleIfNull(final String pluginName, final ServerWebEx return chain.execute(exchange); } - private Pair matchSelector(final ServerWebExchange exchange, final Collection selectors) { - List 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)); - } - } - - private SelectorData manyMatchSelector(final List 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>> 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> pairs = collect.get(max); - return pairs.stream().map(Pair::getRight).min(Comparator.comparing(SelectorData::getSort)).orElse(null); - } - - private Boolean filterSelector(final SelectorData selector, final ServerWebExchange exchange) { - if (selector.getType() == SelectorTypeEnum.CUSTOM_FLOW.getCode()) { - if (CollectionUtils.isEmpty(selector.getConditionList())) { - return false; - } - return MatchStrategyFactory.match(selector.getMatchMode(), selector.getConditionList(), exchange); - } - return true; - } - - private Pair matchRule(final ServerWebExchange exchange, final Collection rules) { - List 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)); - } - } - - private RuleData manyMatchRule(final List filterRuleData) { - Map>> 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> pairs = collect.get(max); - return pairs.stream().map(Pair::getRight).min(Comparator.comparing(RuleData::getSort)).orElse(null); - } - - private Boolean filterRule(final RuleData ruleData, final ServerWebExchange exchange) { - return ruleData.getEnabled() && MatchStrategyFactory.match(ruleData.getMatchMode(), ruleData.getConditionDataList(), exchange); - } - - private SelectorData trieMatchSelector(final ServerWebExchange exchange, final String pluginName, final String path) { - if (!selectorMatchConfig.getTrie().getEnabled()) { - return null; - } - SelectorData selectorData = null; - ShenyuTrieNode shenyuTrieNode = selectorTrie.match(path, pluginName); - if (Objects.nonNull(shenyuTrieNode)) { - LogUtils.info(LOG, "{} selector match path from shenyu trie, path:{}", pluginName, path); - List collection = shenyuTrieNode.getPathCache().get(pluginName); - if (CollectionUtils.isNotEmpty(collection)) { - Pair selectorDataPair; - if (collection.size() > 1) { - selectorDataPair = matchSelector(exchange, ListUtil.castList(collection, SelectorData.class::cast)); - } else { - Object selectorObj = collection.stream().findFirst().orElse(null); - SelectorData selector = Objects.nonNull(selectorObj) ? (SelectorData) selectorObj : null; - boolean cached = Objects.nonNull(selector) && selector.getConditionList().stream().allMatch(condition -> URI_CONDITION_TYPE.equals(condition.getParamType())); - selectorDataPair = Pair.of(cached, selector); - } - selectorData = selectorDataPair.getRight(); - if (selectorDataPair.getLeft() && Objects.nonNull(selectorData)) { - cacheSelectorData(path, selectorData); - } - } - } - return selectorData; - } - - private RuleData trieMatchRule(final ServerWebExchange exchange, final SelectorData selectorData, final String path) { - if (!ruleMatchConfig.getTrie().getEnabled()) { - return null; - } - RuleData ruleData = null; - ShenyuTrieNode shenyuTrieNode = ruleTrie.match(path, selectorData.getId()); - if (Objects.nonNull(shenyuTrieNode)) { - LogUtils.info(LOG, "{} rule match path from shenyu trie", named()); - List collection = shenyuTrieNode.getPathCache().get(selectorData.getId()); - if (CollectionUtils.isNotEmpty(collection)) { - Pair ruleDataPair; - if (collection.size() > 1) { - ruleDataPair = matchRule(exchange, ListUtil.castList(collection, RuleData.class::cast)); - } else { - Object ruleObj = collection.stream().findFirst().orElse(null); - RuleData rule = Objects.nonNull(ruleObj) ? (RuleData) ruleObj : null; - boolean cached = Objects.nonNull(rule) && rule.getConditionDataList().stream().allMatch(condition -> URI_CONDITION_TYPE.equals(condition.getParamType())); - ruleDataPair = Pair.of(cached, rule); - } - ruleData = ruleDataPair.getRight(); - if (ruleDataPair.getLeft() && Objects.nonNull(ruleData)) { - // exist only one rule data, cache rule - cacheRuleData(path, ruleData); - } - } - } - return ruleData; - } - - private SelectorData defaultMatchSelector(final ServerWebExchange exchange, final List selectors, final String path) { - Pair matchSelectorPair = matchSelector(exchange, selectors); - SelectorData selectorData = matchSelectorPair.getRight(); - if (Objects.nonNull(selectorData)) { - LogUtils.info(LOG, "{} selector match success from default strategy", named()); - // cache selector data - if (matchSelectorPair.getLeft()) { - cacheSelectorData(path, selectorData); - } - return selectorData; - } else { - // if not match selector, cache empty selector data. - if (matchSelectorPair.getLeft()) { - SelectorData emptySelectorData = SelectorData.builder().pluginName(named()).build(); - cacheSelectorData(path, emptySelectorData); - } - return null; - } - } - - private RuleData defaultMatchRule(final ServerWebExchange exchange, final List rules, final String path) { - Pair matchRulePair = matchRule(exchange, rules); - RuleData ruleData = matchRulePair.getRight(); - if (Objects.nonNull(ruleData)) { - LOG.info("{} rule match path from default strategy", named()); - // cache rule data - if (matchRulePair.getLeft()) { - cacheRuleData(path, ruleData); - } - return ruleData; - } else { - // if not match rule, cache empty rule data. - if (matchRulePair.getLeft()) { - RuleData emptyRuleData = RuleData.builder().pluginName(named()).build(); - cacheRuleData(path, emptyRuleData); - } - return null; - } - } - /** * print selector log. * please don't delete this method or refactor {@linkplain org.apache.shenyu.plugin.base.AbstractShenyuPlugin#printLog} @@ -430,7 +162,7 @@ private void printLog(final SelectorData selectorData, final String pluginName) LOG.info("{} selector success match , selector name :{}", pluginName, selectorData.getName()); } } - + /** * print rule log. * @@ -442,5 +174,5 @@ private void printLog(final RuleData ruleData, final String pluginName) { LOG.info("{} rule success match , rule name :{}", pluginName, ruleData.getName()); } } - + } diff --git a/shenyu-plugin/shenyu-plugin-base/src/main/java/org/apache/shenyu/plugin/base/maker/AbstractMatchDecisionMaker.java b/shenyu-plugin/shenyu-plugin-base/src/main/java/org/apache/shenyu/plugin/base/maker/AbstractMatchDecisionMaker.java new file mode 100644 index 000000000000..95d6ce703554 --- /dev/null +++ b/shenyu-plugin/shenyu-plugin-base/src/main/java/org/apache/shenyu/plugin/base/maker/AbstractMatchDecisionMaker.java @@ -0,0 +1,97 @@ +/* + * 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.maker; + +import org.apache.shenyu.common.dto.BaseData; +import org.apache.shenyu.common.dto.SelectorData; +import org.apache.shenyu.plugin.api.ShenyuPluginChain; +import org.apache.shenyu.plugin.base.provider.DataProvider; +import org.springframework.web.server.ServerWebExchange; +import reactor.core.publisher.Mono; +import java.util.List; + +/** + * Abstract base class for making match decisions in plugins using the Template Method pattern. + * + *

+ * This class provides a template for processing data matching logic in plugins. Subclasses are expected + * to implement the abstract methods to define specific behaviors for handling empty data, matching data, + * and determining whether processing should continue. + *

+ * + *

+ * The Template Method pattern is used here to allow subclasses to override certain steps of the algorithm + * without changing its structure. + *

+ * + * @param the type of data to be matched, extending {@link BaseData} + */ +public abstract class AbstractMatchDecisionMaker { + + protected static final String URI_CONDITION_TYPE = "uri"; + + private final DataProvider dataProvider; + + + /** + * Constructs an AbstractMatchDecisionMaker with the specified data provider. + * + * @param dataProvider the data provider used to retrieve data for matching + */ + protected AbstractMatchDecisionMaker(final DataProvider dataProvider) { + this.dataProvider = dataProvider; + } + + /** + * Retrieves a list of data associated with the given key. + * + * @param key the key used to retrieve data + * @return a list of data objects associated with the key + */ + public List getData(final String key) { + return dataProvider.getData(key); + } + + /** + * Handles the scenario when no matching data is found for the given plugin. + * + * @param pluginName the name of the plugin + * @param exchange the current server web exchange + * @param chain the plugin chain to continue processing + * @return a {@link Mono} that completes when the handling is done + */ + protected abstract Mono handleEmpty(String pluginName, ServerWebExchange exchange, ShenyuPluginChain chain); + + /** + * Matches the appropriate data from the provided list based on the exchange and path. + * + * @param exchange the current server web exchange + * @param dataList the list of data to match against + * @param path the request path to use for matching + * @return the matched data object, or {@code null} if no match is found + */ + protected abstract T matchData(ServerWebExchange exchange, String dataName, List dataList, String path, SelectorData selectorData); + + /** + * Determines whether processing should continue based on the matched data. + * + * @param data the matched data object + * @return {@code true} if processing should continue, {@code false} otherwise + */ + protected abstract boolean shouldContinue(T data); +} \ No newline at end of file diff --git a/shenyu-plugin/shenyu-plugin-base/src/main/java/org/apache/shenyu/plugin/base/maker/PluginDataDecisionMaker.java b/shenyu-plugin/shenyu-plugin-base/src/main/java/org/apache/shenyu/plugin/base/maker/PluginDataDecisionMaker.java new file mode 100644 index 000000000000..b851770239d2 --- /dev/null +++ b/shenyu-plugin/shenyu-plugin-base/src/main/java/org/apache/shenyu/plugin/base/maker/PluginDataDecisionMaker.java @@ -0,0 +1,127 @@ +/* + * 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.maker; + +import org.apache.shenyu.common.dto.PluginData; +import org.apache.shenyu.common.dto.SelectorData; +import org.apache.shenyu.plugin.api.ShenyuPluginChain; +import org.apache.shenyu.plugin.base.provider.PluginDataProvider; +import org.springframework.web.server.ServerWebExchange; +import reactor.core.publisher.Mono; + +import java.util.List; + +/** + * A specialized decision maker implementation responsible for plugin data matching and decision-making + * within the Apache ShenYu gateway ecosystem. + * + *

This class extends {@link AbstractMatchDecisionMaker} to provide plugin-specific matching + * capabilities for {@link PluginData} objects. It serves as the foundational decision maker in + * ShenYu's three-tier matching system (Plugin → Selector → Rule), determining which plugins should + * process incoming requests based on their enabled status and configuration data.

+ * + *

In the ShenYu gateway architecture, this component operates at the highest level of the + * decision hierarchy: + *

    + *
  1. Plugin Level: Determines if a plugin is enabled and should process the request
  2. + *
  3. Selector Level: Handles coarse-grained routing decisions
  4. + *
  5. Rule Level: Applies fine-grained processing rules
  6. + *
+ * The PluginDataDecisionMaker ensures that only active and properly configured plugins participate + * in request processing, maintaining gateway efficiency and reliability.

+ * + *

This decision maker integrates with ShenYu's plugin chain mechanism through the provided + * {@link PluginDataProvider}, which supplies the plugin configuration data needed for matching + * decisions. It follows the gateway's reactive programming model by returning {@link Mono} types + * for all asynchronous operations.

+ */ +public class PluginDataDecisionMaker extends AbstractMatchDecisionMaker { + + /** + * Constructs a new PluginDataDecisionMaker with a PluginDataProvider. + * + *

This constructor initializes the decision maker with a dedicated data provider + * that supplies plugin configuration information. The provider enables access to + * plugin metadata including enabled status, configuration parameters, and execution + * order settings.

+ */ + public PluginDataDecisionMaker() { + super(new PluginDataProvider()); + } + + /** + * Handles the scenario when no plugin data is found for the specified plugin. + * + *

When no matching plugin data is available, this method ensures the request + * continues through the plugin chain without interruption. This graceful degradation + * approach maintains system reliability by allowing requests to proceed even when + * specific plugin configurations are missing or unavailable.

+ * + * @param pluginName the name of the plugin being processed + * @param exchange the current server web exchange containing request and response data + * @param chain the plugin chain for continued request processing + * @return a Mono indicating completion of the empty handler operation + */ + @Override + public Mono handleEmpty(final String pluginName, final ServerWebExchange exchange, final ShenyuPluginChain chain) { + return chain.execute(exchange); + } + + /** + * Matches plugin data against the current request context. + * + *

This method implements the core plugin matching logic, selecting the appropriate + * plugin data from the available candidates. It employs a straightforward selection + * strategy: when multiple plugin data entries are available, it returns the first one + * from the list, assuming the list is pre-sorted by priority or relevance.

+ * + *

In typical ShenYu usage, plugin data lists contain configuration for a specific + * plugin name, so returning the first entry is semantically correct as there should + * be only one active configuration per plugin.

+ * + * @param exchange the current server web exchange containing request information + * @param dataName the name of the plugin data being matched + * @param dataList the list of candidate plugin data for matching + * @param path the request path (currently unused in base implementation) + * @param selectorData the selector data context (currently unused in base implementation) + * @return the matched PluginData, or null if the data list is empty + */ + @Override + public PluginData matchData(final ServerWebExchange exchange, final String dataName, final List dataList, final String path, final SelectorData selectorData) { + return dataList.isEmpty() ? null : dataList.get(0); + } + + /** + * Determines whether the plugin chain should continue processing after the current plugin. + * + *

This decision is based solely on the plugin's enabled status. Only plugins that are + * explicitly enabled will allow continuation of the plugin chain. This provides a simple + * but effective mechanism for controlling plugin execution flow within the gateway.

+ * + *

The continuation mechanism enables complex processing scenarios where certain plugins + * might terminate request processing (e.g., authentication failures) while others allow + * progression to subsequent processing stages.

+ * + * @param data the plugin data to evaluate for continuation + * @return true if the plugin is enabled and should continue processing, false otherwise + */ + @Override + public boolean shouldContinue(final PluginData data) { + return data.getEnabled(); + } +} \ No newline at end of file diff --git a/shenyu-plugin/shenyu-plugin-base/src/main/java/org/apache/shenyu/plugin/base/maker/RuleDataDecisionMaker.java b/shenyu-plugin/shenyu-plugin-base/src/main/java/org/apache/shenyu/plugin/base/maker/RuleDataDecisionMaker.java new file mode 100644 index 000000000000..ba69ee888d39 --- /dev/null +++ b/shenyu-plugin/shenyu-plugin-base/src/main/java/org/apache/shenyu/plugin/base/maker/RuleDataDecisionMaker.java @@ -0,0 +1,329 @@ +/* + * 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.maker; + +import org.apache.commons.collections4.CollectionUtils; +import org.apache.commons.lang3.StringUtils; +import org.apache.commons.lang3.tuple.Pair; +import org.apache.shenyu.common.config.ShenyuConfig; +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.MatchModeEnum; +import org.apache.shenyu.common.enums.TrieCacheTypeEnum; +import org.apache.shenyu.common.utils.ListUtil; +import org.apache.shenyu.common.utils.LogUtils; +import org.apache.shenyu.plugin.api.ShenyuPluginChain; +import org.apache.shenyu.plugin.api.utils.SpringBeanUtils; +import org.apache.shenyu.plugin.base.cache.MatchDataCache; +import org.apache.shenyu.plugin.base.condition.strategy.MatchStrategyFactory; +import org.apache.shenyu.plugin.base.provider.RuleDataProvider; +import org.apache.shenyu.plugin.base.trie.ShenyuTrie; +import org.apache.shenyu.plugin.base.trie.ShenyuTrieNode; +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; + +import static org.apache.shenyu.plugin.api.ShenyuPlugin.LOG; + +/** + * A specialized decision maker implementation that handles rule matching logic within the Apache ShenYu gateway. + * + *

This class extends {@link AbstractMatchDecisionMaker} to provide sophisticated rule matching + * capabilities for {@link RuleData} objects. It implements the core decision-making logic for determining + * which specific rules should be applied to incoming requests based on path matching, conditions, and + * configurable matching strategies.

+ * + *

The RuleDataDecisionMaker operates as the second level in ShenYu's two-tier routing system: + *

    + *
  1. Selector Level: Coarse-grained routing using {@link SelectorData}
  2. + *
  3. Rule Level: Fine-grained processing logic using {@link RuleData}
  4. + *
+ * After a selector matches a request, this component determines the specific rule to apply within that selector. + *

+ * + *

The decision maker employs multiple matching strategies: + *

    + *
  • Trie-based path matching: Efficient URI path lookup using {@link ShenyuTrie}
  • + *
  • Condition evaluation: Rule condition validation using {@link MatchStrategyFactory}
  • + *
  • Priority resolution: Conflict resolution when multiple rules match the same request
  • + *
  • Configurable caching: Performance optimization through {@link MatchDataCache}
  • + *
+ *

+ * + *

This component integrates with ShenYu's plugin architecture to provide precise rule-based + * request processing, supporting complex routing scenarios with conditional logic and performance + * optimization through intelligent caching mechanisms.

+ */ +public class RuleDataDecisionMaker extends AbstractMatchDecisionMaker { + + private ShenyuConfig.RuleMatchCache ruleMatchConfig; + + private ShenyuTrie ruleTrie; + + /** + * Constructs a new RuleDataDecisionMaker with a RuleDataProvider. + * + *

Initializes the cache configuration and trie data structure specifically optimized + * for rule matching operations. The constructor sets up the necessary infrastructure + * for efficient rule lookup and matching performance.

+ */ + public RuleDataDecisionMaker() { + super(new RuleDataProvider()); + initCacheConfig(); + } + + /** + * Handles the scenario when no rule data is found for the given rule name. + * + *

When no matching rules are found, this method allows the request to continue + * through the plugin chain without rule-specific processing. This ensures that + * the gateway can handle requests even when no specific rules are configured, + * maintaining system reliability and avoiding request processing interruptions.

+ * + * @param pluginName the name of the plugin being executed + * @param exchange the current server web exchange containing request and response data + * @param chain the plugin chain for continued request processing + * @return a Mono indicating completion of the empty handler operation + */ + @Override + public Mono handleEmpty(final String pluginName, final ServerWebExchange exchange, final ShenyuPluginChain chain) { + return chain.execute(exchange); + } + + /** + * Matches rule data against the current request context. + * + *

This method implements the core rule matching logic, providing two matching strategies: + *

    + *
  1. Direct matching: Uses pre-loaded rule data from the provided list when available
  2. + *
  3. Trie-based matching: Falls back to sophisticated trie-based path matching + * when the data list is empty
  4. + *
+ * The method ensures optimal performance by prioritizing cached data while maintaining + * comprehensive matching capabilities through the trie structure. + *

+ * + * @param exchange the current server web exchange containing request information + * @param ruleName the name of the rule requesting matching + * @param dataList the list of candidate rule data for matching (may be empty) + * @param path the request path to match against rules + * @param selectorData the parent selector data context for rule matching + * @return the matched RuleData, or null if no suitable rule is found + */ + @Override + public RuleData matchData(final ServerWebExchange exchange, final String ruleName, final List dataList, final String path, final SelectorData selectorData) { + return dataList.isEmpty() ? trieMatchRule(exchange, ruleName, selectorData, path) : dataList.get(0); + } + + /** + * Determines whether the plugin chain should continue processing after the current rule. + * + *

Rule continuation is based on the rule's enabled status. Only enabled rules + * permit further processing in the plugin chain. This allows for complex rule + * sequences where specific rules can terminate processing while others allow + * continuation to subsequent rules or plugins.

+ * + * @param data the rule data to evaluate for continuation + * @return true if the rule is enabled and should continue processing, false otherwise + */ + + @Override + public boolean shouldContinue(final RuleData data) { + return data.getEnabled(); + } + + /** + * Initializes the cache configuration and trie data structure for rule matching. + * + *

This method performs lazy initialization of the rule matching infrastructure, + * retrieving configuration from ShenYu's global configuration and obtaining the + * appropriate trie implementation based on the rule cache type enumeration.

+ */ + private void initCacheConfig() { + if (Objects.isNull(ruleMatchConfig)) { + ShenyuConfig shenyuConfig = SpringBeanUtils.getInstance().getBean(ShenyuConfig.class); + ruleMatchConfig = shenyuConfig.getRuleMatchCache(); + } + + if (Objects.isNull(ruleTrie)) { + ruleTrie = SpringBeanUtils.getInstance().getBean(TrieCacheTypeEnum.RULE.getTrieType()); + } + } + + /** + * Performs trie-based matching for rules when no pre-loaded data is available. + * + *

This method implements sophisticated trie-based matching that: + *

    + *
  • Validates that trie matching is enabled in the configuration
  • + *
  • Uses the ShenyuTrie to find path matches based on selector context
  • + *
  • Handles both single and multiple rule matches with appropriate resolution
  • + *
  • Applies caching for performance optimization when conditions permit
  • + *
+ *

+ * + * @param exchange the current server web exchange + * @param ruleName the name of the rule being processed + * @param selectorData the parent selector data providing context for rule matching + * @param path the request path to match + * @return the matched RuleData, or null if no match is found or trie matching is disabled + */ + private RuleData trieMatchRule(final ServerWebExchange exchange, final String ruleName, final SelectorData selectorData, final String path) { + if (!ruleMatchConfig.getTrie().getEnabled()) { + return null; + } + RuleData ruleData = null; + ShenyuTrieNode shenyuTrieNode = ruleTrie.match(path, selectorData.getId()); + if (Objects.nonNull(shenyuTrieNode)) { + LogUtils.info(LOG, "{} rule match path from shenyu trie", ruleName); + List collection = shenyuTrieNode.getPathCache().get(selectorData.getId()); + if (CollectionUtils.isNotEmpty(collection)) { + Pair ruleDataPair; + if (collection.size() > 1) { + ruleDataPair = matchRule(exchange, ListUtil.castList(collection, RuleData.class::cast)); + } else { + Object ruleObj = collection.stream().findFirst().orElse(null); + RuleData rule = Objects.nonNull(ruleObj) ? (RuleData) ruleObj : null; + boolean cached = Objects.nonNull(rule) && rule.getConditionDataList().stream().allMatch(condition -> URI_CONDITION_TYPE.equals(condition.getParamType())); + ruleDataPair = Pair.of(cached, rule); + } + ruleData = ruleDataPair.getRight(); + if (ruleDataPair.getLeft() && Objects.nonNull(ruleData)) { + // exist only one rule data, cache rule + cacheRuleData(path, ruleData); + } + } + } + return ruleData; + } + + /** + * Filters and matches a collection of rules against the current exchange. + * + *

This method applies rule conditions and matching strategies to determine + * which rules are appropriate for the current request. It handles conflict + * resolution when multiple rules match and provides caching recommendations + * based on the matching results.

+ * + * @param exchange the current server web exchange + * @param rules the collection of rules to filter and match + * @return a Pair where the left value indicates cacheability and the right value contains the matched RuleData + */ + private Pair matchRule(final ServerWebExchange exchange, final Collection rules) { + List 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)); + } + } + + /** + * Filters a single rule based on its enabled status and conditions. + * + *

This method validates that a rule is enabled and applies the appropriate + * match strategy using the MatchStrategyFactory. It serves as the core + * condition evaluation mechanism for rule matching.

+ * + * @param ruleData the rule to filter + * @param exchange the current server web exchange for condition evaluation + * @return true if the rule is enabled and matches the current request conditions, false otherwise + */ + private Boolean filterRule(final RuleData ruleData, final ServerWebExchange exchange) { + return ruleData.getEnabled() && MatchStrategyFactory.match(ruleData.getMatchMode(), ruleData.getConditionDataList(), exchange); + } + + /** + * Resolves conflicts when multiple rules match the same request. + * + *

This method implements sophisticated priority resolution logic that: + *

    + *
  1. Groups rules by the number of AND conditions matched
  2. + *
  3. Selects the group with the highest number of AND conditions
  4. + *
  5. Within that group, chooses the rule with the lowest sort value
  6. + *
+ * This ensures that more specific rules (with more conditions) take precedence + * over general ones, providing predictable and configurable rule prioritization. + *

+ * + * @param filterRuleData the list of matching rules to resolve + * @return the highest priority rule according to the resolution rules + */ + private RuleData manyMatchRule(final List filterRuleData) { + Map>> 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> pairs = collect.get(max); + return pairs.stream().map(Pair::getRight).min(Comparator.comparing(RuleData::getSort)).orElse(null); + } + + /** + * Caches matched rule data for improved performance on subsequent requests. + * + *

This method implements configurable caching that stores rule data when: + *

    + *
  • Caching is enabled in the configuration
  • + *
  • The rule data is not null
  • + *
  • The rule doesn't represent a RESTful match
  • + *
  • All conditions are URI-based (ensuring cache validity)
  • + *
+ * The caching mechanism respects the configured initial capacity and maximum + * size parameters to optimize memory usage while maintaining performance. + *

+ * + * @param path the request path used as cache key + * @param ruleData the rule data to cache + */ + private void cacheRuleData(final String path, final RuleData ruleData) { + // if the ruleCache is disabled or rule data is null, not cache rule data. + if (Boolean.FALSE.equals(ruleMatchConfig.getCache().getEnabled()) || Objects.isNull(ruleData) + || Boolean.TRUE.equals(ruleData.getMatchRestful())) { + return; + } + int initialCapacity = ruleMatchConfig.getCache().getInitialCapacity(); + long maximumSize = ruleMatchConfig.getCache().getMaximumSize(); + if (StringUtils.isBlank(ruleData.getId())) { + MatchDataCache.getInstance().cacheRuleData(path, ruleData, initialCapacity, maximumSize); + return; + } + List conditionList = ruleData.getConditionDataList(); + if (CollectionUtils.isNotEmpty(conditionList)) { + boolean isUriCondition = conditionList.stream().allMatch(v -> URI_CONDITION_TYPE.equals(v.getParamType())); + if (isUriCondition) { + MatchDataCache.getInstance().cacheRuleData(path, ruleData, initialCapacity, maximumSize); + } + } + } +} \ No newline at end of file diff --git a/shenyu-plugin/shenyu-plugin-base/src/main/java/org/apache/shenyu/plugin/base/maker/SelectorDataDecisionMaker.java b/shenyu-plugin/shenyu-plugin-base/src/main/java/org/apache/shenyu/plugin/base/maker/SelectorDataDecisionMaker.java new file mode 100644 index 000000000000..8c1a8b0ac051 --- /dev/null +++ b/shenyu-plugin/shenyu-plugin-base/src/main/java/org/apache/shenyu/plugin/base/maker/SelectorDataDecisionMaker.java @@ -0,0 +1,327 @@ +/* + * 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.maker; + +import org.apache.commons.collections4.CollectionUtils; +import org.apache.commons.lang3.StringUtils; +import org.apache.commons.lang3.tuple.Pair; +import org.apache.shenyu.common.config.ShenyuConfig; +import org.apache.shenyu.common.dto.ConditionData; +import org.apache.shenyu.common.dto.SelectorData; +import org.apache.shenyu.common.enums.MatchModeEnum; +import org.apache.shenyu.common.enums.SelectorTypeEnum; +import org.apache.shenyu.common.enums.TrieCacheTypeEnum; +import org.apache.shenyu.common.utils.ListUtil; +import org.apache.shenyu.common.utils.LogUtils; +import org.apache.shenyu.plugin.api.ShenyuPluginChain; +import org.apache.shenyu.plugin.api.utils.SpringBeanUtils; +import org.apache.shenyu.plugin.base.cache.MatchDataCache; +import org.apache.shenyu.plugin.base.condition.strategy.MatchStrategyFactory; +import org.apache.shenyu.plugin.base.provider.SelectorDataProvider; +import org.apache.shenyu.plugin.base.trie.ShenyuTrie; +import org.apache.shenyu.plugin.base.trie.ShenyuTrieNode; +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; + +import static org.apache.shenyu.plugin.api.ShenyuPlugin.LOG; + +/** + * The core decision-making engine for selector matching in Apache ShenYu gateway. + * + *

This class implements the matching logic that determines which selector should handle + * incoming requests based on request attributes, path patterns, and configured conditions. + * It extends the abstract matching framework and specializes in selector-level routing decisions.

+ * + *

In the ShenYu architecture, selectors represent the first level of request routing where + * coarse-grained filtering occurs before more specific rule matching. This decision maker + * evaluates various matching strategies including trie-based path matching, condition-based + * filtering, and custom flow selectors.

+ * + *

The class employs multiple matching techniques: + *

    + *
  • Trie-based matching: For efficient path pattern matching using prefix trees
  • + *
  • Condition-based filtering: Using match strategies from {@link MatchStrategyFactory}
  • + *
  • Custom flow selectors: For advanced, user-defined routing logic
  • + *
+ *

+ * + *

Caching is extensively used to optimize performance, with configurable cache settings + * for both trie structures and match results. The caching behavior can be tuned through + * {@link ShenyuConfig.SelectorMatchCache} configuration.

+ */ +public class SelectorDataDecisionMaker extends AbstractMatchDecisionMaker { + + private ShenyuConfig.SelectorMatchCache selectorMatchConfig; + + private ShenyuTrie selectorTrie; + + /** + * Constructs a new SelectorDataDecisionMaker with a SelectorDataProvider. + * Initializes cache configuration and trie structures for selector matching. + */ + public SelectorDataDecisionMaker() { + super(new SelectorDataProvider()); + initCacheConfig(); + } + + /** + * Handles the case when no selector data is found for the given plugin. + * + *

When no matching selectors are found, this method allows the request to continue + * through the plugin chain without selector-specific processing. This ensures that + * requests can still be processed by subsequent plugins even when selector matching + * doesn't yield results.

+ * + * @param pluginName the name of the plugin being executed + * @param exchange the current server web exchange + * @param chain the plugin chain for continued processing + * @return a Mono indicating completion of the empty handling operation + */ + @Override + public Mono handleEmpty(final String pluginName, final ServerWebExchange exchange, final ShenyuPluginChain chain) { + return chain.execute(exchange); + } + + /** + * Performs the core selector matching logic for incoming requests. + * + *

This method implements the selector matching algorithm that determines which + * selector should process the current request. It supports both direct list-based + * matching and advanced trie-based matching for path patterns.

+ * + *

The matching process follows this hierarchy: + *

    + *
  1. If pre-filtered data is available in the dataList, use the first match
  2. + *
  3. Otherwise, perform trie-based matching on the request path
  4. + *
  5. Apply condition filtering for custom flow selectors
  6. + *
  7. Handle multiple matches using priority-based selection
  8. + *
+ *

+ * + * @param exchange the current server web exchange containing request information + * @param pluginName the name of the plugin requesting selector matching + * @param dataList pre-filtered list of selector data (may be empty) + * @param path the request path used for matching + * @param selectorData additional selector context (currently unused in base implementation) + * @return the matched SelectorData, or null if no match found + * + */ + @Override + public SelectorData matchData(final ServerWebExchange exchange, final String pluginName, final List dataList, final String path, final SelectorData selectorData) { + return dataList.isEmpty() ? trieMatchSelector(exchange, pluginName, path) : dataList.get(0); + } + + /** + * Determines whether the plugin chain should continue processing after selector matching. + * + *

A selector can control the flow of the plugin chain by specifying continuation + * behavior. This method checks the selector's enabled status and continuation flag + * to determine if processing should proceed to the next plugin.

+ * + * @param data the selector data to evaluate for continuation + * @return true if the selector is enabled and configured to continue processing, + * false otherwise + */ + @Override + public boolean shouldContinue(final SelectorData data) { + return data.getEnabled() && data.getContinued(); + } + + /** + * Initializes the cache configuration and trie structures for selector matching. + * + *

This method lazily initializes the caching infrastructure by retrieving + * configuration from Spring context. It ensures that cache settings and trie + * instances are properly configured before matching operations begin.

+ */ + private void initCacheConfig() { + if (Objects.isNull(selectorMatchConfig)) { + ShenyuConfig shenyuConfig = SpringBeanUtils.getInstance().getBean(ShenyuConfig.class); + selectorMatchConfig = shenyuConfig.getSelectorMatchCache(); + } + + if (Objects.isNull(selectorTrie)) { + selectorTrie = SpringBeanUtils.getInstance().getBean(TrieCacheTypeEnum.SELECTOR.getTrieType()); + } + } + + /** + * Performs trie-based matching for selectors when no pre-filtered data is available. + * + *

This method implements efficient path matching using a trie data structure, + * which provides O(m) time complexity where m is the path length. It's particularly + * effective for routing scenarios with large numbers of path-based selectors.

+ * + *

The matching process includes: + *

    + *
  • Checking if trie matching is enabled in configuration
  • + *
  • Finding the matching trie node for the given path
  • + *
  • Handling single and multiple matches appropriately
  • + *
  • Caching results for future requests when conditions allow
  • + *
+ *

+ * + * @param exchange the current server web exchange + * @param pluginName the plugin name for context-specific matching + * @param path the request path to match against selector patterns + * @return the matched SelectorData, or null if no match found + */ + private SelectorData trieMatchSelector(final ServerWebExchange exchange, final String pluginName, final String path) { + if (!selectorMatchConfig.getTrie().getEnabled()) { + return null; + } + SelectorData selectorData = null; + ShenyuTrieNode shenyuTrieNode = selectorTrie.match(path, pluginName); + if (Objects.nonNull(shenyuTrieNode)) { + LogUtils.info(LOG, "{} selector match path from shenyu trie, path:{}", pluginName, path); + List collection = shenyuTrieNode.getPathCache().get(pluginName); + if (CollectionUtils.isNotEmpty(collection)) { + Pair selectorDataPair; + if (collection.size() > 1) { + selectorDataPair = matchSelector(exchange, ListUtil.castList(collection, SelectorData.class::cast)); + } else { + Object selectorObj = collection.stream().findFirst().orElse(null); + SelectorData selector = Objects.nonNull(selectorObj) ? (SelectorData) selectorObj : null; + boolean cached = Objects.nonNull(selector) && selector.getConditionList().stream().allMatch(condition -> URI_CONDITION_TYPE.equals(condition.getParamType())); + selectorDataPair = Pair.of(cached, selector); + } + selectorData = selectorDataPair.getRight(); + if (selectorDataPair.getLeft() && Objects.nonNull(selectorData)) { + cacheSelectorData(path, selectorData, selectorMatchConfig); + } + } + } + return selectorData; + } + + /** + * Filters and matches a collection of selectors against the current exchange. + * + *

This method applies condition-based filtering to determine which selectors + * are appropriate for the current request. It handles both single and multiple + * matches, with special logic for resolving conflicts when multiple selectors + * match the same request.

+ * + * @param exchange the server web exchange to match against + * @param selectors the collection of selectors to filter + * @return a Pair containing a Boolean indicating if the result can be cached, + * and the matched SelectorData + */ + private Pair matchSelector(final ServerWebExchange exchange, final Collection selectors) { + List 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)); + } + } + + /** + * Filters individual selectors based on their type and conditions. + * + *

Custom flow selectors undergo comprehensive condition matching using the + * configured match strategy, while other selector types are accepted by default + * assuming they've passed earlier filtering stages.

+ * + * @param selector the selector to evaluate + * @param exchange the server web exchange for condition evaluation + * @return true if the selector should be considered for matching, false otherwise + */ + private Boolean filterSelector(final SelectorData selector, final ServerWebExchange exchange) { + if (selector.getType() == SelectorTypeEnum.CUSTOM_FLOW.getCode()) { + if (CollectionUtils.isEmpty(selector.getConditionList())) { + return false; + } + return MatchStrategyFactory.match(selector.getMatchMode(), selector.getConditionList(), exchange); + } + return true; + } + + /** + * Resolves conflicts when multiple selectors match the same request. + * + *

This method implements a priority-based selection algorithm that considers: + *

    + *
  • Match mode (AND conditions are prioritized)
  • + *
  • Number of matching conditions
  • + *
  • Selector sort order for tie-breaking
  • + *
+ *

+ * + * @param filterCollectors the list of matching selectors + * @return the highest priority selector according to the conflict resolution rules + */ + private SelectorData manyMatchSelector(final List 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>> 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> pairs = collect.get(max); + return pairs.stream().map(Pair::getRight).min(Comparator.comparing(SelectorData::getSort)).orElse(null); + } + + /** + * Caches selector matching results for performance optimization. + * + *

This method implements the caching strategy for selector matches, with + * configurable cache size and eligibility criteria. Only selectors with + * URI-based conditions are cached to ensure cache consistency.

+ * + * @param path the request path used as cache key + * @param selectorData the selector data to cache + * @param selectorMatchConfig the cache configuration parameters + */ + private void cacheSelectorData(final String path, final SelectorData selectorData, final ShenyuConfig.SelectorMatchCache selectorMatchConfig) { + if (Boolean.FALSE.equals(selectorMatchConfig.getCache().getEnabled()) || Objects.isNull(selectorData) + || Boolean.TRUE.equals(selectorData.getMatchRestful())) { + return; + } + int initialCapacity = selectorMatchConfig.getCache().getInitialCapacity(); + long maximumSize = selectorMatchConfig.getCache().getMaximumSize(); + if (StringUtils.isBlank(selectorData.getId())) { + MatchDataCache.getInstance().cacheSelectorData(path, selectorData, initialCapacity, maximumSize); + return; + } + List conditionList = selectorData.getConditionList(); + if (CollectionUtils.isNotEmpty(conditionList)) { + boolean isUriCondition = conditionList.stream().allMatch(v -> URI_CONDITION_TYPE.equals(v.getParamType())); + if (isUriCondition) { + MatchDataCache.getInstance().cacheSelectorData(path, selectorData, initialCapacity, maximumSize); + } + } + } +} \ No newline at end of file diff --git a/shenyu-plugin/shenyu-plugin-base/src/main/java/org/apache/shenyu/plugin/base/provider/DataProvider.java b/shenyu-plugin/shenyu-plugin-base/src/main/java/org/apache/shenyu/plugin/base/provider/DataProvider.java new file mode 100644 index 000000000000..0a23ee1b8bfa --- /dev/null +++ b/shenyu-plugin/shenyu-plugin-base/src/main/java/org/apache/shenyu/plugin/base/provider/DataProvider.java @@ -0,0 +1,43 @@ +/* + * 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.provider; + +import java.util.List; + +/** + * A generic interface for providing data based on a given key. + * + * @param the type of data provided by this provider + */ +public interface DataProvider { + /** + * Retrieves a list of data items associated with the specified key. + * + * @param key the key used to look up the data; its meaning is defined by the implementation + * @return a list of data items of type {@code T} associated with the given key, + * or an empty list if no data is found + */ + /** + * Retrieves a list of data items associated with the specified key. + * + * @param key the key used to look up the data; its meaning is defined by the implementation + * @return a list of data items of type {@code T} associated with the given key, + * or an empty list if no data is found + */ + List getData(String key); +} \ No newline at end of file diff --git a/shenyu-plugin/shenyu-plugin-base/src/main/java/org/apache/shenyu/plugin/base/provider/PluginDataProvider.java b/shenyu-plugin/shenyu-plugin-base/src/main/java/org/apache/shenyu/plugin/base/provider/PluginDataProvider.java new file mode 100644 index 000000000000..2b5d1d2b2e83 --- /dev/null +++ b/shenyu-plugin/shenyu-plugin-base/src/main/java/org/apache/shenyu/plugin/base/provider/PluginDataProvider.java @@ -0,0 +1,47 @@ +/* + * 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.provider; + +import org.apache.shenyu.common.dto.PluginData; +import org.apache.shenyu.plugin.base.cache.BaseDataCache; + +import java.util.Collections; +import java.util.List; +import java.util.Objects; + +/** + * A concrete implementation of the data provider pattern for retrieving plugin data + * in the ShenYu gateway system. + */ +public class PluginDataProvider implements DataProvider { + + /** + * Retrieves plugin data from the base data cache for the specified plugin name. + * + * @param pluginName the name of the plugin to retrieve. This parameter should not be null, + * though the current implementation may return an empty list rather than + * throwing an exception for null values. + * @return an immutable list containing a single {@link PluginData} object if the plugin + * is found, otherwise an empty immutable list. The return value will never be null. + */ + @Override + public List getData(final String pluginName) { + PluginData data = BaseDataCache.getInstance().obtainPluginData(pluginName); + return Objects.nonNull(data) ? Collections.singletonList(data) : Collections.emptyList(); + } +} \ No newline at end of file diff --git a/shenyu-plugin/shenyu-plugin-base/src/main/java/org/apache/shenyu/plugin/base/provider/RuleDataProvider.java b/shenyu-plugin/shenyu-plugin-base/src/main/java/org/apache/shenyu/plugin/base/provider/RuleDataProvider.java new file mode 100644 index 000000000000..80fad15bc714 --- /dev/null +++ b/shenyu-plugin/shenyu-plugin-base/src/main/java/org/apache/shenyu/plugin/base/provider/RuleDataProvider.java @@ -0,0 +1,45 @@ +/* + * 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.provider; + +import org.apache.shenyu.common.dto.RuleData; +import org.apache.shenyu.plugin.base.cache.BaseDataCache; + +import java.util.List; + +/** + * A data provider implementation for retrieving rule data from the base data cache. + * This class follows the Data Provider pattern to abstract data access logic and provide + * a consistent interface for obtaining rule configuration data throughout the Shenyu plugin system. + * + *

The RuleDataProvider serves as a bridge between the plugin infrastructure and the cached + * rule data, ensuring efficient data retrieval while maintaining separation of concerns.

+ */ +public class RuleDataProvider implements DataProvider { + + /** + * Retrieves rule data from the base data cache based on the specified rule name. + * This method implements the data provider pattern by delegating to the singleton + * + */ + @Override + public List getData(final String ruleName) { + List data = BaseDataCache.getInstance().obtainRuleData(ruleName); + return data; + } +} \ No newline at end of file diff --git a/shenyu-plugin/shenyu-plugin-base/src/main/java/org/apache/shenyu/plugin/base/provider/SelectorDataProvider.java b/shenyu-plugin/shenyu-plugin-base/src/main/java/org/apache/shenyu/plugin/base/provider/SelectorDataProvider.java new file mode 100644 index 000000000000..b9448349c2da --- /dev/null +++ b/shenyu-plugin/shenyu-plugin-base/src/main/java/org/apache/shenyu/plugin/base/provider/SelectorDataProvider.java @@ -0,0 +1,77 @@ +/* + * 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.provider; + +import org.apache.shenyu.common.dto.SelectorData; +import org.apache.shenyu.plugin.base.cache.BaseDataCache; + +import java.util.List; + +/** + * A concrete implementation of the {@code DataProvider} interface that provides SelectorData + * retrieval functionality within the Apache ShenYu gateway ecosystem. + * + *

This class implements the Data Provider pattern by serving as a specialized data source + * for selector information, which is crucial for request routing decisions in the gateway. + * Selectors define the coarse-grained routing rules that determine how incoming requests + * are matched to appropriate backend services.

+ * + *

In the ShenYu architecture, this provider works within the plugin system to deliver + * selector configuration data that has been synchronized from the admin dashboard to the + * gateway's local cache. This enables efficient, in-memory lookup of routing rules during + * request processing without requiring database queries.

+ * + *

The provider leverages the {@link BaseDataCache} singleton instance to access + * selector information that is maintained current through ShenYu's data synchronization + * mechanisms (WebSocket, ZooKeeper, Nacos, etc.).

+ * + */ +public class SelectorDataProvider implements DataProvider { + + /** + * Retrieves a list of SelectorData objects associated with the specified selector name. + * + *

This method implements the core provider contract by querying the gateway's + * cache infrastructure for selector configuration data. Selectors contain matching + * rules and conditions that determine which plugin and backend service should handle + * incoming requests.

+ * + *

In the ShenYu request processing pipeline, selectors work together with rules + * to form a two-level routing system: + *

    + *
  1. Selectors perform coarse-grained routing based on request attributes
  2. + *
  3. Rules apply fine-grained processing logic within matched selectors
  4. + *
+ *

+ * + *

If no selectors are found for the given name, an empty list is returned rather + * than null, ensuring null-safe operation for consumers of this provider.

+ * + * @param selectorName the name of the selector to retrieve data for; this typically + * corresponds to a plugin name or specific selector identifier + * configured in the ShenYu admin dashboard + * @return a list of SelectorData objects associated with the specified name, or an + * empty list if no selectors are found for the given name (never null) + * + */ + @Override + public List getData(final String selectorName) { + List data = BaseDataCache.getInstance().obtainSelectorData(selectorName); + return data; + } +} \ No newline at end of file