GraalVM基础教程

2,463 阅读6分钟

GraalVM基础教程

前置知识什么是GraalVM?或访问链接:juejin.cn/post/716328…

1、安装与配置

熟悉安装JDK环境与配置的同学,安装GraalVM过程和jdk类似,因为GraalVM本身就是基于JDK对应的版本的,所有我们可以暂时理解就是安装一个多功能的JDK。废话不多说,开搞。

我这里示例是mac电脑,windows系统及Linux系统请参考安装教程

1.1、下载安装包

下载地址,找到自己使用的jdk版本对应的整合版本下载,根据自己的操作系统及系统架构下载对应的版本,如下图:

image-20221106213240512

我这里下载的是macOS arm64位的graalvm-ce-java11-darwin-aarch64-22.3.0.tar.gz。

linux或者mac系统如果不知道,可以通过如下命令查看

uname -a

image-20221106214123095

image-20221106213519668

1.2、解压安装包

  • 通过如下命令进行解压
tar -xvzf graalvm-ce-java11-darwin-aarch64-22.3.0.tar.gz
  • 将解压后的文件目录移到java虚拟机目录,mac的java虚拟机目录为/Library/Java/JavaVirtualMachines/如果不知道的可以通过如下命令查看。

查看本机安装的jdk列表

/usr/libexec/java_home -V

image-20221106214854266

移动解压后的GraalVM目录到java虚拟机安装目录

sudo mv graalvm-ce-java11-22.3.0 /Library/Java/JavaVirtualMachines/
  • 配置环境变量

对于多JDK环境可以在~/.bash_profile文件中进行配置,这里可以参考如下配置通过别名进行快速切换。参考文档

### JDK definition
JAVA_08=$(/usr/libexec/java_home -v 1.8)
JAVA_11=$(/usr/libexec/java_home -v 11)
JAVA_17=$(/usr/libexec/java_home -v 17)
JAVA_19=$(/usr/libexec/java_home -v 19)
GRAALVM_11="/Library/Java/JavaVirtualMachines/graalvm-ce-java11-22.3.0/Contents/Home"
THE_F=~/.bash_profile

### alias
alias java8='sed -i "" "/export JAVA_HOME=\//d" $THE_F && echo "export JAVA_HOME=$JAVA_08" >> $THE_F && source $THE_F && echo "switch to Java8"'
alias java11='sed -i "" "/export JAVA_HOME=\//d" $THE_F && echo "export JAVA_HOME=$JAVA_11" >> $THE_F && source $THE_F && echo "switch to Java11"'
alias java17='sed -i "" "/export JAVA_HOME=\//d" $THE_F && echo "export JAVA_HOME=$JAVA_17" >> $THE_F && source $THE_F && echo "switch to Java17"'
alias java19='sed -i "" "/export JAVA_HOME=\//d" $THE_F && echo "export JAVA_HOME=$JAVA_19" >> $THE_F && source $THE_F && echo "switch to Java19"'
alias graalvm='sed -i "" "/export JAVA_HOME=\//d" $THE_F && echo "export JAVA_HOME=$GRAALVM_11" >> $THE_F && source $THE_F && echo "switch to Graalvm"'

修改后保存并推出,然后进行执行刷新命令就可以进行切换了

source ~/.bash_profile && graalvm
  • 验证

查看jdk版本,可以看出配置生效了。

java -version

image-20221106222119637

2、实战

2.1、IDEA运行项目

  • 创建项目

我们在IDEA中创建一个springboot 的graalvm-demo项目,sdk选择我们上面配置的graalvm,先验证下,配置是否生效,项目能否跑起来。

image-20221111144929296

  • 写一个测试接口
@RestController
@RequestMapping
public class HelloController {
    @RequestMapping("/hello")
    public JSONObject hello() {
        JSONObject data = new JSONObject();
        data.put("code", "000000");
        data.put("message", "处理成功");
        return data;
    }

}
  • 运行项目

