Google アカウントで Google API を利用 - google-api-services-gmail

前回はサービスアカウントを使う方法を試しましたが、今回は Google アカウントを使って Google API を利用してみます。

ソースは http://github.com/fits/try_samples/tree/master/blog/20150621/

はじめに

API 利用までの手順は次の通りです。

  • (1) クライアント ID を発行
  • (2) Google アカウントで API の利用を承認し、コードを取得
  • (3) (2) で取得したコードを使ってリフレッシュトークンを取得
  • (4) (3) で取得したリフレッシュトークンからアクセストークンを取得し API を利用

基本的に (2) と (3) は (API を利用する) Google アカウント毎に 1回だけ実施します。

(3) でアクセストークンも取得できますが、アクセストークンには有効期限があるため、通常は (4) でリフレッシュトークンからアクセストークンを取得し直します。

(1) クライアント ID を発行

Google Developers Console へログインし、ネイティブ アプリケーション用のクライアント ID を発行します。

  1. プロジェクトを選択
  2. APIと認証」の「認証情報」をクリック
  3. 「新しいクライアントIDを作成」をクリック
  4. 「インストールされているアプリケーション」を選択し、「クライアントIDを作成」 をクリック (アプリケーションの種類は "その他")
  5. JSON をダウンロード」をクリックしファイルを保存

保存した JSON ファイルは後の処理で使用します。

サービスアカウントで取得した JSON ファイルとは中身が異なりますのでご注意ください。

JSON ファイルの例
{
    "installed":{
        "auth_uri":"https://accounts.google.com/o/oauth2/auth",
        "client_secret":"・・・",
        "token_uri":"https://accounts.google.com/o/oauth2/token",
        "client_email":"",
        "redirect_uris":["urn:ietf:wg:oauth:2.0:oob","oob"],
        "client_x509_cert_url":"",
        "client_id":"・・・.apps.googleusercontent.com",
        "auth_provider_x509_cert_url":"https://www.googleapis.com/oauth2/v1/certs"
    }
}

(2) Google アカウントで API 利用を承認し、コードを取得

この作業は基本的に Web 画面を使って行います。

https://accounts.google.com/o/oauth2/auth?redirect_uri=<リダイレクト先URL>&response_type=code&client_id=<クライアントID>&scope=<利用するAPIのURL> へアクセスし、Google アカウントでログインして API の承認画面で 「承認」 ボタンをクリックします。

scope には承認する API の URL を %20 (半角スペースを URLエンコードしたもの) 区切りで指定します。

以下のように PhantomJS 等を利用し UI レスで実施する事も一応可能でしたが、Captcha の入力があった場合等には使えませんので実用性は低めだと思います。

approve_api.groovy
@Grab('org.gebish:geb-core:0.10.0')
@Grab('com.codeborne:phantomjsdriver:1.2.1')
import geb.Browser
import org.openqa.selenium.phantomjs.PhantomJSDriver
import org.openqa.selenium.remote.DesiredCapabilities

import groovy.json.JsonSlurper

def json = new JsonSlurper()
def conf = json.parse(new File(args[0])).installed

def userId = args[1]
def password = args[2]
// Gmail の API を承認するためのスコープ設定
def scope = [
    'https://mail.google.com/',
    'https://www.googleapis.com/auth/gmail.compose',
    'https://www.googleapis.com/auth/gmail.modify'
].join('%20')

def code = null

Browser.drive {
    setDriver(new PhantomJSDriver(new DesiredCapabilities()))

    def url = "${conf.auth_uri}?redirect_uri=${conf.redirect_uris[0]}&response_type=code&client_id=${conf.client_id}&scope=${scope}"

    go url
    // メールアドレス入力
    $('input[name="Email"]').value(userId)
    $('input[type="submit"]').click()

    // パスワード入力画面に変わるまで待機
    waitFor(30) { $('div.second div.slide-in').isDisplayed() }

    // パスワード入力
    $('input[name="Passwd"]').value(password)
    $('div.second input[type="submit"]').click()

    // API の承認ボタンが有効になるまで待機
    waitFor(30) { $('button[id="submit_approve_access"]').isDisabled() == false }

    // 承認ボタンをクリック
    $('button[id="submit_approve_access"]').click()

    def codeInput = waitFor(30) { $('input[id="code"]') }

    // コードを取得
    code = codeInput.value()

    quit()
}
println code

PhantomJS を実行できるように環境変数 PATH を設定し実行します。

コマンドライン引数の第1引数には (1) で保存した JSON ファイルを指定します。

実行例
> groovy approve_api.groovy client_secret.json xxxx@gmail.com ****
・・・

4/3vJ9・・・

(3) リフレッシュトークンの取得

(2) で取得したコードを使ってリフレッシュトークンを取得します。

https://www.googleapis.com/oauth2/v3/token へコードなどの情報を POST すれば、JSON データとしてアクセストークンとリフレッシュトークンを取得できます。

client_id, client_secret, redirect_uri の値には (1) で保存した JSON ファイル内の情報を使っています。

get_refresh-token.groovy
@Grab("org.apache.httpcomponents:httpclient:4.5")
import org.apache.http.client.entity.UrlEncodedFormEntity
import org.apache.http.client.methods.HttpPost
import org.apache.http.impl.client.HttpClientBuilder
import org.apache.http.message.BasicNameValuePair

import groovy.json.JsonSlurper

def json = new JsonSlurper()
def conf = json.parse(new File(args[0])).installed

def code = args[1]

def param = { name, value -> new BasicNameValuePair(name, value) }

def client = HttpClientBuilder.create().build()

def post = new HttpPost('https://www.googleapis.com/oauth2/v3/token')

post.entity = new UrlEncodedFormEntity([
    param('code', code),
    param('client_id', conf.client_id),
    param('client_secret', conf.client_secret),
    param('grant_type', 'authorization_code'),
    param('redirect_uri', conf.redirect_uris[0]) //urn:ietf:wg:oauth:2.0:oob
])

def res = client.execute(post)

// 実行結果(JSON)の出力
res.entity.writeTo(System.out)

実行結果は以下の通りです。

実行例
> groovy get_refresh-token.groovy client_secret.json 4/3vJ9・・・ > token.json

成功すると以下のような JSON を取得できます。

処理結果
{
 "access_token": "・・・",
 "token_type": "Bearer",
 "expires_in": 3600,
 "refresh_token": "・・・"
}

(4) アクセストークンの取得と Gmail API 利用

最後に GmailAPI を使ってメールの情報を取得してみます。

ここでは以下のモジュールを使いました。

リフレッシュトークンを GoogleCredentialsetRefreshToken で設定しておけばアクセストークンを自動的に取得して処理してくれるようです。

gmail_list.groovy
@Grab('com.google.apis:google-api-services-gmail:v1-rev31-1.20.0')
import com.google.api.client.googleapis.auth.oauth2.GoogleCredential
import com.google.api.client.googleapis.util.Utils
import com.google.api.services.gmail.Gmail

import groovy.json.JsonSlurper

def json = new JsonSlurper()

def conf = json.parse(new File(args[0])).installed
def token = json.parse(new File(args[1]))

def credential = new GoogleCredential.Builder()
    .setTransport(Utils.getDefaultTransport())
    .setJsonFactory(Utils.getDefaultJsonFactory())
    .setClientSecrets(conf.client_id, conf.client_secret)
    .build()
    .setRefreshToken(token.refresh_token) //リフレッシュトークンの設定

def gmail = new Gmail.Builder(
    Utils.getDefaultTransport(), 
    Utils.getDefaultJsonFactory(), 
    credential
).setApplicationName('sample').build()

// メールの情報を最大 3件取得
gmail.users().messages().list('me').setMaxResults(3).execute().messages.each {
    // メールの内容を minimal フォーマットで取得
    def msg = gmail.users().messages().get('me', it.id).setFormat('minimal').execute()
    println msg
}

実行結果は以下の通りです。

実行例
> groovy gmail_list.groovy client_secret.json token.json

[historyId:1000, ・・・ snippet:詳細な管理によって Google アカウントを保護・・・]
・・・

Google API 用のアクセストークンをサービスアカウントで取得 - Google API Client Library for Java

Google の各種 API を使うためのアクセストークンをサービスアカウントを使って取得してみました。

ライブラリは Google API Client Library for Java を使います。

ソースは http://github.com/fits/try_samples/tree/master/blog/20150608/

はじめに

今のところ Google API を使用するには以下のような方法があるようで、 今回は (1) の方法で行います。

番号 方法 用途 アクセストークンの取得方法 Google アカウントで API 利用
(1) サービスアカウント利用 サーバーアプリケーション JSON Web Tokens (JWTs) 制限あり
(2) Google アカウントで承認 Webサーバーアプリケーション, 組み込みアプリケーション Google アカウントで API 利用を承認 or リフレッシュトークン 可能

(1) の方法はログイン等を行わずに API を利用できる反面、API を自分の Google アカウントで利用したい場合には以下のような事をしなければならないようです。

どちらにしても、ドメインgmail.comGmail アカウント等へ成り代わるのは無理そうですし、サービスアカウントで使える API は限られるかもしれません。

