在Java中运行ES6代码

Java自带Nashorn引擎,可以在Java程序中运行JavaScript代码,从而实现可灵活配置动态接口。然而Nashorn引擎只支持老旧的语法,不支持ES6或更新的东西,而且在较新的Java中也已停止支持,因此本文选择替代品Javet

Javet是一个能够整合Java与Node.js的库,使开发人员可以很方便地调用ES6的代码。

引入Javet

官方仓库指出了具体的方法。例如使用Maven时需要在pom.xml中加入:

<!-- Linux and Windows (x86_64) -->
<dependency>
    <groupId>com.caoccao.javet</groupId>
    <artifactId>javet</artifactId>
    <version>2.1.2</version>
</dependency>

<!-- Linux (arm64) -->
<dependency>
    <groupId>com.caoccao.javet</groupId>
    <artifactId>javet-linux-arm64</artifactId>
    <version>2.1.2</version>
</dependency>

<!-- Mac OS (x86_64 and arm64) -->
<dependency>
    <groupId>com.caoccao.javet</groupId>
    <artifactId>javet-macos</artifactId>
    <version>2.1.2</version>
</dependency>

调用JavaScript代码

普通调用

public Object runJsCode(String code) throws JavetException {
    Object result = null;
    try (V8Runtime v8Runtime = V8Host.getV8Instance().createV8Runtime()) {
        JavetProxyConverter javetProxyConverter = new JavetProxyConverter();
        v8Runtime.setConverter(javetProxyConverter);

        // TODO 可在这里引入Java对象,以便作为js的全局变量使用
        v8Runtime.getGlobalObject().set("XXXUtil", xxxUtil);

        V8Value val = v8Runtime.getExecutor(code).execute();
        result = parseV8Value(val);
    }
    return result;
}

private Object parseV8Value(V8Value val) throws JavetException {
    if (val instanceof V8ValueString) {
        return ((V8ValueString) val).getValue();
    } else if (val instanceof V8ValueBigInteger) {
        return ((V8ValueBigInteger) val).getValue();
    } else if (val instanceof V8ValueBoolean) {
        return ((V8ValueBoolean) val).getValue();
    } else if (val instanceof V8ValueDouble) {
        return ((V8ValueDouble) val).getValue();
    } else if (val instanceof V8ValueInteger) {
        return ((V8ValueInteger) val).getValue();
    } else if (val instanceof V8ValueLong) {
        return ((V8ValueLong) val).getValue();
    } else if (val instanceof V8ValueArray) {
        // TODO 根据需要进行处理
        return ((V8ValueArray) val).toArray();
    } else if (val instanceof V8ValueObject) {
        // TODO 根据需要进行处理
        return ((V8ValueObject) val);
    }
    // TODO 根据需要进行处理
    return val;
}

使用时调用runJsCode("js代码")即可,返回值为Java对象。但由于JS的数组和Object灵活性较强,本程序仍返回Javet的对象,编码时应考虑根据实际需求进行处理。

将对象转为JSON

如果用于JSON,例如配置动态接口,将上述代码返回的对象直接转成JSON,结果可能会出错,因此需要在转JSON之前做一些加工处理。

为便于处理JSON,下面引入了Hutool,因此需要在pom.xml中引入:

<dependency>
    <groupId>cn.hutool</groupId>
    <artifactId>hutool-all</artifactId>
    <version>5.6.1</version>
</dependency>

上面代码的runJsCode不变,对parseV8Value进行修改,替换成下面的代码:

