Skip to content
oldmanpushcart@gmail.com edited this page Apr 14, 2017 · 14 revisions

JavaScriptSupport(JavaScript支持)

命令介绍

js命令几经波折,几乎在两个重要版本中绝迹,所以我不得不先介绍这个命令的历史背景。

在GREYS的1.5版本时代,字节码增强使用的是javassist进行,里面的黑盒严重,我比较难介入和调试其中的字节码生成过程。在这种情况下我们发现了一个JavaScript引擎rhino与Javassist结合存在严重漏洞的问题却没有更多调试的手段。

考虑到性能、后续扩展的发展,GREYS从1.6开始替换成asm,之后我拥有了最精细的字节码控制权限。在1.7.4.0版本中我恢复了GREYS对JavaScript的支持,并通过了之前BUG的测试。一切正常。

详细用法

运行第一个JavaScript

  • 目标

    我们编写一个watch.js脚本,这个脚本的主要是在方法运行之前输出方法的大概信息。类似于

    watch -b *Test print* clazz.name+"."+method.name+"()"
    
  • 步骤

    1. 首先我们先生成一个脚本文件

      touch /tmp/watch.js
      

      然后往里面写入以下内容

      require(['greys'], function (greys) {
          greys.watching({
      
              before: function (o, a) {
                  o.println(a.clazz.name+"."+a.method.name+"()");
              },
      
          });
      })
    2. 接下来启动GREYS之行命令运行

      js *Test print* /tmp/watch.js
      

      执行效果

      ga?>js *Test print* /tmp/watch.js
      Press Ctrl+D to abort.
      Affect(class-cnt:1 , method-cnt:2) cost in 35 ms.
      com.alibaba.AgentTest.printAddress()
      com.alibaba.AgentTest.printUser()
      com.alibaba.AgentTest.printAddress()
      com.alibaba.AgentTest.printUser()
      com.alibaba.AgentTest.printUser()
      com.alibaba.AgentTest.printAddress()
      

运行一个远程的JavaScript

除了本地临时写代码之外,你也可以将平时积累好的脚本代码放在远程服务端(比如Github),可以使用远程加载的方式运行。

我在Github上提前准备了一个watch.js文件,内容和原来差不多

  • 执行命令

    js *Test print* https://raw.githubusercontent.com/oldmanpushcart/images/master/greys/watch.js
    
  • 执行效果

    ga?>js *Test print* https://raw.githubusercontent.com/oldmanpushcart/images/master/greys/watch.js
    Press Ctrl+D to abort.
    Affect(class-cnt:1 , method-cnt:2) cost in 43 ms.
    call from remote script: com.alibaba.AgentTest.printUser()
    call from remote script: com.alibaba.AgentTest.printAddress()
    call from remote script: com.alibaba.AgentTest.printUser()
    call from remote script: com.alibaba.AgentTest.printAddress()
    

参数解析

通过help js命令,我们可以看到js命令一共拥有2个命名参数、3个位置参数

ga?>help js
+---------+----------------------------------------------------------------------------------+
|   USAGE | -[c:E] class-pattern method-pattern script-path                                  |
|         | Enhanced JavaScript                                                              |
+---------+----------------------------------------------------------------------------------+
| OPTIONS |             [c:] | The character of script-path                                  |
|         | -----------------+-------------------------------------------------------------- |
|         |              [E] | Enable regular expression to match (wildcard matching by def  |
|         |                  | ault)                                                         |
|         | -----------------+-------------------------------------------------------------- |
|         |    class-pattern | Path and classname of Pattern Matching                        |
|         | -----------------+-------------------------------------------------------------- |
|         |   method-pattern | Method of Pattern Matching                                    |
|         | -----------------+-------------------------------------------------------------- |
|         |      script-path | Path of javascript, support file:// or http://                |
+---------+----------------------------------------------------------------------------------+
| EXAMPLE | js *StringUtils isBlank /tmp/watch.js                                            |
+---------+----------------------------------------------------------------------------------+
Affect(row-cnt:1) cost in 6 ms.
ga?>
参数名称 参数说明
[c:] 指定脚本字符编码
[E] 支持正则表达式匹配
class-pattern 类名表达式匹配
method-pattern 方法名表达式匹配
script-path 脚本存放位置,支持HTTP/HTTPS

JavaScript编写

CommonJS Modules/1.0规范

