Skip to content

Commit

Permalink
发布1.0.1版本
Browse files Browse the repository at this point in the history
  • Loading branch information
p-moon committed Dec 10, 2024
1 parent 1542f39 commit 006d391
Show file tree
Hide file tree
Showing 14 changed files with 230 additions and 106 deletions.
19 changes: 19 additions & 0 deletions .github/workflows/create-plantuml.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
name: generate plantuml
on: push
jobs:
generate_plantuml:
runs-on: ubuntu-latest
name: plantuml
steps:
- name: checkout
uses: actions/checkout@v1
with:
fetch-depth: 1
- name: plantuml
id: plantuml
uses: grassedge/[email protected]
with:
path: doc/img
message: "Render PlantUML files"
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
File renamed without changes.
1 change: 1 addition & 0 deletions .mvn/settings.xml.weibo
68 changes: 56 additions & 12 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,16 +1,12 @@
## 一、说在前面的话

这是一种基于动态代理与配置中心切换 bean 的实现类来达到服务降级与流量灰度发布方法研究
本项目描述并实现了一种基于动态代理与配置中心切换 bean 的实现类来达到服务降级与流量灰度发布方法的研究

这是一个通过动态切换接口的实现类(springboot 中的 bean ),实现了一种高效而灵活的解决方案,用于应对复杂系统中的服务降级和功能灰度发布。

**该项目提出并实现了一种使用动态代理和配置中心来管理某个接口的多个业务实现,通过动态切换 Bean 的实现类快速实现服务降级。
此外,借助 [JEXL](https://commons.apache.org/proper/commons-jexl/) 自定义方法的解析规则,还提供了一种小流量功能灰度发布的解决方案。**

在当今复杂的业务系统中,服务降级和功能灰度发布已成为保障系统稳定性和灵活性的关键需求。本项目通过动态切换接口的实现类(Spring Boot中的Bean),提出了一种高效而灵活的解决方案。
利用动态代理和配置中心的结合,以及[JEXL](https://commons.apache.org/proper/commons-jexl/)自定义方法的解析规则,我们实现了对接口的多种业务实现进行动态管理,
从而快速应对服务降级和小流量功能灰度发布的场景。

## 二、相关工作 & 设计理念

**相关工作**
Expand All @@ -32,15 +28,16 @@ ISP)则要求我们确保流程中的任何接口实现都可以被替换,

实现原理其实很简单,通过代理类来执行规则判定即可,通过时序图描述的逻辑如下:

![](doc/img/idea.png)

![](doc/img/execution-sequence-diagram.svg)

你可以像这样子引入他:

```xml
<dependency>
<groupId>plus.jdk</groupId>
<artifactId>spring-smart-ioc-starter</artifactId>
<version>1.0.0</version>
<version>1.0.2</version>
</dependency>
```

Expand All @@ -55,11 +52,11 @@ ISP)则要求我们确保流程中的任何接口实现都可以被替换,
**`@ConditionOnRule`执行 eval逻辑时运行环境中的一些魔法变量的说明**

