54、Java脚本编程:从JKScript引擎到JavaFX中的Groovy应用

Java脚本编程:从JKScript引擎到JavaFX中的Groovy应用

1. Java中导入类

Groovy运行在JVM之上,因此可以像在Java类文件中一样,将标准库中的Java类导入到Groovy脚本中。对于项目中包含的库提供的类以及项目中定义的类,同样适用。示例代码如下:

// A class from the standard library
import java.text.SimpleDateFormat
// A class defined elsewhere in the project
import java17.script.SomeJavaClass
// Some library class. Must be inside the classpath.
import com.foo.superlib.Foo
def obj = new SomeJavaClass(8)
def sdf = new SimpleDateFormat("yyyy-MM-dd")
def foo = new Foo()

其他脚本语言可能有自己导入Java类的方式,具体可查阅其文档。

2. 实现脚本引擎

实现一个完整的脚本引擎并非易事,这里将实现一个简单的脚本引擎——JKScript引擎,用于计算算术表达式。其规则如下:
- 计算由两个操作数和一个运算符组成的算术表达式。
- 操作数可以是两个数字字面量、两个变量或一个数字字面量和一个变量,数字字面量必须为十进制格式,不支持十六进制、八进制和二进制数字字面量。
- 表达式中的算术运算仅限于加、减、乘、除。
- 识别 + - * / 作为算术运算符。
- 引擎将返回一个 Double 对象作为表达式的结果。
- 表达式中的操作数可以通过引擎的全局作用域或引擎作用域绑定传递给引擎。
- 允许从 String 对象和 java.io.Reader 对象执行脚本,但 Reader 对象的内容只能包含一个表达式。
- 不实现 Invocable Compilable 接口。

一些有效的表达式示例如下:
- 10 + 90
- 10.7 + 89.0
- +10 + +90
- num1 + num2
- num1 * num2
- 78.0 / 7.5

2.1 模块声明

脚本引擎使用服务提供者机制来发现,服务类型是 javax.script.ScriptEngineFactory 接口。需要将JKScript引擎打包到一个名为 jdojo.jkscript 的单独模块中,模块声明如下:

// module-info.java
module jdojo.jkscript {
    requires java.scripting;
    provides javax.script.ScriptEngineFactory
        with com.jdojo.jkscript.JKScriptEngineFactory;
}

该模块依赖 java.scripting 模块,并提供 javax.script.ScriptEngineFactory 服务接口的实现。

2.2 开发的类

为了实现JKScript脚本引擎,需要开发三个类,具体信息如下表所示:
| 类名 | 描述 |
| ---- | ---- |
| Expression | 脚本引擎的核心类,负责解析和计算算术表达式,在 JKScriptEngine 类的 eval() 方法中使用。 |
| JKScriptEngine | ScriptEngine 接口的实现类,继承自 AbstractScriptEngine 类,需要实现 Object eval(String, ScriptContext) Object eval(Reader, ScriptContext) 两个版本的 eval() 方法。 |
| JKScriptEngineFactory | ScriptEngineFactory 接口的实现类,是 javax.script.ScriptEngineFactory 服务接口的服务提供者。 |

2.2.1 Expression

Expression 类的主要逻辑是解析和计算算术表达式,代码如下:

// Expression.java
package com.jdojo.jkscript;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import javax.script.ScriptContext;
public class Expression {
    private String exp;
    private ScriptContext context;
    private String op1;
    private char op1Sign = '+';
    private String op2;
    private char op2Sign = '+';
    private char operation;
    private boolean parsed;
    public Expression(String exp, ScriptContext context) {
        if (exp == null || exp.trim().equals("")) {
            throw new IllegalArgumentException(
                this.getErrorString());
        }
        this.exp = exp.trim();
        if (context == null) {
            throw new IllegalArgumentException(
                "ScriptContext cannot be null.");
        }
        this.context = context;
    }
    public String getExpression() {
        return exp;
    }
    public ScriptContext getScriptContext() {
        return context;
    }
    public Double eval() {
        // Parse the expression
        if (!parsed) {
            this.parse();
            this.parsed = true;
        }
        // Extract the values for the operand
        double op1Value = getOperandValue(op1Sign, op1);
        double op2Value = getOperandValue(op2Sign, op2);
        // Evaluate the expression
        Double result = null;
        switch (operation) {
            case '+':
                result = op1Value + op2Value;
                break;
            case '-':
                result = op1Value - op2Value;
                break;
            case '*':
                result = op1Value * op2Value;
                break;
            case '/':
                result = op1Value / op2Value;
                break;
            default:
                throw new RuntimeException(
                    "Invalid operation:" + operation);
        }
        return result;
    }
    private double
    getOperandValue(char sign, String operand) {
        // Check if operand is a double
        double value;
        try {
            value = Double.parseDouble(operand);
            return sign == '-' ? -value : value;
        } catch (NumberFormatException e) {
            // Ignore it. Operand is not in a format that
            // can be converted to a double value.
        }
        // Check if operand is a bind variable
        Object bindValue = context.getAttribute(operand);
        if (bindValue == null) {
            throw new RuntimeException(operand +
                " is not found in the script context.");
        }
        if (bindValue instanceof Number) {
            value = ((Number) bindValue).doubleValue();
            return sign == '-' ? -value : value;
        } else {
            throw new RuntimeException(operand +
                " must be bound to a number.");
        }
    }
    public void parse() {
        // Supported expressions are of the form v1 op v2,
        // where v1 and v2 are variable names or numbers,
        // and op could be +, -, *, or /
        // Prepare the pattern for the expected expression
        String operandSignPattern = "([+-]?)";
        String operandPattern = "([\\p{Alnum}\\p{Sc}_.]+)";
        String whileSpacePattern = "([\\s]*)";
        String operationPattern = "([+*/-])";
        String pattern = "^" + operandSignPattern
                + operandPattern
                + whileSpacePattern + operationPattern
                + whileSpacePattern
                + operandSignPattern + operandPattern
                + "$";
        Pattern p = Pattern.compile(pattern);
        Matcher m = p.matcher(exp);
        if (!m.matches()) {
            // The expression is not in the expected format
            throw new IllegalArgumentException(
                this.getErrorString());
        }
        // Get operand-1
        String temp = m.group(1);
        if (temp != null && !temp.equals("")) {
            this.op1Sign = temp.charAt(0);
        }
        this.op1 = m.group(2);
        // Get operation
        temp = m.group(4);
        if (temp != null && !temp.equals("")) {
            this.operation = temp.charAt(0);
        }
        // Get operand-2
        temp = m.group(6);
        if (temp != null && !temp.equals("")) {
            this.op2Sign = temp.charAt(0);
        }
        this.op2 = m.group(7);
    }
    private String getErrorString() {
        return "Invalid expression[" + exp + "]"
                + "\nSupported expression syntax is: "
                + "op1 operation op2"
                + "\n where op1 and op2 can be a number "
                + " or a bind variable"
                + " , and operation can be"
                + " +, -, *, and /.";
    }
    @Override
    public String toString() {
        return "Expression: " + this.exp + ", op1 Sign = "
                + op1Sign + ", op1 = " + op1
                + ", op2 Sign = " + op2Sign
                + ", op2 = " + op2
                + ", operation = " + operation;
    }
}

该类的主要组件包括:
- 实例变量 exp context 分别表示要计算的表达式和 ScriptContext
- 实例变量 op1 op2 表示表达式中的第一个和第二个操作数, op1Sign op2Sign 表示操作数的符号。
- 实例变量 operation 表示要对操作数执行的算术运算。
- 实例变量 parsed 用于跟踪表达式是否已被解析。

构造函数接受表达式和 ScriptContext ,并确保它们不为空。 parse() 方法使用正则表达式将表达式解析为操作数和运算符, eval() 方法计算表达式的值。

2.2.2 JKScriptEngine

JKScriptEngine 类是 ScriptEngine 接口的实现类,代码如下:

// JKScriptEngine.java
package com.jdojo.jkscript;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.Reader;
import javax.script.AbstractScriptEngine;
import javax.script.Bindings;
import javax.script.ScriptContext;
import javax.script.ScriptEngineFactory;
import javax.script.ScriptException;
import javax.script.SimpleBindings;
public class JKScriptEngine extends AbstractScriptEngine {
    private final ScriptEngineFactory factory;
    public JKScriptEngine(ScriptEngineFactory factory) {
        this.factory = factory;
    }
    @Override
    public Object
    eval(String script, ScriptContext context)
    throws ScriptException {
        try {
            Expression exp =
                new Expression(script, context);
            Object result = exp.eval();
            return result;
        } catch (Exception e) {
            throw new ScriptException(e.getMessage());
        }
    }
    @Override
    public Object
    eval(Reader reader, ScriptContext context)
    throws ScriptException {
        // Read all lines from the Reader
        BufferedReader br = new BufferedReader(reader);
        String script = "";
        try {
            String str;
            while ((str = br.readLine()) != null) {
                script = script + str;
            }
        } catch (IOException e) {
            throw new ScriptException(e);
        }
        // Use the String version of eval()
        return eval(script, context);
    }
    @Override
    public Bindings createBindings() {
        return new SimpleBindings();
    }
    @Override
    public ScriptEngineFactory getFactory() {
        return factory;
    }
}

eval(String, ScriptContext) 方法创建 Expression 对象并调用其 eval() 方法计算表达式的值, eval(Reader, ScriptContext) 方法将 Reader 中的内容读取并拼接成字符串,然后调用 eval(String, ScriptContext) 方法。

2.2.3 JKScriptEngineFactory

JKScriptEngineFactory 类是 ScriptEngineFactory 接口的实现类,代码如下:

// JKScriptEngineFactory.java
package com.jdojo.jkscript;
import java.util.List;
import javax.script.ScriptEngine;
import javax.script.ScriptEngineFactory;
public class JKScriptEngineFactory
        implements ScriptEngineFactory {
    @Override
    public String getEngineName() {
        return "JKScript Engine";
    }
    @Override
    public String getEngineVersion() {
        return "1.0";
    }
    @Override
    public List<String> getExtensions() {
        return List.of("jks");
    }
    @Override
    public List<String> getMimeTypes() {
        return List.of("text/jkscript");
    }
    @Override
    public List<String> getNames() {
        return List.of("jks", "JKScript", "jkscript");
    }
    @Override
    public String getLanguageName() {
        return "JKScript";
    }
    @Override
    public String getLanguageVersion() {
        return "1.0";
    }
    @Override
    public Object getParameter(String key) {
        switch (key) {
            case ScriptEngine.ENGINE:
                return getEngineName();
            case ScriptEngine.ENGINE_VERSION:
                return getEngineVersion();
            case ScriptEngine.NAME:
                return getEngineName();
            case ScriptEngine.LANGUAGE:
                return getLanguageName();
            case ScriptEngine.LANGUAGE_VERSION:
                return getLanguageVersion();
            case "THREADING":
                return "MULTITHREADED";
            default:
                return null;
        }
    }
    @Override
    public String
    getMethodCallSyntax(String obj, String m, String[] p) {
        return "Not implemented";
    }
    @Override
    public String
    getOutputStatement(String toDisplay) {
        return "Not implemented";
    }
    @Override
    public String
    getProgram(String[] statements) {
        return "Not implemented";
    }
    @Override
    public ScriptEngine
    getScriptEngine() {
        return new JKScriptEngine(this);
    }
}

该类的一些方法返回 "Not Implemented" 字符串,因为不支持这些方法暴露的功能。可以使用 ScriptEngineManager 通过 jks JKScript jkscript 名称获取JKScript引擎的实例。

2.3 打包JKScript文件

要让其他人使用JKScript引擎,只需提供 jdojo.jkscript 模块的模块化JAR文件。

2.4 使用JKScript脚本引擎

