ラベル Google Cloud Endpoints の投稿を表示しています。 すべての投稿を表示
ラベル Google Cloud Endpoints の投稿を表示しています。 すべての投稿を表示

2013年4月12日金曜日

24.appengine ja night #24 Google Cloud Endpoints and BigQuery

このエントリーをはてなブックマークに追加



Ryo Yamasaki(@vierjp)です。

4/10の「appengine ja night #24」で登壇させていただき、
「Google Cloud Endpoints」と「BigQueryの新機能」について発表したので、
補足も含めて記事にしておこうと思います。

こちらが発表資料です。
appengine ja night #24 Google Cloud Endpoints and BigQuery


◯Google Cloud Endpoints

・概要
Endpointsを使うことでできる事について簡単に説明しています。


・基本的な実装手順
これまでに書いたエントリーと重なるところも多いですが、
新しい内容としては「エラー処理の方法」、「JSでAngularJSと組み合わせて利用する方法」について書いています。


・OAuth2に関する説明と実装方法
これまでこのブログで触れて来なかった、
「Cloud EndpointsでOAuth2を利用する方法」について解説しています。


・デモ
簡単なあしあと帳のようなアプリで、
サーバー側のAPは「一覧取得「メッセージ投稿」の二種類のAPIを作成しています。
それらをJSとAndroidのクライアント側から実行します。
メッセージ投稿APIは要認証なので、クライアント側でOAuth2の認証もしています。


・質問:「JavaScript見た感じjQueryと比べてあまり工数変わらない感じがするけど?」
この質問、会場で上手に答えられなかったのでまとめておきます。

たしかにJS側の実装だけ見るとjQueryでAjaxした場合と記述量はあまり変わらない気もしますが、
佐藤さんや小川さんが答えてくれた事も含めて、以下のように考えます。
 ・クライアントがJSだけでもサーバー側の通信周りの処理が不要になる分「サーバー側の実装コスト」が減る
 ・クライアントとしてJS以外にAndroidやiOSも検討しているなら総合的な工数はさらに減る
 ・GoogleアカウントでのOAuth2認証を使うのが簡単(クライアント・サーバー側両方)
 ・JS側で他のGoogle APIも使うなら、使い方が同じなので相性が良い。
 ・API Explorerを利用可能 (自動で「Google APIs Discovery Service」に準拠する)



◯「BigQueryの新機能」

当初はCloud Endpointsだけの予定でしたが、
前回の記事「BigQueryの新機能 (2013/03/15)」の後に
Google佐藤さんより依頼があったので、追加でデモを行いました。

・デモ
JOIN EACH(Big JOIN)とGROUP EACH BYのデモを行いました。
デモでお見せした内容は「BigQueryの新機能 (2013/03/15)」と全く同じですので、
こちらのエントリーをご覧いただければと思います。


・JOIN EACH(Big JOIN)とGROUP EACH BYで何が変わるか
これまでBigQueryを使う上で、JOINのサイズ制限が使い勝手の上で結構苦労が多く、
この制限を使う回避するために割く時間はBigQueryを扱う中で結構な時間を占めていました。
また、長期的に見た場合にデータの増加や新しい集計パターンに対処できなくなる懸念もありました。
新しい機能によってその手間や懸念が無くなる、というのが良いところです。


・質問:JOINサイズの制限
「ドキュメント上は制限がなくなったとあるが、実際にはあまりに大きいテーブルをJOINした場合にエラーになる事がある」という話に対して
「では限界はどのくらいなのか」という質問をいただきましたが、
残念ながらこちらは把握できていません。

限界値を探るために数億件レベルのサイズのテーブルをJOINしてクエリを投げまくると、
さすがに課金が発生してしまうので個人ではちょっと痛いのです。。
(企業なら気にしないレベルの金額だと思いますが)


・質問:「常にEACHをつけてしまえばOK?」


1.「基本はEACHを付ける」
2.「EACHを付けた場合に速度に問題があるならSmall JOINを検討する」
という手順が良いと思います。


ドキュメントに「Small JOINの方が速い」と明記されており、
前回のエントリーでも実際にクエリを投げて検証したように、実際にSmall Joinの方が速いようです。
(前回の検証では、あるクエリについてBig JOINが14.0sでSmall JOINが4.9s)

そのため、「工数」と「求める速度」のトレードオフで決めるのが良いとおもいます。

「夜中にバッチ処理から実行する」等でクエリの実行速度をそこまで求めない場合には
BIG JOINにしてしまえばクエリ作成が楽ですし、
運用期間を重ねるに連れてテーブルサイズが増大しSmall JOINでは動かなくなる、
という心配も減ります。
よってこの場合は「常にEACHをつける」で良いと思います。

逆に「ユーザーに見せるためにWebアプリからAPI経由でBigQueryを実行して結果を表示する」ような、
速度が要求される場合かつJOINするテーブルについて今後のサイズ増大の懸念が無いなら
Small JOINを検討する、のが良いでしょう。



◯最後に

以上で"自身によるまとめ"とさせていただきたいと思います。

まともに登壇させていただくのは今回が初めてでしたが、良い経験になりました。
聞いていただいた皆様ありがとうございました。m(__)m