运行后,我们发现使用了新的sdk环境,而且整个应用启动非常快速,仅仅用了0.679s。

image-20221111151830811

  • 接口测试

请求一下我们刚写的接口http://localhost:8080/hello,发现服务正常。

image-20221111152221911

至此基本环境算是搭建完成了。

2.2、多语言支持验证

在上一篇文章中文我们说了Graalvm有个特性是支持多语言运行,如下图Java,JavaScript,python,Ruby,R,WASM。下面我们就来说说熟悉的语言JavaScript在Graalvm中的运行执行情况。

image-20221112215225123

2.2.1 运行JavaScript

2.2.1.1、组件安装

Graalvm运行js脚本,需要安装对应的组件(Since GraalVM 22.2, the JavaScript support is packaged in a separate GraalVM component,意思是从22.2版本开始拆分为单独的组件的),上图中的语言需要支持,都需要安装对应的依赖环境组件。我们可以通过Graalvm提供的gu工具进行安装。

  • 查看可用组件
gu available

image-20221112215838314

  • 安装js组件

因为要从github下载,所以网络很重要,可能很慢。

gu install js
  • 验证安装情况

查看已安装组件

gu list

image-20221112220434359

2.2.1.2、验证

在java代码中运行javascript脚本。

  • 简单打印
package com.jianjang.graalvm.demo.js;

import org.graalvm.polyglot.Context;

/**
 * @program: graalvm-demo
 * @description: 测试用例1
 * @author: Jian Jang
 * @create: 2022-11-12 22:06:36
 */
public class JavaScriptDemo1 {
    public static void main(String[] args) {
        Context context = Context.create();
        context.eval("js", "console.log('Hello GraalVM! from JavaScript')");
    }
}

执行结果

image-20221112221606735

  • 有返回值的js函数调用
package com.jianjang.graalvm.demo.js;

import org.graalvm.polyglot.Context;
import org.graalvm.polyglot.Value;

/**
 * @program: graalvm-demo
 * @description: 测试用例2
 * @author: Jian Jang
 * @create: 2022-11-12 22:06:36
 */
public class JavaScriptDemo2 {
    /***
     * 运行脚本
     * 注意:这里()包着,固定约束
     */
    private static String JS_CODE = "(function hello(param){console.log('hello '+param); return new Date();})";

    public static void main(String[] args) {
        Context context = Context.create();
        //JS 带参数及返回值
        Value value = context.eval("js", JS_CODE);
        Value result = value.execute("Jack");
        System.out.println(result.toString());
    }
}

执行结果

image-20221112221646820

  • 运行指定js文件中的函数

我这里写了一个dateUtils.js工具类,里面有一个常量和两个函数分别是auth,getDate和getTimeStamp,源码如下

/***
 * 定义常量auth
 * @type {string}
 */
const auth='Jian Jang';

/***
 * 获取当前日期
 * @returns {string}
 */
function getDate(){
    var timestamp = Date.parse(new Date());
    var date = new Date(timestamp);
    //获取年份
    var Y =date.getFullYear();
    //获取月份
    var M = (date.getMonth() + 1 < 10 ? '0' + (date.getMonth() + 1) : date.getMonth() + 1);
    //获取当日日期
    var D = date.getDate() < 10 ? '0' + date.getDate() : date.getDate();
    return Y+'-'+M+'-'+D;
}

/***
 * 获取当前时间戳
 * @returns {number}
 */
function getTimeStamp(){
    return new Date().getTime();
}

java代码

package com.jianjang.graalvm.demo.js;

import org.graalvm.polyglot.Context;
import org.graalvm.polyglot.Source;
import org.graalvm.polyglot.Value;
import org.springframework.core.io.ClassPathResource;

import java.io.File;
import java.io.IOException;

/**
 * @program: graalvm-demo
 * @description: 测试用例3
 * @author: Jian Jang
 * @create: 2022-11-12 22:06:36
 */
public class JavaScriptDemo3 {
    /***
     * 运行脚本
     */