使用JKScript脚本引擎的步骤如下:
1. 将 jdojo.jkscript.jar 添加到应用程序的模块路径中。
2. 使用 ScriptEngineManager 获取JKScript引擎的实例。示例代码如下:

// Create the JKScript engine
ScriptEngineManager manager = new ScriptEngineManager();
ScriptEngine engine = manager.getEngineByName("JKScript");
if (engine == null) {
    System.out.println(
        "JKScript engine is not available. ");
    System.out.println(
        "Add jkscript.jar to CLASSPATH.");
} else {
    // Evaluate your JKScript
}

以下是一个使用JKScript脚本引擎计算不同类型表达式的示例程序:

// JKScriptTest.java
package com.jdojo.script;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.Reader;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import javax.script.ScriptEngine;
import javax.script.ScriptEngineManager;
import javax.script.ScriptException;
public class JKScriptTest {
    public static void
    main(String[] args)
    throws FileNotFoundException, IOException {
        // Create JKScript engine
        ScriptEngineManager manager =
            new ScriptEngineManager();
        ScriptEngine engine = manager.getEngineByName(
            "JKScript");
        if (engine == null) {
            System.out.println(
                "JKScript engine is not available. ");
            System.out.println(
                "Add jkscript.jar to CLASSPATH.");
            return;
        }
        // Test scripts as String
        testString(manager, engine);
        // Test scripts as a Reader
        testReader(manager, engine);
    }
    public static void
    testString(ScriptEngineManager manager,
            ScriptEngine engine) {
        try {
            // Use simple expressions with numeric literals
            String script = "12.8 + 15.2";
            Object result = engine.eval(script);
            System.out.println(script + " = " + result);
            script = "-90.0 - -10.5";
            result = engine.eval(script);
            System.out.println(script + " = " + result);
            script = "5 * 12";
            result = engine.eval(script);
            System.out.println(script + " = " + result);
            script = "56.0 / -7.0";
            result = engine.eval(script);
            System.out.println(script + " = " + result);
            // Use global scope bindings variables
            manager.put("num1", 10.0);
            manager.put("num2", 20.0);
            script = "num1 + num2";
            result = engine.eval(script);
            System.out.println(script + " = " + result);
            // Use global and engine scopes bindings.
            // num1 from engine scope and num2 from
            // global scope will be used.
            engine.put("num1", 70.0);
            script = "num1 + num2";
            result = engine.eval(script);
            System.out.println(script + " = " + result);
            // Try mixture of number literal and bindings.
            // num1 from the engine scope bindings will be
            // used
            script = "10 + num1";
            result = engine.eval(script);
            System.out.println(script + " = " + result);
        } catch (ScriptException e) {
            e.printStackTrace();
        }
    }
    public static void
    testReader(ScriptEngineManager manager,
            ScriptEngine engine) {
        try {
            Path scriptPath = Paths.get("jkscript.txt").
                toAbsolutePath();
            if (!Files.exists(scriptPath)) {
                System.out.println(scriptPath +
                    " script file does not exist.");
                return;
            }
            try (Reader reader = Files.
                    newBufferedReader(scriptPath);) {
                Object result = engine.eval(reader);
                System.out.println("Result of " +
                    scriptPath + " = " + result);
            }
        } catch (ScriptException | IOException e) {
            e.printStackTrace();
        }
    }
}

该程序使用JKScript引擎计算不同类型的表达式,包括数字字面量和绑定变量的表达式。

3. JavaFX中的Groovy应用

可以使用脚本加速JavaFX开发,将Java代码和脚本混合使用有助于分离前端和后端逻辑,并且由于脚本比Java代码更简洁,可以节省一些开发时间。以下是一个简单的HelloWorld风格的JavaFX应用程序示例:

package com.jdojo.groovyfx;
import javax.script.Invocable;
import javax.script.ScriptEngine;
import javax.script.ScriptEngineManager;
import javax.script.ScriptException;
import javafx.application.Application;
import javafx.stage.Stage;
public class HelloGroovyFX extends Application {
    private Invocable inv;
    public static void main(String[] args) {
        launch(args);
    }
    @Override
    public void init() {
        // Create a script engine manager
        ScriptEngineManager manager =
            new ScriptEngineManager();
        // Obtain a Groovy script engine from the manager
        ScriptEngine engine =
            manager.getEngineByName("Groovy");
        // Store the Groovy script in a String
        String script = """
import javafx.scene.Scene
import javafx.scene.control.Button
import javafx.scene.layout.StackPane
import javafx.beans.property.SimpleStringProperty as SP
def go(def primaryStage) {
  primaryStage.setTitle "Hello World!"
  Button btn = new Button()
  btn.text = "Say 'Hello World'"
  btn.onAction = { def event ->
      println("Hello World!")
  }
  StackPane root = new StackPane()
  root.children.add(btn)
  primaryStage.scene = new Scene(root, 300, 250)
  primaryStage.show()
}
            """;
        try {
            // Execute the script
            engine.eval(script);
            inv = (Invocable) engine;
        } catch (ScriptException e) {
            e.printStackTrace();
        }
    }
    @Override
    public void start(Stage primaryStage) {
        try {
            inv.invokeFunction("go", primaryStage);
        } catch (Exception e) {
            e.printStackTrace();
        }
   }
}

要使该应用程序正常工作,需要添加JavaFX库。对于Maven项目,可以在 pom.xml 文件的 <dependencies> 部分添加以下依赖:

<dependency>
  <groupId>org.openjfx</groupId>
  <artifactId>javafx-base</artifactId>
  <version>16</version>
</dependency>
<dependency>
  <groupId>org.openjfx</groupId>
  <artifactId>javafx-graphics</artifactId>
  <version>16</version>
</dependency>
<dependency>
  <groupId>org.openjfx</groupId>
  <artifactId>javafx-controls</artifactId>
  <version>16</version>
</dependency>
<dependency>
  <groupId>org.openjfx</groupId>
  <artifactId>javafx-web</artifactId>
  <version>16</version>
</dependency>

Groovy版本的前端代码比Java更简单,例如可以使用属性调用Java类的方法,添加按钮的事件处理程序也更方便。示例代码如下:

btn.text = "Say 'Hello World'"
btn.onAction = { def event ->
    println("Hello World!")
}

综上所述,通过实现JKScript引擎和在JavaFX中使用Groovy脚本,可以提高Java编程的效率和灵活性。在实际开发中,可以根据具体需求选择合适的脚本引擎和脚本语言,以满足不同的业务需求。

4. 代码执行流程分析

4.1 JKScript 引擎执行流程

下面是 JKScript 引擎执行算术表达式计算的流程图:

graph TD;
    A[创建 ScriptEngineManager] --> B[获取 JKScript 引擎];
    B --> C{引擎是否可用};
    C -- 是 --> D[设置表达式和上下文];
    C -- 否 --> E[提示添加 jkscript.jar 到 CLASSPATH];
    D --> F[创建 Expression 对象];
    F --> G[解析表达式];
    G --> H[获取操作数的值];
    H --> I[执行运算];
    I --> J[返回结果];

具体步骤如下:
1. 创建 ScriptEngineManager

ScriptEngineManager manager = new ScriptEngineManager();
  1. 获取 JKScript 引擎
ScriptEngine engine = manager.getEngineByName("JKScript");
  1. 检查引擎是否可用
if (engine == null) {
    System.out.println("JKScript engine is not available. ");
    System.out.println("Add jkscript.jar to CLASSPATH.");
} else {
    // 继续执行
}
  1. 设置表达式和上下文 :在 eval 方法中,传入表达式和 ScriptContext
  2. 创建 Expression 对象
Expression exp = new Expression(script, context);
  1. 解析表达式 :调用 Expression 对象的 parse 方法。
this.parse();
  1. 获取操作数的值 :调用 getOperandValue 方法。
