Java语言十五讲(第十一讲 Script)

大家好。我前两年意识到一件事情,就是Java里面有一个很好的功能许多人都没有注意到,而要做大型一点的系统或者是做扩展性好一点的系统都会用到,这就是script技术。
学技术之前,还是先看场景。    
我们做的应用系统里面,经常需要设置一些业务规则。比如权限控制,不管是网络层的防火墙还是应用层的数据权限,一般用户是希望设置一个规则,网络权限像IP between 192.168.2.101 and 192.168.2.149,像port=80 or port=8080 or port=21等等,数据权限像level>3 and department in (‘’IT,’ADMIN’)等等。还有一些计算方法,不希望写死在代码中,而是拉出来让用户自己配置,比如算保费,可能是根据一堆条件和计算公式算出来的,更关键的是每年可能都会有改动,修改源代码自然可以,但是如果把计算公式设计成可以配置的,系统就更加灵活可扩展了。
应用程序员的一大痛苦就是要面对每天不断的需求变化,有的时候令人崩溃。这个情况在中国尤其普遍,因为中国的商业习惯是把系统打成一个包,不管付出多少劳动都是这个包的价格,用户习惯上就不断加东西不断修改东西,觉今是而昨非,销售部门觉得不就是这么一点改动吗?还不nice一点。于是程序员们只好不停地修改,疲于奔命。还不落好,我25年前的公司领导是北大中文系的,他开玩笑跟我说过“看你们程序员一个个天天咬牙切齿一副很努力工作的样子,也出不来什么东西,你们都在干什么呀?”这种日子必须了结,不然职业生涯太没有成就感了,为稻粱谋真是辛苦。
从那个时候开始,我就在想,能不能把这些权限规则,计算公式,流程之类的都外部化为配置文件,让用户自己定义,我们有一个通用程序来解释。这样从技术本身的角度缓解这个状况,不用一动就改源代码。自然,大家肯定也想到了是有这样的技术的,我们大学课本上都学过的《编译原理》,我们可以对外提供一种表达式或者语言,让用户自己定义。
这次不会讲解编译器的原理,那是单独的一本书,我是打算今后单独开一个讲座讲解怎么编写解释器让用户自定义计算规则。那个时候再详谈。
有了这个场景的背景知识,我们再回头开Java里面提供的功能。

JDK1.6开始增加了一个javax.script包,提供了对脚本引擎的支持。第一句话一说出来,就有人懵了。一般人的反应是想了解Java里面究竟规定了什么样的脚本语法让人编写,怎么一上来就谈脚本引擎呢?我们确实一开始不能谈脚本语言文法,而是这个引擎,因为Java的设计者们立意高远,并不是想着就只提供一个规定好的脚本语言,而是想着自己是一个平台,可以支持多种脚本语言,所以他们先实现的是对脚本引擎的支持,让引擎自己规定脚本语言文法,这一点跟Taglib的设计有异曲同工之妙,海纳百川,有容乃大。
好,那我们先看看怎么获取Java里面支持的脚本引擎,代码如下(Test1.java):

import java.util.List;
import javax.script.ScriptEngine;
import javax.script.ScriptEngineFactory;
import javax.script.ScriptEngineManager;

public class Test1 {
    public static void main(String[] args) {
        ScriptEngineManager manager = new ScriptEngineManager();
        List<ScriptEngineFactory> list = manager.getEngineFactories();

        for (ScriptEngineFactory f : list) {
          System.out.println("Engine Name:" + f.getEngineName());
          System.out.println("Engine Version:" + f.getEngineVersion());
          System.out.println("Language Name:" + f.getLanguageName());
          System.out.println("Language Version:" + f.getLanguageVersion());
          System.out.println("Engine Short Names:" + f.getNames());
          System.out.println("Extensions:"+f.getExtensions());
          System.out.println("Mime Types:" + f.getMimeTypes());
          System.out.println("Method call:"+f.getMethodCallSyntax("object", "method", "param1","param2"));
        }
    }
}