あと、発表の練習場所とアドバイスをくれたもーりさん、ありがとうございましたヽ(´▽`)ノ


他の方の発表については下記のブログでまとめられているので、こちらをご覧ください。
appengine ja night #24 #ajn24 に行ってきました - @thorikiriのてょりっき
(この方はいつも色々な勉強会を鬼のような速さでまとめていて驚きます)





このエントリーをはてなブックマークに追加

2013年3月2日土曜日

22.Google Cloud Endpointsを試してみた (3/3)

このエントリーをはてなブックマークに追加






Ryo Yamasaki(@vierjp)です。

今回もGoogle Cloud Endpointsについてです。

前回・前々回の
Google Cloud Endpointsを試してみた (1/3)
Google Cloud Endpointsを試してみた (2/3)
では、「Server側(バックエンド)の作成」と「Androidクライアントの作成」を試してみました。

その後、Web先端技術味見部#17 (今回は「Google Cloud Endpoints」!)
という勉強会でのサポートの依頼があったので、
JavaScriptからのEndpointsの呼び出しも試してみました。

隣でサポートするだけの予定が急遽前に出て話す事になったので
当日の説明はグタグダになってしまいましたが。。orz


◯JavaScriptからの呼び出し方

JavaScriptからAPIを呼び出す場合もAndroidの場合と同様、
関数を呼び出すだけです。

以下は一覧取得APIを実行して結果をコンソールログに出力するだけの簡単なサンプルです。

<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="utf-8">
<title>Endpointsのテスト</title>
<script type="text/javascript">
function init() {
 var ROOT = 'http://localhost:8888/_ah/api';
 // APIを使うための初期化処理
 gapi.client.load('testEndpoint', 'v2', function() {
  // 一覧取得APIの実行
  gapi.client.testEndpoint.tests.list().execute(function(resp) {
   console.log(JSON.stringify(resp));
  });
 }, ROOT);
}
</script>
<script src="https://apis.google.com/js/client.js?onload=init"></script>
</head>

<body>
 <div>
  <h1>Endpointsのテスト</h1>
 </div>
</body>
</html>

注意点として、公式ドキュメントに書いてある以下の記述について、

var ROOT = 'https://your_app_id.appspot.com/_ah/api';
gapi.client.load('your_app_id', 'v1', function() {
    doSomethingAfterLoading();
}, ROOT);

「load」関数の第一引数として書かれている「your_app_id」はおそらく間違いで、
@APIアノテーションの「name」で指定したAPI名を指定するのが正しいと思います。
(実際にドキュメントの後半のサンプルでは「API名」を指定しています)

参考:
Using Endpoints in a JavaScript Client


◯JavaScriptからアクセスする場合は明示的なクライアント・ライブラリの生成が不要

JavaScriptでアクセスする場合には、
Androidの場合に行ったような「明示的なクライアント・ライブラリの生成作業」は必要ありません。
2013/03/29追記
「WEB-INF/*****.api」ファイルは自動的に生成されますが、
・「WEB-INF/*****-rest.discovery」
・「WEB-INF/*****-rpc.discovery」
の2ファイルは明示的に「クライアント・ライブラリの生成作業」を行うことで生成されます。


Endpointsを使用して作成したAPIは、「Google Discovery API」の仕様に準拠するために必要な機能が自動で用意されます。

そして前項のサンプルのように、
この仕様に基づいてAPIの実行に必要な情報をアプリから取得(load)し、
目的のAPIを実行(execute)できるようになっています。

第一回で触れた「API Explorer」で、
作成した各APIをそれぞれのAPIの仕様に沿ったフォームから簡単に実行できるのも
「Google Discovery API」の仕様に準拠しているためです。

Googleが公開している各種のAPIも多くが「Google Discovery API」の仕様に準拠しています。
例えば以下のブログはEndpointsとは関係の無い記事ですが、
JavaScriptだけでGoogle URL Shortener APIを使う | せかいろぐ

Googleが提供する短縮URLを生成するためのサービス「Url Shortner」の
JavaScript APIも、具体的なAPI名や関数名を除けば
前項のEndpointsのサンプルと使い方が同じである事がわかると思います。

このようにEndpointsで公開したAPIは「Google Discovery API」の仕様に準拠しているため、
特に専用のクライアントライブラリを必要とすることなく、
Googleの各種APIを扱うのと同じように利用することができるわけです。

参考:
API Reference - Google APIs Discovery Service — Google Developers


◯サンプルアプリ

JavaScript経由でEndpointsを通じてCRUDする動作サンプル
突貫で作ったので結構適当です(;´∀`)

サンプルアプリのeclipseプロジェクト
Bitbucketの公開リポジトリに配置しています。
サーバー側の実装も前回までに使用したものとは少し変わっていますが、やってることはほとんど同じです。

サンプルアプリのAPI Explorer
Endpointsの第一回に書いた「API Explorer」を実際に動作させることが可能です。


◯まとめ

Endpointsを使う場合、JavaScriptからAPIにアクセスする場合にも
自分で非同期通信の処理等を書くことなく、
関数を呼ぶだけでAPIを呼び出して処理を実行して結果を取得することができます。

Endpointsで作成したAPIは、自動的に「Google Discovery API」の仕様に準拠します。

そのため明示的なクライアント・ライブラリの生成を行う必要はなく、
JavaScriptで「Google Discovery API」の仕様に沿って呼び出すことで、APIを実行することが可能です。


といったところで、Endpointsの第三回は「JavaScriptからの実行方法」でした。
もしかしたらEndpoints でのOAuth2.0対応についてもそのうち調べて書くかも?