※ 同じドメインに所属する全てのユーザーへ成り代わる権限を
   サービスアカウントへ付与する機能

(2) の方法は自分のアカウントで API を利用できますが、基本的に Web 画面でログインして API の利用を承認する必要があります。(ただし、初回のみ)

承認するとアクセストークン(有効期限あり)とリフレッシュトークン(有効期限なし)を取得できるので、次からはリフレッシュトークンでアクセストークンを取得し直す事ができます。

サービスアカウントの発行

まずは Google Developers Console へログインしサービスアカウントを発行します。

  1. プロジェクトを選択
  2. APIと認証」の「認証情報」をクリック
  3. 「新しいクライアントIDを作成」をクリック
  4. 「サービスアカウント」を選択し、「クライアントIDを作成」 をクリック (キータイプは JSON
  5. キーファイルを保存

JSON キーファイルの方が実装するコードが少なくなるので、今回は JSON キータイプ (デフォルト) を選んでいます。

アクセストークンの取得

JSON キーファイルを使ってアクセストークンを取得し、出力する処理は以下のようになります。

SampleApp.java
import com.google.api.client.googleapis.auth.oauth2.GoogleCredential;

import java.io.FileInputStream;
import java.util.Arrays;

public class SampleApp {
    public static void main(String... args) throws Exception {
        FileInputStream jsonKeyFile = new FileInputStream(args[0]);

        GoogleCredential credential = GoogleCredential.fromStream(jsonKeyFile)
                .createScoped(Arrays.asList("https://spreadsheets.google.com/feeds/"));
        // アクセストークンの取得
        credential.refreshToken();

        System.out.println("access token: " + credential.getAccessToken());
    }
}

createScoped を使ってアクセストークンで利用する API の URL を指定しています。 (今回は Google スプレッドシートAPI を指定)

refreshToken メソッドを実行する事でアクセストークンを取得します。

なお、refreshToken の実行前に getAccessToken メソッドを実行すると null を返します。

JSON キーファイルの代わりに P12 キーファイルを使う場合は GoogleCredential.Builder 等を使う事になります。

実行

Gradle による実行結果は以下の通りです。 (JSON キーファイルは sample.json です)

実行結果
> gradle -q run -Pargs=sample.json

access token:  yb17.iwR01J2W8Left-・・・

Gradle のビルド定義ファイルは以下の通りです。

build.gradle
apply plugin: 'application'

repositories {
    jcenter()
}

dependencies {
    compile 'com.google.api-client:google-api-client-java6:1.20.0'
}

mainClassName = 'SampleApp'

run {
    if (project.hasProperty('args')) {
        args project.args.split(' ')
    }
}

Google スプレッドシートAPI 利用 - REST

次は、アクセストークンを取得して Google スプレッドシートの単純な API を呼び出してみます。

スプレッドシート用のライブラリは使わず、Apache の HttpClient を使って https://spreadsheets.google.com/feeds/spreadsheets/private/full を GET しました。

アクセストークンは HTTP ヘッダー (Authorization) へ設定します。

SampleApp2.java
import com.google.api.client.googleapis.auth.oauth2.GoogleCredential;
import org.apache.http.HttpResponse;
import org.apache.http.client.HttpClient;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.impl.client.HttpClientBuilder;

import java.io.FileInputStream;
import java.util.Arrays;

public class SampleApp2 {
    private static final String SPREADSHEETS_FEED_BASE = 
            "https://spreadsheets.google.com/feeds/";
    private static final String SPREADSHEETS_FEED_FULL =
            SPREADSHEETS_FEED_BASE + "spreadsheets/private/full";

    public static void main(String... args) throws Exception {
        FileInputStream jsonKeyFile = new FileInputStream(args[0]);

        GoogleCredential credential = GoogleCredential.fromStream(jsonKeyFile)
                .createScoped(Arrays.asList(SPREADSHEETS_FEED_BASE));

        // アクセストークン取得
        credential.refreshToken();

        HttpClient client = HttpClientBuilder.create().build();

        HttpGet get = new HttpGet(SPREADSHEETS_FEED_FULL);
        // アクセストークンをヘッダーへ設定
        get.addHeader("Authorization", "Bearer " + credential.getAccessToken());

        HttpResponse res = client.execute(get);

        // 結果の出力
        res.getEntity().writeTo(System.out);
        System.out.println("");
    }
}

実行

実行結果は以下の通りです。

最初はサービスアカウントで使えるスプレッドシートが存在していないので以下のようになると思います。

実行結果1 (出力結果は見易いように加工しています)
> gradle -q run -Pargs=sample.json

<?xml version='1.0' encoding='UTF-8'?>
<feed xmlns='http://www.w3.org/2005/Atom' xmlns:openSearch='http://a9.com/-/spec/opensearchrss/1.0/'>
  <id>https://spreadsheets.google.com/feeds/spreadsheets/private/full</id>
  <updated>2015-06-07T11:58:26.141Z</updated>
  <category scheme='http://schemas.google.com/spreadsheets/2006' term='http://schemas.google.com/spreadsheets/2006#spreadsheet'/>
  <title type='text'>Available Spreadsheets - ・・・</title>
  <link rel='alternate' type='text/html' href='http://docs.google.com'/>
  <link rel='http://schemas.google.com/g/2005#feed' type='application/atom+xml' href='https://spreadsheets.google.com/feeds/spreadsheets/private/full'/>
  <link rel='self' type='application/atom+xml' href='https://spreadsheets.google.com/feeds/spreadsheets/private/full'/>
  <openSearch:totalResults>0</openSearch:totalResults>
  <openSearch:startIndex>1</openSearch:startIndex>
</feed>

Google スプレッドシートスプレッドシートを開き、「共有」をクリックして JSON キーファイルの client_email の値 (サービスアカウントのメールアドレス) を登録した後、再度実行してみると以下のようになりました。

実行結果2 (出力結果は見易いように加工しています)
> gradle -q run -Pargs=sample.json

<?xml version='1.0' encoding='UTF-8'?>
<feed xmlns='http://www.w3.org/2005/Atom' ・・・>
  ・・・
  <entry>
    <id>https://spreadsheets.google.com/feeds/spreadsheets/private/full/・・・</id>
    <updated>2015-06-07T11:46:41.314Z</updated>
    <category scheme='http://schemas.google.com/spreadsheets/2006' ・・・ />
    <title type='text'>sample</title>
    <content type='text'>sample</content>
    ・・・
  </entry>
</feed>

Gradle のビルド定義ファイルは以下の通りです。

build.gradle
apply plugin: 'application'

repositories {
    jcenter()
}

dependencies {
    compile 'com.google.api-client:google-api-client-java6:1.20.0'
    compile 'org.apache.httpcomponents:httpclient:4.5'
}

mainClassName = 'SampleApp2'

run {
    if (project.hasProperty('args')) {
        args project.args.split(' ')
    }
}

Gradle の起動スクリプト自動生成を無効化する - application プラグイン

Gradle の application プラグインにおける起動スクリプトの自動生成を無効化する方法です。

  • Gradle 2.4

ソースは http://github.com/fits/try_samples/tree/master/blog/20150607/

はじめに

application プラグインを使用して gradle build を実行すると、下記のような構成の zip と tar ファイルが build/distributions へ生成されます。

  • bin へ起動スクリプトを自動生成 (bat と bash 用)
  • lib へ各種 JAR ファイル ※ を配置
  • src/dist 内のディレクトリやファイルをそのままの構成で配置
※ ビルドによって生成された JAR と依存モジュールの JAR

例えば zip ファイルの中身は以下のようになります。

zip ファイル内の構成例
  • sample_app.zip
    • bin
      • sample_app
      • sample_app.bat
    • lib
      • sample_app.jar

起動スクリプトを自動生成してくれるのは非常に便利なのですが、自前で起動スクリプトを用意したい場合もあると思います。

src/dist/bin へ同名の起動スクリプトを用意する方法では、zip・tar ファイル内の bin に同名ファイルが 2重に登録されてしまいました。 (自動生成の方が後に追加される)

これでは不便なので自動生成を無効化する方法を探ってみました。

起動スクリプトの自動生成を無効化

結果的に startScripts.enabled へ false を設定すれば無効化できました。

build.gradle
apply plugin: 'application'

// 起動スクリプトの自動生成を無効化
startScripts.enabled = false
・・・

Java のアノテーションプロセッサで Haskell の do 記法のようなものを簡易的に実現3

Javaアノテーションプロセッサを使って下記と同等の機能を実現する試みの第三弾です。

  • Haskell の do 記法
  • Scala の for 内包表記
  • F# のコンピュテーション式

前回 のものを改良し、ようやく下記のような構文を実現しました。

Optional<String> res = opt$do -> {
    let a = Optional.of("a");
    let b = Optional.of("b");
    let c = opt$do -> {
        let c1 = Optional.of("c1");
        let c2 = Optional.of("c2");
        return c1 + "-" + c2;
    };
    return a + b + "/" + c;
};

ソースは http://github.com/fits/try_samples/tree/master/blog/20150516/

はじめに

環境

下記のような環境を使ってビルド・実行しています。

  • JavaSE Development Kit 8u45 (1.8.0_45)
  • Gradle 2.4

構文

前回 からの変更点は以下の通りです。

  • 対象のラムダ式JCLambda)を完全に置換し、Supplier を不要にした
  • let で入れ子に対応