主要涉及到三个类和接口,javax.script.ScriptEngine,javax.script.ScriptEngineFactory,javax.script.ScriptEngineManager。
ScriptEngineManager是负责管理script engine的类,查找和实例化script engine,由JDK本身提供。ScriptEngineManager 使用 Jar 文件规范 中描述的服务提供者机制来获取所有当前 ClassLoader 中可用的 ScriptEngineFactory 实例。ScriptEngine是主要的接口,引擎的提供者要负责实现它,而每一个script engine都要有一个ScriptEngineFactory对应,负责创建script engine,这是工厂模式。
在我的电脑里面运行结果如如下:

Engine Name:Oracle Nashorn
Engine Version:1.8.0_171
Language Name:ECMAScript
Language Version:ECMA - 262 Edition 5.1
Short Names:[nashorn, Nashorn, js, JS, JavaScript, javascript, ECMAScript, ecmascript]
Extensions:[js]
Mime Types:[application/javascript, application/ecmascript, text/javascript, text/ecmascript]
Method call:object.method(param1,param2)

从结果看出,我的电脑里面只安装了一个引擎,就是Java8自带的Nashorn。它完全支持ECMA - 262 Edition 5.1。

下面看一个简单的例子,看怎么执行脚本,代码如下(Test2.java):

public class Test2{
    public static void main(String[] args) {
        ScriptEngineManager manager = new ScriptEngineManager();
        ScriptEngine engine = manager.getEngineByName("JavaScript");

        try {
            engine.eval("print(\"Hello World!\")");
            Object result = null;
            result = engine.eval("1 + 2;");
            System.out.println(result);
            result = engine.eval("1 + 2; 3 + 4;");
            System.out.println(result);
            result = engine.eval("3 + 4; var v=9;");
            System.out.println(result);
            result = engine.eval("1 + 2; 3 + 4; var v = 5; v = 6;");
            System.out.println(result);
            result = engine.eval("print(1 + 2)");
            System.out.println(result);                 
        } catch (ScriptException e) {
            e.printStackTrace();
        }
    }   
}

运行结果如下:

Hello World!
3
7
7
6
3
Null

简单解释一下:

ScriptEngine engine = manager.getEngineByName("JavaScript");

通过manager拿到了支持JavaScript的引擎。Manager本身从classpath下加载jar包。
引擎通过eval()执行脚本。如engine.eval("print(\"Hello World!\")");就打印出了Hello World!
eval()有一个返回值,它返回的是javascript中最后一个计算赋值语句的结果值。如engine.eval("1 + 2; 3 + 4;");返回的就是7。而engine.eval("3 + 4; var v=9;");返回的还是7,因为var v=9被看成一个定义语句而不是执行语句,所以engine.eval("1 + 2; 3 + 4; var v = 5; v = 6;");会返回6,因为最后执行了v=6。同理,engine.eval("print(1 + 2)");最后返回的是null,因为print()语句只是打印,并没有计算赋值这些操作。

光有脚本执行,如果没有与外面的数据交互,用处也不大,我们再看一个有数据传入传出的例子,代码如下(Test3.java):

public class Test3 {
    public static void main(String[] args) {
        ScriptEngineManager manager = new ScriptEngineManager();
        ScriptEngine engine = manager.getEngineByName("JavaScript");

        try {
            String script1 = "print(msg)";
            engine.put("msg", "Hello from Java program");
            engine.eval(script1);

            String script2 = "var year = 2019";
            engine.eval(script2);
            Object year = engine.get("year");
            System.out.println("year class:" + year.getClass().getName());
            System.out.println("year value:" + year);
        } catch (ScriptException e) {
            e.printStackTrace();
        }
    }
}

