GraalVM安装配置与简单使用

说明

JDK自带一个Nashorn脚本引擎来执行JS,但随着时间的推移,也逐渐暴露出不足:

  • 支持的EMACScript版本过低?。Nashorn支持“ECMAScript -262 Edition 5.1”,而非更流行更强大的ES6,也即ECMAScript 2015,更不用说更新的版本了,很多新特性无法使用。对现在更熟悉ES6标准的JS开发者来说也造成了麻烦。
  • ?Oracle官方不再支持Nashorn。Oracle官方宣布Nashorn将在JDK11弃用?,现在JDK17已经正式移除了Nashorn。

其替代品就是GraalVM?。

本文?内容主要描述了:

  1. 如何将GraalVM部署在服务器(Docker方式),运行JS、Java程序(以JAR的方式)、
  2. 作为开发人员,如何在本地基于GRAALVM进行开发。

本地开发

笔者使用的Mac系统,Eclipse?。

下载GraalVM

GraalVM?社区版下载地址:

https://github.com/graalvm/graalvm-ce-builds/releases

选择基于java17的最新版22.0.02。“
graalvm-ce-java17-darwin-amd64-22.0.0.2.tar.gz”。下载并解压缩。

其包含JVM,GraalVM编译器,JavaScript Runtime,LLVM Runtime。

版本如下:

% java -version
openjdk version "17.0.2" 2022-01-18
OpenJDK Runtime Environment GraalVM CE 22.0.0.2 (build 17.0.2+8-jvmci-22.0-b05)
OpenJDK 64-Bit Server VM GraalVM CE 22.0.0.2 (build 17.0.2+8-jvmci-22.0-b05, mixed mode, sharing)
% js -version
GraalVM JavaScript (GraalVM CE Native 22.0.0.2)
% lli --version
LLVM 12.0.1 (GraalVM CE Native 22.0.0.2)

设置环境变量

设置GraalVM的环境变量与JVM是一样的,只是将JAVA_HOME指向GraalVM所在目录即可。

笔者设置的是~/.zshrc文件,也有设置在.bashrc的。

#这是原JDK8的目录
#JAVA_HOME=/Library/Java/JavaVirtualMachines/jdk1.8.0_311.jdk/Contents/Home
#现在指向了GraalVM的目录
JAVA_HOME=/绝对路径/graalvm-ce-java17-22.0.0.2/Contents/Home
PATH=$JAVA_HOME/bin:$PATH:.
CLASSPATH=$JAVA_HOME/lib/tools.jar:$JAVA_HOME/lib/dt.jar:.
export JAVA_HOME
export CLASSPATH

修改后【source ~/.zshrc】即可生效。在终端命令行处输入上一节的三条命令,就可以看到相关的版本号,证明环境变量设置成功。

设置开发环境

Eclipse按照GraalVM

【Preferences】-【Java 】-【Installed JREs】-【Add】

选择GraalVM目录,并命名:

在要使用GraalVM的项目中指定GraalVM:

【项目右键】-【Build Path】-【Configure Build Path】

在原JDK处选择GraalVM:

设置工程编译版本

在工程的【pom.xml】文件中,一般要指定编译的版本号,通常是:


  org.apache.maven.plugins
  maven-compiler-plugin
  3.10.1
  
    1.8
    1.8
    UTF-8
  

在我们使用的GraalVM中,内置的JDK是17,此处也要修改如下:


  org.apache.maven.plugins
  maven-compiler-plugin
  3.10.1
  
    17
    17
    UTF-8
  

完整的pom.xml文件,会在后文提供。

设置Maven编译版本

通常都是JDK8,现在也要做更改。

打开Maven的settings.xml,修改如下:


  jdk-17
  
   true
   17
  
  
   17
   17
   17
  

至此,开发人员本机的系统环境、开发环境、Maven编译环境设置完毕。

代码

本节内容主要介绍如何在GraalVM进行开发、打包,涉及JS、Java代码的运行,相互调用等。

之前基于Nashorn进行开发的代码,都是使用“javax.script.ScriptEngine”,GraalVM也是兼容的,但是官方明确指出这种兼容只是出于遗留原因,官方强烈鼓励使用“
org.graalvm.polyglot.Context”,所以本文的代码采用官方推荐的Context进行编码,而非ScriptEngine。

创建一个简单的Maven工程,pom.xml如下:


  4.0.0
  org.leo
  testgraalvm
  0.0.1-SNAPSHOT
  jar
?
?
  
    UTF-8
    UTF-8
  
  
     
    
    
      commons-io
      commons-io
      2.11.0
    
    
    
      com.google.code.gson
      gson
      2.9.0
    
?
    
    
      com.google.guava
      guava
      31.1-jre
    
   