対象のラムダ式を別の式 (JCMethodInvocation) で完全に置換し、Supplier を無くした事でまともな構文になったと思います。

変換前 (アノテーションプロセッサ処理前)
Optional<String> res = opt$do -> {
    let a = Optional.of("a");
    let b = Optional.of("b");
    let c = opt$do -> {
        let c1 = Optional.of("c1");
        let c2 = Optional.of("c2");
        return c1 + "-" + c2;
    };
    return a + b + "/" + c;
};
変換後 (アノテーションプロセッサ処理後)
Optional<String> res = opt.bind(
    Optional.of("a"), 
    (a) -> opt.bind(
        Optional.of("b"), 
        (b) -> opt.bind(
            opt.bind(
                Optional.of("c1"), 
                (c1) -> opt.bind(
                    Optional.of("c2"), 
                    (c2) -> opt.unit(c1 + "-" + c2)
                )
            ), 
            (c) -> opt2.unit(a + b + "/" + c)
        )
    )
);

また、変数への代入だけではなく、メソッドの引数にも上記構文を使えるようにしました。

メソッド引数としての使用例
System.out.println(opt$do -> {
    let a = Optional.of("a");
    let b = Optional.of("b");
    return "***" + b + a;
});

アノテーションプロセッサの実装

Processor の実装

前回 とほぼ同じですが、DoExprVisitor の extends 元を com.sun.tools.javac.tree.TreeScanner へ変えたので、accept メソッドの呼び出し箇所が多少変わっています。

なお、JCTree へキャストしていますが、JCCompilationUnit へキャストしても問題ありません。

src/main/java/sample/DoExprProcessor.java
package sample;

import java.util.Set;
import javax.annotation.processing.*;

import javax.lang.model.SourceVersion;
import javax.lang.model.element.Element;
import javax.lang.model.element.TypeElement;

import com.sun.source.tree.CompilationUnitTree;
import com.sun.source.util.Trees;
import com.sun.source.util.TreePath;

import com.sun.tools.javac.processing.JavacProcessingEnvironment;
import com.sun.tools.javac.tree.JCTree;
import com.sun.tools.javac.util.Context;

@SupportedSourceVersion(SourceVersion.RELEASE_8)
@SupportedAnnotationTypes("*")
public class DoExprProcessor extends AbstractProcessor {
    private Trees trees;
    private Context context;

    @Override
    public void init(ProcessingEnvironment procEnv) {
        trees = Trees.instance(procEnv);
        context = ((JavacProcessingEnvironment)procEnv).getContext();
    }

    @Override
    public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
        roundEnv.getRootElements().stream().map(this::toUnit).forEach(this::processUnit);
        return false;
    }

    private CompilationUnitTree toUnit(Element el) {
        TreePath path = trees.getPath(el);
        return path.getCompilationUnit();
    }

    private void processUnit(CompilationUnitTree cu) {
        if (cu instanceof JCTree) {
            ((JCTree)cu).accept(new DoExprVisitor(context));
            // 変換内容を出力
            System.out.println(cu);
        }
    }
}

TreeVisitor の実装