private Object parseV8Value(V8Value val) throws JavetException {
    try (V8Scope v8Scope = val.getV8Runtime().getV8Scope()) {
        v8Scope.add(val);
        val = parseV8ValueToBasic(val, v8Scope);

        if (val instanceof V8ValueString) {
            return ((V8ValueString) val).getValue();
        } else if (val instanceof V8ValueBigInteger) {
            return ((V8ValueBigInteger) val).getValue();
        } else if (val instanceof V8ValueBoolean) {
            return ((V8ValueBoolean) val).getValue();
        } else if (val instanceof V8ValueDouble) {
            Double d = ((V8ValueDouble) val).getValue();
            // 无穷大和NaN转JSON会报错,这里直接返回null
            if (d.isInfinite() || d.isNaN()) {
                d = null;
            }
            return d;
        } else if (val instanceof V8ValueInteger) {
            return ((V8ValueInteger) val).getValue();
        } else if (val instanceof V8ValueLong) {
            return ((V8ValueLong) val).getValue();
        } else if (val instanceof V8ValueArray) {
            String json = ((V8ValueArray) val).toJsonString();
            if (json == null) {
                return null;
            }
            if (json.startsWith("TypeError")) {
                throw new RuntimeException(json);
            }
            return JSONUtil.parseArray(json, false);
        } else if (val instanceof V8ValueTypedArray) {
            String json = ((V8ValueTypedArray) val).toJsonString();
            if (json == null) {
                return null;
            }
            if (json.startsWith("TypeError")) {
                throw new RuntimeException(json);
            }
            return JSONUtil.parseObj(json, false);
        } else if (val instanceof V8ValueObject) {
            String json = ((V8ValueObject) val).toJsonString();
            if (json == null) {
                return null;
            }
            if (json.startsWith("TypeError")) {
                throw new RuntimeException(json);
            }
            return JSONUtil.parseObj(json, false);
        }
        // 其他类型无法转成JSON,直接返回null
        return null;
    }
}

private V8Value parseV8ValueToBasic(V8Value val, V8Scope v8Scope) throws JavetException {
    if (val instanceof V8ValueString) {
        return val;
    } else if (val instanceof V8ValueBigInteger) {
        return val;
    } else if (val instanceof V8ValueBoolean) {
        return val;
    } else if (val instanceof V8ValueDouble) {
        return val;
    } else if (val instanceof V8ValueInteger) {
        return val;
    } else if (val instanceof V8ValueLong) {
        // 处理js中Number与Java中Double的差异
        Long u = ((V8ValueLong) val).getValue();
        if (u == null) {
            return v8Scope.createV8ValueNull();
        } else {
            long v = u.longValue();
            if (v <= 9007199254740991L && v >= -9007199254740991L) {
                return v8Scope.createV8ValueDouble(v);
            } else {
                return val;
            }
        }
    } else if (val instanceof V8ValueProxy) {
        // 将Java对象实例变成JS对象,进而方便转JSON
        V8ValueProxy proxy = (V8ValueProxy) val;
        if (proxy.has("getClass") && proxy.has("hashCode") && proxy.has("equals") && proxy.has("toString") && proxy.has("notify") && proxy.has("notifyAll") && proxy.has("wait")) {
            V8ValueObject v8ValueObject = v8Scope.createV8ValueObject();
            Class clz = proxy.invokeObject("getClass");
            // 获取对象的getter并进行调用
            for (Method method : clz.getMethods()) {
                if (method.getParameterCount() == 0 && !Modifier.isStatic(method.getModifiers())) {
                    String methodName = method.getName();
                    String propertyName = null;
                    if (methodName.startsWith(METHOD_PREFIX_IS) && !EXCLUDED_METHODS.contains(methodName)
                            && methodName.length() > METHOD_PREFIX_IS.length()) {
                        propertyName = methodName.substring(METHOD_PREFIX_IS.length(), METHOD_PREFIX_IS.length() + 1).toLowerCase(Locale.ROOT)
                                + methodName.substring(METHOD_PREFIX_IS.length() + 1);
                    } else if (methodName.startsWith(METHOD_PREFIX_GET) && !EXCLUDED_METHODS.contains(methodName)
                            && methodName.length() > METHOD_PREFIX_GET.length()) {
                        propertyName = methodName.substring(METHOD_PREFIX_GET.length(), METHOD_PREFIX_GET.length() + 1).toLowerCase(Locale.ROOT)
                                + methodName.substring(METHOD_PREFIX_GET.length() + 1);
                    }
                    if (propertyName != null) {
                        V8Value newVal = proxy.invoke(methodName);
                        if (newVal != null) {
                            v8Scope.add(newVal);
                            newVal = parseV8ValueToBasic(newVal, v8Scope);
                            v8ValueObject.set(propertyName, newVal);
                        }
                    }
                }
            }
            return v8ValueObject;
        }
    } else if (val instanceof V8ValueArray) {
        V8ValueArray arr = (V8ValueArray) val;
        // 递归处理每个元素
        for (int i = 0; i < arr.getLength(); i++) {
            V8Value u = arr.get(i);
            V8Value v = parseV8ValueToBasic(u, v8Scope);
            if (u != v) {
                arr.set(i, v);
            }
        }
        return arr;
    } else if (val instanceof V8ValueTypedArray) {
        V8ValueTypedArray arr = (V8ValueTypedArray) val;
        V8ValueArray arr2 = v8Scope.createV8ValueArray();
        // 递归处理每个元素
        for (int i = 0; i < arr.getLength(); i++) {
            V8Value u = arr.get(i);
            V8Value v = parseV8ValueToBasic(u, v8Scope);
            arr2.push(v);
        }
        return arr2;
    } else if (val instanceof V8ValueObject) {
        V8ValueObject obj = (V8ValueObject) val;
        // 递归处理每个元素
        for (String key : obj.getOwnPropertyNameStrings()) {
            V8Value u = obj.get(key);
            V8Value v = parseV8ValueToBasic(u, v8Scope);
            if (u != v) {
                obj.set(key, v);
            }
        }
        return obj;
    }
    // 其他类型(例如Function)无法转成JSON,直接返回null
    return v8Scope.createV8ValueNull();
}

