ServiceNow – Script IncludeにおけるgetParameter()の戻り値はJavaオブジェクトなので型変換が必要

開発・導入

早速ですが問題です。Client callableなScript Includeの次のコードはうまく動きません。何が問題でしょう?なお、nullチェックをしていないのは簡単のためです。

// Client callable Script Include内の関数
serverFunc: function() {
    var sysids = this.getParameter('sysparm_sysids'); // クライアント側からはカンマ区切りのSysIDを文字列で渡している
    var sysid_list = sysids.split(','); // 文字列を分割してリスト化 ★ココが期待通り動かない
},
// 呼び出し側のクライアントサイドスクリプト
var ga = new GlideAjax('<Script Include name>');
ga.addParam('sysparm_name', 'serverFunc');
ga.addParam('sysparm_sysids', <SysIDをカンマ区切りで繋いだ文字列>);
ga.getXMLAnswer(callback, null, sysid);

答えは、Script IncludeにおけるgetParameter()の戻り値はJavaScriptのStringではなくJavaオブジェクトであるため、String()でJavaScriptのStringに変換してからでないとsplit()メソッドが想定通り動かないからです。

結論はこれだけなのですが、マニアに向けてもう少し深堀りして解説したいと思います。解決方法だけ知りたい方は下記のナレッジ記事をご覧ください。
KB0826476: Calling getParameter in GlideAjax returns an object and not a string

Script Includeの基本的な部分について知り方かった方はこちらをご覧ください。

Script IncludeのgetParameter()で取得できる引数はJavaのStringオブジェクト

上述の通り、Script IncludeにおけるgetParameter()の戻り値はJavaScriptのStringではなくJavaオブジェクトです。確かめてみます。

// Client callable Script Include内の関数
serverFunc: function() {
    var arg1 = this.getParameter('sysparm_arg1'); // 引数を取得
		
    var arg1_type = Object.prototype.toString.call(arg1);
    gs.info("[Script Include Parameter Test] typeof arg1: " + typeof arg1);
    gs.info("[Script Include Parameter Test] Object.prototype.toString.call(arg1): " + arg1_type);

		gs.info("[Script Include Parameter Test] arg1: " + arg1);
		gs.info("[Script Include Parameter Test] arg1.split(','): " + arg1.split(','));
},
var ga = new GlideAjax('global.MyClientCallableScriptInclude');
ga.addParam('sysparm_name', 'serverFunc');
ga.addParam('sysparm_arg1', 'string,from,client-side');
ga.getXMLAnswer(callback, null, null);

結果は次の通りです。まずわかるのは、こいつはJavaScriptの文字列ではなくJavaオブジェクトであるということです。さらに、文字列として出力すると正しく取得できるが、split()メソッドを呼び出すと、Javaオブジェクト自体を表す文字列が表示されてしまっています。当然ながらこれは期待した挙動ではありません。

