Java: JDK9 jshellを使ってみたメモ
jdk9でjshellが追加されました。
簡単なバッチプログラムをjavaで書くことが多いのですが、コンパイルしたclassファイルよりも、テキストのjavaコードをそのまま実行するほうが好都合な時があります。
例えば、後で変更が見えてるような場合、柔軟に変更に対応できる設定ファイルの仕様を考えるより、プログラムファイル自体をテキストで用意する方が簡単です。
テキストのjavaコードを実行する仕組みとして、BeanShellを使っていましたが、使い勝手が良ければ今後はjshellをメインに使っていこうかと思っています。
以下、jshellをバッチ処理用途として使ってみてわかったことのメモになります。
コマンド形式
jshell -s --execution local <スクリプトファイル>
スクリプトファイルを実行する場合、最小限のコマンドは上記となります。
jshell -h とうつことで、コマンドの引数が一覧でわかります。
最小限として -s と --execution local は必須としました。
-sを指定することでフィードバックを最小限としています。
そして、--execution local を指定することで、スクリプトの戻り値を取得できるようにしています。--execution local を指定しない場合、スクリプト内でSystem.exit(1); としてもjshellを完了できません。(これ以降 --execution localの前提での記載となります)
さらにclasspathを追加する場合は、--class-path を使います。
スクリプトへ引数を渡す
jshellコマンドには、スクリプトファイルへ引数を渡す仕組みがありません。
システム変数か、環境変数で渡すことになります。
rem 1.環境変数の場合
set envvar1=test1
jshell -s --execution local
-> System.out.println( System.getenv("envvar1"));
test1
rem 2.システム変数の場合
jshell -s --execution local -J-Dpropvar1=var2
-> System.out.println( System.getProperty("propvar1"));
var2
スクリプトファイルの文字コード
jshellの引数のスクリプトファイルは、Windowsであれば、Windows-31Jとなりますが、どうしてもutf8で実行したい場合は、システム変数で指定します。
jshell -s --execution local -J-Dfile.encoding=utf8 <スクリプトファイル>
ただし、utf8のスクリプトファイルがBOM有だとエラーが出ます。
vm引数を指定する
メモリサイズ(-Xms -Xmx)などや -server オプションなどは、-J のオプションで指定します。既に上記でシステム変数として使っていますけど。。
jshell -s --execution local -J-server -J-Xmx3G
--execution local を指定しているので -J のオプションですが、--execution local を指定しない場合、-R のオプションで指定します。
※--execution localを指定しないと、jshellの中でさらに別のvmを起動するようで、そのvmへのオプションは -R として指定するようです。
スクリプトの戻り値を取得する
--execution local を指定してjshellを起動し、System.exitで終了します。
jshell -s --execution local
-> System.exit(5);
echo %ERRORLEVEL%
5
スクリプト構文
BeanShellと大体同じだなーと思いました。多少違いがあって、スクリプト内の関数はstaticでは定義できなかったりします。BeanShellでは、ジェネリクスや可変引数が使用できませんでしたが、jshellでは使えるのが嬉しいです。
スクリプト内の関数は、呼び出す前に定義しておくことが必要、java.utilなどは最初からimportされているのはBeanShellと同じです。
//サンプルスクリプト
public void func1(){
System.out.println("func1 called");
}
func1();
System.out.println(Arrays.asList("test1","test2","test3"));
System.exit(4);
スクリプト実行の性能
気になるのが、実行性能だと思います。jdk9でjrunscript , jshell BeanShell , コンパイル済みコードで比較してみたいと思います。
比較で使用したのは次のコードです。(フィボナッチ数列です。引数に40を指定しました)
public int fibonacci(int n) {
return ( n==1 || n==0 ) ? n:fibonacci(n-2) + fibonacci(n-1);
}
結果は次の表の通りとなり、jshellは、コンパイル済みjavaとほぼ同じとなっています。(バイトコードを作成する仕組みなのかなと思いますが、詳しくことはわかりません)
BeanShellは、比較するととても遅いですが、性能を気にしなくていい時だけ使うので実用上はそこまで問題にはなりません。どちらかといえばnashornの性能の良さに目を見張りました。rhinoよりかなり良くなっていると思います。
| 実行プログラム | 所要時間 (ミリ秒) |
実行コード |
| java コンパイル済み |
661 |
|
| jshell | 672 |
|
|
jrunscript [nashorn] |
1488 |
|
| BeanShell | 389118 |
|
※性能は、同じPCで同じJDK9で実施しました。
最後に
BeanShellだとジェネリクスや可変長引数が使えなかったりして構文エラーにつまづくことがあります。
最新のJava構文が利用できて、性能面も問題ないのでjdk9が普及してきたらjshellは積極的に使っていきたいなと思いました。でも当面はjdk8を使うと思うので、BeanShellを使っていきます。
Salesforce: apache httpclient / commons http を使ってREST APIでファイル(ContentVersion)を登録する
Salesforceへバイナリファイルを連携するには、通常はデータローダをはじめとするSOAP APIを使いbase64変換した文字列を送信します。
ファイル(ContentVersion)は2GBと大きいサイズでも登録が可能ですが、SOAP APIでは、Base64変換後のサイズで50MBのサイズ制限があります。
(Base64変換では1割以上サイズが増えるため物理サイズとしてはもっと小さくなります)
ですが、REST APIではその制限はなく50MBより大きなサイズでも登録できます。SOAP APIだとできなくて、REST APIだとできることの1つだと思います。
javaプログラムで、REST APIを使ったファイルを登録するサンプルを作りました。
また、サンプルでは、apache httpclient 4.5.3と、commons http 3.1を使ったプログラムを作成してみました。
apache httpclient 4.5.3を使ったサンプル
(さらに、SOAP API用にwsc、Json用にGsonを利用しています。)
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.util.HashMap;
import org.apache.http.HttpResponse;
import org.apache.http.client.HttpClient;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.entity.ContentType;
import org.apache.http.entity.mime.MultipartEntityBuilder;
import org.apache.http.impl.client.HttpClientBuilder;
import com.google.gson.Gson;
import com.sforce.soap.partner.Connector;
import com.sforce.soap.partner.LoginResult;
import com.sforce.soap.partner.PartnerConnection;
import com.sforce.ws.ConnectorConfig;
public class MainHTTPClient4 {
public static void main(String[] args) throws Exception{
ConnectorConfig cc = new ConnectorConfig();
cc.setManualLogin(true);
PartnerConnection pc = Connector.newConnection(cc);
LoginResult lr = pc.login("sampleuser@sample.user", "samplepassword");
String url =lr.getServerUrl();
String sessionId = lr.getSessionId();
HttpPost post = new HttpPost(url.substring(0, url.indexOf("/services")) + "/services/data/v41.0/sobjects/ContentVersion");
post.addHeader("Authorization", ":Bearer " + sessionId);
File file = new File("登録したいファイル");
HashMap<String, String> contentvalue = new HashMap<String, String>();
contentvalue.put("Title","sampletitle");
contentvalue.put("SharingOption","A");
contentvalue.put("SharingPrivacy","N");
contentvalue.put("PathOnClient",file.getName());
contentvalue.put("ContentLocation","S");
String jsonstr =new Gson().toJson(contentvalue);
MultipartEntityBuilder entity = MultipartEntityBuilder.create();
entity.addTextBody("entity_data", jsonstr,ContentType.APPLICATION_JSON);
entity.addBinaryBody("VersionData", file);
post.setEntity(entity.build());
HttpClient client = HttpClientBuilder.create().build();
HttpResponse response = client.execute(post);
ByteArrayOutputStream byteout = new ByteArrayOutputStream();
response.getEntity().writeTo(byteout);
System.out.println( new String(byteout.toByteArray()) );
}
}
commons http 3.1を使ったサンプル
(さらに、SOAP API用にwsc、Json用にGsonを利用しています。)
import java.io.File;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.HashMap;
import org.apache.commons.httpclient.HttpClient;
import org.apache.commons.httpclient.methods.PostMethod;
import org.apache.commons.httpclient.methods.multipart.FilePart;
import org.apache.commons.httpclient.methods.multipart.MultipartRequestEntity;
import org.apache.commons.httpclient.methods.multipart.Part;
import org.apache.commons.httpclient.methods.multipart.StringPart;
import com.google.gson.Gson;
import com.sforce.rest.ContentType;
import com.sforce.rest.RestConnection;
import com.sforce.soap.partner.Connector;
import com.sforce.soap.partner.LoginResult;
import com.sforce.soap.partner.PartnerConnection;
import com.sforce.ws.ConnectorConfig;
public class MainCommonsHttp3 {
public static void main(String[] args) throws Exception{
ConnectorConfig cc = new ConnectorConfig();
cc.setManualLogin(true);
PartnerConnection pc = Connector.newConnection(cc);
LoginResult lr = pc.login("sampleuser@sample.user", "samplepassword");
String url =lr.getServerUrl();
String sessionId = lr.getSessionId();
HttpClient client = new HttpClient();
PostMethod post = new PostMethod(url.substring(0, url.indexOf("/services")) + "/services/data/v41.0/sobjects/ContentVersion");
post.setRequestHeader("Authorization", ":Bearer " + sessionId);
File file = new File("登録したいファイル");
HashMap<String, String> contentvalue = new HashMap<String, String>();
contentvalue.put("Title","test1");
contentvalue.put("SharingOption","A");
contentvalue.put("SharingPrivacy","N");
contentvalue.put("PathOnClient",file.getName());
contentvalue.put("ContentLocation","S");
String jsonstr =new Gson().toJson(contentvalue);
StringPart contentrec = new StringPart("entity_data",jsonstr);
contentrec.setContentType("application/json");
FilePart fp = new FilePart("VersionData", file);
fp.setContentType("application/excel");
fp.setCharSet("binary");
Part[] parts = new Part[] { contentrec,fp };
post.setRequestEntity(new MultipartRequestEntity(parts, post.getParams()));
client.executeMethod(post);
System.out.println( new String(post.getResponseBody()) );
}
}
最後に
実はcommons http3.1だけでサンプルプログラムを作って試していたのですが、commons http3.1って相当古いよなーと思い、新しいライブラリで試したので2つのサンプルコードとなっています。
commons http3.1とapache httpclient 4.5.3では、バージョンアップとかそういう次元ではなく別物なんですね。。そもそも名前が変わっていますし。apache httpclient 4.5.3の方が心なしかすっきりしているような気がします。
また、REST APIなら大き目のファイルをアップロードできますと書いてはみたもの1GB とかのファイルでは試していません。(200MB程度のファイルは登録してみました。)
後、画面から登録するより多少遅い気がします。
Salesforce: Rest API で思ったこと
SalesforceのRest APIで思ったことのメモです。
まずSalesforceのRest APIは次の特徴を持ちます。(個人的に思っていることです。。詳細な仕様はSalesforceのヘルプを参照ください)
httpsの通信ができれば、SalesforceとAPIのやり取りが可能
TSL1.1以上でという制限はありますが、httpsの通信ができれば、SOAP APIのようにwsdlをソースコードに展開する仕組みがなくても簡単に利用できます。
なので、VBScriptや、Excelマクロ、PowerShell(Invoke-RestMethod
)から簡単に利用できます。
REST API以外のSalesforceのAPIを利用するには、ログインURLとログイン情報(ユーザ、パスワード)を指定してログインし、ログイン後Salesforceから返却されるインスタンスURLとセッションIDを使用してメソッドをコールします。
REST APIでも「https://login.salesforce.com/services/oauth2/authorize?response_type=code&client_id=XXX&redirect_uri=YYY&state=mystate」というREST APIでの認証が用意されています。REST APIでの認証をするためには、コンシューマ鍵を別途設定して取得する必要があります。
ですが、認証後で必要な情報はセッションID(RESTではaccess_tokenという名前)とインスタンスURLなので、無理にコンシューマ鍵を用意してREST APIで認証する必要はありません。通常のSOAP APIでログインして、セッションIDとインスタンスURLを取得すればいいだけです。
APIの送受信の形式はJSON文字列かXMLか選べる
REST APIのリクエストとレスポンスの形式はJSON文字列かXMLを選べます。JSONのパーサーが利用できればJSONの方が使いやすいと思います。
デフォルトの形式はJSONですが、リクエストヘッダに「Accept: application/xml」とするか、リクエストのURLに「.xml」の拡張子をつけることでXMLに変更できます。
(URLでXMLを指定する例:https://apX.salesforce.com/services/data/v41.0/query.xml?q=~)
REST APIでのみ提供されている機能がある
SOQLやビューの実行プランはREST APIでのみ取得可能という記事を書いています。(記事内容は実行プランを取得するExcelマクロを作成したという内容です)
それ以外にも、大きなバイナリデータを取得する時は、SOAP APIよりREST APIの方が適している時があります。
静的リソース(StaticResource)、ドキュメント(Document)、添付ファイル(Attachment)は登録できるサイズが制限されているので、SOAP APIで取得できますが、ファイル(ContentVersion)やイベントログ(EventLogFile)は、サイズが大きくなってくるとSOAP APIで取得できない場合があります。
そんな時は、REST APIでバイナリデータを取得することになります。
例:ファイルの取得
https://apX.salesforce.com/services/data/v41.0/sobjects/ContentVersion/対象のSalesforceID/VersionData
例:イベントログの取得
https://apX.salesforce.com/services/data/v41.0/sobjects/EventLogFile/対象のSalesforceID/LogFile
上記のREST APIを実行すると、HTTPのレスポンスをストリームで受け取れるので大きなバイナリデータもファイルに出力することができます。
最後に
JavaでREST APIを扱うサンプルプログラムを書いておきたいと思います。
ちょっと長いのですが、通常のhttpsの通信だけでSalesforceのファイル(ContentVersion)からファイルを取得する処理です。
import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.FileOutputStream;
import java.io.InputStream;
import java.io.OutputStreamWriter;
import java.net.HttpURLConnection;
import java.net.URL;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import org.w3c.dom.Document;
public class RestSample {
public static void main(String[] args) throws Exception{
String loginendpoint = "https://login.salesforce.com/services/Soap/u/41.0";
String loginusername = "sampleuser@sample.user";
String loginpassword = "samplepassword";
String loginxml =
"<?xml version="1.0" encoding="UTF-8"?><env:Envelope " +
" xmlns:env="http://schemas.xmlsoap.org/soap/envelope/" xmlns:xsd="http://www.w3.org/2001/XMLSchema" " +
" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"> " +
" <env:Body> " +
" <m:login xmlns:m="urn:partner.soap.sforce.com" xmlns:sobj="urn:sobject.partner.soap.sforce.com"> " +
" <m:username>#sfusername#</m:username> " +
" <m:password>#sfpassword#</m:password> " +
" </m:login> " +
" </env:Body> " +
"</env:Envelope> " ;
URL url = new URL(loginendpoint);
HttpURLConnection loginhttp = (HttpURLConnection)url.openConnection();
loginhttp.setRequestProperty("Content-Type", "text/xml; charset=utf-8");
loginhttp.setRequestProperty("SOAPAction", "login");
loginhttp.setRequestMethod("POST");
String loginenv = loginxml.replace("#sfusername#", loginusername);
loginenv = loginenv.replace("#sfpassword#", loginpassword);
loginhttp.setDoOutput(true);
OutputStreamWriter out = new OutputStreamWriter(loginhttp.getOutputStream());
out.write(loginenv);
out.close();
loginhttp.connect();
final int status = loginhttp.getResponseCode();
if (status != HttpURLConnection.HTTP_OK) {
loginhttp.disconnect();
throw new Exception("通信エラーが発生 ResponseCode:" + status);
}
InputStream in = loginhttp.getInputStream();
DocumentBuilder doc = DocumentBuilderFactory.newInstance().newDocumentBuilder();
Document document = doc.parse(in);
in.close();
loginhttp.disconnect();
String serverURL = null;
String sessionId = null;
try{
sessionId = document.getElementsByTagName("sessionId").item(0).getTextContent();
serverURL = document.getElementsByTagName("serverUrl").item(0).getTextContent();
} catch(Exception e){
throw new Exception("ログインで失敗:" + e);
}
String instanceurl = serverURL.substring(0, serverURL.indexOf("/services"));
String resturl = instanceurl + "/services/data/v40.0/sobjects/ContentVersion/対象のSalesforceID/VersionData";
HttpURLConnection resthttp = (HttpURLConnection)new URL(resturl).openConnection();
resthttp.setRequestProperty("Content-Type", "application/octet-stream");
resthttp.setRequestProperty("Authorization", ":Bearer " + sessionId);
resthttp.setRequestMethod("GET");
resthttp.connect();
InputStream responseStream = resthttp.getInputStream();
BufferedInputStream bStream = new BufferedInputStream(responseStream);
BufferedOutputStream bFileStream = new BufferedOutputStream(new FileOutputStream("保存するファイル名"));
while(true){
int val = bStream.read();
if(val == -1){
break;
}
bFileStream.write(val);
}
bStream.close();
bFileStream.close();
}
}
補足ですが、ContentVersionは、自分の参照できるファイルしかアクセスできません。例えば他の人がアップロードしたファイルはシステム管理者であってもContentVersionからは取得できません。
Chatterファイルは、自分が参加しているChatterGroupであれば取得可能です。
Salesforce: ログインユーザが利用可能なレコードタイプの一覧を取得する
レコードタイプの選択を、Visualforceで行いたい時があります。
標準のレコードタイプ選択画面であれば、ログインしているユーザのプロファイルに応じたリストが表示されますが、 そのリストをApexで作成する場合のメモです。
[サンプルコード] ※Sample__cは適宜置き換えてください。
List<RecordTypeInfo> rtlist = Sample__c.SObjectType.getDescribe().getRecordTypeInfos();
List<SelectOption> rtoptions = new List<SelectOption>();
for( RecordTypeInfo rt : rtlist ){
//System.debug(' getName() : ' + rt.getName() +
// '/ getRecordTypeId(): ' + rt.getRecordTypeId()+
// '/ isActive(): ' + rt.isActive() +
// '/ isAvailable(): ' + rt.isAvailable() +
// '/ isMaster: ' + rt.isMaster());
if( rt.isMaster() == false && rt.isAvailable() == true ){
rtoptions.add( new SelectOption(rt.getRecordTypeId() , rt.getName() ) );
}
}
//System.debug(rtoptions);
上のコードでプロファイルに応じたリストが取得できます。
ポイントは次です。
- RecordTypeInfo.getName() は、翻訳が指定されていれば翻訳された後の値となります。
- RecordTypeInfo.isAvailable()で、プロファイルで利用可能かが判断できます。RecordTypeInfo.isActive()がfalseの場合は、RecordTypeInfo.isAvailable()は常にfalseとなります。
- RecordTypeInfo.isMaster()がtrueの場合、"マスタ"レコードタイプが取得できますが除外しています。
最後に
RecordTypeオブジェクトでは、RecordTypeInfo.isActive()までしか取得できず、ログインユーザが利用できるレコードタイプを一覧化するには、getRecordTypeInfos()を使う必要があります。
Schema.getGlobalDescribe()を使えば、文字列で指定したオブジェクトの利用可能なレコードタイプ一覧を取得する関数を作成することが可能になります。
Java: XML中の制御コードについて
XMLでは、制御コードは改行やタブなどの決められたもの以外は許可されていません。(こちらの説明が詳しいです)
なので、XMLで値をセットする際には許可されていない文字コードは除去する必要があります。(除去する方法はこの記事を参考にさせて頂きました)
そして、XMLを扱ってデータのやり取りを行うWebServiceでもこのXMLの許可されていない制御コードの対応が必要です。
以前JavaのWebServiceクライアントについて記載したことがありますが、それぞれについて許可されていない制御コードは次の通りの動きです。
| Java ライブラリ | XML中の許可されていない制御コード | 備考 |
| Axis1.4 | エラー | リクエスト用のXMLを作成する際に、許可されていない制御コードがあるとエラーとなる。 |
| Axis2 (xmlbeans) | エラー | リクエスト用のXMLを作成する際に、許可されていない制御コードがあるとエラーとなる。 |
| wsimport | エラー | リクエスト用のXMLを作成する際に、許可されていない制御コードがあるとエラーとなる。 |
| apache cxf | エラー | リクエスト用のXMLを作成する際に、許可されていない制御コードがあるとエラーとなる。 |
| wsc(Salesforce専用) | 除去する |
エラーとならない。 |
上記の通りとなり、Salesforce専用のwscは許可されていない制御コードを指定してもエラーとなりません。(wscを利用しているデータローダもエラーとなりません)
レガシーから移行したデータや、Web画面から入力されたデータには、垂直タブやシフトイン・シフトアウトなど入っている場合があります。
wsc以外のライブラリではデータの受け渡しで許可されていない制御コードは取り除く処理が必要です。
最後に
Salesforceとの連携プログラムを作成するのはwscが便利なのだとあらためて感じました。
といっても、wscも癖はあるので用途に応じて使い分けが必要だなと思います。
SalesforceからSalesforceに連携する場合は、wsc以外であれば、取得した値をそのままcreate、upsert、updateに使えばいいのですが、wscだけはint型はintへ、date型はdateへ、datetime型はCalendar、base64型はbinaryへ変換などの処理が必要になります。
連携プログラムを作る際の参考になれば幸いです。
Salesforce: 時間型(ベータ)が追加されている件と項目作成マクロの更新の件
Winter18のバージョンアップでカスタム項目の型に、時間型が追加されていますね。

まだベータですが、時分を入力する項目を次のように用意することが多いので便利になります。
・時分を入力するテキスト欄を用意
・HH:MMを保証する入力規則を用意
・もしくは時間の選択リスト、分の選択リストを用意
Apex Class では、Time Classが時間型に対応するようです。
後、以前ご紹介したExcelでカスタム項目を作成するマクロですが、ヘルプテキストと説明項目を追加しました。
crmprogrammer38.hatenablog.com
さすがに、API名、ラベル、型だけしか定義できないのはどうかと思い、改修をしました。
お試しください。
Java: 外部ライブラリの定数を参照する時は静的コンパイルに気をつけよう
他のライブラリを参照して開発する時に、参照先の定数を使うときがあります。
例えば、WebServiceのWSDLを展開したライブラリで、WebSerivceのendpointは定数として定義されていて、その定数を使いたい時があります。
開発しているプログラムでその定数を使う時に注意することのメモです。
結論としては、Javaの静的コンパイルを正しく理解した上で開発する必要があります。
例えば、次のようなライブラリがあります。
[Test2.jar]
public class Test2 {
public static final String str1 = "string1";
public static final String str2 = "string2";
public static String getStr2(){
return str2;
}
}
そして次のプログラムで上記のTest2.jarを呼び出します。
public class Test1 {
public static void main(String[] args) throws Exception{
System.out.println(Test2.str1);
System.out.println(Test2.getStr2());
}
}
実行結果は、当たり前ですが
string1
string2
となります。
ですが、この後Test2.jarを次のように更新します。
[Test2.jar]
public class Test2 {
public static final String str1 = "update:string1";
public static final String str2 = "update:string2";
public static String getStr2(){
return str2;
}
}
そして先ほどのTest1を再度コンパイルし直さないで実行すると実行結果は、
string1
update:string2
となります。
これはJavaの静的コンパイルという仕組みで、定数をプログラムで使用するとコンパイル時に定数が文字列として展開されるからになります。
参照先のライブラリの定数が変更される可能性がある場合、参照先の定数を使う箇所は次のような対応が考えられます。
①参照先のライブラリが更新されたら、常に参照元のコンパイルし直す。
②参照先のライブラリの定数を参照しないよう参照先でgetterを用意してもらい、getterを使用する。
③参照先のライブラリの定数を静的コンパイルされないようリフレクションで定数を取得する。
リフレクションの例 Test2.class.getField("str1").get(null);
①は確実です。ちゃんとantでビルドすれば問題ないですが、手間がかかります。(複数のjarが混在しているとなかなか大変になります)
②は参照先のライブラリの開発側に対応を依頼することになります。対応してくれればいいですが、何らかの要因で頼めない場合はこの方法はとれません。
③は最後の手段ですが、①の手段以外で、どうしても静的コンパイルを回避したいならこの方法になります。
最後に
定数って静的コンパイルされるんだよなーというのがわかっていれば事前に対応しておく、もしくは運用を考えておくということができますが、後から問題に直面すると原因特定に時間がかかってしまうものだと思います。