| 变量名 | 说明 | 补充 |
|---------|---------------------------------------------------------------------------------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| args | 当前调用的方法的入参, 允许在condition rule里面使用函数入参判定是否调用该实现类的方法,这个参数每次执行方法调用的时候都会作为临时变量写入运行环境 | 例如,你的方法定义为```String greeting(String name, String sex);```, 那么你可以在 condition rule里面通过 `args.name``args.sex` 来访问被调用的方法(Method)的入参 |
| global | 全局的变量或工具类 | 你可以使用 `global` 在condition rule中访问你写入的全局变量或一些工具类,例如,当你使用 `globalSmartIocContext.registerGlobalVar("random", new Random()))`注册`random`后,可以使用这样子的表达式 `@ConditionOnRule("global.randdom.nextInt(100) % 10 >= 8")`来将 20% 的流量来打到当前的这个实现类上 |
| current | 允许在 condition rule 维度来访问当前的 beanName(`current.beanName`) 和 methodName(`current.methodName`) | 这里你可以通过监听配置中心使用 `@ConditionOnRule("current.beanName == global.myServiceName")` 来指定当前要切换为哪个实现类. 这里的 `myServiceName`可以通过 `GlobalSmartIocContext#registerGlobalVar(name, obj)`写入 |
| 变量名 | 说明 | 补充 |
|---------|---------------------------------------------------------------------------------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| args | 当前调用的方法的入参, 允许在condition rule里面使用函数入参判定是否调用该实现类的方法,这个参数每次执行方法调用的时候都会作为临时变量写入运行环境 | 例如,你的方法定义为```String greeting(String name, String sex);```, 那么你可以在 condition rule里面通过 `args.name``args.sex` 来访问被调用的方法(Method)的入参 |
| global | 全局的变量或工具类 | 你可以使用 `global` 在condition rule中访问你写入的全局变量或一些工具类,例如,当你使用 `globalSmartIocContext.registerGlobalVar("random", new Random()))`注册`random`后,可以使用这样子的表达式 `@ConditionOnRule("global.random.nextInt(100) % 10 >= 8")`来将 20% 的流量来打到当前的这个实现类上 |
| current | 允许在 condition rule 维度来访问当前的 beanName(`current.beanName`) 和 methodName(`current.methodName`) | 这里你可以通过监听配置中心使用 `@ConditionOnRule("current.beanName == global.myServiceName")` 来指定当前要切换为哪个实现类. 这里的 `myServiceName`可以通过 `GlobalSmartIocContext#registerGlobalVar(name, obj)`写入 |

**写入全局变量:**

Expand Down Expand Up @@ -88,6 +85,53 @@ public class SetGlobalConfigTest {
}
}
```
接下来你可以通过`global`变量来访问这里注册的全局变量和全局函数.例如, 你可以像下面这样,指定当 qps 小于 2000 时利用注册的 random 函数将 20% 的流量打到这个实现上:

```java
import plus.jdk.smart.ioc.annotations.SmartService;