以上代码还有一些缺陷,例如Java对象不是VO类型的类,转换时候就会出问题,不过现在的代码对于转JSON这种需求来说够用了。

Docker报错处理

如果将应用部署到Docker中,代码可能会无法正常执行:

com.caoccao.javet.exceptions.JavetException: Javet library is not loaded because <null>
    at com.caoccao.javet.interop.JavetClassLoader.load(JavetClassLoader.java:93)
    at com.caoccao.javet.interop.V8Host.loadLibrary(V8Host.java:418)
    at com.caoccao.javet.interop.V8Host.<init>(V8Host.java:67)
    at com.caoccao.javet.interop.V8Host.<init>(V8Host.java:43)
    at com.caoccao.javet.interop.V8Host$V8InstanceHolder.<clinit>(V8Host.java:459)
    at com.caoccao.javet.interop.V8Host.getV8Instance(V8Host.java:119)
    ……
Caused by: java.lang.reflect.InvocationTargetException
    at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
    at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
    at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
    at java.lang.reflect.Method.invoke(Method.java:498)
    at com.caoccao.javet.interop.JavetClassLoader.load(JavetClassLoader.java:88)
    ... 170 more
Caused by: com.caoccao.javet.exceptions.JavetException: Failed to read /tmp/javet/1/libjavet-v8-linux-x86_64.v.3.0.1.so
    at com.caoccao.javet.interop.loader.JavetLibLoader.load(JavetLibLoader.java:375)
    ... 175 more
Caused by: java.lang.UnsatisfiedLinkError: /tmp/javet/1/libjavet-v8-linux-x86_64.v.3.0.1.so: /lib/x86_64-linux-gnu/libc.so.6: version `GLIBC_2.33' not found (required by /tmp/javet/1/libjavet-v8-linux-x86_64.v.3.0.1.so)
    at java.lang.ClassLoader$NativeLibrary.load(Native Method)
    at java.lang.ClassLoader.loadLibrary0(ClassLoader.java:1934)
    at java.lang.ClassLoader.loadLibrary(ClassLoader.java:1817)
    at java.lang.Runtime.load0(Runtime.java:810)
    at java.lang.System.load(System.java:1088)
    at com.caoccao.javet.interop.loader.JavetLibLoader.load(JavetLibLoader.java:360)
    ... 175 more

这是因为选择了alpine或slim版本的映像,容器里面缺少glibc,因此需要将image换成完整版,例如openjdk:8