double op1Value = getOperandValue(op1Sign, op1);
double op2Value = getOperandValue(op2Sign, op2);
  1. 执行运算 :根据运算符进行相应的运算。
switch (operation) {
    case '+':
        result = op1Value + op2Value;
        break;
    case '-':
        result = op1Value - op2Value;
        break;
    case '*':
        result = op1Value * op2Value;
        break;
    case '/':
        result = op1Value / op2Value;
        break;
    default:
        throw new RuntimeException("Invalid operation:" + operation);
}
  1. 返回结果 :返回计算结果。

4.2 JavaFX 中 Groovy 脚本执行流程

下面是 JavaFX 中 Groovy 脚本执行的流程图:

graph TD;
    A[创建 ScriptEngineManager] --> B[获取 Groovy 脚本引擎];
    B --> C[存储 Groovy 脚本];
    C --> D[执行脚本];
    D --> E[获取 Invocable 对象];
    E --> F[启动 JavaFX 应用];
    F --> G[调用脚本中的方法];

具体步骤如下:
1. 创建 ScriptEngineManager

ScriptEngineManager manager = new ScriptEngineManager();
  1. 获取 Groovy 脚本引擎
ScriptEngine engine = manager.getEngineByName("Groovy");
  1. 存储 Groovy 脚本 :将 Groovy 脚本存储在字符串中。
String script = """
import javafx.scene.Scene
import javafx.scene.control.Button
import javafx.scene.layout.StackPane
import javafx.beans.property.SimpleStringProperty as SP
def go(def primaryStage) {
  primaryStage.setTitle "Hello World!"
  Button btn = new Button()
  btn.text = "Say 'Hello World'"
  btn.onAction = { def event ->
      println("Hello World!")
  }
  StackPane root = new StackPane()
  root.children.add(btn)
  primaryStage.scene = new Scene(root, 300, 250)
  primaryStage.show()
}
""";
  1. 执行脚本
engine.eval(script);
  1. 获取 Invocable 对象
inv = (Invocable) engine;
  1. 启动 JavaFX 应用
public static void main(String[] args) {
    launch(args);
}
  1. 调用脚本中的方法
inv.invokeFunction("go", primaryStage);

5. 注意事项和扩展建议

5.1 JKScript 引擎注意事项

  • 操作数格式 :JKScript 引擎目前只支持十进制数字字面量,不支持十六进制、八进制和二进制数字字面量。如果需要支持这些格式,可以扩展 getOperandValue 方法。
  • 异常处理 :在使用 JKScript 引擎时,要注意处理可能出现的异常,如 ScriptException RuntimeException 等。

5.2 JavaFX 中 Groovy 应用注意事项

  • 库依赖 :使用 JavaFX 中的 Groovy 脚本时,要确保添加了 JavaFX 库的依赖,否则会出现运行时错误。
  • 线程安全 :在 JavaFX 应用中,要注意线程安全问题,避免在非 JavaFX 应用线程中更新 UI。

5.3 扩展建议

  • JKScript 引擎扩展 :可以扩展 JKScript 引擎,支持更多的运算符和函数,如幂运算、三角函数等。
  • JavaFX 中 Groovy 应用扩展 :可以将 Groovy 脚本和 JavaFX 的 FXML 结合使用,进一步分离前端和后端逻辑。

6. 总结

本文介绍了 Java 中的脚本编程,包括在 Groovy 中导入 Java 类、实现 JKScript 脚本引擎以及在 JavaFX 中使用 Groovy 脚本。通过实现 JKScript 引擎,我们可以自定义算术表达式的计算规则;在 JavaFX 中使用 Groovy 脚本,可以加速开发过程,分离前端和后端逻辑。

在实际开发中,可以根据具体需求选择合适的脚本引擎和脚本语言,以提高开发效率和代码的灵活性。同时,要注意处理可能出现的异常和依赖问题,确保程序的稳定性。

希望本文对你理解 Java 中的脚本编程有所帮助,如果你有任何问题或建议,欢迎留言讨论。

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符  | 博主筛选后可见
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值