    public static void main(String[] args) throws IOException {
        Context context = Context.create();
        //执行指定js文件中的方法
        File resource = new ClassPathResource("dateUtils.js").getFile();
        Source fibSrc = Source.newBuilder("js", resource).build();
        context.eval(fibSrc);
        //获取js文件中的成员关键字
        System.out.println("Polyglot JS members are:" + context.getBindings("js").getMemberKeys());
        Value auth = context.getBindings("js").getMember("auth");
        System.out.println(auth.toString());
        //找到执行函数
        Value getDate = context.getBindings("js").getMember("getDate");
        Value result = getDate.execute(new String[]{});
        System.out.println(result.toString());
        Value getTimeStamp = context.getBindings("js").getMember("getTimeStamp");
        Value result2 = getTimeStamp.execute(new String[]{});
        System.out.println(result2.asLong());
    }
}

运行结果

image-20221112223158831

2.2.2、运行Python

对于不同语言的支持,需要安装对应的组件,python就要安装python组件。

2.2.2.1、组件安装

  • 安装python组件
gu install python
  • 验证安装情况
gu list

image-20221112231815975

2.2.2.2、验证

  • 简单函数调用
package com.jianjang.graalvm.demo.js;

import org.graalvm.polyglot.Context;

import java.io.IOException;

/**
 * @program: graalvm-demo
 * @description: 测试用例1
 * @author: Jian Jang
 * @create: 2022-11-12 22:06:36
 */
public class PythonDemo1 {
    /***
     * 运行脚本
     */

    public static void main(String[] args) throws IOException {
        Context context = Context.create();
        context.eval("python", "print('Hello GraalVM! from Python')");
    }
}

运行结果

image-20221112232042895

  • 运行指定的py文件中的函数

写一个python验证脚本,里面包含一个获取当前日期的函数getDate脚本如下:

#!/usr/bin/env python
# coding=utf-8

import datetime

def getDate():
    return datetime.date.today().strftime('%Y-%m-%d')

java调用

package com.jianjang.graalvm.demo.js;

import org.graalvm.polyglot.Context;
import org.graalvm.polyglot.Source;
import org.graalvm.polyglot.Value;
import org.springframework.core.io.ClassPathResource;

import java.io.File;
import java.io.IOException;

/**
 * @program: graalvm-demo
 * @description: 测试用例2
 * @author: Jian Jang
 * @create: 2022-11-12 22:06:36
 */
public class PythonDemo2 {
    /***
     * 运行脚本
     */

    public static void main(String[] args) throws IOException {
        Context context = Context.create();
        //执行指定python文件中的方法
        File resource = new ClassPathResource("MyFunc.py").getFile();
        Source fibSrc = Source.newBuilder("python", resource).build();
        context.eval(fibSrc);
        //获取python文件中的成员关键字
        System.out.println("Polyglot python members are:" + context.getBindings("python").getMemberKeys());
        //找到执行函数
        Value getDate = context.getBindings("python").getMember("getDate");
        Value result = getDate.execute(new String[]{});
        System.out.println("getDate="+result.toString());
    }
}

运行结果

image-20221112232443695

备注:其他语言的验证,此处略,大致思路是差不多的。

3、Native Image

Native Image是一种将Java代码提前编译为二进制文件的技术,即本机可执行文件。本机可执行文件仅包括运行时所需的代码,即应用程序类、标准库类、语言运行时和JDK中静态链接的本机代码。此处我们是不是想到了docker镜像,docker镜像:包括应用本身,及引用运行所依赖的环境,组件等。他们似乎在某些方面的原理是相同的。所以Native image也可以翻译成原生镜像,不同的操作系统性生成的都是本机的可执行文件(windows下的exe应用,Linux下的可执行应用),不同点就是,docker 镜像可以在不同的系统环境下的docker中运行。docker本身屏蔽了系统的差异。

3.1、Native Image的优点

  • Uses a fraction of the resources required by the Java Virtual Machine, so is cheaper to run

