
business-utilbusiness-util是一个面向 Java 8 的多维业务配置匹配库。它把散落在业务代码中的条件判断、兜底规则和优先级排序收敛为一次配置、重复查询的匹配模型。典型场景包括渠道策略、地区配置、用户分层、价格区间、时间窗口、灰度路由、营销规则和风控规则。为什么使用这个项目多维配置匹配看似只是几个if/else实际很快会遇到以下问题维度可以缺省地区 渠道 用户等级会产生大量组合和兜底路径。“匹配维度最多”与“高价值维度绝对优先”是两套不同的优先级规则。除了相等判断还可能需要前缀、区间、时间范围等业务谓词。规则重复、匹配顺序和最终命中的具体配置值不容易排查。配置量和维度增加后逐组合扫描的成本会明显上升。如果不使用 business-util以“地区、渠道、用户等级”三个维度为例如果约定配置中的空值代表兜底并采用“匹配维度越多越优先”的规则业务代码通常需要同时负责过滤、排序和结果聚合publicListRuleselectRules(Requestrequest,ListRulerules){ListRulematchednewArrayList();for(Rulerule:rules){intshapeshape(rule);if(shape0){continue;// 没有有效维度的配置不参与匹配}if(matches(request.getRegion(),rule.getRegion())matches(request.getChannel(),rule.getChannel())matches(request.getUserLevel(),rule.getUserLevel())){matched.add(rule);}}// NUMBER_OF_MATCHES维度数量优先数量相同时region channel userLevelmatched.sort((left,right)-{intleftShapeshape(left);intrightShapeshape(right);intcountInteger.compare(Integer.bitCount(rightShape),Integer.bitCount(leftShape));returncount!0?count:Integer.compare(rightShape,leftShape);});if(matched.isEmpty()){returnCollections.emptyList();}// 同一个最高优先级组合可能存在多条配置需要一起返回intwinningShapeshape(matched.get(0));returnmatched.stream().filter(rule-shape(rule)winningShape).collect(Collectors.toList());}privatebooleanmatches(StringrequestValue,StringconfiguredValue){returnisMissing(configuredValue)||(!isMissing(requestValue)Objects.equals(requestValue,configuredValue));}privateintshape(Rulerule){intvalue0;if(!isMissing(rule.getRegion())){value|12;}if(!isMissing(rule.getChannel())){value|11;}if(!isMissing(rule.getUserLevel())){value|1;}returnvalue;}privatebooleanisMissing(Stringvalue){returnvaluenull||value.isEmpty();}这段代码还只处理了三个字符串维度和一种优先级模式。继续增加需求时还要自行维护ABSOLUTE_VALUE的另一套排序算法。前缀、数字区间、时间范围等不同类型的匹配分支。多个模糊 key 同时命中后的稳定聚合顺序。重复完整 key 的统计、告警、异常和样例限制。命中维度及实际配置值的诊断输出。大配置集下的索引、剪枝和树状查询。使用business-util后这些通用逻辑由匹配器统一承担业务代码只需要声明配置、维度取值方式和必要的自定义谓词。business-util提供统一的建模方式来处理这些问题能力带来的价值配置驱动的多维匹配业务代码不再维护成片的条件分支两种内置优先级模式明确控制“更具体”或“更重要”的规则优先精确匹配与自定义谓词同时支持普通值、前缀、数字区间和时间范围缺失维度自动形成兜底配置中的null或不参与该条规则的维度组合NAME:VALUE命中说明直接知道本次实际匹配了哪些维度和值创建阶段重复 key 自检查在流量进入前发现歧义配置可警告或阻止启动普通/树状两种查询引擎根据配置规模和命中分布选择执行方式环境与安装Java 8 或更高版本Maven 3.xMaven 依赖dependencygroupIdcn.ykccchen/groupIdartifactIdbusiness-util/artifactIdversion1.1.0/version/dependency从源码构建gitclone https://gitee.com/xiaokuntt/business-util.gitgitclone https://github.com/xiaokuntt/business-util.gitcdbusiness-util mvn cleaninstall-Dgpg.skiptrue快速开始假设请求对象Request和配置对象Rule都提供getRegion()、getChannel()Rule另外提供getId()。ListRulerulesArrays.asList(newRule(CN_APP,CN,APP),newRule(CN_DEFAULT,CN,null),newRule(GLOBAL,null,null));PriorityFetcherRequest,Rule,StringfetcherPriorityAssembler.from(Request.class,Rule.class,String.class).initConfig(rules).addPriorityMatchFunction(region,Request::getRegion,Rule::getRegion).addPriorityMatchFunction(channel,Request::getChannel,Rule::getChannel).create();PriorityMatchResultListRuleresultfetcher.match(newRequest(CN,APP));System.out.println(result.getResult().get(0).getId());// CN_APPSystem.out.println(result.getName());// region_channelSystem.out.println(result.getNameAndValue());// region:CN_channel:APP完整流程只有三步通过PriorityAssembler加载配置。按重要性从高到低注册匹配维度第一个维度的 priority 为0。调用create()构建可重复使用的PriorityFetcher再执行查询。配置中的null和真正的空字符串表示该维度缺失。上例中的CN_DEFAULT因此只属于region组合可作为更完整规则未命中时的兜底。所有维度都缺失的配置不会形成可查询规则。核心对象对象职责PriorityAssemblerS,C,K加载配置、注册维度并选择优先级和检查策略PriorityMatchFunctionS,C,K描述一个维度如何从请求/配置取值以及如何比较PriorityFetcherS,C,K保存构建后的索引并执行匹配PriorityMatchResultT返回优先级组合、NAME:VALUE说明和业务配置其中S是请求类型C是配置类型K是维度 key 类型。同一个装配器中的维度共用K维度值类型不同时可以使用共同父类型例如Object并在自定义谓词中完成类型判断。获取一个结果或全部结果// 返回最高优先级结果没有命中时返回 nullPriorityMatchResultListRulewinnerfetcher.match(request);// 返回所有命中组合按照优先级从高到低排列没有命中时返回空集合ListPriorityMatchResultListRuleallfetcher.match(request,true);同一个优先级组合可能包含多条配置所以结果值始终是ListC。一个组合最多产生一个PriorityMatchResult。配置优先级维度注册顺序代表维度价值越早注册优先级越高。NUMBER_OF_MATCHES默认模式。先比较参与匹配的维度数量维度越多越优先数量相同时再按维度注册顺序比较。assembler.initPriorityHandler(PriorityMode.NUMBER_OF_MATCHES);例如注册顺序为A、B、C、DACD高于AB因为三维高于二维。ABC高于ABD因为相同维度数下C比D更早注册。ABSOLUTE_VALUE高价值维度具有绝对优先权按每个维度“是否存在”的序列逐位比较。assembler.initPriorityHandler(PriorityMode.ABSOLUTE_VALUE);例如注册顺序为A、B、C、DAB高于ACD因为B的价值高于C和D。ABC高于AB因为前两维相同前者还包含C。如果内置模式不满足业务需求可以实现PriorityHandler并传给initPriorityHandler(...)。精确匹配、内置匹配器与自定义匹配三个参数的addPriorityMatchFunction使用 Javaequals进行精确匹配assembler.addPriorityMatchFunction(region,Request::getRegion,Rule::getRegion);内置匹配器PriorityMatchers提供常用规则所有匹配器的参数顺序固定为“请求值、配置值”类别内置方法通用equal、notEqual大小比较greaterThan、greaterThanOrEqual、lessThan、lessThanOrEqual区间rangeContains、rangeNotContains、numberRangeContains、numberRangeNotContains、timeRangeContains、timeRangeNotContains、rangesOverlap、rangesDisjoint字符串stringEqualsIgnoreCase、stringNotEqualsIgnoreCase、stringStartsWith、stringStartsWithIgnoreCase、stringNotStartsWith、stringNotStartsWithIgnoreCase、stringEndsWith、stringEndsWithIgnoreCase、stringNotEndsWith、stringNotEndsWithIgnoreCase、stringContains、stringContainsIgnoreCase、stringNotContains、stringNotContainsIgnoreCase正则stringMatchesRegex、stringNotMatchesRegex单值与集合elementInCollection、elementNotInCollection集合与单值collectionContainsElement、collectionNotContainsElement集合与集合collectionIntersects、collectionContainsAll、collectionContainedBy、collectionDisjoint请求值和配置值类型不同时使用addPriorityMatcher(...)。例如请求是数字配置必须是明确的PriorityRange不能用单个数字冒充区间PriorityAssemblerRequest,Rule,ObjectassemblerPriorityAssembler.from(Request.class,Rule.class,Object.class).initConfig(ruleList).addPriorityMatcher(amount,Request::getAmount,Rule::getAmountRange,PriorityMatchers.BigDecimalnumberRangeContains());创建区间PriorityRangeBigDecimalpricePriorityRange.closedOpen(newBigDecimal(10),newBigDecimal(20));// [10,20)PriorityRangeInstantactiveTimePriorityRange.closed(start,end);// [start,end]支持closed、open、closedOpen、openClosed、atLeast、greaterThan、atMost和lessThan。非法、倒置或实际为空的区间在创建时直接抛出异常。字符串和集合示例assembler.addPriorityMatcher(path,Request::getPath,Rule::getPathPrefix,PriorityMatchers.stringStartsWith()).addPriorityMatcher(role,Request::getRole,Rule::getAllowedRoles,PriorityMatchers.StringelementInCollection());集合配置会在创建索引时复制为不可修改值调用方后续修改原集合不会破坏匹配索引。成员/集合关系匹配会忽略集合内部的null和equal、notEqual则保留这些元素并按完整集合做 Java 相等性判断。为避免改变 Java 相等语义equal、notEqual的集合配置必须实现List或SetArrayDeque等其他Collection会在创建索引时明确抛出异常可改用成员/集合关系匹配器或自定义匹配器。空集合本身始终是有效配置并遵循相应匹配器的语义。自定义匹配器四个参数的重载接收BiPredicateK, K。第一个参数是请求值第二个参数是配置值。下面的规则允许请求文本匹配多个配置前缀assembler.addPriorityMatchFunction(prefix,Request::getPath,Rule::getPathPrefix,(requestPath,configuredPrefix)-requestPath.startsWith(configuredPrefix));请求值和配置值类型不同时也可以实现PriorityMatcherSV,CVPriorityMatcherBigDecimal,PriorityRangeBigDecimalmatcher(amount,range)-range.contains(amount);组件不会自动转换时区或数字精度。建议数字先统一为同一种类型和精度例如BigDecimal。时间先统一到同一时间线例如Instant。在加载配置前拒绝倒置区间和无效区间。匹配器保持确定、无副作用抛出的异常会原样传播。空值与空字符以下规则同时作用于请求值和配置值null缺失不参与匹配。缺失不参与匹配。 、\t等空白字符有效值不会自动trim()。非字符串 key只判断null不会被当作空字符串。如果业务希望忽略首尾空白应在 getter 中先完成标准化。查看实际命中的 NAME:VALUEPriorityMatchResult.getNameAndValue()展示实际命中的配置 key而不是请求输入值region:CN_channel:APPNAME 默认来自addPriorityMatchFunction(name, ...)。未提供名称时使用priority[n]。同一路径的维度用_连接。自定义谓词命中多个配置 key 时去重后用;连接例如prefix:U;prefix:US。如需自定义显示规则实现PriorityNameAndValueHandlerpublicfinalclassBusinessNameAndValueHandlerimplementsPriorityNameAndValueHandlerRequest,Rule,String{OverridepublicStringhandle(StringdefaultName,intpriority,Requestsource,Ruleconfig,StringsourceValue,StringmatchedConfigValue){return业务-defaultNamematchedConfigValue;}}assembler.initPriorityNameAndValueHandler(newBusinessNameAndValueHandler());处理器可以使用名称、priority、请求对象、配置对象、请求值和实际命中的配置值。返回null会忽略当前维度异常会原样传播。创建时检查重复完整 key重复检查默认关闭。检查对象是配置的“完整有效 key”即所有非null、非维度及其实际值。PriorityFetcherRequest,Rule,Stringfetcherassembler.initDuplicateKeyCheck(DuplicateKeyCheckLevel.WARNING).create();DuplicateKeyCheckReportRule,Stringreportfetcher.getDuplicateKeyCheckReport();System.out.println(report.getDuplicateGroupCount());// 重复组数System.out.println(report.getDuplicateRecordCount());// 重复配置总数System.out.println(report.getSamples());// 最多 10 条配置样例等级行为OFF默认值不执行重复分组检查WARNING创建成功保存报告并通过java.util.logging输出一次警告EXCEPTION发现重复时中止create()并抛出DuplicateMatchKeyException异常中的getReport()可以获取相同的统计信息。WARNING不会删除配置也不会改变匹配结果或顺序。重复检查使用 key 的 Javaequals不会推断两个正则、区间或自定义谓词是否存在语义重叠。不同维度组合也不会互相判重。普通模式与树状模式默认使用按优先级组合执行的普通模式PriorityFetcherRequest,Rule,StringlevelFetcherassembler.create();配置创建后可以启用树状模式PriorityFetcherRequest,Rule,StringtreeFetcherassembler.create().tree();当前树引擎用于match(source, true)的全优先级查询match(source)仍按 processor 顺序提前结束。两种引擎返回相同的结果分组、顺序和nameAndValue。树模式并非在所有负载下都更快。项目内置的 50,000 条配置、14 个维度、16,383 种有效组合基准中查询负载普通模式中位数树模式中位数结论无命中536.917 μs86.750 μs树模式约快 6.19 倍选择性命中2,811.833 μs759.250 μs树模式约快 3.70 倍几乎全命中17,247.042 μs33,765.667 μs普通模式约快 1.96 倍这些数据只用于说明负载差异不是固定性能承诺。μs表示微秒即百万分之一秒。请用真实配置和请求分布选择执行方式mvn-DtestPriorityFetcherPerformanceTesttest行为约定只有配置中真实存在的维度组合会创建处理器不会预先构造全部维度幂集。自定义谓词命中多个 key 桶时按 key 首次加载顺序合并桶内保持配置加载顺序。返回的配置列表是浅层副本修改列表本身不会影响后续查询。add(PriorityMatchFunction)的 priority 必须从0开始并与注册顺序连续。BiPredicate、PriorityHandler和PriorityNameAndValueHandler不能为null。建议在应用启动阶段完成配置加载和create()/tree()请求阶段只调用match(...)。测试运行全部测试mvntest项目测试覆盖精确匹配、空值/空字符、优先级顺序、数字与时间范围边界、重复 key 自检查、普通/树状等价性、大批量配置和性能对比。参与贡献Fork 项目并创建分支。增加实现及对应测试。运行mvn test。提交 Pull Request。LicenseApache License 2.0