?
  
    testgraalvm 
    
      
      
        org.apache.maven.plugins
        maven-compiler-plugin
        3.10.1
        
          17
          17
          UTF-8
        
      
?
      
      
        org.apache.maven.plugins
        maven-dependency-plugin
        3.3.0
        
          
            copy-dependencies
            package
            
              copy-dependencies
            
            
              ${project.build.directory}/lib
              false
              false
              true
            
          
        
      
?
      
        org.apache.maven.plugins
        maven-assembly-plugin
        3.3.0
        
          
            
              
            
          
          
          
            jar-with-dependencies
          
        
        
          
            make-assembly 
            package 
            
              single
            
          
        
      
    
  

相关Java类:

User类,目的是Java与JS交互传递实体类数据用。

注意:通常的写法,声明变量用的是“private”,但是在此处必须用“public”,原因后述。

public class User implements Serializable {
  private static final long serialVersionUID = 7439853194483038885L;
  public String name;//必须用public
  public Integer age;

  public User() {
    super();
  }

  public User(String name, Integer age) {
    super();
    this.name = name;
    this.age = age;
  }

  //省略GET、SET
  @Override
  public String toString() {
    return "User [name=" + name + ", age=" + age + "]";
  }

}

TestUtil类,JS调用Java方法用。

public class TestUtil {
  public static Integer add(Integer a,Integer b) {
    return a+b;
  }
}

JS文件,Java调用JS用。

// JS使用JAVA方法
var ju=Java.type('TestUtil');
function test3(a,b){
 return ju.add(a,b);
}
// JS将传入的参数整合为JSON,并返回
function test4(name,age){
 var json={};
 json.name=name;
 json.age=age;
 return json;
}

TestGraalVM类。

import java.io.File;
import java.io.IOException;
import java.util.List;
import java.util.Map;

import org.apache.commons.io.FileUtils;
import org.graalvm.polyglot.Context;
import org.graalvm.polyglot.Source;
import org.graalvm.polyglot.TypeLiteral;
import org.graalvm.polyglot.Value;

import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.google.gson.reflect.TypeToken;

public class TestGraalVM {