解释一下。第一段是演示参数传入,先在Java程序和中给变量赋值engine.put("msg", "Hello from Java program"); 这里msg被看成脚本中的变量,脚本是script1 = "print(msg)"; 执行脚本engine.eval(script1);这个时候会用到从外部传进来的变量值Hello from Java program。
第二段演示的是传出。先在脚本中定义变量并给初值var year = 2019;执行之后外部的Java程序用engine.get("year");语句拿到变量值并赋给Java中的一个对象。javascript中的值和Java中的值有一个类型转换的问题,这是引擎要处理的。
运行结果如下:

Hello from Java program
year class:java.lang.Integer
year value:2019

从这个例子我们看到了外部Java的变量值怎么传入,内部Javascript的变量怎么传出。

最后,我们用一个对象,在Java与引擎之间传递对象数据,这个模式接近于实际业务场景了。
先定义这个对象,代码如下(Person.java):

public class Person {
    private int val = -1;
    private int flag = -1;

    public Person(int val) {
        this.val = val;
    }
    public void setValue(int x) {
        val = x;
    }
    public int getValue() {
        return val;
    }
    public void setFlag(int x) {
        flag = x;
    }
    public int getFlag() {
        return flag;
    }
}

简单,不解释了。
再用一个程序传递person对象,代码如下(Test4.java):

public class Test4 {
    public static void main(String[] args) {
        Person p1 = new Person(70);
        ScriptEngineManager manager = new ScriptEngineManager();
        ScriptEngine engine = manager.getEngineByName("JavaScript");
        try {
            engine.put("person", p1);
            String script = "var returnVal; if (person.getValue()>60) { person.setFlag(1); returnVal=\"Passed\"} else { person.setFlag(0);returnVal=\"Failed\"}";
            Object result=engine.eval(script);
            System.out.println("Result is " + result);
            System.out.println("returnVal is " + engine.get("returnVal"));
            System.out.println("flag is " + p1.getFlag());
        } catch (ScriptException e) {
            e.printStackTrace();
        }
    }
}

运行结果如下:

Result is Passed
returnVal is Passed
flag is 1

这个例子演示了外部的对象,如何传进去,内部如何调用该对象。

到此为止,我讲了如何使用Script,其实蛮简单的。Java的很多功能都有这个特点,说起来高大上,使用起来一点都不困难,起码是简单的任务不困难。这个源自Java设计团队的传统:Java API的设计哲学是keep simple things and hard things work(让常规的事情很简单,让复杂的事情能工作)。为此,Java API的一个特点是method比较多,简单的事情用简单的method,参数少,甚至没有参数,复杂的事情才用到参数多的method,我们把这个叫multiple method设计方法。在这一点上,跟Oracle API的设计哲学不同,我们看Oracle 数据库的API,method比较少,而每一个method的参数特别多,通常有十几个,我们把这个叫multi-purpose method设计方法。至于优劣,见仁见智,我个人偏好Java的设计风格,看到Oracle的API我就头大。
至于怎么自己定义一个完整的脚本引擎,我会在单独的系列讲座里再谈,到时候我们给JDK里面增加自己的脚本引擎解释用户脚本程序。我这里只简单介绍一下怎么做一个最简单的脚本引擎,如解释算术表达式的引擎。
其实上面提到过,核心的接口就是ScriptEngineFactory和ScriptEngine。我们实现它们,生成一个jar包,声明里面的service就可以了。
先实现factory,代码如下(MyScriptFactory.java):

package com.myscript;

import java.util.Arrays;
import java.util.List;
import javax.script.ScriptEngine;
import javax.script.ScriptEngineFactory;

public class MyScriptFactory implements ScriptEngineFactory {
    public MyScriptFactory() {
    }

