# 概述
动态插件调用是FizzGate提供的独特能力,主要解决了以下两个问题:
1、动态热插拔插件: 允许开发者自行开发和选择市场提供的插件,实现动态热插拔,不影响节点功能。
2、ClassLoader互相隔离: 插件与节点之间以及插件之间使用ClassLoader进行互相隔离,确保即使使用不同版本的API也能兼容。
FizzGate团队与阿里Sofa团队进行了深度合作,充分利用了Sofa的隔离技术。这种合作不仅满足了Sofa在应用场景和社区反馈能力方面的需求,还充分发挥了Sofa的Serverless能力,从而显著提升了FizzGate的整体能力。
动态插件的来源主要有两个途径:
1、自行利用模板进行二次开发。
2、从官方市场下载插件。
# 动态插件开发
# 动态插件开发
下载sample代码,https://gitee.com/fizzgate/fizz-dynamic-plugin
以下是一个插件示例的主要代码:
package com.fizzgate.plugin.extension;
import com.alipay.sofa.runtime.api.annotation.SofaService;
import com.alipay.sofa.runtime.api.annotation.SofaServiceBinding;
import com.fizzgate.plugin.FizzPluginFilter;
import com.fizzgate.plugin.FizzPluginFilterChain;
import org.springframework.stereotype.Component;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;
import java.util.Map;
@SofaService(uniqueId = LogPlugin.PLUGIN_ID, bindings = {@SofaServiceBinding(serialize = false)})
@Component
public class LogPlugin implements FizzPluginFilter {
public static final String PLUGIN_ID = "logPlugin"; // 插件 id
public void init(String pluginConfig) {
FizzPluginFilter.super.init(pluginConfig);
}
public Mono<Void> filter(ServerWebExchange exchange, Map<String, Object> config) {
System.err.println("this is my plugin"); // 本插件只输出这个
return FizzPluginFilterChain.next(exchange); // 执行后续逻辑
}
}
上述代码中,@SofaService 注解声明了该组件为动态组件,需要暴露服务能力给节点调用。
需要注意的是,uniqueId 必须与插件后台配置一致。其他开发细节可以参考静态插件开发方式。
# 动态插件打包
使用打包命令进行编译
mvn clean package -DskipTests
组件打包之后会在target目录下载生成对应两个的jar包。其中一个jar包以{name}-ark-biz.jar为名字,在上传动态插件时,需要使用这个jar包。
# 动态插件上传
在后台配置中,需要进行以下步骤:
点击扩展中心,然后点击新增;
编辑插件相关信息,确保插件名与 uniqueId 一致;
上传动态插件文件;
点击保存。
这些步骤能够确保插件被成功配置并能够在系统中使用。
# 动态组件开发
下载sample代码,https://gitee.com/fizzgate/fizz-dynamic-plugin,可以在组件中找到动态组件fizz-node-mysql的样例。
# 动态组件开发
# 编写节点逻辑
所有可执行的节点逻辑需要继承 com.fizzgate.aggregate.core.flow.Node 或者 comcom.fizzgate.aggregate.web.flow.RPCNode 。该接口定义了节点执行逻辑,需要实现 singleRun 方法。
public abstract Mono<NodeResponse> singleRun();
RPCNode继承了Node,因此可以对Node进行RPC调用的通用扩展。fizz-node-mysql的MysqlNode对进行的RPC调用进行了封装,实现了RPCNode。除此之外,还需实现NodeConfig或者RPCNodeConfig,用于配置节点参数。MysqlNodeConfig对界面参数的配置进行了封装。为了是的该组件在注册之后第一时间启用,项目中编写了 MysqlComponentAutoConfiguration.class,该类继承了ComponentAutoConfiguration,实现了组件的注册。
@Configuration
public class MysqlComponentAutoConfiguration {
private static final Logger LOGGER = LoggerFactory.getLogger(MysqlComponentAutoConfiguration.class);
public MysqlComponentAutoConfiguration(){
if (NodeFactory.hasBuilder(MysqlNode.TYPE)){
NodeFactory.unRegisterBuilder(MysqlNode.TYPE);
LOGGER.info("do have component mysql , will replace it");
}
LOGGER.info("register component type:{}",MysqlNode.TYPE);
NodeFactory.registerBuilder(MysqlNode.TYPE, new MysqlNode.MysqlNodeBuilder());
}
}
因为节点并没有数据库的pom引用,所以该项目需要自行引入的mysql的pom支持。
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.33</version>
<scope>runtime</scope>
</dependency>
# 编译打包
使用maven进行编译打包,其中一个jar包以{name}-ark-biz.jar为名字,在上传动态组件时,需要使用这个jar包。
mvn clean package -DskipTests
# 编写前端组件
下载sample代码,https://gitee.com/fizzgate/fizz-frontend-node 在modules目录中找到fizz-node-mysql的样例。 可以在src/views找到两个文件:Home.vue和Node.vue。Home.vue是节点代码,Mysql.vue是节点弹窗代码。节点代码主要是展示节点信息,节点弹窗代码主要对节点进行编辑和保存。
Home.vue
<template>
<div>
<div class="node-request-head">MYSQL节点ID:{{model.id}}</div>
<div class="node-request-body">
<div>
{{model.properties.serviceName ? "服务名:"+model.properties.serviceName: ""}}
</div>
<div>
{{model.properties.path ? "路径:"+model.properties.path: ""}}
</div>
</div>
<div class="node-request-footer">
<span v-if="model.properties.type" class='node-request-logo'>
{{ model.properties.type }}
</span>
<span @click="onComponentClick">组件:{{componentCount}}</span>
</div>
</div>
</template>
<script>
export default {
name:"home",
props: {
model:{
type: Object,
default: () => ({
id:"",
properties:{
components:[]
}
})
},
graphModel:{
type: Object
}
},
data () {
return {
}
},
methods:{
onComponentClick(event){
window.event? window.event.cancelBubble = true : event.stopPropagation();
const { graphModel, model } = this.$props;
const data = model.getData();
graphModel.eventCenter.emit("node:components:click",
{
target:graphModel,
model:data
}
);
return false;
}
},
computed:{
componentCount(){
return this.model.properties.components ? this.model.properties.components.length: 0;
}
},
mounted () {
},
watch: {
}
}
</script>
<style scoped>
</style>
Node.vue
<template>
<el-form ref="requestForm" :rules="rules" :model="requestForm" size="small"
label-width="110px" >
<el-form-item label="节点名称" prop="name" key="name">
<el-input v-model="requestForm.name"
placeholder="节点名称"></el-input>
</el-form-item>
<el-form-item label="连接地址" prop="URL" key="URL">
<el-input v-model="requestForm.URL" clearable></el-input>
<span class="key-tips">数据库链接地址,如:r2dbcs:mysql://root:password@localhost:3306/archer?useSSL=false&useUnicode=true&characterEncoding=utf-8&zeroDateTimeBehavior=convertToNull&transformedBitIsBoolean=true&serverTimezone=GMT%2B8&nullCatalogMeansCurrent=true&allowPublicKeyRetrieval=true</span>
</el-form-item>
<el-form-item label="查询数据SQL" prop="sql" key="sql">
<el-input type="textarea" v-model="requestForm.sql" clearable></el-input>
<span class="key-tips">示例:Select dd* from users 请勿以分号结尾</span>
</el-form-item>
<el-form-item label="绑定参数" prop="binds" key="binds">
<el-input v-model="requestForm.binds" clearable></el-input>
<span class="key-tips">输入使用JSON{"id":"1"}</span>
</el-form-item>
<footer class="drawer-footer">
<el-button size="small" type="primary" @click="submitForm()" v-if="!disabled && dialogType !== 'detail'">
确 定
</el-button>
<el-button size="small" @click="onCancel">
{{!disabled && dialogType !== 'detail' ? ' 取 消' : '关 闭'}}
</el-button>
</footer>
</el-form>
</template>
<script>
export default {
name: 'node',
props: {
model:{
type: Object,
default: () => ({
id:"",
properties:{
components:[]
}
})
},
lf:{
type: Object
},
graphModel:{
type: Object
},
closeDialog:{
type: Function
}
},
data() {
return {
rules: {
URL: [
{ required: true, message: 'URL是必填', trigger: 'change' }
],
sql: [
{ required: true, message: '必填', trigger: 'change' },
],
binds: [
{ required: true, message: '必填', trigger: 'change' }
],
'fallback.defaultResult': [
{
validator: (rule, value, callback) => {
if (value && !validateJson(value)) {
callback(new Error('请输入正确格式的JSON'));
} else {
callback();
}
}, trigger: 'blur'
}
]
},
disabled:false,
dialogType:'create',
requestForm: {
URL:"",
sql:"",
binds:""
}
}
},
created(){
const { properties, id} = this.model.getData();
this.requestForm = {...properties, name:id};
},
methods: {
submitForm() {
this.$refs.requestForm && this.$refs.requestForm.validate().then(() => {
const nodeData = this.model.getData();
const {name, ...properties} = this.$data.requestForm
this.lf.setProperties(this.model.id, properties);
const closeDialog = this.closeDialog;
if (closeDialog){
closeDialog();
}
}).catch(() => {
this.$message.error('请完善步骤信息');
})
},
onCancel(){
const closeDialog = this.closeDialog;
if (closeDialog){
closeDialog();
}
}
}
}
</script>
除此之外,还需要注意编辑器工具栏图标信息,该文件位置在src/public/dynamic.json
{
"name":"mysql",
"entry": "//localhost:1890",
"nodeSize":{
"width":"140",
"height":"80"
},
"panelItem": {
"text": "mysql",
"type": "mysql",
"class": "node-mysql",
"style": "background: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAUCAYAAAH6ji2bAAAABGdBTUEAALGPC/xhBQAAAOVJREFUOBGtVMENwzAIjKP++2026ETdpv10iy7WFbqFyyW6GBywLCv5gI+Dw2Bluj1znuSjhb99Gkn6QILDY2imo60p8nsnc9bEo3+QJ+AKHfMdZHnl78wyTnyHZD53Zzx73MRSgYvnqgCUHj6gwdck7Zsp1VOrz0Uz8NbKunzAW+Gu4fYW28bUYutYlzSa7B84Fh7d1kjLwhcSdYAYrdkMQVpsBr5XgDGuXwQfQr0y9zwLda+DUYXLaGKdd2ZTtvbolaO87pdo24hP7ov16N0zArH1ur3iwJpXxm+v7oAJNR4JEP8DoAuSFEkYH7cAAAAASUVORK5CYII=');background-size: cover;}"
}
}
# 编译打包
使用vue编译打包命令进行打包
npm run build
项目会在dist 目录下生成一个dist目录,里面就是打包好的文件,最后自行在dist目录下将所有的文件压缩为zip包(请注意需要进入dist目录下进行压缩)。后台配置中管理端包上传该zip包即可。