前回 からの変更点は以下の通りです。

  • (a) コード生成部分を別クラス化
  • (b) 対象のラムダ式JCLambda) を全置換
  • (c) メソッド引数への対応
  • (d) extends 元を com.sun.tools.javac.tree.TreeScanner へ変更 (前回までは com.sun.source.util.TreeScanner

(b) を実現するため changeNode へ置換処理 (JCLambdaJCMethodInvocation へ差し替える事になる) を設定するようにしました。

主な処理内容は次のようになっています。

  • (1) 変数定義(JCVariableDecl)やメソッド実行(JCMethodInvocation)の箇所で該当部分を差し替えるための処理を changeNode へ設定
  • (2) ラムダの内容からソースコードを生成 (対象外なら何もしない)
  • (3) ソースコードJCExpression へパースして (実体は JCMethodInvocationpos の値を調整
  • (4) changeNode を実行しラムダ箇所を差し替え
src/main/java/sample/DoExprVisitor.java
package sample;

import com.sun.tools.javac.parser.ParserFactory;
import com.sun.tools.javac.tree.JCTree;
import com.sun.tools.javac.tree.JCTree.*;
import com.sun.tools.javac.tree.TreeScanner;
import com.sun.tools.javac.util.Context;

import java.util.function.BiConsumer;
import java.util.stream.Stream;

public class DoExprVisitor extends TreeScanner {
    private ParserFactory parserFactory;
    private BiConsumer<JCLambda, JCExpression> changeNode = (lm, ne) -> {};
    private DoExprBuilder builder = new DoExprBuilder();

    public DoExprVisitor(Context context) {
        parserFactory = ParserFactory.instance(context);
    }

    @Override
    public void visitVarDef(JCVariableDecl node) {
        if (node.init != null) {
            // (b) (1)
            changeNode = (lm, ne) -> {
                // 変数への代入式を置換
                if (node.init == lm) {
                    node.init = ne;
                }
            };
        }
        super.visitVarDef(node);
    }

    // (c)
    @Override
    public void visitApply(JCMethodInvocation node) {
        if (node.args != null && node.args.size() > 0) {
            // (b) (1)
            changeNode = (lm, ne) -> {
                // メソッドの引数部分を置換
                if (node.args.contains(lm)) {
                    Stream<JCExpression> newArgs = node.args.stream().map(a -> (a == lm)? ne: a);
                    node.args = com.sun.tools.javac.util.List.from(newArgs::iterator);
                }
            };
        }
        super.visitApply(node);
    }

    @Override
    public void visitLambda(JCLambda node) {
        // (a) (2)
        builder.build(node).ifPresent(expr -> {
            // (3)
            JCExpression ne = parseExpression(expr);
            fixPos(ne, node.pos);

            // (b) (4) ラムダ部分を差し替え
            changeNode.accept(node, ne);
        });

        super.visitLambda(node);
    }

    // pos 値の修正
    private void fixPos(JCExpression ne, final int basePos) {
        ne.accept(new TreeScanner() {
            @Override
            public void scan(JCTree tree) {
                if(tree != null) {
                    tree.pos += basePos;
                    super.scan(tree);
                }
            }
        });
    }

    // 生成したソースコードをパース
    private JCExpression parseExpression(String doExpr) {
        return parserFactory.newParser(doExpr, false, false, false).parseExpression();
    }
}

コード生成処理の実装

該当のラムダ式を変換したソースコードを生成する処理です。

src/main/java/sample/DoExprBuilder.java
package sample;

import com.sun.tools.javac.tree.JCTree.*;

import java.util.HashMap;
import java.util.Map;
import java.util.Optional;

public class DoExprBuilder {
    private static final String DO_TYPE = "$do";
    private static final String VAR_PREFIX = "#{";
    private static final String VAR_SUFFIX = "}";
    // let 用のコードテンプレート
    private static final String LET_CODE = "#{var}.bind(#{rExpr}, #{lExpr} -> #{body})";
    // return 用のコードテンプレート
    private static final String RETURN_CODE = "#{var}.unit( #{expr} )";

    private Map<Class<? extends JCStatement>, CodeGenerator<JCStatement>> builderMap = new HashMap<>();

    public DoExprBuilder() {
        // let 用のコード生成
        builderMap.put(JCVariableDecl.class, (n, v, b) -> generateCodeForLet(cast(n), v, b));
        // return 用のコード生成
        builderMap.put(JCReturn.class, (n, v, b) -> generateCodeForReturn(cast(n), v, b));
    }

    public Optional<String> build(JCLambda node) {
        return getDoVar(node).map(var -> createExpression((JCBlock)node.body, var));
    }

    private String createExpression(JCBlock block, String var) {
        String res = "";

        for (JCStatement st : block.stats.reverse()) {
            res = builderMap.getOrDefault(st.getClass(), this::generateNoneCode).generate(st, var, res);
        }
        return res;
    }

    private String generateNoneCode(JCStatement node, String var, String body) {
        return body;
    }

    // let 用のソースコード生成
    private String generateCodeForLet(JCVariableDecl node, String var, String body) {
        String res = body;

        if ("let".equals(node.vartype.toString())) {
            Map<String, String> params = createParams(var);
            params.put("body", res);
            params.put("lExpr", node.name.toString());
            params.put("rExpr", node.init.toString());

            // 入れ子への対応
            if (node.init instanceof JCLambda) {
                JCLambda lm = cast(node.init);

                getDoVar(lm).ifPresent(childVar ->
                        params.put("rExpr", createExpression((JCBlock) lm.body, childVar)));
            }
            res = buildTemplate(LET_CODE, params);
        }

        return res;
    }

    // return 用のソースコード生成
    private String generateCodeForReturn(JCReturn node, String var, String body) {
        Map<String, String> params = createParams(var);
        params.put("expr", node.expr.toString());

        return buildTemplate(RETURN_CODE, params);
    }

    // 処理変数名の抽出
    private Optional<String> getDoVar(JCLambda node) {
        if (node.params.size() == 1) {
            String name = node.params.get(0).name.toString();

            if (name.endsWith(DO_TYPE)) {
                return Optional.of(name.replace(DO_TYPE, ""));
            }
        }
        return Optional.empty();
    }

    private Map<String, String> createParams(String var) {
        Map<String, String> params = new HashMap<>();

        params.put("var", var);

        return params;
    }

    // テンプレート処理
    private String buildTemplate(String template, Map<String, String> params) {
        String res = template;

        for(Map.Entry<String, String> param : params.entrySet()) {
            res = res.replace(VAR_PREFIX + param.getKey() + VAR_SUFFIX, param.getValue());
        }
        return res;
    }

    @SuppressWarnings("unchecked")
    private <S, T> T cast(S obj) {
        return (T)obj;
    }

    private interface CodeGenerator<T> {
        String generate(T node, String var, String body);
    }
}

Service Provider 設定ファイルやビルド定義は 前回 と同じものです。

Service Provider 設定ファイル

src/main/resources/META-INF/services/javax.annotation.processing.Processor
sample.DoExprProcessor
build.gradle
apply plugin: 'java'

def enc = 'UTF-8'
tasks.withType(AbstractCompile)*.options*.encoding = enc

dependencies {
    compile files("${System.properties['java.home']}/../lib/tools.jar")
}

ビルド

ビルド実行
> gradle build

:compileJava
:processResources UP-TO-DATE
:classes
:jar
:assemble
:compileTestJava UP-TO-DATE
:processTestResources UP-TO-DATE
:testClasses UP-TO-DATE
:test UP-TO-DATE
:check UP-TO-DATE
:build

BUILD SUCCESSFUL

ビルド結果として build/libs/java_do_expr.jar3 が生成されます。

動作確認

下記のサンプルコードを使ってアノテーションプロセッサの動作確認を行います。

example/DoExprSample.java
import java.util.function.Function;
import java.util.function.Supplier;
import java.util.Optional;

public class DoExprSample {
    public static void main(String... args) {
        Optional<Integer> o1 = Optional.of(2);
        Optional<Integer> o2 = Optional.of(3);

        Opt<Integer> opt = new Opt<>();

        Optional<Integer> res = opt$do -> {
            let a = o1;
            let b = o2;
            let c = Optional.of(4);
            return a + b + c * 2;
        };
        // Optional[13]
        System.out.println(res);

        Opt<String> opt2 = new Opt<>();

        Optional<String> res2 = opt2$do -> {
            let a = Optional.of("a");
            let b = Optional.of("b");
            let c = opt2$do -> {
                let c1 = Optional.of("c1");
                let c2 = Optional.of("c2");
                return c1 + "-" + c2;
            };
            return a + b + "/" + c;
        };
        // Optional[ab/c1-c2]
        System.out.println(res2);

        // Optional[***ba]
        System.out.println(opt2$do -> {
            let a = Optional.of("a");
            let b = Optional.of("b");
            return "***" + b + a;
        });
    }

    static class Opt<T> {
        public Optional<T> bind(Optional<T> x, Function<T, Optional<T>> f) {
            return x.flatMap(f);
        }

        public Optional<T> unit(T v) {
            return Optional.ofNullable(v);
        }
    }
}

java_do_expr3.jar を使って上記ソースファイルをコンパイルします。

出力内容(変換後のソースコード)を見る限りは変換できているようです。

コンパイル
> javac -cp ../build/libs/java_do_expr3.jar DoExprSample.java

・・・
public class DoExprSample {
    ・・・
    public static void main(String... args) {
        Optional<Integer> o1 = Optional.of(2);
        Optional<Integer> o2 = Optional.of(3);
        Opt<Integer> opt = new Opt<>();
        Optional<Integer> res = opt.bind(o1, (a)->opt.bind(o2, (b)->opt.bind(Optional.of(4), (c)->opt.unit(a + b + c * 2))));
        System.out.println(res);
        Opt<String> opt2 = new Opt<>();
        Optional<String> res2 = opt2.bind(Optional.of("a"), (a)->opt2.bind(Optional.of("b"), (b)->opt2.bind(opt2.bind(Optional.of("c1"), (c1)->opt2.bind(Optional.of("c2"), (c2)->opt2.unit(c1 + "-" + c2))), (c)->opt2.unit(a + b + "/" + c))));
        System.out.println(res2);
        System.out.println(opt2.bind(Optional.of("a"), (a)->opt2.bind(Optional.of("b"), (b)->opt2.unit("***" + b + a))));
    }
    ・・・
}

DoExprSample を実行すると正常に動作しました。

実行結果
> java DoExprSample

Optional[13]
Optional[ab/c1-c2]
Optional[***ba]

Java のアノテーションプロセッサで Haskell の do 記法のようなものを簡易的に実現2

前回 に引き続き、今回も Javaアノテーションプロセッサを使って下記と同等機能を実現します。

  • Haskell の do 記法
  • Scala の for 内包表記
  • F# のコンピュテーション式

今回は、F# のコンピュテーション式を模した下記のような構文 (前回断念したもの) を使用します。

Supplier<Optional<Integer>> res = opt$do -> {
    let a = o1;
    let b = o2;
    return a + b;
};

ソースは http://github.com/fits/try_samples/tree/master/blog/20150513/

改良版は 「Java のアノテーションプロセッサで Haskell の do 記法のようなものを簡易的に実現3」 を参照

はじめに

基本的な変換方法は 前回 と同じですが、かなりシンプルになっていると思います。

変数名$do の $do は変換対象としてマーキングするために付けています。

変換前 (アノテーションプロセッサ処理前)
Supplier<Optional<Integer>> res = opt$do -> {
    let a = o1;
    let b = o2;
    return a + b;
};
変換後 (アノテーションプロセッサ処理後)
Supplier<Optional<Integer>> res = () -> opt.bind(o1, a -> opt.bind(o2, b -> opt.unit(a + b)));

アノテーションプロセッサの実装

Processor の実装

アノテーションプロセッサの本体は 前回 と同じです。

src/main/java/sample/DoExprProcessor.java
package sample;

import java.util.Set;
import javax.annotation.processing.*;

import javax.lang.model.SourceVersion;
import javax.lang.model.element.Element;
import javax.lang.model.element.TypeElement;

import com.sun.source.tree.CompilationUnitTree;
import com.sun.source.util.Trees;
import com.sun.source.util.TreePath;

import com.sun.tools.javac.processing.JavacProcessingEnvironment;
import com.sun.tools.javac.util.Context;

@SupportedSourceVersion(SourceVersion.RELEASE_8)
@SupportedAnnotationTypes("*")
public class DoExprProcessor extends AbstractProcessor {
    private Trees trees;
    private Context context;

    @Override
    public void init(ProcessingEnvironment procEnv) {
        trees = Trees.instance(procEnv);
        context = ((JavacProcessingEnvironment)procEnv).getContext();
    }

    @Override
    public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
        roundEnv.getRootElements().stream().map(this::toUnit).forEach(this::processUnit);
        return false;
    }

    private CompilationUnitTree toUnit(Element el) {
        TreePath path = trees.getPath(el);
        return path.getCompilationUnit();
    }

    private void processUnit(CompilationUnitTree cu) {
        // AST 変換
        cu.accept(new DoExprVisitor(context), null);
        // 変換後のソースを出力
        System.out.println(cu);
    }
}

TreeVisitor の実装

基本的な変換内容は 前回 と同じですが、下記の点が異なります。

  • (1) 対象処理を変換したソースコードを作って JCExpression へパース
  • (2) 生成した JCExpression 内の全 pos の値を修正
  • (3) JCLambda の body を差し替え

(2) が重要で、posソースコード内の位置) の値を調整しておかないと変換後の AST をコンパイルする段階でエラーになります。 (前回失敗した理由)

新しく生成した JCExpression木構造をたどって全要素の pos を変更するために com.sun.tools.javac.tree.TreeScannerscan メソッドをオーバーライドして使っています。

また、今回の構文ではラムダの paramKindIMPLICIT となりますので(前回はラムダ引数の型を指定していたので EXPLICIT だった)、ラムダの引数を消去した際に paramKindEXPLICIT へ変更しています。

src/main/java/sample/DoExprVisitor.java
package sample;

import com.sun.source.tree.*;
import com.sun.source.util.TreeScanner;
import com.sun.tools.javac.parser.ParserFactory;
import com.sun.tools.javac.tree.JCTree;
import com.sun.tools.javac.tree.JCTree.*;
import com.sun.tools.javac.util.Context;

import java.util.HashMap;
import java.util.Map;
import java.util.Optional;
import java.util.stream.Stream;

public class DoExprVisitor extends TreeScanner<Void, Void> {
    private static final String DO_TYPE = "$do";

    private ParserFactory parserFactory;
    private Map<String, TemplateBuilder> builderMap = new HashMap<>();