CommonJS Modules/1.0 是目前JavaScript模块化的事实标准,虽然其已经被 CommonJS Modules/1.1 所替代,但是1.0的适用范围非常广,支持者也很多,其中包括Flusspferd, GLUEscript, GPSEE, JSBuild, Narwhal (0.1), Persevere, RingoJS, SproutCore 1.1/Tiki, node.js, TeaJS (formerly v8cgi), CouchDB, Smart Platform, Yabble, Wakanda, XULJet等,所以翻译此规范还是很有必要的,以下为正文。

但比较遗憾的是JDK6默认所自带的JavaScriptEngine实现Rhino并不支持CommonJs,GREYS的定位是需要在JDK6+的版本上运行,所以为了CommonJs规范我不得不自己实现了这套规范的部分子集。

在精简版本的实现中,一共定义了2个全局函数:require,define,和3个魔法模块:require,exports,module

因为这里不是科普CJS的地方,所以我就简单的放WIKI链接了,有兴趣了解CJS规范的同学可以自行脑补。但需要强调的是,GREYS在1.7.5.0版本之后,将部分支持CJS规范,所有模块都将会在这个规范下发展和完善。

CJS规范中并未规定define全局函数,仅规范了require,但目前大部分CJS的实现如requirejsseajs都带了define函数的实现,确实非常方便,所以我在实现CJS的时候也加入了define函数的实现,目前我参考时限的是AMD规范中关于define的定义。

GREYS脚本结构

上边一堆废话,接下来我们进入正题:解析GREYS的脚本结构

/**
 * 模版
 */
require(['greys'], function (greys) {
    greys.watching({

        /**
         * 脚本创建函数
         * 在脚本第一次运行时候执行,可以在这个函数中进行脚本初始化工作
         * @param output 输出器
         */
        create: function (output) {

        },

        /**
         * 脚本销毁函数
         * 在脚本运行完成时候执行,可以在这个函数中进行脚本销毁工作
         * @param output 输出器
         */
        destroy: function (output) {

        },

        /**
         * 方法执行前回调函数
         * 在Java方法执行之前执行该函数
         * @param output    输出器
         * @param advice    通知点
         * @param context   方法执行上下文(线程安全)
         */
        before: function (output, advice, context) {

        },

        /**
         * 方法返回回调函数
         * 在Java方法执行成功之后,Java方法返回之前执行该函数
         * @param output    输出器
         * @param advice    通知点
         * @param context   方法执行上下文(线程安全)
         */
        returning: function (output, advice, context) {

        },

        /**
         * 方法抛异常回调函数
         * 在Java方法内部执行抛异常之后,Java方法对外抛异常之前执行该函数
         * @param output    输出器
         * @param advice    通知点
         * @param context   方法执行上下文(线程安全)
         */
        throwing: function (output, advice, context) {

        },

    });
})

GREYS的内部支撑我都写入到了greys模块中,这个模块约定了一个watching函数

/**
 * 添加监听器,凡是被js命令所拦截到的方法都会流入到注册的监听器中
 * watching(listener)
 * @param listener 监听器
 */
watching: function (listener) {
    _listeners.push(listener);
},

watching函数接受一个参数listener,凡是被js命令所拦截到的方法都会流入到注册的监听器中,watching的主要作用就是注册这些监听器。

这里需要重点讲解下listener的5个函数中几个参数的含义。

  • output

    output参数作为脚本对客户端返回的输出器,他一共定义了3个函数

    /**
      * 输出字符串
      */
    function print(string);
    
    /**
      * 换行输出字符串
      */
    function println(string);
    
    /**
      * 结束脚本调用过程
      * 该方法执行之后将会主动结束整个脚本的执行
      */
    function finish();

    output对象被设计为链式执行的方式,所以你可以这样写

    output
       .print("hello ")
       .println("world!")
       .finish();
  • advice

    这个对象与watch命令的advice对象结构相同,这里不做过多阐述。你可以从watch命令的帮助中获取到方法执行的所有相关信息。

  • context

    context作为方法beforereturningthrowing三个函数之间传递的上下文,定义了2个主要函数。

    对于多线程调用的场景而言,不用担心context的线程安全问题,这个对象被设计为线程安全的。

    /**
      * 将K,V放入上下文
      */
    function put(string,object){
    
    }
    
    /**
     * 根据K从上下文中获取V
     */
    function get(string) {
    
    }

脚本存放位置

  • 本地文件存放

    本地文件存放目前仅支持目标JVM所在机器的本地文件绝对路径,要求目标JVM的用户拥有对脚本文件的读权限。

    不推荐相对路径,因为容器极有可能会更改JVM的相对路径根目录位置,所以为了减少大家不必要的麻烦,推荐绝对路径。

  • 远程HTTP/HTTPS存放

    当你精雕细琢了一个不错的脚本文件之后,希望共享给其他的同行。但不能每次都拷贝脚本过去呀,此时你就可以将脚本上传到提供HTTP/HTTPS访问的服务器上,我一般使用Github。

    js命令会检查script-path参数的开头是否是http/https,如检测到,将会驱动程序从网络上进行加载。