それではまた(`・ω・´)ノシ

* 2013/04/12 追記 OAuth2については以下をどうぞ

appengine ja night #24 Google Cloud Endpoints and BigQuery
のスライドで説明しています。






このエントリーをはてなブックマークに追加

2013年2月22日金曜日

21.Google Cloud Endpointsを試してみた (2/3)

このエントリーをはてなブックマークに追加




Ryo Yamasaki(@vierjp)です。

今回は、前回の「Google Cloud Endpointsを試してみた (1/2)」の続きです。

前回はEndpoints使用時のサーバー側について書きましたが、
今回はEndpointsの真骨頂(たぶん)、「自動生成されたクライアントライブリを使ってAPIを実行する」を
Androidで試してみました。

と言っても悩んだところやハマったところはだいたいサーバー側で、
Endpointsに関するクライアント側の実装は本当に簡単です。


◯Androidアプリの環境設定

・プロジェクトの作成

eclipseで「新規」→「Android」→「Android アプリケーション・プロジェクト」で
プロジェクトを作成する。

対応するAndroidのバージョンは
・「最小必須SDK」はAPI 8 (Android 2.2 Froyo)
・ターゲットSDKは API 17 (Android 4.2 Jelly Bean)
としました。
(この範囲のバージョンで動けばいまどき十分なんじゃないかな、ということで)

・AndroidManifest.xmlにパーミッションを追加

Endpoints を使うために必要なパーミッションはインターネットアクセスのみです。
<uses-permission android:name="android.permission.INTERNET"/>

・依存ライブラリの配置

サーバーアプリプロジェクトで生成したクライアントコードのディレクトリ「endpoint-libs」以下、
前回の例なら「PROJECT_LOC/endpoint-libs/libnewTestEndpoint-v2/newTestEndpoint/libs」
から下記のファイルを取得してAndroidプロジェクトの「libs」にコピーします。
・google-api-client-1.13.2-beta.jar
・google-oauth-client-1.13.1-beta.jar
・google-http-client-1.13.1-beta.jar
・guava-jdk5-jdk5-13.0.jar
・jsr305-1.3.9.jar
・google-http-client-gson-1.13.1-beta.jar (when using GSON)
・gson-2.1.jar
・google-api-client-android-1.13.2-beta.jar
・google-http-client-android-1.13.1-beta.jar

参考:
Using Endpoints in an Android Client - Google App Engine — Google Developers

・生成されたクライアントコードの配置

・Android側プロジェクトの直下に「endpoint-libs」フォルダを作成する 
・「endpoint-libs」直下からサーバー側の「libnewTestEndpoint-v2」フォルダにリンクする
 「endpoint-libs」を右クリック→「新規」→「フォルダー」
 →「拡張」→「Link to alternate location」をチェック
 →「参照」でサーバー側プロジェクトの「endpoint-libs/libnewTestEndpoint-v2」を選択してOK。
 (ワークスペースのディレクトリは「WORKSPACE_LOC」という変数でも指定できます)



この機能は「Linked Resources」という、eclipse内でのシンボリック・リンクのようなものです。
通常は「自動生成されたクライアント・ライブラリ」のフォルダを
サーバー側プロジェクトからクライアント側プロジェクトにコピーするのですが、
「Linked Resources」にしておけば
「クライアント・ライブラリ」を再生成した際に毎回クライアント側にコピーする必要が無くなります。
Endpoints以外でも便利に使えるタイミングがあるので地味にオススメです。

・ビルド・パスの設定

プロジェクトを右クリック→「プロパティ」→「Javaのビルド・パス」→「ソース」タブ→「フォルダの追加」で、
「endpoint-libs/libnewTestEndpoint-v2/newTestEndpoint/newtestendpoint-v2-generated-source」
を追加。
これによってサーバー側で生成したコードをAndroid側で使用できるようになります。


これでAndroidでEndpointsを利用するための準備が整いました。


◯Android側の実装サンプル

・クライアント・ライブラリの実行例

前回書いたのと同じですが、
クライアント側からの呼び出しは簡単で以下のように自動生成されたメソッドを呼び出すだけです。
NewTestEndpoint.Builder builder = new NewTestEndpoint.Builder(AndroidHttp.newCompatibleTransport(),
   new GsonFactory(), null);
mEndpoint = builder.build();
ListData listdata = mEndpoint.listData().setLimit(20);
CollectionResponseNewTestV2Dto response = listdata.execute();

・実装サンプル

作ったのはAppEngineのカーソルを使ったシンプルなオートページングのサンプルです。
よく見る「一定件数ずつデータを取得して必要に応じて続きを取得する」機能ですね。


・MainActivity.java

public class MainActivity extends Activity {
 private static final String TAG = "EndpointsAndroidSample";
 /**
  * ページング件数
  */
 private static int PAGING_LIMIT = 20;
 /**
  * Endpoint
  */
 private NewTestEndpoint mEndpoint;
 /**
  * ListView
  */
 private ListView mListView;
 /**
  * ListViewのフッター(ローディング画像の表示)
  */
 private View footer;
 /**
  * ListViewが使うAdapterクラス
  */
 private TestAdapter mAdapter;
 /**
  * ListViewに表示するListデータ
  */
 private List<NewTestV2Dto> mList;
 /**
  * 次ページのデータを取得するためのCursor(AppEngineの)
  */
 private String mCursor;
 /**
  * Endointからデータを取得するためのAsyncTask
  */
 private AsyncTask<Void, Void, CollectionResponseNewTestV2Dto> mTask;

 @Override
 protected void onCreate(Bundle savedInstanceState) {
  super.onCreate(savedInstanceState);
  setContentView(R.layout.activity_main);

  // ListViewを生成
  ListView listView = (ListView) getListView();
  listView.addFooterView(getFooter());
  listView.setAdapter(getAdapter());
  listView.setOnScrollListener(new OnScrollListener() {
   @Override
   public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount, int totalItemCount) {
    if (totalItemCount == firstVisibleItem + visibleItemCount) {
     Log.d(TAG, "onScroll");
     additionalReading(false);
    }
   }

   @Override
   public void onScrollStateChanged(AbsListView view, int scrollState) {
   }
  });

 }

 private ListView getListView() {
  if (mListView == null) {
   mListView = (ListView) findViewById(R.id.listview);
  }
  return mListView;
 }

 private View getFooter() {
  if (footer == null) {
   footer = getLayoutInflater().inflate(R.layout.listview_footer, null);
  }
  return footer;
 }

 private void visibleFooter() {
  getListView().addFooterView(getFooter());
 }

 private void invisibleFooter() {
  getListView().removeFooterView(getFooter());
 }

 private TestAdapter getAdapter() {
  if (mAdapter == null) {
   mAdapter = new TestAdapter(this, R.layout.list_row, getList());
   additionalReading(true);
  }
  return mAdapter;
 }

 private List<NewTestV2Dto> getList() {
  if (mList == null) {
   mList = new ArrayList<NewTestV2Dto>();
  }
  return mList;
 }

 /**
  * ListViewのデータをクリアする(reload用)
  */
 private void clearList() {
  if (mList == null) {
   return;
  } else {
   mList.clear();
  }
 }

 private void additionalReading(boolean isForce) {

  Log.d(TAG, "additionalReading isForce:" + isForce);

  // 既に読み込み中ならスキップ
  if (mTask != null && mTask.getStatus() == AsyncTask.Status.RUNNING) {
   Log.d(TAG, "running asyncTask.");
   return;
  }

  // 読み込み回数が最大値以上ならスキップ。
  if (isForce == false && mCursor == null) {
   Log.d(TAG, "got AllData.");
   return;
  }
  // AsyncTaskを実行してデータを取得する
  mTask = new QueryDataTask(this, PAGING_LIMIT, mCursor).execute();
 }

 /**
  * ListViewのデータを追加する
  */
 private void addListData(List<NewTestV2Dto> addList) {
  List<NewTestV2Dto> list = getList();
  list.addAll(addList);
 }

 private class QueryDataTask extends AsyncTask<Void, Void, CollectionResponseNewTestV2Dto> {

  private Context context;
  private int limit;
  private String cursor;

  public QueryDataTask(Context context, int limit, String cursor) {
   this.context = context;
   this.limit = limit;
   this.cursor = cursor;
  }

  @Override
  protected CollectionResponseNewTestV2Dto doInBackground(Void... unused) {
   CollectionResponseNewTestV2Dto response = null;
   try {
    if (mEndpoint == null) {
     NewTestEndpoint.Builder builder = new NewTestEndpoint.Builder(AndroidHttp.newCompatibleTransport(),
       new GsonFactory(), null);
     mEndpoint = builder.build();
    }
    ListData listdata = mEndpoint.listData().setLimit(limit);
    if (cursor != null) {
     listdata.setCursor(cursor);
    }
    Log.d(TAG, "execute API.");
    response = listdata.execute();

    return response;
   } catch (IOException e) {
    // Log.d(TAG, e.getMessage(), e);
    // throw new RuntimeException(e);
    Toast.makeText(context, "Error: " + e.getMessage(), Toast.LENGTH_SHORT).show();
    return null;
   }
  }

  @Override
  protected void onPostExecute(CollectionResponseNewTestV2Dto response) {
   if (response == null || response.getItems() == null) {
    return;
   }
   // 結果のListデータを取得する
   List<NewTestV2Dto> items = response.getItems();
   // ログ
   for (NewTestV2Dto dto : items) {
    Log.d(TAG, "onPostExecute - " + dto);
   }

   // 結果のListデータをセットして描画
   addListData(items);
   getListView().invalidateViews();

   // カーソルを保持する
   mCursor = response.getNextPageToken();
   // カーソルを取得できなければ次ページは無いのでfooterを消す
   if (mCursor == null) {
    invisibleFooter();
   }
  }
 }

 @Override
 public boolean onCreateOptionsMenu(Menu menu) {
  // Inflate the menu; this adds items to the action bar if it is present.
  getMenuInflater().inflate(R.menu.main, menu);
  return true;
 }

 @Override
 public boolean onOptionsItemSelected(MenuItem item) {
  Toast.makeText(this, "Selected Item: " + item.getTitle(), Toast.LENGTH_SHORT).show();
  if (item.getItemId() == R.id.action_reload) {
   clearList();
   visibleFooter();
   new QueryDataTask(this, PAGING_LIMIT, null).execute();
  }
  return true;
 }
}

・TestAdapter.java

public class TestAdapter extends ArrayAdapter<NewTestV2Dto> {
 private List<NewTestV2Dto> items;
 private LayoutInflater inflater;
 private int textViewResourceId;

 public TestAdapter(Context context, int textViewResourceId, List<NewTestV2Dto> items) {
  super(context, textViewResourceId, items);
  this.textViewResourceId = textViewResourceId;
  this.items = items;
  this.inflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
 }

 @Override
 public View getView(int position, View convertView, ViewGroup parent) {
  View view = convertView;
  if (view == null) {
   // Viewがnullなら新しくビューを生成
   view = inflater.inflate(textViewResourceId, null);
  }
  // 表示すべきデータの取得
  NewTestV2Dto item = items.get(position);
  if (item != null) {
   // ID
   TextView idView = (TextView) view.findViewById(R.id.textview_testlist_id);
   if (idView != null) {
    idView.setText(item.getId());
   }
   // 名前
   TextView nameView = (TextView) view.findViewById(R.id.textview_testlist_name);
   if (nameView != null) {
    nameView.setText(item.getName());
   }
   // 作成時刻
   TextView createDateView = (TextView) view.findViewById(R.id.textview_testlist_createDate);
   if (createDateView != null) {
    String dateString = item.getCreateDate().toString();
    createDateView.setText(dateString);
   }
  }
  return view;
 }
}


作成したのはこの2クラスのみです。(リソースのxmlは除く)
 やはりAPIの通信部分が無くなると実装量は減ります。
 APIやEndpoints に関連するのは「MainActivity.java」の157行目から167行目の呼び出し部分くらいで、
それ以外のコードは純粋にAndroidのコードです。

ほとんど下記ページを参考にさせてもらいました。
visible true: ListViewで最後尾までスクロールしたら自動的に要素を追加読み込みするサンプル

多少でも手を入れてるとソースまるごと貼り付けるのは勇気が要りますね。
私は一応Androidディベロッパーですが、Androidの経験は少ないのでお手柔らかに。(;´∀`)