    public DoExprVisitor(Context context) {
        parserFactory = ParserFactory.instance(context);
        // let 用の変換内容
        builderMap.put("let",
                new TemplateBuilder("${var}.bind(${rExpr}, ${lExpr} -> ${body})", this::createBindParams));
        // return 用の変換内容
        builderMap.put("return",
                new TemplateBuilder("${var}.unit( ${expr} )", this::createBasicParams));
    }

    @Override
    public Void visitLambdaExpression(LambdaExpressionTree node, Void p) {
        if (node instanceof JCLambda) {
            JCLambda lm = (JCLambda)node;

            if (lm.params.size() == 1) {
                getDoVar(lm.params.get(0)).ifPresent(var -> {
                    // ラムダの引数を消去
                    lm.params = com.sun.tools.javac.util.List.nil();
                    lm.paramKind = JCLambda.ParameterKind.EXPLICIT;

                    // (1) 対象処理を変換したソースコードを作って JCExpression へパース
                    JCExpression ne = parseExpression(createExpression((JCBlock)lm.body, var));
                    // (2) 生成した JCExpression 内の全 pos の値を修正
                    fixPos(ne, lm.pos);
                    // (3) JCLambda の body を差し替え
                    lm.body = ne;
                });
            }
        }
        return super.visitLambdaExpression(node, p);
    }

    // pos の値を修正する
    private void fixPos(JCExpression ne, int basePos) {
        ne.accept(new com.sun.tools.javac.tree.TreeScanner() {
            @Override
            public void scan(JCTree tree) {
                if(tree != null) {
                    tree.pos += basePos;
                    super.scan(tree);
                }
            }
        });
    }

    // 対象処理を変換したソースコード (Expression) を生成
    private String createExpression(JCBlock block, String var) {
        Stream<String> revExpr = block.stats.reverse().stream().map(s -> s.toString().replaceAll(";", ""));

        return revExpr.reduce("", (acc, v) -> {
            int spacePos = v.indexOf(" ");
            String action = v.substring(0, spacePos);

            if (builderMap.containsKey(action)) {
                acc = builderMap.get(action).build(var, acc, v.substring(spacePos + 1));
            }

            return acc;
        });
    }

    // 生成したソースコード (Expression) を JavacParser で JCExpression へ変換
    private JCExpression parseExpression(String doExpr) {
        return parserFactory.newParser(doExpr, false, false, false).parseExpression();
    }

    private Optional<String> getDoVar(JCVariableDecl param) {
        String name = param.name.toString();

        return name.endsWith(DO_TYPE)? Optional.of(name.replace(DO_TYPE, "")): Optional.empty();
    }

    private Map<String, String> createBindParams(String var, String body, String expr) {
        Map<String, String> params = createBasicParams(var, body, expr);

        String[] vexp = expr.split("=");
        params.put("lExpr", vexp[0]);
        params.put("rExpr", vexp[1]);

        return params;
    }

    private Map<String, String> createBasicParams(String var, String body, String expr) {
        Map<String, String> params = new HashMap<>();

        params.put("var", var);
        params.put("body", body);
        params.put("expr", expr);

        return params;
    }

    private interface ParamCreator {
        Map<String, String> create(String var, String body, String expr);
    }

    private class TemplateBuilder {
        private static final String VAR_PREFIX = "\\$\\{";
        private static final String VAR_SUFFIX = "\\}";

        private String template;
        private ParamCreator paramCreator;

        TemplateBuilder(String template, ParamCreator paramCreator) {
            this.template = template;
            this.paramCreator = paramCreator;
        }

        public String build(String var, String body, String expr) {
            return buildTemplate(template, paramCreator.create(var, body, expr));
        }

        private String buildTemplate(String template, Map<String, String> params) {
            return params.entrySet().stream().reduce(template,
                    (acc, v) -> acc.replaceAll(VAR_PREFIX + v.getKey() + VAR_SUFFIX, v.getValue()),
                    (a, b) -> a);
        }
    }
}

Service Provider 設定ファイルやビルド定義も 前回 と同じものです。

Service Provider 設定ファイル

src/main/resources/META-INF/services/javax.annotation.processing.Processor
sample.DoExprProcessor
build.gradle
apply plugin: 'java'

def enc = 'UTF-8'
tasks.withType(AbstractCompile)*.options*.encoding = enc

dependencies {
    compile files("${System.properties['java.home']}/../lib/tools.jar")
}

ビルド

ビルド実行
> gradle build

:compileJava
:processResources UP-TO-DATE
:classes
:jar
:assemble
:compileTestJava UP-TO-DATE
:processTestResources UP-TO-DATE
:testClasses UP-TO-DATE
:test UP-TO-DATE
:check UP-TO-DATE
:build

BUILD SUCCESSFUL

ビルド結果として build/libs/java_do_expr.jar2 が生成されました。

動作確認

下記のサンプルコードを使ってアノテーションプロセッサの動作確認を行います。

example/DoExprSample.java
import java.util.function.Function;
import java.util.function.Supplier;
import java.util.Optional;

public class DoExprSample {
    public static void main(String... args) {
        Optional<Integer> o1 = Optional.of(2);
        Optional<Integer> o2 = Optional.of(3);

        Opt<Integer> opt = new Opt<>();
        // アノテーションプロセッサで変換する処理1
        Supplier<Optional<Integer>> res = opt$do -> {
            let a = o1;
            let b = o2;
            let c = Optional.of(4);
            return a + b + c * 2;
        };

        // Optional[13]
        System.out.println(res.get());

        Opt<String> opt2 = new Opt<>();
        // アノテーションプロセッサで変換する処理2
        Supplier<Optional<String>> res2 = opt2$do -> {
            let a = Optional.of("a");
            let b = Optional.of("b");
            return a + b;
        };

        // Optional["ab"]
        System.out.println(res2.get());
    }
    // Optional 用の bind・unit メソッド実装クラス
    static class Opt<T> {
        public Optional<T> bind(Optional<T> x, Function<T, Optional<T>> f) {
            return x.flatMap(f);
        }

        public Optional<T> unit(T v) {
            return Optional.ofNullable(v);
        }
    }
}

java_do_expr2.jar を使って上記ソースファイルをコンパイルします。

出力内容(変換後のソースコード)を見る限り正常に変換できているようです。

コンパイル
> javac -cp ../build/libs/java_do_expr2.jar DoExprSample.java

・・・
public class DoExprSample {
    ・・・
    public static void main(String... args) {
        Optional<Integer> o1 = Optional.of(2);
        Optional<Integer> o2 = Optional.of(3);
        Opt<Integer> opt = new Opt<>();
        Supplier<Optional<Integer>> res = ()->opt.bind(o1, (a)->opt.bind(o2, (b)->opt.bind(Optional.of(4), (c)->opt.unit(a + b + c * 2))));
        System.out.println(res.get());
        Opt<String> opt2 = new Opt<>();
        Supplier<Optional<String>> res2 = ()->opt2.bind(Optional.of("a"), (a)->opt2.bind(Optional.of("b"), (b)->opt2.unit(a + b)));
        System.out.println(res2.get());
    }
    ・・・
}

DoExprSample を実行すると正常に動作しました。

実行結果
> java DoExprSample

Optional[13]
Optional[ab]

Java のアノテーションプロセッサで Haskell の do 記法のようなものを簡易的に実現

アノテーションプロセッサで AST 変換 - Lombok を参考にして変数の型をコンパイル時に変更」の応用編です。

前回は変数の型を var から java.lang.Object へ変更しただけでしたが、今回は下記と同等な機能の簡易版をアノテーションプロセッサで実現してみます。

  • Haskell の do 記法
  • Scala の for 内包表記
  • F# のコンピュテーション式

ソースは http://github.com/fits/try_samples/tree/master/blog/20150511/

改良版は 「Java のアノテーションプロセッサで Haskell の do 記法のようなものを簡易的に実現3」 を参照

はじめに

前回 と同様にアノテーションプロセッサにおける下記の特徴を利用し、コンパイル前に AST (抽象構文木) を書き換えます。

変換例

今回は、以下のような変換をアノテーションプロセッサ内で実施します。 Haskell の do 記法というよりは F# のコンピュテーション式に近くなっています。

変換前 (アノテーションプロセッサ処理前)
Supplier<Optional<Integer>> res = ($do<Optional, Integer> opt) -> {
    let a = o1;
    let b = o2;
    return a + b;
};
変換後 (アノテーションプロセッサ処理後)
Supplier<Optional<Integer>> res = ()->{
    return opt.bind(o1, new java.util.function.Function<Integer, Optional<Integer>>(){
        @Override
        public Optional<Integer> apply(Integer a) {
            return opt.bind(o2, new java.util.function.Function<Integer, Optional<Integer>>(){
                @Override
                public Optional<Integer> apply(Integer b) {
                    return opt.unit(a + b);
                }
            });
        }
    });
};

変換内容

変換前の構文と変換方法を簡単に説明します。

ラムダの引数部分は変換に必要な情報を渡すために使用し、$do という未定義のクラスは変換対象かどうかをマーキングする目的で使います。