    @Override
    public String getEngineName() {
        return "MyScript";
    }
    @Override
    public String getEngineVersion() {
        return "1.0";
    }
    @Override
    public List<String> getExtensions() {
        return null;
    }
    @Override
    public String getLanguageName() {
        return "MyScript";
    }
    @Override
    public String getLanguageVersion() {
        return "1.0";
    }
    @Override
    public String getMethodCallSyntax(String obj, String m, String... args) {
        return null;
    }
    @Override
    public List<String> getMimeTypes() {
        return null;
    }
    @Override
    public List<String> getNames() {
        return Arrays.asList("MyScript");
    }
    @Override
    public String getOutputStatement(String toDisplay) {
        return null;
    }
    @Override
    public Object getParameter(String key) {
        return null;
    }
    @Override
    public String getProgram(String... statements) {
        return null;
    }
    @Override
    public ScriptEngine getScriptEngine() {
        return new MyScriptEngine();
    }
}

我只是演示如何自己做,所以只实现了最基本的几个方法:getScriptEngine(),getEngineName(),getNames()等等几个。最核心的是记住我们的名字叫MyScript。
然后实现引擎,代码如下(MyScriptEngine.java):

package com.myscript;

import java.io.Reader;
import javax.script.AbstractScriptEngine;
import javax.script.Bindings;
import javax.script.ScriptContext;
import javax.script.ScriptEngine;
import javax.script.ScriptEngineFactory;
import javax.script.ScriptException;

public class MyScriptEngine extends AbstractScriptEngine implements ScriptEngine {
    public MyScriptEngine() {
    }
    public MyScriptEngine(Bindings arg0) {
        super(arg0);
    }

    @Override
    public Bindings createBindings() {
        return null;
    }
    @Override
    public Object eval(String script) throws ScriptException {
        return Expression.eval(script);
    }
    @Override
    public ScriptEngineFactory getFactory() {
        return null;
    }
    @Override
    public Object eval(String arg0, ScriptContext arg1) throws ScriptException {
        return null;
    }
    @Override
    public Object eval(Reader arg0, ScriptContext arg1) throws ScriptException {
        return null;
    }
}

也是最简单的实现,用到了JDK自己的AbstractScriptEngine。最核心的是

public Object eval(String script) throws ScriptException {
    return Expression.eval(script);
}

我们用了另一个工具类Expression来解析数学表达式。也是最简单实现,代码如下(Expression.java):

package com.myscript;

public class Expression {
    public Expression() {
    }

    public static Object eval(String sExpression) {
        return new Double(sExpression);
    }
}

一看,好家伙,什么也没有干,根部不是算术表达式的解释引擎,就是一个简单的把字符串转成数值的功能。没事,因为我们这里不是讲怎么写解释器,先不要在意。
做好了这些,准备打jar包。按照jar文件的规范,先在资源目录下建好META-INF/services目录,放一个文件名字叫javax.script.ScriptEngineFactory,文件内容写上com.myscript.MyScriptFactory。好了,可以生成jar包了。
我们再写一个程序,引用上面的jar包,利用自定义的脚本引擎计算数学表达式。代码如下(MyScriptTest.java):

import java.util.List;
import javax.script.ScriptEngine;
import javax.script.ScriptEngineFactory;
import javax.script.ScriptEngineManager;
import javax.script.ScriptException;

public class MyScriptTest {
    public static void main(String[] args) {
        ScriptEngineManager manager = new ScriptEngineManager();
        List<ScriptEngineFactory> list = manager.getEngineFactories();

        for (ScriptEngineFactory f : list) {
          System.out.println("Engine Name:" + f.getEngineName());
          System.out.println("Engine Version:" + f.getEngineVersion());
          System.out.println("Language Name:" + f.getLanguageName());
          System.out.println("Language Version:" + f.getLanguageVersion());
          System.out.println("Engine Short Names:" + f.getNames());
          System.out.println("Extensions:"+f.getExtensions());
          System.out.println("Mime Types:" + f.getMimeTypes());
          System.out.println("Method call:"+f.getMethodCallSyntax("object", "method", "param1","param2"));

          System.out.println("===");
        }

        ScriptEngine engine = manager.getEngineByName("MyScript");   
        try {
            Object result = null;
            result = engine.eval("3");
            System.out.println(result);
        } catch (ScriptException e) {
            e.printStackTrace();
        }
    }   
}