本当はデータの追加・更新・削除も作りたかったんですが、
慣れないAndroidの実装が大変で力尽きました。。
まあEndpointsを試す目的においてはこれで十分かな、と。
やってみたかったオートページングはやりきったゾ。(`・ω・´)


・おまけ

・Java Scriptでのクライアント実装方法

Using Endpoints in a JavaScript Client - Google App Engine — Google Developers
今回試していませんが、Android以上に楽そうな気がします。
こっちでサンプル作るべきだったかも。

・iOSでのクライアント実装方法

Using Endpoints in an iOS Client - Google App Engine — Google Developers

iOSに関してはこちらのサイトで詳しく解説されています。
「Google Cloud Endpoints」 - Web先端技術味見部 - NAVER まとめ


・まとめ

Androidのクライアント側の実装内容は以上です。
重要なところは前回で書いてしまっていた気もしますが、
特にクライアント側に関しては環境設定の作業もAPI呼び出し部分の実装量も少なく、
実装がかなり楽になりそうに感じました。

個人的には、あとはFrontend Cacheが動作することさえ確認できれば、
自前でAPI作る形式から乗り換えるのはアリかな、と思ってます。

特にiPhone・Android両方に対応する必要があるようなケースや、
Endpointsが自動でやってくれている通信部分の処理を隠蔽する自前の基盤を現時点で持っていないなら、
Endpointsを使うことで結構な工数を削減できるように思います。


ではまた(`・ω・´)ノシ