処理変数(変換例の opt)には bind・unit メソッドを持つオブジェクトインスタンスの変数名を指定します。

また、型推論は難しそうだったので、コンテナ型 (モナドの型) と要素の型を $do クラスの型引数で指定するようにしています。

変換前
・・・ = ($do<コンテナ型, 要素型> 処理変数) -> {
    let 変数1 = 式1;
    ・・・
    return 結果式;
};

変換は以下のように実施します。

ラムダの引数部分 ($do の箇所) を全て消去します。 (変換に必要な情報を渡しているだけなので)

(a) ($do<コンテナ型, 要素型> 処理変数) -> {} の変換内容
・・・ = () -> {
    ・・・
}

let の部分は bind メソッドを使った処理へ変換します。

(b) let 変数1 = 式1; の変換内容
return 処理変数.bind(式1, new java.util.function.Function<要素型, コンテナ型<要素型>>(){
    @Override
    public コンテナ型<要素型> apply(要素型 変数1) {
        ・・・
    }
});

return の部分は unit メソッドを使った処理へ変換します。

(c) return 結果式; の変換内容
return 処理変数.unit( 結果式 );

備考

本当は以下のようなシンプルな仕様で実現したかったのですが、変換後のコンパイル時にエラーが発生してしまい、うまく解決できなかったので今回は断念しました。

変換前 (失敗版)
Supplier<Optional<Integer>> res = opt$do -> {
    let a = o1;
    let b = o2;
    return a + b;
};
変換後 (失敗版)
Supplier<Optional<Integer>> res = () -> opt.bind(o1, a -> opt.bind(o2, b -> opt.unit(a + b)));
変換後のコンパイルエラー例 (Java 1.8.0_45)
java.lang.AssertionError: Value of x -1
        at com.sun.tools.javac.util.Assert.error(Assert.java:133)
        at com.sun.tools.javac.util.Assert.check(Assert.java:94)
        at com.sun.tools.javac.util.Bits.incl(Bits.java:200)
        at com.sun.tools.javac.comp.Flow$AbstractAssignAnalyzer.visitLambda(Flow.java:2254)
        at com.sun.tools.javac.tree.JCTree$JCLambda.accept(JCTree.java:1624)

ただし、変換後のソースをファイルへ出力し javac で普通にコンパイルすれば問題なく成功しますので、実現不可能では無いと思います。

と書きましたが、生成した箇所の pos の値を全て調整し直せば、 上記の構文で実現できることが判明しました。 (Java のアノテーションプロセッサで Haskell の do 記法のようなものを簡易的に実現2 参照)

アノテーションプロセッサの実装

それでは本題に入ります。

Processor の実装

まずは、アノテーションプロセッサの本体を実装します。

こちらは 前回SampleProcessor2.java とほぼ同じ内容です。

変換後のソースを確認するため accept 後の CompilationUnitTreeprintln するようにしています。

src/main/java/sample/DoExprProcessor.java
package sample;

import java.util.Set;
import javax.annotation.processing.*;

import javax.lang.model.SourceVersion;
import javax.lang.model.element.Element;
import javax.lang.model.element.TypeElement;

import com.sun.source.tree.CompilationUnitTree;
import com.sun.source.util.Trees;
import com.sun.source.util.TreePath;

import com.sun.tools.javac.processing.JavacProcessingEnvironment;
import com.sun.tools.javac.util.Context;

@SupportedSourceVersion(SourceVersion.RELEASE_8)
@SupportedAnnotationTypes("*")
public class DoExprProcessor extends AbstractProcessor {
    private Trees trees;
    private Context context;

    @Override
    public void init(ProcessingEnvironment procEnv) {
        trees = Trees.instance(procEnv);
        context = ((JavacProcessingEnvironment)procEnv).getContext();
    }

    @Override
    public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
        roundEnv.getRootElements().stream().map(this::toUnit).forEach(this::processUnit);
        return false;
    }

    private CompilationUnitTree toUnit(Element el) {
        TreePath path = trees.getPath(el);
        return path.getCompilationUnit();
    }

    private void processUnit(CompilationUnitTree cu) {
        // AST 変換
        cu.accept(new DoExprVisitor(context), null);
        // 変換後のソースを出力
        System.out.println(cu);
    }
}

TreeVisitor の実装

次に、変換処理を実装します。

ラムダを変更するため TreeScanner を extends し visitLambdaExpression メソッドをオーバーライドします。

LambdaExpressionTree だとラムダの内容を変更できないので JCLambda へキャストし、以下のような処理を実施します。

  • (1) 変換対象かどうかをチェック (引数の型に $do を使っているかどうか等)
  • (2) ラムダの引数を消去
  • (3) ラムダの処理内容(let や return)を元に変換後のソースを生成
  • (4) (3) を JavacParser を使って JCStatement へ変換
  • (5) ラムダの処理内容を (4) の結果で差し替え (JCLambdabody.stats の値を変更)

通常は、JCStatement を直接構築して差し替えると思うのですが、JCStatement を自前で構築するのは大変そうだったので、部分的なソースコードを作り (3) 、それを JavacParser にパースさせる事で JCStatement を得ています (4)。

また、JCTree 内で使用されている List クラスは java.util.List ではなく com.sun.tools.javac.util.List なので注意。

src/main/java/sample/DoExprVisitor.java
package sample;

import com.sun.source.tree.LambdaExpressionTree;
import com.sun.source.util.TreeScanner;
import com.sun.tools.javac.parser.ParserFactory;
import com.sun.tools.javac.tree.JCTree.*;
import com.sun.tools.javac.util.Context;

import java.util.HashMap;
import java.util.Map;
import java.util.function.BiFunction;
import java.util.stream.Stream;

public class DoExprVisitor extends TreeScanner<Void, Void> {
    private static final String DO_TYPE = "$do";
    // let 変換用のソースコード
    private static final String BIND_CODE = " return ${var}.bind(${rExpr}, new java.util.function.Function<${vType}, ${mType}<${vType}>>(){" +
            "  @Override public ${mType}<${vType}> apply(${vType} ${lExpr}){ ${body} }" +
            " });";
    // return 変換用のソースコード
    private static final String UNIT_CODE = "  return ${var}.unit( ${expr} );";

    private ParserFactory parserFactory;
    private Map<String, TemplateBuilder> builderMap = new HashMap<>();

    public DoExprVisitor(Context context) {
        parserFactory = ParserFactory.instance(context);
        // (b) let 用の変換内容
        builderMap.put("let", new TemplateBuilder(BIND_CODE, this::createBindParams));
        // (c) return 用の変換内容
        builderMap.put("return", new TemplateBuilder(UNIT_CODE, this::createUnitParams));
    }

    @Override
    public Void visitLambdaExpression(LambdaExpressionTree node, Void p) {
        if (node instanceof JCLambda) {
            JCLambda lm = (JCLambda)node;

            // (1) 変換対象かどうかをチェック
            if (isSingleTypeApplyParam(lm)) {
                JCVariableDecl param = lm.params.get(0);

                // (1) 変換対象かどうかをチェック
                if (isDoType(param)) {
                    // (2) ラムダの引数を消去
                    lm.params = com.sun.tools.javac.util.List.nil();

                    JCBlock block = (JCBlock)lm.body;
                    // 変換後の処理内容(Statement)を作成 (3) (4)
                    JCStatement newStats = parseStatement(createStatement(block, createBaseParams(param)));
                    // (5) ラムダの処理内容を差し替え
                    block.stats = com.sun.tools.javac.util.List.of(newStats);
                }
            }
        }
        return super.visitLambdaExpression(node, p);
    }
    // (3) ラムダの処理内容を変換したソースコードを生成
    private String createStatement(JCBlock block, Map<String, String> params) {
        // ラムダの内容 (JCStatement のリスト) を逆順化して個別に文字列化
        Stream<String> revStats = block.stats.reverse().stream().map(s -> s.toString().replaceAll(";", ""));

        // 逆順化したリストを順次変換
        return revStats.reduce("", (acc, v) -> {
            int spacePos = v.indexOf(" ");
            String action = v.substring(0, spacePos);

            if (builderMap.containsKey(action)) {
                acc = builderMap.get(action).build(params, acc, v.substring(spacePos + 1));
            }

            return acc;
        });
    }
    // (4) 生成したソースコードを JavacParser で JCStatement へ変換
    private JCStatement parseStatement(String doStat) {
        return parserFactory.newParser(doStat, false, false, false).parseStatement();
    }
    // (1)
    private boolean isDoType(JCVariableDecl param) {
        String type = ((JCTypeApply)param.vartype).clazz.toString();
        return DO_TYPE.equals(type);
    }
    // (1)
    private boolean isSingleTypeApplyParam(JCLambda lm) {
        return lm.params.size() == 1
                && lm.params.get(0).vartype instanceof JCTypeApply;
    }

