# Overview

Dynamic plug-in calling is a unique capability provided by FizzGate, which mainly solves the following two problems:

  1. Dynamic hot-swappable plug-ins: Allow developers to develop and select plug-ins provided by the market to achieve dynamic hot-swappable plug-ins without affecting node functions.

  2. ClassLoaders are isolated from each other: ClassLoaders are used to isolate plug-ins and nodes and between plug-ins to ensure compatibility even if different versions of the API are used.

The FizzGate team has conducted in-depth cooperation with the Alibaba Sofa team and made full use of Sofa's isolation technology. This cooperation not only meets Sofa's needs in terms of application scenarios and community feedback capabilities, but also gives full play to Sofa's Serverless capabilities, thereby significantly improving FizzGate's overall capabilities.

There are two main sources of dynamic plug-ins:

  1. Use templates for secondary development by yourself.

  2. Download the plug-in from the official market.

# Dynamic plug-in development

# Dynamic plug-in development

Download sample code, https://gitee.com/fizzgate/fizz-dynamic-plugin

Here is the main code for a plugin example:

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"; // Plug-in 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"); // This plugin only outputs this
         return FizzPluginFilterChain.next(exchange); //Execute subsequent logic
     }
}

In the above code, the @SofaService annotation declares that the component is a dynamic component and needs to expose service capabilities for node calls.

It should be noted that the uniqueId must be consistent with the plug-in background configuration. For other development details, please refer to the static plug-in development method.

# Dynamic plug-in packaging

Compile using packaging command

mvn clean package -DskipTests

After the component is packaged, two corresponding jar packages will be downloaded and generated in the target directory. One of the jar packages is named {name}-ark-biz.jar. This jar package needs to be used when uploading dynamic plug-ins.

# Dynamic plug-in upload

In the background configuration, the following steps need to be performed:

  • Click the Extension Center and then click Add;

  • Edit plug-in related information and ensure that the plug-in name is consistent with uniqueId;

  • Upload dynamic plug-in files;

  • Click Save.

These steps ensure that the plug-in is successfully configured and available for use on the system.

# Dynamic component development

Download the sample code, https://gitee.com/fizzgate/fizz-dynamic-plugin, you can find a sample of the dynamic component fizz-node-mysql in the component.

# Dynamic component development

# Write node logic

All executable node logic needs to inherit com.fizzgate.aggregate.core.flow.Node or comcom.fizzgate.aggregate.web.flow.RPCNode. This interface defines the node execution logic and needs to implement the singleRun method.

public abstract Mono<NodeResponse> singleRun();

RPCNode inherits Node, so general extensions to RPC calls can be made to Node. MysqlNode of fizz-node-mysql encapsulates the RPC calls made and implements RPCNode. In addition, NodeConfig or RPCNodeConfig must be implemented to configure node parameters. MysqlNodeConfig encapsulates the configuration of interface parameters. In order to enable the component immediately after registration, MysqlComponentAutoConfiguration.class was written in the project, which inherits ComponentAutoConfiguration and implements component registration.

@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());
     }
}

Because the node does not have a pom reference to the database, the project requires self-introduced mysql pom support.

<dependency>
     <groupId>mysql</groupId>
     <artifactId>mysql-connector-java</artifactId>
     <version>8.0.33</version>
     <scope>runtime</scope>
</dependency>

# Compile and package

Use maven to compile and package. One of the jar packages is named {name}-ark-biz.jar. This jar package needs to be used when uploading dynamic components.

mvn clean package -DskipTests

# Write front-end components

Download the sample code, https://gitee.com/fizzgate/fizz-frontend-node and find the fizz-node-mysql sample in the modules directory. Two files can be found in src/views: Home.vue and Node.vue. Home.vue is the node code, and Mysql.vue is the node pop-up code. The node code mainly displays node information, and the node pop-up code mainly edits and saves nodes.

Home.vue

<template>
   <div>
     <div class="node-request-head">MYSQL node ID: {{model.id}}</div>
     <div class="node-request-body">
     <div>
       {{model.properties.serviceName ? "Service Name: "+model.properties.serviceName: ""}}
     </div>
     <div>
       {{model.properties.path ? "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">Component: {{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="Node name" prop="name" key="name">
             <el-input v-model="requestForm.name"
                 placeholder="node name"></el-input>
           </el-form-item>
        
       <el-form-item label="Connection address" prop="URL" key="URL">
               <el-input v-model="requestForm.URL" clearable></el-input>
               <span class="key-tips">Database link address, such as: 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="Query data SQL" prop="sql" key="sql">
               <el-input type="textarea" v-model="requestForm.sql" clearable></el-input>
               <span class="key-tips">Example: Select dd* from users Do not end with semicolon</span>
       </el-form-item>
       <el-form-item label="bind parameters" prop="binds" key="binds">
               <el-input v-model="requestForm.binds" clearable></el-input>
               <span class="key-tips">Input uses JSON{"id":"1"}</span>
       </el-form-item>
       <footer class="drawer-footer">
       <el-button size="small" type="primary" @click="submitForm()" v-if="!disabled && dialogType !== 'detail'">
         Sure
       </el-button>
       <el-button size="small" @click="onCancel">
         {{!disabled && dialogType !== 'detail' ? 'Cancel' : 'Close'}}
       </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 is required', trigger: 'change' }
         ],
         sql: [
           { required: true, message: 'required', trigger: 'change' },
         ],
         binds: [
           { required: true, message: 'required', trigger: 'change' }
         ],
         'fallback.defaultResult': [
           {
             validator: (rule, value, callback) => {
               if (value && !validateJson(value)) {
                 callback(new Error('Please enter JSON in the correct format'));
               } 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('Please complete the step information');
       })
   
     },

     onCancel(){
       const closeDialog = this.closeDialog;
       if (closeDialog){
         closeDialog();
       }
     }
   }

}
</script>

In addition, you also need to pay attention to the editor toolbar icon information. The file location is 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+Dw2Bluj1znuSjh b99Gkn6QILDY2imo60p8nsnc9bEo3+QJ+AKHfMdZHnl78wyTnyHZD53Zzx73MRSgYvnqgCU Hj6gwdck7Zsp1VOrz0Uz8NbKunzAW+Gu4fYW28bUYutYlzSa7B84Fh7d1kjLwhcSdYAYrdkM QVpsBr5XgDGuXwQfQr0y9zwLda+DUYXLaGKdd2ZTtvbolaO87pdo24hP7ov16N0zArH1ur3iwJpXxm+v7oAJNR4JEP8DoAuSFEkYH7cAAAAASUVORK5CYII=');background-size: cover;}"
     }
}

# Compile and package

Use vue compilation and packaging commands for packaging

npm run build

The project will generate a dist directory in the dist directory, which contains the packaged files. Finally, it will compress all the files into a zip package in the dist directory (please note that you need to enter the dist directory for compression). In the background configuration, the management package can be uploaded to the zip package.