  public static void main(String[] args) throws IOException {
    Gson gson = new GsonBuilder().create();
    // 必须设置allowAllAccess(true),否则JS无法使用JAVA方法
    Context context = Context.newBuilder().allowAllAccess(true).build();

    // 获取JS数组的某个值
    Value array = context.eval("js", "[1,2,42,4]");
    int result = array.getArrayElement(2).asInt();
    System.out.println("JS数组第三个数值:" + result);

    // 执行JS Function 无返回结果
    System.out.println("=======执行JS Function 无返回结果=======");
    String jsFunWithoutResult = "function test(a,b,c){var result= a+b+c;console.log('JS执行结果:'+result);}";
    context.eval(Source.create("js", jsFunWithoutResult));
    context.getBindings("js").getMember("test").execute(1, 2, 3);

    System.out.println("======执行JS内置函数 有返回结果=====");
    Value vMath = context.getBindings("js");
    vMath.putMember("a", 1);
    vMath.putMember("b", 2);
    System.out.println("执行JS内置函数:" + context.eval(Source.create("js", "Math.max(a,b)")).asInt());

    // 执行JS Function,并返回结果
    System.out.println("=======执行JS Function 有返回结果=======");
    String jsFun = "function add(x,y){var result=x+y;console.log('JS返回结果:'+result);return result;}";
    context.eval(Source.create("js", jsFun));
    Long re = context.getBindings("js").getMember("add").execute(10, 20).asLong();
    System.out.println("执行JS Function,并返回结果: " + re);

    // 获取JS定义的对象、数组。
    String jsObj = "var intarr=[1,2,3];var msg = 'hello';var json={'name':'张三','age':18};var users=[{'name':'张三','age':18},{'name':'李四','age':22}]";
    context.eval("js", jsObj);
    System.out.println("======Int数组=====");
    Value v = context.getBindings("js").getMember("intarr");
    TypeLiteral> INT_LIST = new TypeLiteral>() {
    };
    List ll = v.as(INT_LIST);
    for (Integer i : ll) {
      System.out.println("JS定义的INT数组:" + i);
    }

    System.out.println("JS定义的字符串:" + context.getBindings("js").getMember("msg").asString());

    System.out.println("=======JS定义的 JSON 实体类========");
    User userPojo = gson.fromJson(gson.toJson(context.getBindings("js").getMember("json").as(Map.class)),
        new TypeToken() {
        }.getType());
    System.out.println(userPojo.toString());

    System.out.println("======JS定义的 JSON 实体类列表=====");
    // JS的JSONArr转为JAVA实体类的写法一
    Value uv = context.getBindings("js").getMember("users");
    System.out.println("列表总数:" + uv.getArraySize());

    for (int i = 0; i < uv.getArraySize(); i++) {
      System.out.println(new User(uv.getArrayElement(i).getMember("name").asString(),
          uv.getArrayElement(i).getMember("age").asInt()).toString());
    }
    // JS的JSONArr转为JAVA实体类的写法二
    System.out.println("--------------------");
    String jsonList = gson.toJson(context.getBindings("js").getMember("users").as(List.class));
    List us = gson.fromJson(jsonList, new TypeToken>() {
    }.getType());
    for (User user : us) {
      System.out.println(user.toString());
    }

    // GraalVM官方是很不推荐使用JS访问Java类或文件系统的,所以默认采用了安全的方法
    // 但是在我们之前使用Nashorn的时候,因为其支持的ECMAScript版本较低,很多基本特性无法使用,例如没有Map、Set等。为了适应业务需要,有大量JS调用Java对象的代码,此处也写了相关的Demo。
    // 不过还是不推荐这么做,GraalVM已经支持ES6,相关特性应该足以替代Java对象。
    // 即便是JS做不到的功能,也推荐采用HTTP接口的方式进行调用。
    System.out.println("=======JS Function 执行JAVA类方法=======");
    String myJavaFun = "var ju=Java.type('TestUtil');function test2(a,b){return ju.add(a,b)}";
    context.eval(Source.create("js", myJavaFun));
    Integer i2 = context.getBindings("js").getMember("test2").execute(1, 2).asInt();
    System.out.println(i2);

    System.out.println("========JS 接收对象============");
    User param = new User("姓名", 44);
    //User类的属性在这里必须设置成public,只有这样,JS里面才能通过user.name的形式获取到值
    String jsPojoParam = "function test5(user){console.log(user);console.log(user.name);console.log(user.age)}";
    context.eval(Source.create("js", jsPojoParam));
    context.getBindings("js").getMember("test5").execute(param);

    System.out.println("=========JS文件===========");
    //注意此处test.js的地址,在本机或服务器运行时,要根据实际路径进行修改
    String jsFile = FileUtils.readFileToString(new File("test.js"), "utf-8");
    context.eval(Source.create("js", jsFile));
    Integer i3 = context.getBindings("js").getMember("test3").execute(1, 2).asInt();
    System.out.println("执行Function:" + i3);
    User user2 = gson.fromJson(
        gson.toJson(context.getBindings("js").getMember("test4").execute("张三", 66).as(Map.class)),
        new TypeToken() {
        }.getType());
    System.out.println("获取返回JSON:" + user2.toString());

    System.out.println("=========ES 6===========");
    context.eval(Source.create("js",
        "var map=new Map();map.set('name','张三');map.set('age',55);map.set('name','李四');for(var [k,v] of map){console.log(k+'='+v)}"));
    context.eval(Source.create("js",
        "let arr=['苹果','桔子','梨'];console.log(...arr);let breakfast=arr.map(fruit => {return '吃'+fruit;});console.log(breakfast);"));
  }

}

使用Maven打包后生成的【
testgraalvm-jar-with-dependencies.jar】就是包含了所有第三方JAR,可以直接运行的JAR包。

服务器运行

本文的运行环境基于Docker,仅做演示用,如果应用到实际生产环境,还需专业的运维人员进行处理。

本文不描写如何在系统中安装Docker环境,笔者使用的是Docker Desktop。

Image下载

Docker Hub里没有GraalVM的Official Image,社区版放在了
https://github.com/graalvm/container。

?拉取:

docker pull ghcr.io/graalvm/graalvm-ce:latest

Container配置

?运行:

-- 容器命名为 mygraalvm
docker run --name mygraalvm -it ghcr.io/graalvm/graalvm-ce:latest bash

启动&停止:

docker start mygraalvm

docker stop mygraalvm

复制之前编译好的JAR包,?JS文件到Docker:

-- 复制到Docker根目录下,改名为tg.jar
docker cp /绝对地址/testgraalvm-jar-with-dependencies.jar 容器ID或name:/tg.jar
docker cp /绝对地址/test.js 容器ID或name:/test.js

在运行的容器中执行命令:

docker exec -it mygraalvm bash

此时执行ls,应该能看到前面上传的tg.jar。

修改Docker时区、中文字符集:

在本文中,仅是临时修改,供开发人员暂时测试使用。

-- 进入容器后执行
export TZ=CST-8
export LANG=C.UTF-8

JAR运行

执行?JAR:

-- 前文的代码,没有设置包名
java -cp tg.jar 实际包名.TestGraalVM

运行结果?:


原文链接:,转发请注明来源!