案例脚本

logger.js

  • 脚本目标

    设计一个日志脚本,用于拦截并记录下方法的耗时、入参、返回值、抛出异常等信息。

  • 脚本执行

    脚本内容相对文章内容颇多,如果全放上来有点喧宾夺主的嫌疑,所以这里设计为远程执行

    js *Test printAddress https://raw.githubusercontent.com/oldmanpushcart/images/master/greys/logger.js
    

    其中https://raw.githubusercontent.com/oldmanpushcart/images/master/greys/logger.js是我预先写好放在Github上的一个脚本,有兴趣的可以点击查看。

  • 执行结果

    ga?>js *Test printAddress https://raw.githubusercontent.com/oldmanpushcart/images/master/greys/logger.js
    Press Ctrl+D to abort.
    Affect(class-cnt:1 , method-cnt:1) cost in 50 ms.
    2016-02-11 14:35:52.101 com.alibaba.AgentTest printAddress : cost=1ms;params[8142];return[2];
    2016-02-11 14:35:53.229 com.alibaba.AgentTest printAddress : cost=0ms;params[8144];throwing[com.alibaba.AddressException: java.lang.RuntimeException: test];
    com.alibaba.AddressException: java.lang.RuntimeException: test
        at com.alibaba.manager.DefaultAddressManager.toStringPass1(DefaultAddressManager.java:22)
        at com.alibaba.manager.DefaultAddressManager.toString(DefaultAddressManager.java:15)
        at com.alibaba.AgentTest.printAddress(AgentTest.java:80)
        at com.alibaba.AgentTest.access$300(AgentTest.java:7)
        at com.alibaba.AgentTest$3.run(AgentTest.java:53)
    Caused by: java.lang.RuntimeException: test
          at com.alibaba.manager.DefaultAddressManager.throwRuntimeException(DefaultAddressManager.java:39)
          at com.alibaba.manager.DefaultAddressManager.toStringPass2(DefaultAddressManager.java:29)
          at com.alibaba.manager.DefaultAddressManager.toStringPass1(DefaultAddressManager.java:20)
          ... 4 more
    
    2016-02-11 14:35:54.292 com.alibaba.AgentTest printAddress : cost=0ms;params[8145];return[1];
    2016-02-11 14:35:55.310 com.alibaba.AgentTest printAddress : cost=0ms;params[8146];return[2];
    

聊聊脚本的并发

也许熟悉JavaScript的人会比较疑惑,ECMAScript-262中并未定义关于多线程、并发的支持,同样的JavaScript语法、标准库和各个扩展库中也都从未定义关于锁、多线程的支持。但GREYS所编写的脚本会被Java的多线程并发调用,哪我们所编写的GREYS脚本是否线程安全的呢?

在JDK所带的ScriptEngine实现中,称为JSR-223。JDK6/7所带的javascript语言实现引擎默认是Rhino,JDK8/9则是Nashorn。

JSR-223对多线程并发有一个规定,并不要求所有ScriptEngine实现一定要支持并发,但需要有一个参数反馈上来。

可以通过这样获取到

engineFactory.getParameter(“THREADING”);
类型 说明
null 引擎实现不是线程安全的,并且无法用来在多个线程上并发执行脚本
"MULTITHREADED" 引擎实现是内部线程安全的,并且脚本可以并发执行,尽管在某个线程上执行脚本的效果对于另一个线程上的脚本是可见的。
"THREAD-ISOLATED" 该实现满足 "MULTITHREADED" 的要求,并且引擎为不同线程上执行的脚本中的符号维护独立的值。
"STATELESS" 该实现满足 "THREAD-ISOLATED" 的要求,此外,脚本执行不改变 Bindings 中的映射关系,该 Bindings 是 ScriptEngine 的引擎范围。具体来说,Bindings 及其关联值中的键在执行脚本之前和之后是相同的。|

以上表格引用自jsr223-Java中的script引擎接口

脚本并发支持的变量作用域范围可以参考下图

GREYS-JS-MULTITHREADED

所以可以放心的在脚本中实现统计类的功能,而不用担心各个脚本的变量作用域污染的问题,理解如同各个GREYS的命令相互隔离一般。

聊聊BMD规范

