{"id":23170835,"url":"https://github.com/jdk-plus/spring-smart-ioc-starter","last_synced_at":"2025-04-04T23:41:17.761Z","repository":{"id":266055776,"uuid":"893940079","full_name":"JDK-Plus/spring-smart-ioc-starter","owner":"JDK-Plus","description":null,"archived":false,"fork":false,"pushed_at":"2025-02-03T06:32:38.000Z","size":287,"stargazers_count":1,"open_issues_count":0,"forks_count":0,"subscribers_count":1,"default_branch":"master","last_synced_at":"2025-02-10T08:14:15.873Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":null,"language":"Java","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/JDK-Plus.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2024-11-25T13:26:57.000Z","updated_at":"2025-02-03T06:32:04.000Z","dependencies_parsed_at":"2024-12-31T04:28:00.882Z","dependency_job_id":"3b812d7d-bf59-4cc5-b586-1fa4dd5de50b","html_url":"https://github.com/JDK-Plus/spring-smart-ioc-starter","commit_stats":null,"previous_names":["jdk-plus/spring-smart-ioc-starter"],"tags_count":8,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/JDK-Plus%2Fspring-smart-ioc-starter","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/JDK-Plus%2Fspring-smart-ioc-starter/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/JDK-Plus%2Fspring-smart-ioc-starter/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/JDK-Plus%2Fspring-smart-ioc-starter/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/JDK-Plus","download_url":"https://codeload.github.com/JDK-Plus/spring-smart-ioc-starter/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":247266480,"owners_count":20910832,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":[],"created_at":"2024-12-18T04:15:12.118Z","updated_at":"2025-04-04T23:41:17.737Z","avatar_url":"https://github.com/JDK-Plus.png","language":"Java","funding_links":[],"categories":[],"sub_categories":[],"readme":"## 一、说在前面的话\n\n本项目描述并实现了一种基于动态代理与配置中心实时判定动态切换接口的实现类来达到服务降级与流量灰度发布的方法。\n\n这是一个通过动态切换接口的实现类（springboot 中的 bean ），实现了一种高效而灵活的解决方案，用于应对复杂系统中的服务降级和功能灰度发布的解决方案。\n\n**该项目提出并实现了一种使用动态代理和配置中心来管理某个接口的多个业务实现，通过动态切换 Bean 的实现类快速实现服务降级。\n此外，借助 [JEXL](https://commons.apache.org/proper/commons-jexl/)  自定义方法的解析规则，还提供了一种小流量功能灰度发布的解决方案。**\n\n## 二、相关工作 \u0026 设计理念\n\n**相关工作**\n\n目前，社区中关于切换 Bean 实现类的实现仅有 [spring-smart-di](https://github.com/burukeYou/spring-smart-di) 这个仓库。\n该实现完全基于项目配置，在服务启动时通过 BaseAutowiredBeanProcessor 根据配置内容为不同的 Bean 实例装配不同的实现类。\n这意味着在服务启动后，无法动态修改 Bean 的实现类，存在一定的局限性。而且这个实现其实和spring `@Primary`注解的功能一致了。\n\n**设计理念**\n\n在日常编码中，六大设计原则已深入人心。依赖倒置原则（Dependency Inversion Principle, DIP）和开闭原则（Open/Closed Principle,\nOCP）要求我们面向接口编程，\n这样在编程过程中无需关注底层业务的具体实现，而是通过接口来定义流程中的逻辑执行和数据流转。\n里氏替换原则（Liskov Substitution Principle, LSP）和接口隔离原则（Interface Segregation Principle,\nISP）则要求我们确保流程中的任何接口实现都可以被替换，\n且替换后程序行为不会发生变化。这些原则为我们的设计和实现提供了坚实的理论依据。\n\n**实现原理**\n\n实现原理其实很简单，通过代理类来执行规则判定即可，通过时序图描述的逻辑如下：\n\n\n![](doc/img/execution-sequence-diagram.svg)\n\n你可以像这样子引入他：\n\n```xml\n\u003cdependency\u003e\n    \u003cgroupId\u003eplus.jdk\u003c/groupId\u003e\n    \u003cartifactId\u003espring-smart-ioc-starter\u003c/artifactId\u003e\n    \u003cversion\u003e1.0.6\u003c/version\u003e\n\u003c/dependency\u003e\n```\n\n## 三、高级用法-自定义全局变量和全局函数\n\n组件中提供了一个 bean ```GlobalSmartIocContext#registerGlobalVar(name, obj)``` 来注册全局的变量的功能，示例如下：\n\n\u003e 注意, 这个registerGlobalVar是全局的，会存在堆空间里面, 本质上其实对应 bean 实例里面的一个`ConcurrentHashMap`\n\u003e ，后续每次执行方法或类的\n\u003e condition rule的时候，都会把全局变量注册到 jexl 的 global 属性中，当然，你也可以写入一个工具类进来执行函数调用，但是这里不建议执行过于复杂的逻辑。\n\u003e 并且这里出于安全考虑，不允许写入的脚本执行任何具有副作用（例如篡改全局变量或函数入参、执行循环调用等）的逻辑，仅允许执行简单的判定表达式\n\n**`@ConditionOnRule`执行 eval逻辑时运行环境中的一些魔法变量的说明**\n\n| 变量名     | 说明                                                                                          | 补充                                                                                                                                                                                                                                 |\n|---------|---------------------------------------------------------------------------------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|\n| args    | 当前调用的方法的入参, 允许在condition rule里面使用函数入参判定是否调用该实现类的方法，这个参数每次执行方法调用的时候都会作为临时变量写入运行环境            | 例如，你的方法定义为```String greeting(String name, String sex);```, 那么你可以在 condition rule里面通过 `args.name` 或 `args.sex` 来访问被调用的方法(Method)的入参                                                                                                 |\n| global  | 全局的变量或工具类                                                                                   | 你可以使用 `global` 在condition rule中访问你写入的全局变量或一些工具类，例如，当你使用 `globalSmartIocContext.registerGlobalVar(\"random\", new Random()))`注册`random`后，可以使用这样子的表达式 `@ConditionOnRule(\"global.random.nextInt(100) % 10 \u003e= 8\")`来将 20% 的流量来打到当前的这个实现类上 |\n| current | 允许在 condition rule 维度来访问当前的 beanName(`current.beanName`) 和 methodName(`current.methodName`) | 这里你可以通过监听配置中心使用 `@ConditionOnRule(\"current.beanName == global.myServiceName\")` 来指定当前要切换为哪个实现类. 这里的 `myServiceName`可以通过 `GlobalSmartIocContext#registerGlobalVar(name, obj)`写入                                                      |\n\n**写入全局变量：**\n\n```java\nimport java.util.Random;\n\npublic class SetGlobalConfigTest {\n\n    /**\n     * Material recall service instance, used to execute related business logic.\n     */\n    @Resource\n    private MaterialRecallService materialRecallService;\n\n    /**\n     * Global intelligent IOC context instance, used to obtain and manage global configuration information.\n     */\n    @Resource\n    private GlobalSmartIocContext globalSmartIocContext;\n\n    @Test\n    public void degradeTest() {\n        // Tests if qps \u003c= 1000 normally uses the normal vector recall implementation to return recommended content\n        globalSmartIocContext.registerGlobalVar(\"qps\", 1);\n        globalSmartIocContext.registerGlobalVar(\"random\", new Random());\n    }\n}\n```\n接下来你可以通过`global`变量来访问这里注册的全局变量和全局函数.例如, 你可以像下面这样，指定当 qps 小于 2000 时利用注册的 random 函数将 20% 的流量打到这个实现上：\n\n```java\nimport plus.jdk.smart.ioc.annotations.SmartService;\n\n@ConditionOnRule(\"global.qps \u003c 2000 \u0026\u0026 random.nextInt(100) % 10 \u003e 8\")\n@SmartService(group = MyService.class)\npublic class QpsDegradeService implements MyService {\n    // do something\n}\n```\n**最后的魔法**\n\n思虑再三，虽然这样子会带来一些安全性的问题，但是这完全取决于使用者了，不应该舍本逐末。\n最后登场的魔法就是全局`jexl.eval(anyString())`函数了，这个函数允许你直接通过全局变量指定脚本内容。\n\u003e 当然了，你也可以是用局部变量来传递脚本来执行，但是注意，这是非常不安全的一种做法，因为函数的输入大多来自外部输入，\n\u003e 如果你把这个功能开放给外部输入，那你最好保证调用方是可信的，值得你去给他最高级别的执行权限！！！否则就不要这么做。\n\u003e 甚至于，在上述功能足够满足你需求的情况下，尽量不要使用这个魔法函数，这里只是为了满足一些上述功能不能满足需求的场景\n\n通过这个功能，你可以把你想要执行的脚本配置在 配置中心（或 mysql、redis 都可以，主要你能读取到） 中，当服务启动或配置变化时，通过如下指令来更新你的脚本到 `global` 对象中:\n\n```java\npublic class SmartIocSelectorFactoryTest {\n\n    /**\n     * Global intelligent IOC context instance, used to obtain and manage global configuration information.\n     */\n    @Resource\n    private GlobalSmartIocContext globalSmartIocContext;\n\n    @Test\n    public void evalExpressionTest() {\n        // Test the magic jexl.eval() function\n        globalSmartIocContext.registerGlobalVar(\"testScript\", \"1 + 2\");\n    }\n}\n```\n\n然后使用 `@ConditionOnRule(\"jexl.eval(global.testScript)\")`标注在需要的类或者方法上即可。通过这个魔法方法你可以把你的切换规则放在配置中心，通过配置中心按需求动态下发即可！！！\n\n\u003e 注意, 使用函数输入时，千万不要把 `args` 的成员变量的内容传递给`jexl.eval(anyString())`函数，这会导致你的入参变成可执行的脚本！！！比如你的函数入参分别为 `a=3`、`b=4`,\n\u003e 你可以这么写`@ConditionOnRule(\"jexl.eval(global.testScript) \u003e args.a + args.b\")`, 这是安全的; 但是如果你直接把 args的参数内容直接传递给`jexl.eval(anyString())`，\n\u003e 例如 `@ConditionOnRule(\"jexl.eval(args.b)\")，这无疑是一种裸奔的行为，会导致你的服务受到攻击，甚至导致更严重的后果。\n\n所以，我的朋友，我还是不得不提醒你，\u003cp style=\"color: red; \"\u003e__永远都不要把入参直接当做脚本执行，这不管在哪都是一样的， \n就像[变基的风险](https://git-scm.com/book/zh/v2/Git-%E5%88%86%E6%94%AF-%E5%8F%98%E5%9F%BA.html#_rebase_peril)中提到的，\n如果你遵循这条金科玉律，就不会出差错。否则人民群众会仇恨你，你的朋友和家人也会嘲笑你，唾弃你!!!__\u003cp\u003e\n\n## 四、服务降级 \u0026 小流量灰度的示例\n\n### 4.1、通过函数的调用方法来动态的切换 bean 实例，来达到小流量灰度的效果\n\n\u003e 当然，也可以当成某种策略模式来使用，因为就是可以按照参数来指定具体执行哪个实现类\n\n例如，现在有一个短信发送服务，默认通过腾讯云的服务来发送短信，但是还有阿里云、京东云的备用渠道，定义如下：\n\n```java\npublic interface SmsDispatchService {\n    /**\n     * Ways to send text messages.\n     * @param dispatchContext SMS dispatch context, including SMS related information.\n     * @return Whether the sending was successful.\n     */\n    Boolean dispatchMessage(DispatchContext dispatchContext);\n}\n```\n\n下面给出我们可以通过请求参数中的`name`属性来小流量该次行为调用不同的供应商来发送短信的示例：\n\n**通过腾讯云发送短信：**\n\n```java\n\n@Slf4j\n// 通过注解的 primary 属性标注该实现为默认的短信发送渠道，当其他所有实现类的条件都不匹配是默认调用该渠道来发送\n@SmartService(group = SmsDispatchService.class, primary = true)\npublic class TencentCloudSmsDispatchService implements SmsDispatchService {\n    @Override\n    @ConditionOnRule(\"args.dispatchContext.getName() == 'tencent'\")\n    public Boolean dispatchMessage(DispatchContext dispatchContext) {\n        log.info(\"dispatchMessage by tencent cloud\");\n        dispatchContext.setReceipt(String.format(\"tencent-%s\", new ObjectIdGenerator().next()));\n        return true;\n    }\n}\n```\n\n**通过京东云发送短信：**\n\n```java\n\n@Slf4j\n@SmartService(group = SmsDispatchService.class)\npublic class JdCloudSmsDispatchService implements SmsDispatchService {\n    @Override\n    @ConditionOnRule(\"args.dispatchContext.getName() == 'jd'\")\n    public Boolean dispatchMessage(DispatchContext dispatchContext) {\n        log.info(\"dispatchMessage by jd cloud\");\n        dispatchContext.setReceipt(String.format(\"jd-%s\", new ObjectIdGenerator().next()));\n        return true;\n    }\n}\n```\n\n**通过阿里云发送短信：**\n\n```java\n\n@Slf4j\n@SmartService(group = SmsDispatchService.class)\npublic class AlibabaCloudSmsDispatchService implements SmsDispatchService {\n    @Override\n    @ConditionOnRule(\"args.dispatchContext.getName() == 'ali'\")\n    public Boolean dispatchMessage(DispatchContext dispatchContext) {\n        log.info(\"dispatchMessage by alibaba cloud\");\n        dispatchContext.setReceipt(String.format(\"alibaba-%s\", new ObjectIdGenerator().next()));\n        return true;\n    }\n}\n```\n\n**执行效果如下：**\n\n```java\n\n@Slf4j\n@RunWith(SpringRunner.class)\n@SpringBootTest(classes = TomcatLauncher.class, webEnvironment = SpringBootTest.WebEnvironment.DEFINED_PORT)\npublic class SmartFlowByMethodArgsTest {\n\n    /**\n     * The injected SMS delivery service instance is used to simulate SMS delivery in tests.\n     */\n    @Resource\n    private SmsDispatchService smsDispatchService;\n\n    @Test\n    public void testSmallFlow() throws InterruptedException {\n\n        // The input name parameter is ali, and the Alibaba Cloud interface is called to send text messages.\n        DispatchContext context = new DispatchContext() {\n        };\n        context.setName(\"ali\");\n        Assert.assertTrue(smsDispatchService.dispatchMessage(context));\n        Assert.assertTrue(context.getReceipt().startsWith(\"ali\"));\n\n        // The input name parameter is tencent, and the interface of Tencent Cloud is called to send text messages.\n        context.setName(\"tencent\");\n        Assert.assertTrue(smsDispatchService.dispatchMessage(context));\n        Assert.assertTrue(context.getReceipt().startsWith(\"tencent\"));\n\n        // The input name parameter is jd, and the interface of JD Cloud is called to send text messages.\n        context.setName(\"jd\");\n        Assert.assertTrue(smsDispatchService.dispatchMessage(context));\n        Assert.assertTrue(context.getReceipt().startsWith(\"jd\"));\n\n        // The input name parameter is other, and the default main bean implementation is called to send text messages.\n        context.setName(\"other\");\n        Assert.assertTrue(smsDispatchService.dispatchMessage(context));\n        Assert.assertTrue(context.getReceipt().startsWith(\"tencent\"));\n\n        // test the getOriginTarget method implemented by the proxy class\n        Assert.assertTrue(smsDispatchService instanceof Advised);\n        Assert.assertNotNull(((Advised) smsDispatchService).getOriginTarget());\n\n        // test the toString method implemented by the proxy class\n        Assert.assertTrue(smsDispatchService.toString().contains(\"SDI\"));\n        log.info(\"hello\");\n    }\n}\n```\n\n### 4.2、通过切换配置快速切换服务中的 bean 的实现类来实现服务降级\n\n例如，现在有一个广告系统，需要根据用户的输入来通过向量召回来为用户推荐内容，\n\n当单节点 qps 过高时，为了防止推荐模型被大流量打崩，所以当 qps 超过超过 1000 时，触发降级服务。示例如下：\n\n**首先定义一个召回服务接口：**\n\n```java\n/**\n * Define a standard interface for material recall services\n */\npublic interface MaterialRecallService {\n\n    /**\n     * List of recalled materials\n     */\n    RecallResult recall(RecallContext context);\n}\n```\n\n**实现这个接口**\n\n```java\n\n@ConditionOnRule(\"global.qps \u003c= 1000\")\n@SmartService(group = MaterialRecallService.class)\npublic class MaterialVectorRecallServiceImpl implements MaterialRecallService {\n\n    @Override\n    public RecallResult recall(RecallContext context) {\n        return RecallResult.builder().receipt(\"normal, qps \u003c= 1000\").build();\n    }\n}\n```\n\nqps 过高以后基于数据库、es 等排序的降级方案。（这里只是举例， 不讨论具体两个方案的性能优劣）\n\n```java\n// 默认的使用向量召回的方案。\n@ConditionOnRule(\"global.qps \u003e 1000\")\n@SmartService(group = MaterialRecallService.class, primary = true)\npublic class DegradeSortByDbMaterialRecallServiceImpl implements MaterialRecallService {\n\n    @Override\n    public RecallResult recall(RecallContext context) {\n        return RecallResult.builder().receipt(\"degrade，qps \u003e 1000\").build();\n    }\n}\n```\n\n**执行效果如下：**\n\n```java\n\n@Slf4j\n@RunWith(SpringRunner.class)\n@SpringBootTest(classes = TomcatLauncher.class, webEnvironment = SpringBootTest.WebEnvironment.DEFINED_PORT)\npublic class DegradeByGlobalConfigTest {\n\n    /**\n     * Material recall service instance, used to execute related business logic.\n     */\n    @Resource\n    private MaterialRecallService materialRecallService;\n\n    /**\n     * Global intelligent IOC context instance, used to obtain and manage global configuration information.\n     */\n    @Resource\n    private GlobalSmartIocContext globalSmartIocContext;\n\n    @Test\n    public void degradeTest() {\n        // Tests if qps \u003c= 1000 normally uses the normal vector recall implementation to return recommended content\n        globalSmartIocContext.registerGlobalVar(\"qps\", 1);\n        RecallResult result = materialRecallService.recall(new RecallContext());\n        Assert.assertNotNull(result);\n        Assert.assertEquals(\"normal, qps \u003c= 1000\", result.getReceipt());\n\n        // Test the critical condition judgment under normal circumstances, and execute the normal vector recall strategy when qps \u003c= 1000\n        globalSmartIocContext.registerGlobalVar(\"qps\", 1000);\n        result = materialRecallService.recall(new RecallContext());\n        Assert.assertNotNull(result);\n        Assert.assertEquals(\"normal, qps \u003c= 1000\", result.getReceipt());\n\n        // Test whether to execute the downgrade plan when qps is greater than 1000\n        globalSmartIocContext.registerGlobalVar(\"qps\", 2000);\n        result = materialRecallService.recall(new RecallContext());\n        Assert.assertNotNull(result);\n        Assert.assertEquals(\"degrade，qps \u003e 1000\", result.getReceipt());\n\n        // Test whether to execute the downgrade plan when qps is greater than 1000\n        globalSmartIocContext.registerGlobalVar(\"qps\", 1001);\n        result = materialRecallService.recall(new RecallContext());\n        Assert.assertNotNull(result);\n        Assert.assertEquals(\"degrade，qps \u003e 1000\", result.getReceipt());\n    }\n}\n```","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fjdk-plus%2Fspring-smart-ioc-starter","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fjdk-plus%2Fspring-smart-ioc-starter","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fjdk-plus%2Fspring-smart-ioc-starter/lists"}