• 一文了解微服务低代码实现方式


    欢迎来到👏阿提说说👏的博客。很高兴,您能看到我的文章。

    💡 目前云原生比较火,各公司都在考虑把自己的微服务迁移到云原生架构中,我司也不例外,为了能够更好的将我司的微服务迁移到云原生架构上,需要对目前的服务进行瘦身,首先就是剥离对接第三方子系统的代码,这部分代码会越积越多,决定拆分后使用低代码来实现,在研究了某为Roma、xx集操作系统等公司的产品后,发现其都是使用Java开发的系统,低代码部分语言基本用的JS,于是我尝试并总结了Java中使用脚本语言来实现低代码开发的方式,希望对你有所启发,记得点赞、收藏、评论再走😄。

    tips: 最近CSDN增加了“只看目录”功能,为了您更好的体验,建议点击右下角第一个图标打开“只看目录”

    Java Script Engine

    Java 脚本引擎可以将脚本嵌入Java代码中,可以自定义和扩展Java应用程序,自JDK1.6被引入,基于Rhino引擎,JDK1.8后使用Nashorn引擎,支持ECMAScript 5,但后期还可能会换。
    脚本引擎包位于javax.script中,各个类名及描述如下

    接口

    • Bindings
    键值对映射,所有key都为String
    • Compilable
    由具体的脚本引擎实现,用于将脚本进行编译,可重复使用。
    • Invocable 由具体的脚本引擎实现,其允许调用先前已执行的脚本
    • ScriptContext
    脚本引擎上下文,用于将应用程序与脚本引擎进行绑定
    • ScriptEngine
    由具体的脚本引擎实现,定义了执行脚本的方法、键值对映射关系、脚本引擎上下文
    • ScriptEngineFactory
    脚本引擎工厂,每一个ScriptEngine都有一个对应的工厂。ScriptEngineManager会从ClassLoader中获取所有的ScriptEngineFactories实例

    • AbstractScriptEngine
    ScriptEngine的抽象实现类,提供了ScriptEngine的标准实现
    • CompiledScript
    由存储编译结果的类扩展。可以以Java类、Java类文件或者脚本操作码的形式存储,可以重复执行无需重新解析。每个CompiledScript都与一个ScriptEngine相关联,调用CompiledScript的eval方法会导致ScriptEngine执行
    • ScriptEngineManager
    脚本引擎管理器,提供ScriptEngine的实例化机制,并维护了一些键/值对集合,供所有创建的ScriptEngine共享使用
    • SimpleBindings
    使用HashMap 或者其他Map实现的一种简单键值映射
    • SimpleScriptContext
    ScriptContext 的一种简单实现

    异常

    • ScriptException
    脚本API的通用异常类,抛出的异常类具有文件名、行号、列号信息

    示例

    简单的

    @Test
    public void scriptTest() throws ScriptException {
        ScriptEngineManager engineManager = new ScriptEngineManager();
        //获取JavaScript解析引擎
        ScriptEngine engine = engineManager.getEngineByName("JavaScript");
        //将x变量映射为Hello World!
        engine.put("x", "Hello World!");
        engine.eval("print(x)");
    }
    //输出
    //Hello World!
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    较复杂的

    从文件中读取脚本

    /**
         * 从文件中读取Js脚本
         * test.js 中的内容:
         * var obj = new Object();
         * obj.hello = function (name) {
         *     print('Hello, ' + name);
         * }
         * @throws Exception
         */
    @Test
    public void file() throws Exception{
        ScriptEngineManager manager = new ScriptEngineManager();
        ScriptEngine engine = manager.getEngineByName("JavaScript");
        engine.eval(new FileReader(new File("script/test.js")));
        Invocable inv = (Invocable) engine;
        Object obj = engine.get("obj");
        inv.invokeMethod(obj, "hello", "Script Test!" );
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18

    将Java变量注入脚本中,有可能需要在脚本中使用Java变量

    @Test
    public void scriptVar() throws Exception{
        ScriptEngineManager manager = new ScriptEngineManager();
        ScriptEngine engine = manager.getEngineByName("JavaScript");
        File file = new File("F:/test/test.txt");
        //将File对象f直接注入到js脚本中并可以作为全局变量使用
        engine.put("files", file);
        engine.eval("print(files.getPath());print(files.getName());");
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    调用脚本中的方法
    使用Invocable 调用已经加载脚本中的方法

    @Test
    public void scriptTest1() throws ScriptException, NoSuchMethodException {
        ScriptEngineManager engineManager = new ScriptEngineManager();
        ScriptEngine engine = engineManager.getEngineByName("JavaScript");
        StringBuilder sb = new StringBuilder();
        sb.append("var obj = new Object();");
        sb.append("obj.hello = function(name){print('Hello, ' + name);}");
        engine.eval(sb.toString());
        //Invocable 可以调用已经加载过的脚本
        Invocable invocable = (Invocable) engine;
        //获取脚本的obj对象
        Object object = engine.get("obj");
        //调用obj对象的hello函数
        invocable.invokeMethod(object, "hello", "Script Method!");
    }
    //输出
    //Hello, Script Method!
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17

    多个作用域
    一个脚本引擎,多个scope,x变量并没有覆盖之前的变量

    @Test
    public void scriptTest() throws ScriptException {
        ScriptEngineManager engineManager = new ScriptEngineManager();
        ScriptEngine engine = engineManager.getEngineByName("JavaScript");
        engine.put("x", "Hello World!");
        engine.eval("print(x)");
        ScriptContext context = new SimpleScriptContext();
        //新的Script context绑定ScriptContext的ENGINE_SCOPE
        Bindings bindings = context.getBindings(ScriptContext.ENGINE_SCOPE);
        // 增加一个新变量到新的范围 engineScope 中
        bindings.put("x", "word hello!!");
        // 执行同一个脚本 - 但这次传入一个不同的script context
        engine.eval("print(x);", bindings);
        engine.eval("print(x);");
    }
    //输出
    //Hello World!
    //word hello!!
    //Hello World!
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19

    使用脚本实现Java接口

    @Test
    public void runnableImpl() throws Exception{
        ScriptEngineManager manager = new ScriptEngineManager();
        ScriptEngine engine = manager.getEngineByName("JavaScript");
        // String里定义一段JavaScript代码脚本
        String script = "function run() { print('run called'); }";
        // 执行这个脚本
        engine.eval(script);
        // 从脚本引擎中获取Runnable接口对象(实例). 该接口方法由具有相匹配名称的脚本函数实现。
        Invocable inv = (Invocable) engine;
        // 在上面的脚本中,我们已经实现了Runnable接口的run()方法
        Runnable runnable = inv.getInterface(Runnable.class);
        // 启动一个线程运行上面的实现了runnable接口的script脚本
        Thread thread = new Thread(runnable);
        thread.start();
        Thread.sleep(1000);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17

    如果脚本是基于对象的,则可以通过执行脚本的方法来实现Java接口,避免调用脚本的全局函数。

    @Test
    public void runnableObject() throws ScriptException {
        ScriptEngineManager manager = new ScriptEngineManager();
        ScriptEngine engine = manager.getEngineByName("JavaScript");
        String script = "var obj = new Object();obj.run = function() {println('run method called')}";
        engine.eval(script);
        //获得脚本对象
        Object object = engine.get("obj");
        Invocable invocable = (Invocable) engine;
        Runnable runnable = invocable.getInterface(object, Runnable.class);
        Thread thread = new Thread(runnable);
        thread.start();
        Thread.sleep(1000);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14

    Groovy

    特点

    1. groovy跟java都是基于jvm的语言,可以在java项目中集成groovy并充分利用groovy的动态功能;
    2. groovy兼容几乎所有的java语法,开发者完全可以将groovy当做java来开发,甚至可以不使用groovy的特有语法,仅仅通过引入groovy并使用它的动态能力;
    3. groovy可以直接调用项目中现有的java类(通过import导入),通过构造函数构造对象并直接调用其方法并返回结果;
    4. groovy支持通过GroovyShell预设对象,在groovy动态脚本中直接调用预设对象的方法。因此我们可以通过将spring的bean预设到GroovyShell运行环境中,在groovy动态脚本中直接调用spring容器中bean来调用其方法
    5. 语法较简洁

    Groovy动态脚本的使用

    直接调用java类

    在上一节中集成groovy的好处中提到,groovy可以通过import的方式直接调用java类,直接上代码:

    package pers.doublebin.example.groovy.script.service;
    import groovy.lang.Binding;
    import groovy.lang.GroovyShell;
    import groovy.lang.Script;
    public class TestService {
        public String testQuery(long id){
            return "Test query success, id is " + id;
        }
        public static void main(String[] args) {
            Binding groovyBinding = new Binding();
            GroovyShell groovyShell = new GroovyShell(groovyBinding);
            String scriptContent = "import pers.doublebin.example.groovy.script.service.TestService\n" +
                    "def query = new TestService().testQuery(1L);\n" +
                    "query";
            Script script = groovyShell.parse(scriptContent);
            System.out.println(script.run());
        }
    }
    //返回结果:
    //Test query success, id is 1
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20

    这种方式在groovy动态脚本中将类import后直接new了一个新对象,并调用对象的方法。

    ScriptEngineManager

    按照JSR223,使用标准接口ScriptEngineManager调用。低代码生成文件,groovy从脚本文件加载代码执行。

    ScriptEngineManager factory = new ScriptEngineManager();
    ScriptEngine engine = factory.getEngineByName("groovy");// 每次生成一个engine实例
    Bindings binding = engine.createBindings();
    binding.put("date", new Date()); // 入参
    engine.eval("def getTime(){return date.getTime();}", binding);
    // 如果script文本来自文件,请首先获取文件内容
    //engine.eval(new FileReader(new File("/Users/chenjujun/java-projects/Java-Test/src/test/java/script/test.groovy")));
    engine.eval("def sayHello(name,age){return 'Hello,I am ' + name + ',age' + age;}");
    Long time = (Long) ((Invocable) engine).invokeFunction("getTime", null);// 反射到方法
    System.out.println(time);
    String message = (String) ((Invocable) engine).invokeFunction("sayHello", "zhangsan", 12);
    System.out.println(message);
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    通过GroovyShell预设对象

    在上一节中提到,groovy支持通过GroovyShell预设对象,在groovy动态脚本中直接调用预设对象的方法。直接上代码:

    package pers.doublebin.example.groovy.script.service;
    import groovy.lang.Binding;
    import groovy.lang.GroovyShell;
    import groovy.lang.Script;
    public class TestService {
        public String testQuery(long id){
            return "Test query success, id is " + id;
        }
        public static void main(String[] args) {
            Binding groovyBinding = new Binding();
            groovyBinding.setVariable("testService", new TestService());
            GroovyShell groovyShell = new GroovyShell(groovyBinding);
            String scriptContent = "def query = testService.testQuery(2L);\n" +
                    "query";
            Script script = groovyShell.parse(scriptContent);
            System.out.println(script.run());
        }
    }
    //返回结果:
    //Test query success, id is 1
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20

    这种方式通过Binding对象的setVariable方法设置了预设对象testService,在动态脚本中便可以直接调用testService的方法。简单看下Binding类setVariable方法的源码:

    public void setVariable(String name, Object value) {
            if (this.variables == null) {
                this.variables = new LinkedHashMap();
            }
            this.variables.put(name, value);
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    实际上,Binding对象维护了一个Map类型的属性variables,通过setVariable方法将预设对象和预设对象名称存储到了variables属性中,动态运行时会尝试道variables中获取对应名称的对象,如果存在再尝试调用其方法。

    GroovyClassLoader

    Groovy官方提供GroovyClassLoader类,支持从文件、url或字符串中加载解析Groovy Class,实例化对象,反射调用指定方法。

    GroovyClassLoader groovyClassLoader = new GroovyClassLoader();
      String helloScript = "package com.vivo.groovy.util" +  // 可以是纯Java代码
              "class Hello {" +
                "String say(String name) {" +
                  "System.out.println(\"hello, \" + name)" +
                  " return name;"
                "}" +
              "}";
    Class helloClass = groovyClassLoader.parseClass(helloScript);
    GroovyObject object = (GroovyObject) helloClass.newInstance();
    Object ret = object.invokeMethod("say", "zhangshan"); 
    System.out.println(ret.toString()); 
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    到这里已经很清晰了,我们只要能获取spring容器中所有的bean,通过Binding的setVariable将spring所有的bean预设进GroovyShell运行环境,在动态脚本中便可以直接调用bean的方法。这种我们对spring项目中的service层、controller层、DAO层等注册的bean均可以通过这种方式实现动态调用。

    实践:Springboot接口动态运行Groovy脚本

    下面以一个springboot接口动态运行groovy脚本的示例工程为例,讲述如何在springboot接口中动态运行groovy脚本。

    引入groovy-all依赖

    <dependency>
        <groupId>org.codehaus.groovygroupId>
        <artifactId>groovy-allartifactId>
        <version>2.4.7version>
    dependency>
    
    • 1
    • 2
    • 3
    • 4
    • 5

    Service层示例类

    package pers.doublebin.example.groovy.script.service;
    import groovy.lang.Binding;
    import groovy.lang.GroovyShell;
    import groovy.lang.Script;
    import org.springframework.stereotype.Service;
    @Service
    public class TestService {
        public String testQuery(long id){
            return "Test query success, id is " + id;
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    TestService 类实现了一个简单的testQuery方法,springboot通过扫描到@Service注解会将生成TestService的bean并注册到应用上下文中,beanName为"testService".

    SpringBoot的Configuration类中设置Binding

    首先配置类可以实现org.springframework.context.ApplicationContextAware接口用来获取应用上下文,然后再配置类中通过应用上下文获取所有的bean并注册到groovy的Binding中,看源码:

    package pers.doublebin.example.groovy.script.config;
    import groovy.lang.Binding;
    import org.springframework.beans.BeansException;
    import org.springframework.context.ApplicationContext;
    import org.springframework.context.ApplicationContextAware;
    import org.springframework.context.annotation.Bean;
    import org.springframework.context.annotation.Configuration;
    import java.util.Map;
    @Configuration
    public class GroovyBindingConfig implements ApplicationContextAware {
        private ApplicationContext applicationContext;
        @Bean("groovyBinding")
        public Binding groovyBinding() {
            Binding groovyBinding = new Binding();
            Map<String, Object> beanMap = applicationContext.getBeansOfType(Object.class);
            //遍历设置所有bean,可以根据需求在循环中对bean做过滤
            for (String beanName : beanMap.keySet()) {
                groovyBinding.setVariable(beanName, beanMap.get(beanName));
            }
            return groovyBinding;
        }
        /*@Bean("groovyBinding1")
        public Binding groovyBinding1() {
            Map beanMap = applicationContext.getBeansOfType(Object.class);
            return new Binding(beanMap); //如果不需要对bean做过滤,直接用beanMap构造Binding对象即可
        }*/
        @Override
        public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
            this.applicationContext = applicationContext;
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31

    如果不需要对bean做过滤,可以通过注释掉的方法直接从应用上下文中获取beanMap并直接构造Binding的variables中;当然上面示例applicationContext.getBeansOfType方法也可以指定获取bean的类型。

    需要注意的是:上面这种方法注册的到binding中beanMap是不包含groovyBinding这个对象本身的(先后顺序的原因),如果需要将binding对象本身(也是一个bean)注册,也很简单,只需要将Binding的bean生成放在GroovyBindingConfig之前,并且在实现ApplicationContextAware接口的setApplicationContext方法中进行variables的设置即可,但建议不这样做,因为这样就可以通过脚本对Binding对象本身造成破坏,不太优雅~

    实现用于groovy动态脚本运行的controller

    package pers.doublebin.example.groovy.script.controller;
    import groovy.lang.Binding;
    import groovy.lang.GroovyClassLoader;
    import groovy.lang.GroovyShell;
    import groovy.lang.Script;
    import org.codehaus.groovy.control.CompilerConfiguration;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.web.bind.annotation.RequestBody;
    import org.springframework.web.bind.annotation.RequestMapping;
    import org.springframework.web.bind.annotation.RequestMethod;
    import org.springframework.web.bind.annotation.RestController;
    import pers.doublebin.example.groovy.script.component.TestScript;
    import javax.annotation.PostConstruct;
    @RestController
    @RequestMapping("/groovy/script")
    public class GroovyScriptController {
        @Autowired
        private Binding groovyBinding;
        private GroovyShell groovyShell;
        @PostConstruct
        public void init(){
            GroovyClassLoader groovyClassLoader = new GroovyClassLoader(this.getClass().getClassLoader());
            CompilerConfiguration compilerConfiguration = new CompilerConfiguration();
            compilerConfiguration.setSourceEncoding("utf-8");
            compilerConfiguration.setScriptBaseClass(TestScript.class.getName());
            groovyShell = new GroovyShell(groovyClassLoader, groovyBinding, compilerConfiguration);
        }
        @RequestMapping(value = "/execute", method = RequestMethod.POST)
        public String execute(@RequestBody String scriptContent) {
            Script script = groovyShell.parse(scriptContent);
            return String.valueOf(script.run());
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33

    将binding对象注入后,在初始化方法init()中用binding对象构造GroovyShell对象,在提供的execute接口实现中用GroovyShell对象生成Script脚本对象,并调用Script的run()方法运行动态脚本并返回结果。

    上述示例中只是一个简单实现,在接口方法execute中,每次脚本运行前都会通过groovyShell来parse出一个Script 对象,这其实是有成本的,实际应用中可根据脚本特征(如md5值等)将script存储, 下次运行时可根据脚本特征直接获取Script对象,避免parse的成本。

    实现用于Groovy动态脚本运行的Controller

    上述接口定义了一个post方法,path:/groovy/script/execute,运行后直接用postman调用测试testService的方法,结果如下:
    在这里插入图片描述
    显然,通过接口直接用groovy脚本调用了testService这个bean的方法,非常简单。

    以上就是我对微服务中使用低代码开发功能实现的尝试、思考和总结,希望对你有所启发,记得点赞、收藏、评论再走😄。

  • 相关阅读:
    TypeScript 实用工具类型
    使用docker安装ELK
    机器学习(三)——K最临近方法构建分类模型(matlab)
    fork函数,进程等待,进程终止,写时拷贝
    工具及方法 - TagSpaces
    计算机毕业论文java毕业设计成品源码网站strust2+hibernate网上银行系统[包运行成功]
    游游现在有a个 y ,b个 o ,c个 u ,他想用这些字母拼成一个字符串。
    Scrum Master的职责
    如何优化网站排名(百度SEO指南与优化布局方法)
    Spring Boot整合JWT实现用户认证
  • 原文地址:https://blog.csdn.net/weixin_40972073/article/details/125924341