    private Map<String, String> createBaseParams(JCVariableDecl param) {
        Map<String, String> params = new HashMap<>();

        params.put("var", param.name.toString());

        JCTypeApply paramType = (JCTypeApply)param.vartype;
        params.put("mType", paramType.arguments.get(0).toString());
        params.put("vType", paramType.arguments.get(1).toString());

        return params;
    }

    private Map<String, String> createBindParams(String body, String expr) {
        Map<String, String> params = createUnitParams(body, expr);

        String[] divexp = expr.split("=");
        params.put("lExpr", divexp[0]);
        params.put("rExpr", divexp[1]);

        return params;
    }

    private Map<String, String> createUnitParams(String body, String expr) {
        Map<String, String> params = new HashMap<>();

        params.put("body", body);
        params.put("expr", expr);

        return params;
    }
    // テンプレート処理を実施するためのクラス
    private class TemplateBuilder {
        private static final String VAR_PREFIX = "\\$\\{";
        private static final String VAR_SUFFIX = "\\}";

        private String template;
        private BiFunction<String, String, Map<String, String>> paramCreator;

        TemplateBuilder(String template, BiFunction<String, String, Map<String, String>> paramCreator) {
            this.template = template;
            this.paramCreator = paramCreator;
        }

        public String build(Map<String, String> params, String body, String expr) {
            return buildTemplate(
                    buildTemplate(template, params),
                    paramCreator.apply(body, expr));
        }

        private String buildTemplate(String template, Map<String, String> params) {
            return params.entrySet().stream().reduce(template,
                    (acc, v) -> acc.replaceAll(VAR_PREFIX + v.getKey() + VAR_SUFFIX, v.getValue()),
                    (a, b) -> a);
        }
    }
}

JCLambda の変換イメージ例

JCLambda の大まかな変換イメージを書くと以下のようになります。

ソースコード
($do<Optional, String> opt2) -> {
    let a = Optional.of("a");
    let b = Optional.of("b");
    return a + b;
}

上記のソースを JCLambda 化すると以下のようになります。 実際はもっと複雑ですが、適当に簡略化しています。 (init や expr の内容も実際は JCMethodInvocation 等が入れ子になっています)

変換前 JCLambda の内容
JCLambda(
    params = [
        JCVariableDecl(
            name = opt2,
            vartype = $do<Optional, String>
        )
    ],
    paramKind = EXPLICIT,
    body = JCBlock(
        stats = [
            JCVariableDecl(
                name = a,
                vartype = let,
                init = Optional.of("a")
            ),
            JCVariableDecl(
                name = b,
                vartype = let,
                init = Optional.of("a")
            ),
            JCReturn(
                expr = a + b
            )
        ]
    )
)

DoExprVisitor では上記を以下のように変更します。

変換後 JCLambda の内容
JCLambda(
    params = [],
    paramKind = EXPLICIT,
    body = JCBlock(
        stats = [
            JCReturn(
                expr = opt2.bind(Optional.of("a"), ・・・)
            )
        ]
    )
)

今回は body に設定されている JCBlock をそのまま使いましたが、body の値を直接変更する方法も考えられます。

Service Provider 設定ファイル

アノテーションプロセッサで sample.DoExprProcessor を使用するように Service Provider の設定ファイルを用意します。

src/main/resources/META-INF/services/javax.annotation.processing.Processor
sample.DoExprProcessor

ビルド

ビルド定義ファイルは以下の通り。

build.gradle
apply plugin: 'java'

def enc = 'UTF-8'
tasks.withType(AbstractCompile)*.options*.encoding = enc

dependencies {
    compile files("${System.properties['java.home']}/../lib/tools.jar")
}
ビルド実行
> gradle build

:compileJava
:processResources UP-TO-DATE
:classes
:jar
:assemble
:compileTestJava UP-TO-DATE
:processTestResources UP-TO-DATE
:testClasses UP-TO-DATE
:test UP-TO-DATE
:check UP-TO-DATE
:build

BUILD SUCCESSFUL

ビルド結果として build/libs/java_do_expr.jar が生成されました。

動作確認

最後に、下記のサンプルコードを使ってアノテーションプロセッサの動作確認を行います。

example/DoExprSample.java
import java.util.function.Function;
import java.util.function.Supplier;
import java.util.Optional;

public class DoExprSample {
    public static void main(String... args) {
        Optional<Integer> o1 = Optional.of(2);
        Optional<Integer> o2 = Optional.of(3);

        Opt<Integer> opt = new Opt<>();
        // アノテーションプロセッサで変換する処理1
        Supplier<Optional<Integer>> res = ($do<Optional, Integer> opt) -> {
            let a = o1;
            let b = o2;
            let c = Optional.of(4);
            return a + b + c * 2;
        };

        // Optional[13]
        System.out.println(res.get());

        Opt<String> opt2 = new Opt<>();
        // アノテーションプロセッサで変換する処理2
        Supplier<Optional<String>> res2 = ($do<Optional, String> opt2) -> {
            let a = Optional.of("a");
            let b = Optional.of("b");
            return a + b;
        };

        // Optional["ab"]
        System.out.println(res2.get());
    }
    // Optional 用の bind・unit メソッド実装クラス
    static class Opt<T> {
        public Optional<T> bind(Optional<T> x, Function<T, Optional<T>> f) {
            return x.flatMap(f);
        }

        public Optional<T> unit(T v) {
            return Optional.ofNullable(v);
        }
    }
}

java_do_expr.jar を使って上記ソースファイルをコンパイルします。

出力内容(変換後のソースコード)を見る限り正常に変換できているようです。

コンパイル
> javac -cp ../build/libs/java_do_expr.jar DoExprSample.java

・・・
public class DoExprSample {
    ・・・
    public static void main(String... args) {
        Optional<Integer> o1 = Optional.of(2);
        Optional<Integer> o2 = Optional.of(3);
        Opt<Integer> opt = new Opt<>();
        Supplier<Optional<Integer>> res = ()->{
            return opt.bind(o1, new java.util.function.Function<Integer, Optional<Integer>>(){

                @Override()
                public Optional<Integer> apply(Integer a) {
                    return opt.bind(o2, new java.util.function.Function<Integer, Optional<Integer>>(){

                        @Override()
                        public Optional<Integer> apply(Integer b) {
                            return opt.bind(Optional.of(4), new java.util.function.Function<Integer, Optional<Integer>>(){

                                @Override()
                                public Optional<Integer> apply(Integer c) {
                                    return opt.unit(a + b + c * 2);
                                }
                            });
                        }
                    });
                }
            });
        };
        System.out.println(res.get());
        Opt<String> opt2 = new Opt<>();
        Supplier<Optional<String>> res2 = ()->{
            return opt2.bind(Optional.of("a"), new java.util.function.Function<String, Optional<String>>(){

                @Override()
                public Optional<String> apply(String a) {
                    return opt2.bind(Optional.of("b"), new java.util.function.Function<String, Optional<String>>(){

                        @Override()
                        public Optional<String> apply(String b) {
                            return opt2.unit(a + b);
                        }
                    });
                }
            });
        };
        System.out.println(res2.get());
    }
    ・・・
}

DoExprSample を実行すると正常に動作しました。

実行結果
> java DoExprSample

Optional[13]
Optional[ab]

Spring を使った Web アプリケーションへ Ehcache を適用し JMX でモニタリング

Spring を使った Web アプリケーションへ Ehcache を適用し、JMX でキャッシュ状況を取得できるようにしてみました。

サンプルソースhttp://github.com/fits/try_samples/tree/master/blog/20150508/

Spring へ Ehcache を適用

Spring には Cache Abstraction 機能が用意されており、Cache 用のアノテーションメソッドへ付ければメソッドの戻り値をキャッシュできます。 (デフォルトでは、メソッドの引数がキャッシュキーとなります)

実際のキャッシュ処理には以下のようなライブラリや API が利用でき、今回は Ehcache を使用します。

  • ConcurrentMap
  • Ehcache
  • Guava
  • GemFire
  • JSR-107 (JCache)

なお、キャッシュ用アノテーションは以下が利用でき、今回は JSR-107 のアノテーションを使用します。

Service クラス

まずはキャッシュを適用するメソッドを実装します。

今回は JSR-107 の @CacheResult アノテーションを使いました。

src/main/java/sample/service/SampleService.java
package sample.service;

import org.springframework.stereotype.Service;
import javax.cache.annotation.CacheResult;

@Service
public class SampleService {
    @CacheResult(cacheName = "sample")
    public String sample(String id) {
        return "sample: " + id + ", " + System.currentTimeMillis();
    }
}

Controller クラス

次に、キャッシュを適用したメソッドを呼び出す処理を実装します。

今回のようなケースでは、CGLIB によるプロキシを使ってキャッシュ処理を差し込んでおり、内部的なメソッド呼び出しにはキャッシュを適用しないようなのでご注意ください。

src/main/java/sample/controller/SampleController.java
package sample.controller;

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;
import javax.inject.Inject;
import sample.service.SampleService;

@Controller
public class SampleController {
    @Inject
    private SampleService sampleService;

    @RequestMapping("/sample/{id}")
    @ResponseBody
    public String sample(@PathVariable("id") String id) {
        // キャッシュを適用したメソッドの実行
        return sampleService.sample(id);
    }
}

