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();
- 获取 JKScript 引擎 :
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 {
// 继续执行
}
-
设置表达式和上下文
:在
eval方法中,传入表达式和ScriptContext。 -
创建
Expression对象 :
Expression exp = new Expression(script, context);
-
解析表达式
:调用
Expression对象的parse方法。
this.parse();
-
获取操作数的值
:调用
getOperandValue方法。
double op1Value = getOperandValue(op1Sign, op1);
double op2Value = getOperandValue(op2Sign, op2);
- 执行运算 :根据运算符进行相应的运算。
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);
}
- 返回结果 :返回计算结果。
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();
- 获取 Groovy 脚本引擎 :
ScriptEngine engine = manager.getEngineByName("Groovy");
- 存储 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()
}
""";
- 执行脚本 :
engine.eval(script);
-
获取
Invocable对象 :
inv = (Invocable) engine;
- 启动 JavaFX 应用 :
public static void main(String[] args) {
launch(args);
}
- 调用脚本中的方法 :
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 中的脚本编程有所帮助,如果你有任何问题或建议,欢迎留言讨论。
超级会员免费看
302

被折叠的 条评论
为什么被折叠?