使用Java虚拟机所需资源的一小部分,因此运行起来更容易(便宜)

  • Starts in milliseconds

以毫秒为单位启动

  • Delivers peak performance immediately, with no warmup

立即提供最高性能,无需预热

  • Can be packaged into a lightweight container image for fast and efficient deployment

可以打包成轻量级容器映像,以实现快速高效的部署

  • Presents a reduced attack surface

呈现减少的攻击面

3.1.1、镜像工具类

  • 安装工具native-image
gu install native-image
  • 检查安装情况
gu list

image-20221113214032684

  • 语法

通过如下命令可查看native-image的用法说明,引文版,此处不做过多解释,请自行查看。

native-image --help
This tool can ahead-of-time compile Java code to native executables.

Usage: native-image [options] class [imagename] [options]
           (to build an image for a class)
   or  native-image [options] -jar jarfile [imagename] [options]
           (to build an image for a jar file)
   or  native-image [options] -m <module>[/<mainclass>] [options]
       native-image [options] --module <module>[/<mainclass>] [options]
           (to build an image for a module)

3.1.2、验证

3.1.2.1、普通的java类编译成可执行文件

  • 创建文件

将一个含有main方法的普通java类编译成可执行文件。首先随便找一个目录,确保当前用户权限足够,创建一个HelloWorld.java文件java代码如下:

 public class HelloWorld {
     public static void main(String[] args) {
         System.out.println("Hello, Native World!");
     }
 }
  • 编译java文件
javac HelloWorld.java
  • 创建可执行文件
native-image HelloWorld  

执行结果如下

image-20221112234146759

  • 查看生成的文件
ll

我们可以看出当前目录下多了一个helloword可执行文件,同时生成了一个helloword.build.artifacts.txt文件

image-20221112234335979

  • 运行一下看看

执行可执行文件,我们看到,打印了我们上面main方法中的结果。

image-20221112234357100

  • helloword.build.artifacts.txt内容

我们看到里面几乎没啥东西,只有个可执行文件名称的标识,如下图。

image-20221113215541411

3.1.2.2、springboot项目编译成可执行应用

  • 创建验证接口

使用上面我们创建的springboot项目graalvm-demo,然后改造一下里面的hello接口,改成sayHello。代码如下:

package com.jianjang.graalvm.demo.controller;

import com.alibaba.fastjson2.JSONObject;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

/**
 * @program: graalvm-demo
 * @description: 测试类
 * @author: Jian Jang
 * @create: 2022-11-11 15:03:31
 */
@RestController
@RequestMapping
public class HelloController {
    /***
     * sayHello
     */
    @RequestMapping(value = "/sayHello", method = {RequestMethod.GET, RequestMethod.POST})
    public JSONObject sayHello(@RequestParam("name") String name) {
        JSONObject data = new JSONObject();
        data.put("code", "000000");
        data.put("message", "hello " + name);
        return data;
    }

}
  • 打包项目jar

进入项目根目录执行如下命令

 mvn clean package 

image-20221113222055147

  • 编译成可执行应用

进入jar包所在目录,target目录( /Users/zhangjian/works/workspace_home/graalvm-demo/target/graalvm-demo.jar),然后执行如下脚本

native-image -jar graalvm-demo.jar graalvm-demo

image-20221113222445273

有上面日志可以看出,镜像生成失败了,但是生成了一个回滚镜像,这个镜像的运行,任然依赖于JVM,至于啥原因,我们后面再说。

  • 验证应用

我们在target下可以看到过了一个可执行应用graalvm-demo,如下图:

image-20221113224555897

  • 运行应用
./graalvm-demo

image-20221113224700542

由此看出,应用项目启动起来了,用了995毫秒,可以说是很快了。

  • 接口验证

新开个终端,执行下面命令

curl localhost:8080/sayHello\?name\=zhangjian

image-20221113225122617

本文项目代码:github.com/JianJang201…

至此java拥抱云原生的GraalVM基础就算完了,后面我们继续深入实践。如有问题,可联系我修改正。谢谢。