目前CJS的实现在国内常用的规范有两个

引用自SeaJs作者 @玉伯 在知乎 AMD 和 CMD 的区别有哪些?的回答

AMD和CMD最大的区别就在于对于依赖的模块,AMD是提前执行,CMD是延迟执行。我个人比较推崇AMD规范的提前加载原则,即

define(['json'],function(json){

    // 在这一步json模块已经加载完成
    
})

因为我的个人理念是在写代码的时候应该更关注于逻辑的实现,所有的依赖加载应该都依托于框架来隐性完成,业务代码逻辑不应该感知到框架的实现。

BMD规范

套用上边的解释格式说就是,BMD是gblocking.js在推广过程中对模块定义的规范化产出。gblocking.js是我为了在GREYS上支持CJS所重复造的半个轮子。

代码见gblocking.js

重复造的半个轮子

既然你推崇AMD规范,为啥不直接使用AMD规范的标准实现RequireJS,而是自己造一个轮子?

BMD全称是BlockingModuleDefine,很明显,这里对模块的加载是同步的。

我并不需要异步加载这些模块,异步对GREYS来说是致命的。原因很简单,GREYS的定位是问题排查工具,所以自身应该降低对现有JVM线程的影响,异步加载对前端浏览器的渲染逻辑是有利的(因为现有JS引擎都是单线程,而且JS规范中也没有定义约束多线程),考虑到跨平台运行的要求,我对所有模块的加载都坚持是同步完成,而且是在js命令初始化的线程中完成所有模块的同步加载

Rhino在1.7-RC3之后才支持CommonJS,Nashorn也能支持(测试通过),但我的目标是跨JVM6+版本中完美运行,所以我需要考虑到JVM之间的兼容性问题。

当然我也可以让GREYS自己带一个Rhino的实现,只增加2M的空间(GREYS才3M)。但Rhino相比Nashorn而言,性能差了好几个数量级。究其原因就在于Nashorn使用了JVM7中新的字节码invokedynamics,所以能玩的花样和性能都不可同日而语。

同样的,因为Nashorn依赖了JVM7的规范,所以并不能在JVM6中依赖上Nashorn的Jar包(他也没打)。而GREYS需要支持JVM6的版本。在这个上边最终权衡下,我基于JVM6所带的Rhino引擎支持的ES5规范下,自己实现和造了这个轮子。

关键是,这个轮子并不难,我就花了1个通宵。

RquireJS虽然也号称能通吃前端(浏览器)和后端(Node.js、Rhino、Nashorn)但他并不是原生的支持后端,而是需要进行一个变种版本r.js,他的实现在https://github.com/jrburke/r.js,但这个实现太夸张和丑陋,未压缩竟然有1.14M

为啥说是半个轮子?

之所以说gblocking.js是半个轮子,主要的原因在于,我并未完整的实现整个CJS规范,而是选择性的实现了部分我所需要和关注的。

未实现的部分主要是在配置上,比如我并不需要baseUrlpackagesshim等这些配置项,所以这些我都直接忽略了。

BMD规范的范例

标准推荐写法

  • 配置依赖

    require({
        paths: {
            json: 'http://cdnjs.cloudflare.com/ajax/libs/json3/3.3.2/json3.min.js',
            stream: 'http://cdn.jsdelivr.net/stream.js/latest/stream.min.js',
        }
    })

    作为例子,我使用了两个支持AMD规范的模块:stream.jsjson3,作为例子输出,大家可以简单忽略她们是做啥的。

  • 定义依赖

    为了完成例子,我们还需要一个可以输出到Java终端的模块console

    /**
     * 定义了一个console模块
     * 简单实现,不要吐槽
     */
    define('console', function () {
    
        function print(string) {
            java.lang.System.out.print("" + string);
        }
    
        function println(string) {
            print(string + '\n');
        }
    
        return {
            log: function (msg) {
                println(msg);
            }
        }
    })
  • 开始使用

    接下来是重点,我们开始组合使用stream.jsjson3和我们自己定义的console这三个模块

    require(['json', 'stream', 'console'], function (json, stream, console) {
    
        var s = stream.make(
            {
                name: 'vlinux',
                email: 'oldmanpushcart@gmail.com',
            },
            {
                name: 'dukun',
                email: 'dukun@taobao.com',
            }
        );
    
        while (!s.empty()) {
            console.log(json.stringify(s.head()));
            s = s.tail();
        }
    })

    脚本的输出如下

    {"email":"oldmanpushcart@gmail.com","name":"vlinux"}
    {"email":"dukun@taobao.com","name":"dukun"}