設定クラス

設定クラスでは、下記を実施することで Ehcache を適用できます。

  • @EnableCaching でキャッシュを有効化
  • EhCacheCacheManager を Bean 定義

Ehcache の設定ファイル ehcache.xml をクラスパスから取得するように ClassPathResource を使っています。

今回は JMXモニタリングできるように Ehcache の ManagementService も Bean 定義しています。

src/main/java/sample/config/WebConfig.java
package sample.config;

import net.sf.ehcache.management.ManagementService;
import org.springframework.cache.CacheManager;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.cache.ehcache.EhCacheCacheManager;
import org.springframework.cache.ehcache.EhCacheManagerFactoryBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.io.ClassPathResource;
import org.springframework.jmx.support.MBeanServerFactoryBean;
import org.springframework.web.servlet.config.annotation.EnableWebMvc;

@Configuration
@EnableWebMvc
@EnableCaching //キャッシュ機能の有効化
public class WebConfig {
    // CacheManager の定義
    @Bean
    public CacheManager cacheManager() {
        EhCacheCacheManager manager = new EhCacheCacheManager();
        manager.setCacheManager(ehcache().getObject());

        return manager;
    }

    @Bean
    public EhCacheManagerFactoryBean ehcache() {
        EhCacheManagerFactoryBean ehcache = new EhCacheManagerFactoryBean();
        ehcache.setConfigLocation(new ClassPathResource("ehcache.xml"));

        return ehcache;
    }

    // JMX 設定
    @Bean
    public MBeanServerFactoryBean mbeanServer() {
        MBeanServerFactoryBean factory = new MBeanServerFactoryBean();
        factory.setLocateExistingServerIfPossible(true);

        return factory;
    }
    // Ehcache 用の JMX 設定
    @Bean
    public ManagementService managementService() {
        ManagementService service = new ManagementService(ehcache().getObject(), mbeanServer().getObject(), true, true, true, true);
        service.init();

        return service;
    }
}

実行クラス

Spring Boot で実行するための実行クラスです。

src/main/java/sample/Application.java
package sample;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.context.annotation.ComponentScan;

@ComponentScan
@EnableAutoConfiguration
public class Application {
    public static void main(String... args) {
        SpringApplication.run(Application.class, args);
    }
}

ビルド定義

Spring の Cache Abstraction 機能は 2つのモジュールに分かれており、Ehcache を使用するには spring-context-support が必要です。

モジュール 備考
spring-context Cache の基本機能
spring-context-support 各種キャッシュライブラリ用の実装

今回使用した spring-boot-starter-webspring-context-support を依存関係に含んでいなかったため、依存定義をする必要がありました。

build.gradle
apply plugin: 'spring-boot'

def enc = 'UTF-8'
tasks.withType(AbstractCompile)*.options*.encoding = enc

buildscript {
    repositories {
        jcenter()
    }

    dependencies {
        classpath 'org.springframework.boot:spring-boot-gradle-plugin:1.2.3.RELEASE'
    }
}

repositories {
    jcenter()
}

dependencies {
    compile 'org.springframework.boot:spring-boot-starter-web:1.2.3.RELEASE'
    // 以下は org.springframework.cache.ehcache のために必要
    compile 'org.springframework:spring-context-support:4.1.6.RELEASE'

    // @Inject アノテーションのために必要
    compile 'javax:javaee-api:7.0'
    // @CacheResult アノテーションのために必要
    compile 'javax.cache:cache-api:1.0.0-PFD'

    compile 'net.sf.ehcache:ehcache:2.10.0'
}

キャッシュ設定

Ehcache の設定は以下の通り。 キャッシュの有効期間を 10 秒としています。

src/main/resources/ehcache.xml
<?xml version="1.0" encoding="UTF-8"?>
<ehcache>
  <cache name="sample"
         maxElementsInMemory="100"
         eternal="false"
         timeToLiveSeconds="10" />
</ehcache>

実行

bootRun タスクで Web アプリケーションを起動します。

起動
> gradle bootRun

・・・
:bootRun

・・・
2015-05-04 18:37:07.807  INFO 6704 --- [           main] sample.Application                       : Started Application in 5.674 seconds (JVM running for 6.24)
2015-05-04 18:37:13.436  INFO 6704 --- [nio-8080-exec-1] o.a.c.c.C.[Tomcat].[localhost].[/]       : Initializing Spring FrameworkServlet 'dispatcherServlet'
・・・

何度か同じ URL へアクセスしてみると、10秒間のキャッシュが効いている事を確認できました。

動作確認
$ curl http://localhost:8080/sample/aaa
sample: aaa, 1430732146256

$ curl http://localhost:8080/sample/aaa
sample: aaa, 1430732146256

$ curl http://localhost:8080/sample/aaa
sample: aaa, 1430732146256

$ curl http://localhost:8080/sample/aaa
sample: aaa, 1430732158070

JMX でキャッシュ状況を取得

JMX を使って Ehcache のキャッシュヒット状況などを確認してみます。

Attach API による JMX エージェント (MBean サーバー) の適用

Java の Attach API を使えば、ローカルで実行中の Java プロセスへアタッチして JMX エージェント (MBean サーバー) を動的に適用できます。

そのため、アプリケーションの起動時 (gradle bootRun) に JMX 用の実行時オプションを指定しておく必要はありません。

Attach API を使って JMX エージェントを適用する手順は以下の通りです。

  • (1) Java VM の ID (プロセスID) を指定して VM へアタッチ
  • (2) (1) を使って JMX 用のサービス URL を取得
  • (3) (2) が null の場合に JMX エージェントを適用し (2) を再実施

(2) の戻り値が null の場合、JMX エージェントが未適用という事ですので、management-agent.jarloadAgentJMX エージェントを適用します。

一度 JMX エージェントを適用しておけば、それ以降はサービス URL を取得できるので、その URL を使って MBean サーバーへ接続します。

Ehcache の JMX

Ehcache の JMX オブジェクト名は以下のようになります。

  • net.sf.ehcache:type=CacheStatistics,CacheManager=__DEFAULT__,name=<キャッシュ名>

net.sf.ehcache:type=CacheStatistics,* をオブジェクト名に指定して queryNames すれば Ehcache に関する全オブジェクト名を取得できます。

そうして取得したオブジェクト名を使って CacheHits 等を getAttribute すればキャッシュのヒット数などを取得できます。

Groovy で実装

今回は Ehcache の JMX からキャッシュヒット数などを取得する処理を Groovy で実装してみました。

ehcache_jmx1.groovy
import javax.management.ObjectName
import javax.management.remote.JMXConnectorFactory
import javax.management.remote.JMXServiceURL

import com.sun.tools.attach.VirtualMachine

// JMX のサービス URL 取得処理
def getServiceUrl = {
    it.agentProperties.getProperty('com.sun.management.jmxremote.localConnectorAddress')
}

def pid = args[0]
// (1) Java VM へアタッチ
def vm = VirtualMachine.attach(pid)
// (2) JMX のサービス URL 取得
def url = getServiceUrl(vm)

if (url == null) {
    def javaHome = vm.systemProperties.getProperty('java.home')
    // (3) JMX エージェントを適用
    vm.loadAgent("${javaHome}/lib/management-agent.jar")
    // JMX のサービス URL を再取得
    url = getServiceUrl(vm)
}

vm.detach()

// MBean サーバーへの接続
def con = JMXConnectorFactory.connect(new JMXServiceURL(url))
def server = con.getMBeanServerConnection()

def cacheName = new ObjectName('net.sf.ehcache:type=CacheStatistics,*')
// EhCache に関する JMX オブジェクト名の取得
server.queryNames(cacheName, null).each { name ->
    println "# ${name}"

    // ヒット回数などの属性値を取得
    def res = [
        'CacheHits',   // キャッシュのヒット回数
        'CacheMisses', // キャッシュのミス回数
        'InMemoryHits',
        'InMemoryMisses'
    ].collectEntries { attr ->
        [attr, server.getAttribute(name, attr)]
    }

    println res
}

con.close()
実行例
> groovy ehcache_jmx1.groovy 6704

# net.sf.ehcache:type=CacheStatistics,CacheManager=__DEFAULT__,name=sample
[CacheHits:7, CacheMisses:3, InMemoryHits:8, InMemoryMisses:2]

また、JMX のサービス URL は ConnectorAddressLink.importFrom(<プロセスID>) で取得する事も可能です。

ehcache_jmx2.groovy
・・・
import com.sun.tools.attach.VirtualMachine
import sun.management.ConnectorAddressLink

// JMX のサービス URL 取得処理 (ConnectorAddressLink 利用版)
def getServiceUrl = {
    ConnectorAddressLink.importFrom(it as int)
}

def pid = args[0]
def url = getServiceUrl(pid)

if (url == null) {
    def vm = VirtualMachine.attach(pid)

    def javaHome = vm.systemProperties.getProperty('java.home')
    vm.loadAgent("${javaHome}/lib/management-agent.jar")

    vm.detach()

    url = getServiceUrl(pid)
}
・・・