除了我们的语言支持MyScript之外,我们看不到代码有任何不同。这就是Java采用Bridge设计模式带来的好处,将对上层的接口与具体的实现者分离了。JDBC也是这种模式设计的。
运行一下程序,结果如下:

Engine Name:Oracle Nashorn
Engine Version:1.8.0_171
Language Name:ECMAScript
Language Version:ECMA - 262 Edition 5.1
Engine Short Names:[nashorn, Nashorn, js, JS, JavaScript, javascript, ECMAScript, ecmascript]
Extensions:[js]
Mime Types:[application/javascript, application/ecmascript, text/javascript, text/ecmascript]
Method call:object.method(param1,param2)
===
Engine Name:MyScript
Engine Version:1.0
Language Name:MyScript
Language Version:1.0
Engine Short Names:[MyScript]
Extensions:null
Mime Types:null
Method call:null
===
3.0

我们看到,现在在我的系统里面发现了两个script engine factory,一个是Java内置的Nashorn,一个就是我们自定义的MyScript。并且使用这个自定义的引擎,我们调用后正确的计算出结果来了。自然,我们这个引擎弱爆了,传进去3,它会算,传进去1+2,就不会了。但是这个弱爆的引擎很好地体现了Java 脚本编程的设计思路和功用。
利用Java的Script Engine技术,我们彷佛一下子打开了一个自定义的大门,发现一片豁然开朗的新天地可以自由驰骋。好多以前不敢想的事情一下子都变得能做了,我们写的应用程序可以让用户自定义很多规则与行为,不再只是一个写死的应用程序了。写程序能体会这种别有洞天的感觉,不亦快哉。做技术,不仅仅只有独上高楼衣带渐宽的辛苦,也有蓦然回首那人却在的开怀。好多编程高手和设计大师,历经了技术的沧桑,而依然深爱着它。

我们最后还要要知道,Java中内置的引擎也是变去变来的。Java6最早引入了Rhino,后在Java8被废弃,又引入Nashorn,在Java11中又被拿掉了,拿掉的原因是因为维护太困难了。要说明的是,拿掉之后这些API还在,只是具体的实现要换了,我们如果坚持还要用Nashorn也是可以的,手工引入Jar包就可以了。而有意思的是,提议引入Nashorn和提议拿掉Nashorn的是同一个人:Jim Laskey。
未来会用到什么?我个人希望是v8。但是这是不可能的,因为Oracle和Google是死对头。另外一条道路就是Java本身不提供任何实现,全部交给第三方。

这里我再顺便简单讲一下Oracle现在发布Java的政策。根据Oracle官方说明,从Java8之后,每三年发布一个长期支持版LTS。如Java8就是一个LTS,但是接下来的Java9和Java10不是,而2018年9月发布的Java11就是LTS。所谓长期支持版本,就是会支持几年以上,而非LTS只管半年。最近的几个版本情况见下图:

详细的政策说明可以参看2019年4月15号的官方文档,见:

https://www.oracle.com/technetwork/java/java-se-support-roadmap.html。

技术的走向强烈依赖于商业环境,Internet草创时期,一切都开放,各路英雄豪杰各显神通,二十年来,商业模式逐渐成熟,巨头们开始各自把持一方。从早期的Microsoft到Oracle甚至到Google,都在利用各自的优势地位强化商业,挤压开放组织,Microsoft就不用说了,Oracle把Java看成自己的私有物,Google封闭Android,又利用Chrome事实标准逼宫W3C。作为Internet草莽时期成长的我,看着这一切,内心颇有凄惶之感。Web之父 Tim Berners-Lee 爵士对现在的状况非常不满,他想要拯救互联网,重新去中心化,他们正在开发名为 Solid 的新平台。
一切伟大的技术进步都始于理想主义,而终结于利益,然后用下一个伟大的技术开始下一轮循环。天道如此,我们身在其中,尽人事安天命。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值