[Script Include Parameter Test] typeof arg1: object
[Script Include Parameter Test] Object.prototype.toString.call(arg1): [object JavaObject]
[Script Include Parameter Test] arg1: string,from,client-side
[Script Include Parameter Test] arg1.split(','): [Ljava.lang.String;@67b3a6

普通に使う分には問題にならないことが多い

上述の例の通り、単純に文字列として使う分には特に問題になりません。ですので次に示すような「クライアントサイドからサーバーサイドにSysIDを渡し、サーバーサイドでSysIDからレコードを取得して処理する」というよくあるパターンではデータ型を意識することはないでしょう。

// こんな使い方なら、特に問題なし
serverFunc: function() {
    var user_sysid = this.getParameter('sysparm_user_sysid'); // 引数を取得
    var user = new GlideRecord('sys_user');
    if (user.get(user_sysid)) {
        // 処理
    }
},

とはいえ、この記事ではより良い・正しいコードを書く方法を考えます。

Javaオブジェクト to JavaScriptのStringへの変換方法

getParameter()はJavaオブジェクトを返すので、正しいコードを書くためには、まずJavaScriptのStringに変換してあげなければなりません。これには2つの方法が考えられます。

String()を使う

下記のナレッジ記事でも紹介されている方法は、String()関数を使ってJavaScriptのStringに変換する方法です。実際にやってみます。
KB0826476: Calling getParameter in GlideAjax returns an object and not a string

serverFunc: function() {
    var arg1 = this.getParameter('sysparm_arg1'); // 引数を取得

    var arg1_jsstr = String(arg1);
    var arg1_jsstr_type = Object.prototype.toString.call(arg1_jsstr);
    gs.info("[Script Include Parameter Test] typeof arg1_jsstr: " + typeof arg1_jsstr);
    gs.info("[Script Include Parameter Test] Object.prototype.toString.call(arg1_jsstr): " + arg1_jsstr_type);
        
    gs.info("[Script Include Parameter Test] arg1_jsstr: " + arg1_jsstr);
    gs.info("[Script Include Parameter Test] arg1_jsstr.split(''): " + arg1_jsstr.split(',').toString());
},

結果は次の通りです。無事JavaScriptのStringに変換されています。

[Script Include Parameter Test] typeof arg1_jsstr: string
[Script Include Parameter Test] Object.prototype.toString.call(arg1_jsstr): [object String]
[Script Include Parameter Test] arg1_jsstr: string,from,client-side
[Script Include Parameter Test] arg1_jsstr.split(''): string,from,client-side

「ハイハイ。じゃあgetParameter()するときは毎回String()で変換してから使えばいいのね」と言いたくなりますが、これだとまだ足りません

// ダメな例
var arg1 = String(this.getParameter('sysparm_arg1'));

なぜかというと、引数が必須でない場合、まずは if (arg1) などとして引数の存在チェックを行うと思いますが、このとき先にString()を使うと、String()はnullを”null”という文字列に変換してしまうため、引数が存在しなかった場合(.getParameter()がnullを返した場合)でも、常に if (arg1) がtrueになってしまうのです。具体的なコードを示します。

// ダメな例
var arg1 = String(this.getParameter('sysparm_arg1')); // getParameter()がnullを返した場合でも"null"という有効な文字列になるため、
if (arg1) { // ここが常にtrueになってしまう
    // 引数が存在する場合の処理
}

ですので、より適切には次のようにすべきです。空文字列とnullを区別する必要がなければ、単に if (arg1) でもよいでしょう。

var arg1 = this.getParameter('sysparm_arg1');
if (arg1 != null) arg1 = String(arg1); // 存在チェックをしてから型変換
if (arg1) {
    // 引数が存在(かつ空文字列ではない)する場合の処理
}

.getParameter()を2回呼ぶためオーバーヘッドはありますが、引数取得を1行で書くなら次のようになります。

var arg1 = this.getParameter('sysparm_arg1') != null ? String(this.getParameter('sysparm_arg1')) : null;
if (arg1) {
    // 引数が存在する(かつ空文字列ではない)場合の処理
}

引数として”null”という文字列が来ることはまずないと思うので、その前提を置けるのであれば、次のようにしてもよいでしょう。前半の arg1 && は空文字列を考慮しています。

var arg1 = String(this.getParameter('sysparm_arg1'));
if (arg1 && arg1 != "null") {
    // 引数が存在する(かつ空文字列ではない)場合の処理
}

(おまけ)j2jsを使う

マニア向けの記事ということで、KBではString()といわれているが、ServiceNow的な正解はこっちなのでは、と個人的に思っている方法も紹介しちゃいます。

ServiceNowの標準APIには、getParameter()以外にもJavaのオブジェクトを返す関数があります。このような場合に、JavaオブジェクトをJavaScriptのオブジェクトに変換するための関数を提供する”j2js”というScript Includeが標準で存在します。

使い方は簡単で、gs.include(“j2js”); でインクルードして、j2js()関数にJavaオブジェクトを渡すだけです。

serverFunc: function() {
    gs.include("j2js");
    var arg1 = this.getParameter('sysparm_arg1'); // 引数を取得
    var arg1_jsstr = j2js(arg1); // 型変換
}

この関数はnullの考慮もされているため、String()と違いgetParameter()の戻り値をそのまま渡すことができます

この関数は、API Referenceにも記載がある公式で利用が許容されている機能で、かつ無駄な関数呼び出しがなく1行で変換が可能なのですが、こいつにも欠点があり、j2js Script IncludeはGlobal scope onlyになってます。そのためScopedなScript Includeから使う場合は、GlobalのScript Includeを経由する必要があり、それはそれでめんどくさいという問題があります。そのため、紹介しておいてなんですが、後述のサンプルコードではString()を採用しています。

toString()は使わない

また、toString()を使えば、オーバーヘッドもなく、1行で次のように書けるのでは?と思った方がいるかもしれません。

var arg1 = this.getParameter('sysparm_arg1').toString();

しかし、Javaオブジェクトに対して.toString()を呼び出しても、JavaScriptのStringにはなりません。データ型がどうなるかを見てみましょう。

serverFunc: function() {
    var arg1 = this.getParameter('sysparm_arg1'); // 引数を取得

    var arg1_tostring = arg1.toString();
    var arg1_tostring_type = Object.prototype.toString.call(arg1_tostring);
    gs.info("[Script Include Parameter Test] typeof arg1_tostring: " + typeof arg1_tostring);
    gs.info("[Script Include Parameter Test] Object.prototype.toString.call(arg1_tostring): " + arg1_tostring_type);
},

結果は次の通りです。

[Script Include Parameter Test] typeof arg1_tostring: object
[Script Include Parameter Test] Object.prototype.toString.call(arg1_tostring): [object JavaObject]

そのため、toString()は使えません。

getParameter()のサンプルコード

結論としてgetParameter()をどう使うべきかを表すサンプルコードを貼っておきます。下記のようにして取得すると、クライアントサイドで指定した引数の文字列(空文字列を含む)が正確にサーバーサイドに伝わり、かつクライアントサイドで引数を指定しない場合はnullになります。

serverFunc: function() {
    // retrieve arguments
    var arg1 = this.getParameter('sysparm_arg1');
    if (arg1 != null) arg1 = String(arg1); // convert Java Object from getParameter() to JS String
    var arg2 = this.getParameter('sysparm_arg2');
    if (arg2 != null) arg2 = String(arg2); // convert Java Object from getParameter() to JS String
    var arg3 = this.getParameter('sysparm_arg3');
    if (arg3 != null) arg3 = String(arg3); // convert Java Object from getParameter() to JS String

    // process with arg1, arg2, arg3
}

以上です。

コメント

タイトルとURLをコピーしました