@ConditionOnRule("global.qps < 2000 && random.nextInt(100) % 10 > 8")
@SmartService(group = MyService.class)
public class QpsDegradeService implements MyService {
// do something
}
```
**最后的魔法**

思虑再三,虽然这样子会带来一些安全性的问题,但是这完全取决于使用者了,不应该舍本逐末。
最后登场的魔法就是全局`jexl.eval(anyString())`函数了,这个函数允许你直接通过全局变量自定义脚本。
> 当然了,你也可以是用局部变量来传递脚本来执行,但是注意,这是非常不安全的一种做法,因为函数的输入大多来自外部输入,
> 如果你把这个功能开放给外部输入,那你最好保证调用方是可信的,值得你去给他最高级别的执行权限!!!否则就不要这么做。
> 甚至于,在上述功能足够满足你需求的情况下,尽量不要使用这个魔法函数,这里只是为了满足一些上述功能不能满足需求的场景
通过这个功能,你可以把你想要执行的脚本配置在 配置中心 中,当服务启动或配置变化时,通过如下指令来更新你的脚本到 `global` 对象中:

```java
public class SmartIocSelectorFactoryTest {

/**
* Global intelligent IOC context instance, used to obtain and manage global configuration information.
*/
@Resource
private GlobalSmartIocContext globalSmartIocContext;

@Test
public void evalExpressionTest() {
// Test the magic jexl.eval() function
globalSmartIocContext.registerGlobalVar("testScript", "1 + 2");
}
}
```

然后使用 `@ConditionOnRule("jexl.eval(global.testScript)")`配置在需要的类或者方法上即可。通过这个魔法方法你可以把你的切换规则放在配置中心,通过配置中心按需求动态下发即可!!!

> 注意, 使用函数输入时,千万不要把 `args` 的成员变量的内容传递给`jexl.eval(anyString())`函数,这会导致你的入参变成可执行的脚本!!!比如你的函数入参分别为 `a=3``b=4`,
> 你可以这么写`@ConditionOnRule("jexl.eval(global.testScript) > args.a + args.b")`, 这是安全的; 但是如果你直接把 args的参数内容直接传递给`jexl.eval(anyString())`
> 例如 `@ConditionOnRule("jexl.eval(args.b)"),这无疑是一种裸奔的行为,会导致你的服务受到攻击,甚至导致更严重的后果。
所以,我的朋友,我还是不得不提醒你,<p style="color: red; ">__永远都不要把入参直接当做脚本执行,这不管在哪都是一样的,
就像[变基的风险](https://git-scm.com/book/zh/v2/Git-%E5%88%86%E6%94%AF-%E5%8F%98%E5%9F%BA.html#_rebase_peril)中提交到的,
如果你遵循这条金科玉律,就不会出差错。否则人民群众会仇恨你,你的朋友和家人也会嘲笑你,唾弃你!!!__<p>

## 四、服务降级 & 小流量灰度的示例

Expand Down
100 changes: 100 additions & 0 deletions doc/execution-sequence-diagram.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
```plantuml:execution-sequence-diagram
@startuml
title 启动 & 执行时序图
!theme vibrant
!define purple_color 6A00FF
!define green_color green
!define red_color FF05DE
!define yellow_color yellow
!define STRONG_TEXT(text) <b>text</b>
!define PURPLE_TEXT(text) <font color="purple_color" size=14><b>text</b></font>
!define GREEN_TEXT(text) <font color="green_color" size=14><b>text</b></font>
!define BLUE_TEXT(text) <font color="blue">text</font>
!define GREEN_TEXT(text) <font color="green_color"">text</font>
!define RED_TEXT(text) <font color="red_color">text</font>
!define YELLOW_TEXT(text) <font color="yellow_color">text</font>
!define record_impl_msg GREEN_TEXT("记录bean加载过程中的每个接口的的实现类")
!define create_proxy_msg GREEN_TEXT("创建代理类并注册全局的 bean")
!define create_proxy_msg_note GREEN_TEXT("在这里会为指定interface生成代理类")
!define start_success_msg GREEN_TEXT("服务启动完成")
!define start_success_msg_note GREEN_TEXT("服务启动完成, 其他需要引入的地方使用 @Resource注解 按接口引入即可")
!define bean_load_flow GREEN_TEXT("bean加载流程")
!define logic_exec_group_name RED_TEXT(逻辑执行:通过组件生成的接口的代理类发起方法调用(该类直接通过 @Resource 注解注入即可))
!define logic_exec_msg "通过代理类发起逻辑调用"
!define get_impl_msg 获取接口的实现类
!define parse_condition_rule 解析实现类的切换规则
!define jexl_engine_create_msg PURPLE_TEXT("创建jexl解析引擎")
!define jexl_engine_context_msg PURPLE_TEXT("向jexl注册方法入参")
!define jexl_engine_context_reg_msg PURPLE_TEXT("向jexl注册全局变量和自定义函数")
!define jexl_engine_eval_msg PURPLE_TEXT("通过 jexl 来执行脚本并获取返回结果")
!define jexl_engine_eval_result PURPLE_TEXT("jexl 执行输入的条件表达式并返回结果")
!define jexl_engine_eval_condition PURPLE_TEXT("判定jexl 执行输入的条件表达式是否为 true")
!define loop_bean_impl_msg "遍历所有的 bean 实现"
!define script_eval_true_msg "jexl eval结果为 true"
!define script_eval_false_msg "jexl eval结果为 false"
!define script_eval_false_continue_msg "继续遍历 eval下一个 bean的条件"
!define bean_impl_invoke_method_msg "调用符合条件的实现类的方法"
!define bean_impl_invoke_method_return_msg RED_TEXT("返回函数执行结果")
!define no_matching_rules_were_hit "未命中任何符合条件实现类"
!define get_default_implementation_annotated_via_primary_msg RED_TEXT(获取通过@SmartService的primary)\nRED_TEXT(属性标注的默认实现)
!define bean_load_flow_note GREEN_TEXT(在这里会扫描并记录所有接口被)\nRED_TEXT("@SmartService")GREEN_TEXT( 标注的实现类)
!define jexl_desc_note_line1 YELLOW_TEXT(JEXL(Java Expression Language)是Apache Commons项目中的一个子项目,它提供了 一种轻量级且灵活的表达式语言,)
!define jexl_desc_note_line2 YELLOW_TEXT(用于在Java应用程序中动态计算和操作数据。JEXL的设计初衷是为了使表达式的解析和执行变得简单且高效,同时保持与Java语言的紧密集成。)
!define jexl_desc_note_line3 YELLOW_TEXT(JEXL设计之初就考虑了性能问题,通过编译和缓存机制来提高表达式的执行效率。这里的逻辑判定耗时一般在纳秒级别)
collections 服务启动 as service_start
collections 记录每个接口的实现类 as record_impl
collections 动态代理 as proxy_bean
collections 启动成功 as start_success
collections 业务逻辑调用 as logic_exec
collections Jexl引擎 as jexl_engine
group bean_load_flow
autonumber
service_start -[#green]> record_impl : record_impl_msg
note left record_impl #yellow:bean_load_flow_note
record_impl -[#green]> proxy_bean: create_proxy_msg
note left proxy_bean #yellow: create_proxy_msg_note
proxy_bean -[#green]> start_success:start_success_msg
note left start_success #yellow:start_success_msg_note
end
group logic_exec_group_name
autonumber
logic_exec -[#red]> proxy_bean:RED_TEXT(logic_exec_msg)
proxy_bean -[#red]> record_impl:RED_TEXT(get_impl_msg)
record_impl -[#red]> proxy_bean: RED_TEXT(parse_condition_rule)
loop STRONG_TEXT(RED_TEXT(loop_bean_impl_msg))
note over proxy_bean,start_success #green
jexl_desc_note_line1
jexl_desc_note_line2
jexl_desc_note_line3
end note
proxy_bean -[#purple_color]> jexl_engine: jexl_engine_create_msg
proxy_bean -[#purple_color]> jexl_engine: jexl_engine_context_msg
proxy_bean -[#purple_color]> jexl_engine: jexl_engine_context_reg_msg
proxy_bean -[#purple_color]> jexl_engine: jexl_engine_eval_msg
jexl_engine -[#purple_color]> proxy_bean: jexl_engine_eval_result
proxy_bean --[#purple_color]> proxy_bean: jexl_engine_eval_condition
alt RED_TEXT(script_eval_true_msg)
autonumber 10
proxy_bean -> record_impl: RED_TEXT(bean_impl_invoke_method_msg)
record_impl -> proxy_bean: RED_TEXT(bean_impl_invoke_method_return_msg)
proxy_bean -> logic_exec: RED_TEXT(bean_impl_invoke_method_return_msg)
else RED_TEXT(script_eval_false_msg)
autonumber 10
record_impl -> record_impl: RED_TEXT(script_eval_false_continue_msg)
end
end
alt RED_TEXT(no_matching_rules_were_hit)
autonumber 11
proxy_bean -> record_impl:get_default_implementation_annotated_via_primary_msg
record_impl -> proxy_bean: bean_impl_invoke_method_return_msg
proxy_bean -> logic_exec: bean_impl_invoke_method_return_msg
end
end
@enduml
```
Empty file added doc/img/README.md
Empty file.
Binary file removed doc/img/idea.png
Binary file not shown.
69 changes: 0 additions & 69 deletions doc/plantuml.md

This file was deleted.

2 changes: 1 addition & 1 deletion pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@

<groupId>plus.jdk</groupId>
<artifactId>spring-smart-ioc-starter</artifactId>
<version>1.0.0</version>
<version>1.0.2</version>


<description>spring-smart-ioc-starter</description>
Expand Down
8 changes: 8 additions & 0 deletions src/main/java/plus/jdk/smart/ioc/global/Evalable.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package plus.jdk.smart.ioc.global;

public interface Evalable {
/**
* Execute the specified script and return the results。
*/
Object eval(String script);
}
Loading

0 comments on commit 006d391

Please sign in to comment.