次回 「Google Cloud Endpointsを試してみた (3/3)」 はJavaScriptでのクライアント実装例です。




このエントリーをはてなブックマークに追加

2013年2月19日火曜日

20.Google Cloud Endpointsを試してみた (1/3)

このエントリーをはてなブックマークに追加




Ryo Yamasaki(@vierjp)です。

Google App Engine 1.7.5 から使えるようになった Cloud Endpoints を試してみました。
結構ボリュームが大きくなってしまったので、3部構成です。
今回は概要とサーバー側について、
次回がクライアントをAndroidにした場合の実装例、
三回目がクライアントをJava Scriptにした場合の実装例です。


◯概要

Google Cloud Endpoints は
App Engineのバックエンドからエンドポイントおよびクライアント・ライブラリを生成可能にするツール、
ライブラリ、および機能で構成され、Webアプリケーションへのクライアントアクセスを単純化します。

EndpointsはWebクライアントやAndroidやAppleのiOSなどのモバイルクライアント向けの
Webバックエンド(*1)を作成することを容易にします。

モバイル開発者に対しては、Endpointsは、共有Webバックエンドを開発するための簡単な方法を提供し、
また、OAuth 2.0認証などの重要なインフラストラクチャを提供することで
Endpointsを使わない場合に必要とされる多くの作業を不要にします。

さらに、バックエンドはAppEngineのバックエンドであるため、
モバイル開発者はAppEngineで利用可能な全てのサービスや機能、
例えばDatastore、Google Cloud Storage、Mail、Url Fetch、Task Queues等を使用することができます。
バックエンドにAppEngineを使うことで
開発者は「ロードバランス」「スケーリング」「サーバーメンテナンス」といった、
システム管理者の仕事から開放されます。

EndpointsなしでApp Engineのバックエンド用のモバイルクライアントを作成することは可能です。
しかしながら、Endpointsを使うことでApp Engineとの通信を処理するためのラッパーを記述する必要がなくなるため
このプロセスが容易になります。
Endpointsによって生成されたクライアント·ライブラリを使用すると、
単に直接のAPI呼び出しを行うことが可能になります。(*2)

*1.「バックエンド」はクライアントアプリに対応する「サーバーアプリ」の意味で理解すれば良いと思います。
  AppEngineの機能の「Backends」はここでは関係ありません。
*2.生成されたライブラリのメソッドを実行すれば透過的にAPIを実行して結果を取得することができます。(次項参照)

参考:
Overview of Google Cloud Endpoints - Google App Engine — Google Developers


◯クライアント側のAPI実行例 (Android用)

NewTestEndpoint.Builder builder = new NewTestEndpoint.Builder(AndroidHttp.newCompatibleTransport(),
   new GsonFactory(), null);
mEndpoint = builder.build();
ListData listdata = mEndpoint.listData().setLimit(20);
CollectionResponseNewTestV2Dto response = listdata.execute();
「NewTestEndpoint」「ListData」「CollectionResponseNewTestV2Dto」等のクラスは
サーバー上に作られたAPIに対応する形で生成されたクラスです。
このようにサーバーの存在を意識せず、
ライブラリのメソッドを呼び出すのと同様の感覚でサーバー上のAPIを実行できるわけです。
クライアント側の処理についての詳細は次回に。


◯必要なもの

・Google App Engine Java SDK  (1.7.5以上)
・Google Plugin for Eclipse (1.7.5と一緒にリリースされたバージョン以降)
・Android Development Tools (最新を使いましょう)

参考:
Overview of Google Cloud Endpoints - Google App Engine — Google Developers
Using Endpoints in an Android Client - Google App Engine — Google Developers


◯Endpointsの簡単な処理の流れ

