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 を発行します。
- プロジェクトを選択
- 「APIと認証」の「認証情報」をクリック
- 「新しいクライアントIDを作成」をクリック
- 「インストールされているアプリケーション」を選択し、「クライアントIDを作成」 をクリック (アプリケーションの種類は "その他")
- 「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 利用
最後に Gmail の API を使ってメールの情報を取得してみます。
ここでは以下のモジュールを使いました。
リフレッシュトークンを GoogleCredential
へ setRefreshToken
で設定しておけばアクセストークンを自動的に取得して処理してくれるようです。
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 アカウントで利用したい場合には以下のような事をしなければならないようです。
- Google Apps ドメインの成り代わり機能 ※ を使う
どちらにしても、ドメインが gmail.com
の Gmail アカウント等へ成り代わるのは無理そうですし、サービスアカウントで使える API は限られるかもしれません。
※ 同じドメインに所属する全てのユーザーへ成り代わる権限を サービスアカウントへ付与する機能
(2) の方法は自分のアカウントで API を利用できますが、基本的に Web 画面でログインして API の利用を承認する必要があります。(ただし、初回のみ)
承認するとアクセストークン(有効期限あり)とリフレッシュトークン(有効期限なし)を取得できるので、次からはリフレッシュトークンでアクセストークンを取得し直す事ができます。
サービスアカウントの発行
まずは Google Developers Console へログインしサービスアカウントを発行します。
- プロジェクトを選択
- 「APIと認証」の「認証情報」をクリック
- 「新しいクライアントIDを作成」をクリック
- 「サービスアカウント」を選択し、「クライアントIDを作成」 をクリック (キータイプは JSON)
- キーファイルを保存
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 へ生成されます。
※ ビルドによって生成された JAR と依存モジュールの JAR
例えば zip ファイルの中身は以下のようになります。
zip ファイル内の構成例
- sample_app.zip
- bin
- sample_app
- sample_app.bat
- lib
- sample_app.jar
- bin
起動スクリプトを自動生成してくれるのは非常に便利なのですが、自前で起動スクリプトを用意したい場合もあると思います。
src/dist/bin へ同名の起動スクリプトを用意する方法では、zip・tar ファイル内の bin に同名ファイルが 2重に登録されてしまいました。 (自動生成の方が後に追加される)
これでは不便なので自動生成を無効化する方法を探ってみました。
起動スクリプトの自動生成を無効化
結果的に startScripts.enabled
へ false を設定すれば無効化できました。
build.gradle
apply plugin: 'application' // 起動スクリプトの自動生成を無効化 startScripts.enabled = false ・・・
Java のアノテーションプロセッサで Haskell の do 記法のようなものを簡易的に実現3
Java のアノテーションプロセッサを使って下記と同等の機能を実現する試みの第三弾です。
前回 のものを改良し、ようやく下記のような構文を実現しました。
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
へ置換処理 (JCLambda
を JCMethodInvocation
へ差し替える事になる) を設定するようにしました。
主な処理内容は次のようになっています。
- (1) 変数定義(
JCVariableDecl
)やメソッド実行(JCMethodInvocation
)の箇所で該当部分を差し替えるための処理をchangeNode
へ設定 - (2) ラムダの内容からソースコードを生成 (対象外なら何もしない)
- (3) ソースコードを
JCExpression
へパースして (実体はJCMethodInvocation
)pos
の値を調整 - (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 のアノテーションプロセッサを使って下記と同等機能を実現します。
今回は、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.TreeScanner
の scan
メソッドをオーバーライドして使っています。
また、今回の構文ではラムダの paramKind
が IMPLICIT
となりますので(前回はラムダ引数の型を指定していたので EXPLICIT
だった)、ラムダの引数を消去した際に paramKind
を EXPLICIT
へ変更しています。
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
へ変更しただけでしたが、今回は下記と同等な機能の簡易版をアノテーションプロセッサで実現してみます。
ソースは 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
後の CompilationUnitTree
を println
するようにしています。
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) の結果で差し替え (
JCLambda
のbody.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-web
は spring-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.jar
を loadAgent
し JMX エージェントを適用します。
一度 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) } ・・・