触ってみた感じ、処理の流れとしては
  1. クライアントアプリから「クライアント・ライブラリ」のメソッドを呼び出す。
  2. Cloud Endpoints(「/_ah/api/*」)にアクセスする
  3. Cloud Endpointsが「GAEアプリに配置したServlet」(「/_ah/spi/*」)にアクセスする
  4. 「/_ah/spi/*」が「実装したサービスの処理」を呼び出して結果を返す
という流れになっているようでした。


◯今回やること

・サーバー側にCRUD(データの作成・取得・更新・削除)用のAPIを作成する (第一回)
 (とりあえずOAuth認証はなし)
・クライアントライブラリの生成 (第一回)
・GAEアプリのデプロイ (第一回)
・API ExplorerからAPIを実行して試してみる (第一回)
・Androidアプリを作ってそこからEndpointsを通してAPIを実行する (第二回)


◯サーバー側アプリの環境設定

・プロジェクトの作成

eclipseで「新規」→「プロジェクト」から、
「Slim3プロジェクト」もしくは「AppEngine用Webアプリプロジェクト」を選択して新規のプロジェクトを作成する。
今回DatastoreアクセスにSlim3を使いたかったのでSlim3プロジェクトにしました。
既存のプロジェクトにEndpointsを追加したい場合はそのまま設定を追加すればOKです。

・web.xmlの記述

「AppEngine用Webアプリプロジェクト」を作成すると最初からweb.xmlにEndpointsのための初期設定が書かれていますが、
 Slim3プロジェクトとして作成した場合でも同様の記述を書いてやればOKです。
 
  SystemServiceServlet
  com.google.api.server.spi.SystemServiceServlet
  
   services
   
  
 
 
  SystemServiceServlet
  /_ah/spi/*
  
 

・「init-param」の「services」のvalueは空でOK
Endpointのクラスを作成すると自動的に追記されるので最初は空でOKです。

・「<security-constraint>」はコメントアウトしています。
「<security-constraint>」の指定はドキュメントには書かれていますが自動生成されたweb.xmlには書かれていません。
 SSLでのアクセスをを強制する記述ですが、これをONにした場合、
 EndointsからGAEアプリにAPIの定義情報を取得しにくるため(?)にアクセスされる
「/_ah/spi/BackendService.getApiConfigs」へのリクエストが 正常に完了しないのでコメントアウトしました。

参考:Generating Client Libraries - Google App Engine — Google Developers

・Java7には対応していないようです

Java7の構成にして@Apiアノテーションをつけたクラスを作成すると以下のエラーが発生します。
--------------------------------------------------
ビルド中にエラーが発生しました。
プロジェクト 'EndpointsServerSample' でビルダー 'Google App Engine Project Change Notifier' の実行中にエラーが発生しました。
jp/vier/sample/endpoints/endpoint/v2/NewTestV2Service : Unsupported major.minor version 51.0
--------------------------------------------------
EndPoints関連の自動生成処理でエラーが発生するようです。

* 2013/04/12 最新のSDKとEclipse-Pluginを使ったところ、↑の問題は解消しています。


◯公開するサービスのクラスを作成する

・作成するメソッド

メソッド名処理概要
getDataユニークなIDを指定してデータを一件取得する
deleteDataユニークなIDを指定してデータを一件削除する
putDataユニークなIDと任意で「名称」を指定してデータを一件追加する
listDataデータを一覧で取得する。任意で「limit」と「カーソル」を指定可能

・APIとして公開するサービスクラス

@Api(
        name = "newTestEndpoint",
        version = "v2",
        description = "Test API description hogev2")
public class NewTestV2Service {

    private Logger log = Logger.getLogger(NewTestV2Service.class.getName());

    @ApiMethod(httpMethod = HttpMethod.GET)
    public NewTestV2Dto getData(@Named("id") String id) {
        log.info("test:v2 id:" + id);

        Key key = Test.createkey(id);
        Test test = Datastore.getOrNull(Test.class, key);

        NewTestV2Dto dto = new NewTestV2Dto();
        BeanUtil.copy(test, dto);

        log.info("test:dto" + dto);
        return dto;
    }

    @ApiMethod(httpMethod = HttpMethod.DELETE)
    public NewTestV2Dto deleteData(@Named("id") String id) {
        // log.info("books:" + books.toString());
        log.info("test:v2 id:" + id);
        Key key = Test.createkey(id);
        Test test = Datastore.getOrNull(Test.class, key);
        if (test == null) {
            return null;
        } else {
            Datastore.delete(key);
        }

        NewTestV2Dto dto = new NewTestV2Dto();
        BeanUtil.copy(test, dto);

        return dto;
    }

    @ApiMethod(httpMethod = HttpMethod.PUT)
    public NewTestV2Dto putData(@Named("id") String id,
            @Nullable @Named("name") String name) {
        log.info("test:v2 id:" + id + " name:" + name);

        Key key = Test.createkey(id);

        Test test = Datastore.getOrNull(Test.class, key);
        if (test == null) {
            test = new Test(key);
            if (name == null) {
                name = "name Of " + id;
            }
        }
        test.setId(id);
        test.setName(name);
        Datastore.put(test);

        NewTestV2Dto dto = new NewTestV2Dto();
        BeanUtil.copy(test, dto);

        log.info("test:dto" + dto);
        return dto;
    }

    @ApiMethod(
            httpMethod = HttpMethod.GET,
            cacheControl = @ApiMethodCacheControl(
                    noCache = false,
                    maxAge = 12000))
    public CollectionResponse<Newtestv2dto> listData(
            @Nullable @Named("limit") Integer limit,
            @Nullable @Named("cursor") String cursor) {

        if (limit == null || limit > 20) {
            limit = 20;
        }

        TestMeta meta = TestMeta.get();
        ModelQuery<Test> query =
            Datastore.query(meta).sort(meta.createDate.desc).limit(limit);
        // ページングの指定があれば指定
        if (cursor != null) {
            query.encodedStartCursor(cursor);
        }
        S3QueryResultList<Test> queryResultList = query.asQueryResultList();
        log.info("test:v2 queryResultList:" + queryResultList);

        // DTOに詰め替え
        List<Newtestv2dto> dtoList = new ArrayList<Newtestv2dto>();
        for (Test test : queryResultList) {
            NewTestV2Dto dto = new NewTestV2Dto();
            BeanUtil.copy(test, dto);
            dtoList.add(dto);
        }
        log.info("test:v2 dtoList:" + dtoList);

        Builder<Newtestv2dto> builder =
            CollectionResponse.<Newtestv2dto> builder();
        builder.setItems(dtoList);
        if (queryResultList.hasNext()) {
            builder.setNextPageToken(queryResultList.getEncodedCursor());
        }
        return builder.build();
    }
}

* サンプルコードが長くならないようにトランザクション管理は省略しています。


・アノテーションの種類

アノテーション概要
@Api
APIのクラスを特定するため、クラスに指定します。
以下の3つの属性は後述の「API Explorer」の表示に関わってくるので、最低限下記の3つは指定しましょう。
name
APIの名称です。
Androidのクライアントコードとして生成されるEndpoint関連クラスのクラス名にもなります。
version
APIは多くの場合に複数バージョン並行稼動させで、
そのバージョン名を指定します。(v1等)
description
API ExplorerにAPIの説明として表示されます。
@ApiMethod 
APIのメソッド毎の設定をするために指定します。
特に指定しなくても動くようですが、
なんとなくHttpMethodを明示的に指定したかったのと、
公開しているメソッドがわかりやすくていいかな、ということで付けました。
(明示的にHttpMethodを指定しない場合メソッド名から自動で決められるそうです)
@Namedメソッドの引数に対して指定します。
@ApiMethodのPathで「URLと引数のマッピング」を指定しないなら
必須のようです。
@Nullable引数にNullを許可する場合に指定します。
これを指定する場合には@Named属性が必須です。
他にもドキュメントに書いていないところで
@ApiMethodCacheControl@ApiMethod内で指定できたりします。
FrontendCache使えるのか!?とwktkして「listData」メソッドに指定したのですが、、
キャッシュ効きませんでした。orz
何か条件が足りないのかもしれません。(´・ω・`)

参考:
Adding Endpoint Annotations - Google App Engine — Google Developers


・API定義ファイルの自動生成

前述の@APIを指定したクラスを保存したタイミングで、
「WEB-INF」直下に「*.api」ファイルが生成されます。
上のクラスの場合は「WEB-INF/newTestEndpoint-v2.api」というファイル名になります。
このファイルは「API名 + バージョン番号」に対して一つ生成されます。
例えば
他に同じ名前のバージョン1のクラスがあれば、
「WEB-INF/newTestEndpoint-v1.api」が生成され、
別のAPI名のクラスがあればそれに対応するファイルも生成されます。

@APIを指定したクラス」の記述内容に問題がある場合にこのファイルが生成されない事がよくあります。
記述内容に問題があっても明確なコンパイルエラー以外は保存時にエラーが通知されないので、
このファイルが存在しているかは常にチェックしておく必要があります。
(問題のある記述があるとエラーなしにファイルが削除されます)


◯クライアントライブラリの生成

・eclipse-Pluginから生成する

「プロジェクトを右クリック」→「Generate Cloud Endpoint Client Library」
とするだけで作成できます。
上述の「NewTestV2Service」の場合、以下のファイルが作成されます。
・「WEB-INF/newTestEndpoint-v2-rest.discovery」
・「WEB-INF/newTestEndpoint-v2-rpc.discovery」
・「endpoint-libs/libnewTestEndpoint-v2」フォルダ
 (自動生成されたクライアント向けのライブラリが格納されています)

・コマンドラインから生成する

sh /Applications/eclipse4.2/plugins/com.google.appengine.eclipse.sdkbundle_1.7.5/appengine-java-sdk-1.7.5/bin/endpoints.sh get-java-client-lib\
 jp.vier.sample.endpoints.endpoint.v2.NewTestV2Service
上述の「NewTestV2Service」の場合、以下のファイルが作成されます。
・「WEB-INF/newTestEndpoint-v2-rest.discovery」
・「WEB-INF/newTestEndpoint-v2-rpc.discovery」
・「WEB-INF/newTestEndpoint-v2-java.zip」
 (コマンドから生成した場合に「endpoint-libs/libnewTestEndpoint-v2」の代わりに生成されます。中身はほぼ同じ。)

・生成されるファイルがわかる参考画像



参考:
Generating Client Libraries - Google App Engine — Google Developers


◯デプロイしてみる

通常のGAEアプリと同様にデプロイできます。


◯API Explorerから実行してみる

APIの一覧表示と、APIをテスト実行する機能が提供されます。
URLは
https://[your_app_id].appspot.com/_ah/api/explorer
です。

・API一覧

サービスクラスに定義した各メソッドが一覧で表示されます。


・API実行画面



クライアントを作成しなくてもここからAPIを実行してテストでき、とても便利です。

必要に応じてパラメーターを入力して「Execute」ボタンを実行するとAPIを実行して結果を表示します。

OAuthで認証してのテストもここからできるので、
クライアント無しに一通りテストできるのではないでしょうか。
入力項目はクラスの記述内容に応じて表示されたり、必須項目になったりします。

参考:
Testing and Deploying Endpoints - Google App Engine — Google Developers


◯気がついた挙動


新しいAPIを追加したり、APIの設定を変更した場合に反映までに若干のタイムラグがある
 APIの設定を変更した場合には
 デプロイしてからそのAPIを使えるようになるまで(API Explorerに反映されるまで)
 少し時間がかかります。(数秒〜20秒くらい)
 APIの定義を変えずにメソッド内の挙動を変えただけならすぐに反映されます。
 ログを見た感じ、
 ・デプロイするとAppEngineからEndpointのサーバー(?)に通知が行く。
 ・EndointのサーバーからAppEngineアプリにアクセスしてAPIの定義情報を取得しにくる
  (「/_ah/spi/BackendService.getApiConfigs」というURL)
 これによってAPIExplorerに表示されるようになり、
 かつ実際にそのAPIが使えるようになる。
 この際に、たまに「API backend's app revision」が違う、というエラーで400になるが、
 最後に200を返すまでリトライされてその後使えるようになる。

一度配置したAPI(@Apiの単位)はアプリから削除しても消えない
 クラス内のメソッドの追加・削除はちゃんと反映されました。
 (ミスった時に困る。Datastoreのインデックスみたいにvacuum機能とかあるのだろうか)

一度配置したAPIのdescriptionは変更しても反映されない。
なぜか反映されない。

実装コード中でシステムエラー(500)が発生するとクライアントには503が返る
これはたぶん意図的。


◯ハマリどころとかポイント

・設定を間違えていると「WEB-INF」直下の「*.api」ファイルが生成されない場合がある
 この場合記述で何か間違っているから修正するべし。(理由は表示されない><)

・@Apiのname属性の値を大文字から始めるとエラー
 その場ではエラーにならないが、
 ・Client Libraryの生成でエラーになる
 ・APIs Explorerに認識されない。

・同じAPIのバージョン違いを配置する場合、クラス名を変えたほうがいい。
 クラス名・メソッド名が同じパッケージ違いのクラスを作って@Apiのversionを変える構成にしたところ、
 ApiExplorerからそれぞれのバージョンのAPIを呼んだ際に
 どちらも同じクラス(一方のバージョンのクラス)が実行された。
 AppEngineのアプリへはどちらのバージョンでも
 「/_ah/spi/NewTestService.listTestDatas」というURLにアクセスしていたため、
 区別がつかないようです。
 APIのバージョンはクラスに指定するアノテーションの「@Api」に記述するので、
 元々バージョン毎にクラスを作成する必要があります。
 その上で「クラス名.メソッド名」をバージョン毎にユニークにする必要があるので、
 クラス名にバージョン番号を含めてしまうのが手っ取り早いでしょう。

* 2013/04/12追記 最新のSDKとEclipse-Pluginを使ったところ、↑の問題は解消しています。
おそらくVer.1.7.6で、内部的にアクセスされているURLが
「/_ah/spi/MessageV1Endpoint.list」

「/_ah/spi/jp.vier.sample.endpoints2.message.v1.MessageV1Endpoint.list 」
のようにパッケージ名も含むように変わりました。
そのため
「jp.vier.sample.endpoints2.message.v1.MessageEndpoint」
「jp.vier.sample.endpoints2.message.v2.MessageEndpoint」
のようにパッケージ名だけ変えればそれぞれのバージョンを区別して実行することができます。



・公開するメソッドの名前はJavaの定番のクラス名と被るような名前にしない方が良い
 メソッド名を「list」にするとクライアントコードのクラスに「List」という名前のクラスが生成されて、
  java.util.Listと衝突して面倒。
  「@ApiMethod」で別の「name」を指定してやれば回避できるけど、メソッド名で意識した方が簡単。

・公開メソッドの返り値をStringにしたら「*.api」が生成されなくなった
 静かなエラーになる。intやbooleanも同様。(リテラルはそのまま返せない)

・公開メソッドの返り値がList<String>の場合実行時エラー
 クライアントコードの生成もデプロイもできるが、
 実際にAPIが結果として「空でないList」を返すと実行時に500エラーになる。

・公開メソッドの返り値として自前のクラス型やList<自前のクラス型>は使える
 返り値が自前のクラス型でその中にString等リテラルのフィールドがあるのも問題ない。
・返り値にする自前のクラス型にはgetter・settterが必要。無いと実行時に500エラー。
 getter・settterが無くても代わりにアノテーション付ければ良いかも。
 
おそらくSlim3のModelもそのまま返すことができると思いますが、
わざわざDTOを作っているのは、複数バージョンの同時稼働を意識した場合に
「Entity定義の変更がそのままAPIのレスポンスに影響して良いのか?」という
Endpointそのものの挙動とは関係無い理由です。
Ajaxだけで使う場合のように、クライアントとAPIをまとめてデプロイするなら良いですが、
複数バージョンのクライアントが同時に存在し得るスマホアプリや別のシステムに提供するAPIは
この方針の方が無難じゃないかな、と。


◯まとめ

正直もう少し細かいエラーチェックと詳細なエラー原因の表示がないとちょっときついので、
ここは改善を期待したいところです。

クライアントコードを作成したり実行してみないとわからないエラーが結構あって、
またその際に詳細な理由が表示されない事が多いので
トライアンドエラーで原因の切り分けをするのに結構時間がかかりました。(´・ω・`)

この点が改善されれば簡単にAPIを公開できるソリューションとして良いと思います。


* 2013/02/19 15:29 ↓↓↓エラーログについての追記ここから↓↓↓

Google +でKazunori Satoさんに教えていただきました(´▽`)
クライアント・ライブラリー生成時のエラーは
eclipseの「Error」(エラー・ログ)タブにJavaのStackTraceが出力されていました。

試しに「listData」メソッドの「@Named」アノテーションを外してみると、
以下のように原因を特定できるレベルのStacktraceが出ました。

Caused by: java.lang.IllegalArgumentException: Error while processing method listData: Parameter type class java.lang.Integer cannot be a resource and thus should be annotated with @Named

「postData」メソッドの返り値をString型にしたところ以下のエラーになりました。
この場合はメソッド名が書かれていませんが「返り値の型が違う」ならすぐに探せるでしょう。
Caused by: java.lang.IllegalArgumentException: Type class java.lang.String cannot be used as a return type

先に知ってればだいぶ苦労が減っただろうと思います。orz
(いつも「問題」しか開いてないんだ。。(´・ω・`))

Slim3 PluginのModelの内容チェックのように、エディタ上にエラーの箇所と内容が出てくれたら理想ですが・・・w

* 2013/02/19 15:29 ↑↑↑エラーログについての追記ここまで↑↑↑



既にサーバー側でAPIを作るための基盤を作りこんでいる場合には
API自体の作成工数はそれほど変わらないでしょうが、
「API Explorer」が生成されるのはとても便利です。

特にサーバー側とクライアント側で担当者が分かれている場合には、
サーバー側の担当者はAPI Explorerから実際に実行して簡単に挙動を確認することができますし、
いくらかドキュメント代わりになります。
(Javadocみたいに各項目やAPIに説明を追記できたらさらに良いなぁ、と思ったり)

また、何よりも次回に書く「クライアント側(Android)の実装」はとっても簡単で、
ハマるポイントも少ないと思います。
さらに「Java Script」「iPhone」「Android」と、
3種類のクライアント用コードを自動生成できるのはやはり魅力でしょう。


次回はAndroidでの実装例です。
それではまた(`・ω・´)ノシ

続く↓

Google Cloud Endpointsを試してみた (2/3)






このエントリーをはてなブックマークに追加