2009年10月17日土曜日

#appengine java night #2( #ajn2 )に参加した

ゲットしたノウハウ

  • JDOのEntityGroupで、親Entityに子Entityを保持するOwnedと、KeyのみでのEntityGroupの構築、でパフォーマンスに倍くらいの差がある。
    • これは気づいてなかった!
  • OpenCVの存在

@yuroyoroがスピーカーの勇姿を激写した

感想

参加人数が前回よりはるかに多くなりそうだった&経験者比率が低くなりそうだったので、前回ほどプレゼン中の質問とかが出ないかも?という心配をしていましたが、結構質問も出ていいカンジになってくれて良かったです。ぶいてくのたけざきさんがいい具合に突っ込みを入れてくださったのにも助けられました。また前回同様、ところどころひがさんが補足or質問してくれる形式は良いですね。また、今後発表者となる方は、資料は普段の1.5-2倍時間がかかると思ってくださいw 今回は1.5倍かかりますた。

shin1ogawaの発表資料

自分の内容は経験者向けのトーク、という事で未経験者や前回参加していない方には不親切だったと思います。そのあたり申し訳なかったです…といいつつ、今後スピーカーをやるとしても、そういった路線でやっていきますのでよろしく!
とはいえ私での不手際もやってしまいました。会場のタイムテーブルの流れにあわせて本編で話すべき自動テストのプレゼンとLTを入れ替えて話したんですが、LTの方は自動テストの話ありきで書いていたのです。それなのに逆にしてしまった。そんなワケでLTに関してはかなり「?」となった人も多かったと思います、すんませんすんません。LTの方は、「RemoteAPIの代わりとして使える仕組みを作ったよ!」て話でした。

感謝

めちゃくちゃカッコイイ会場を提供してくださった株式会社リクルート様、リクルートメディアラボの川崎様、大変ありがとうございました。また今回も開催に関するしきりをしてくださったスティルハウス佐藤さん楠元さん(10/19追記:漢字を間違っていたので修正しました、失礼しました)、貴重なお話をしてくださったひがさん、ありがとうございました!

宣伝

マッシュアップと言っても、以前のような「複数のサービスを組み合わせる」といった意味ではなく、最近は「いろんなPlatform、デバイスで動作させる」などなど広い意味で使われるそぅです。対象となっているAPI、Platformのどれかひとつでも使っていれば参加できるそーなので、皆さんも参加してみましょう!もちろん、Google App Engineも対象です。…というワケで、自分も登録しました。嫁ちゃんとペアで参加しようかなーと思います。

2009年10月13日火曜日

[宣伝] #appengine javaの入門者向けのセミナーを開催します

Google周りを触っている会社として売っていこう!という会社の取り組みで、無料セミナーをやる事になりました。

これはappengineの入門者向け(Javaエンジニアが対象)なので、appengine java nightに参加されているような猛者の方とかは来てもたぶん面白くないです。AppEngineの嬉しいトコロか、今までのJavaアプリとの違い・そこからくる注意点とか、んじゃどんなカンジに作ろう?とかの触りの部分、おおまかな概要を1時間程度でかるーく説明する予定です。
もしまだappengineをあまり触っていなくて、触る前に「appengineってどんなカンジなのか?」をザックリと知りたい、という方にはちょうど良い内容なんじゃないかと思いますんでぜひご参加ください。申し込み方法はatndのページで参加登録をして、そこにある登録フォームへのリンクからも登録情報を入力してください。ちょっと面倒な手順となってしまい申し訳ないですがよろしくお願いします。
今後の予定としては、3日間ほどかけて実際にひととおりの機能を実装(主にDatastore周りになるでしょうね)していくような有料セミナーも開催したいなーとか考えていたりします。

もうひとつ宣伝

また弊社ではAppEngine以外にはAppsもやっていて、それについて弊社代表の加藤が入門者向けにAppsの無料セミナーをする予定です。こちらは16日(金)に開催する予定で、Appsの導入を迷っておられる経営者の方が対象のようです。詳細は下記URLからご確認ください。

2009年10月12日月曜日

#appengine javaのdatastore操作は #slim3 がおすすめ

自分はdatastoreのアクセスにJDOを使わない(個人的には、という意味ですが)し、Webアプリとしてのフレームワークという意味ではWicketTester並みの単体テスト環境が無いとイヤなので、slim3は今まで見送ってました。しかし、最近slim3がlow-level APIに対応したといぅ事でちょっと触ってみたりソースを読んでみたところ…S2JDBCを触っていた自分にとってめちゃくちゃ良いフレームワークとなっていました!これはスゴイ使いやすい。モチロンS2JDBC未経験の人でも全然おk。

自分としては「GAE/JのDatastore操作としてJDOから入るとハマる、誤解した理解をしてしまう」という意見をずっと持っていたので、ちょっと触る程度の初心者の方にはLow-Level APIで説明をしていました。ちょっと触ってもらうだけなら今まで通りでもいいですが、「実際に使っていくためのフレームワークは?」という話が出たりそれが前提の場合は、datastoreの操作を行うフレームワークとしてslim3を絶賛オススメします。生でLow-level APIを触るだけでアプリを組むのはなかなか危ないですし、どっちみち何かかぶせるのは間違いないですしね。
これからの自分は次のような手順で説明するかなw

  1. まずはLow-level APIを生で触る事でdatastoreの正しい理解をしてもらう
  2. slim3を使う事で、Low-Level APIをラップすると従来のORM的な便利さが得られる事を理解してもらう
  3. JDOのdatastore実装(datanucleus)の説明を軽く行う(もちろん随所で危険性の説明などを混ぜてdisりつつ説明する)
  4. JPAのdatastore実装(datanucleus)は論外であると切り捨てる。

自分でLow-Level API用のフレームワークを作っちゃう人は、モチロンそれでもいいと思いますけどね。自分はWicket-slim3datastore、jquery|AIR-t2framework-slim3datastore、あたりで使っていくかなぁ。

独自のプロジェクトでSlim3を使う方法

slim3のdatastoreの仕組みとしては2段階の仕組みがあります。

  1. Model用のクラスをslim3用のアノテーションで就職し、それをslim3用のslim3-genを使用して、タイプセーフにModelを扱うためのMetaクラスを生成する
  2. slm3本体のorg.slim3.datastore.Datastoreのメソッドを使用して実際の操作を行う

Slim3では上記のModel用のMetaクラスを生成するためにAPTを使っており、そのための設定をすればおkです。eclipseを前提に手順を書いておきます。お手軽に設定でき、お手軽に試せます。

  1. slim3-gen-${version}.jarslim3-${version}.jarをダウンロード(slim3-gen)(slim3)する。正しいダウンロード元がわからんので、適当にリンクを張ってみましたが、正式なダウンロード元がわかる人はそこからダウンロードしましょう。slim3プロジェクトとslim3-genプロジェクトをそれぞれsvn checkoutしてantを実行して自分でビルドしてもいいと思います。が、その場合は作業中のソースをビルドしてしまったりする可能性がありますのでご注意を。
  2. モジュールをプロジェクトにコピーする
    1. slim3-gen-${version}.jarは、先の説明の通りコンパイル時に必要なモジュールですので、実行時に適用されるクラスパス(war/WEB-INF/libとか)には配置せず、プロジェクト直下にlibとかのフォルダを作ってそこへおいておきましょう。
    2. slim3-${version}.jarは実行時に必要ですから、war/WEB-INF/lib等、実行時に参照されるフォルダに配置します。
    3. slim3-${version}.jarをclasspathに追加します。package explorerからだと図のような操作で簡単に追加sできます。
  3. eclipseのAPTの設定を行う
    1. プロジェクトのプロパティを開き、[Java Copmiler][Annotation Processing]を開き、図のようにチェックを設定します。
    2. すぐ下にある[Java Copmiler][Annotation Processing][Factory Path]を開き、[Add Jars]ボタンをクリックして、先の手順でプロジェクト配下のどこかにコピーしたslim3-gen-${version}.jarを選択します。

これだけの手順でslim3のdatastoreを使用する準備は完了です。モデル用のクラスをorg.slim3.datastore.Modelアノテーションで修飾し、保存するとHogeMetaのようなMetaというサフィクスが付加されたクラスが生成されます。使い方は、下記のサイト(slim3の新datastoreAPIの紹介としては一番最初のエントリかな!?)にも書かれていますし、近いうちにslim3の公式サイトのドキュメントが充実していく事だと思います。

追記

Google Groupへのリンクを貼るのを忘れてました。自分もユーザとなりそうなので、遅ればせながら加入させて頂きますた。

2009年10月3日土曜日

#appengine MakeSyncCallServlet

昨日のappengine-java-nightに参加した皆さんなら、エントリのタイトルだけ見たら中身を見る必要はありませんね!

ちょっと今は時間がないのでコードだけうpしますが、クライアント側の環境で通常通りappengineのサービスにアクセスしたら、なぜかデプロイ環境側のサービスにアクセスする、という仕組みが動作しました。

  1. リクエストされたバイト配列とサービス名、メソッド名、アプリケーション名(うっかり間違ったアプリを触るのを防ぐため。)を使ってmeksynccallをするだけのサーブレットを作成し、デプロイ環境にデプロイする
  2. クライアント側では、makeSyncCallへのリクエストを上記のサーブレットへ転送するだけのApiProxy.Delegateを実装し、ApiProxy#setDelegate(ApiProxyLocalImpl)した後で、ApiProxy#setDelegate()する。
  3. クライアント側で普通にデータストアサービスやMemcacheサービスへアクセスする。
  4. それら全てのサービス(UserServiceはそもそもmakeSyncCallを通らないので無理ですが)へのアクセスがサーバ側で実行される!

サンプル

こんなカンジのサンプルで、デプロイ環境側のデータストアに書き込んだり読み込んだり、memcacheのstatisticsを取得したりする事に成功しました。

@Before
public void setUp() throws MalformedURLException {
  ApiProxy.setEnvironmentForCurrentThread(newEnvironment());
  ApiProxy.setDelegate(new ApiProxyLocalImpl(new File("target")) {});
  // MakeSyncCallServletへアクセスするためのDelegateで上書き!
  ApiProxy.setDelegate(new MakeSyncCallServletDelegate(new URL(
      "${アプリのURL}${MakeSyncCallServletのパス}")));
}

@After
public void tearDown() {
  ApiProxyLocalImpl proxy = (ApiProxyLocalImpl) ((MakeSyncCallServletDelegate) ApiProxy
      .getDelegate()).getOriginal();
  ApiProxy.setDelegate(null);
  ApiProxy.setEnvironmentForCurrentThread(null);
}

@Test public void runQuery() {
  Query query = new Query("_makeSyncCallTest");
  DatastoreService service = DatastoreServiceFactory.getDatastoreService();
  List<Entity> list = service.prepare(query).asList(FetchOptions.Builder.withOffset(0).limit(1000));
  for (Entity entity : list) {
    System.out.println(ToStringBuilder.reflectionToString(entity));
  }
}

@Test public void put() {
  Entity entity = new Entity("_makeSyncCallTest");
  entity.setProperty("timestamp", new Date(System.currentTimeMillis()));
  DatastoreService service = DatastoreServiceFactory.getDatastoreService();
  service.put(entity);
}

@Test public void statistics() {
  MemcacheService service = MemcacheServiceFactory.getMemcacheService();
  Stats statistics = service.getStatistics();
  System.out.println(statistics);
}

MakeSyncCallServlet

package com.shin1ogawa.servlet;

import java.io.IOException;
import java.net.HttpURLConnection;
import java.util.logging.Level;
import java.util.logging.Logger;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.apache.commons.lang.StringUtils;
import org.apache.wicket.util.io.IOUtils;

import com.google.apphosting.api.ApiProxy;

/**
 * リクエストされたbyte配列を{@link ApiProxy#makeSyncCall(String, String, byte[])}へ引き渡し、
 * その結果をレスポンスするだけのServlet.
 * <p>特殊なサーブレットなので、web.xmlにてセキュリティを設定しておくのがおすすめ。</p>
 * <div><ul>
 * <li>HttpHeaderに以下を設定する。
 * <ul><li>{@literal serviceName}</li><li>{@literal methodName}</li>
 * <li>{@litera applicationId}</li></ul></li>
 * <li>payloadにProtocolBufferで出力されたbyte配列を設定して{@literal POST}する。</li>
 * <li>{@literal application/octet-stream}で
 * {@link ApiProxy#makeSyncCall(String, String, byte[])}の結果を返すので、クライアント側でよしなに。</li>
 * </ul></div>
 * 
 * @author shin1ogawa
 */
public class MakeSyncCallServlet extends HttpServlet {

  private static final long serialVersionUID = 2380791176214953417L;
  private static final String APPLICATION_ID = "applicationId";
  private static final String METHOD_NAME = "methodName";
  private static final String SERVICE_NAME = "serviceName";
  private static Logger logger = Logger.getLogger(MakeSyncCallServlet.class.getName());

  @Override
  protected void doPost(HttpServletRequest req, HttpServletResponse resp)
      throws ServletException, IOException {
    String serviceName = req.getHeader(SERVICE_NAME);
    String methodName = req.getHeader(METHOD_NAME);
    String applicationId = req.getHeader(APPLICATION_ID);
    logger.info(applicationId + ":" + serviceName + "#" + methodName);
    if (validateParameters(resp, serviceName, methodName, applicationId) == false) {
      return;
    }
    byte[] requestBytes = IOUtils.toByteArray(req.getInputStream());
    logger.info(applicationId + ":" + serviceName + "#" + methodName + ": requestBytes.length="
        + (requestBytes != null ? requestBytes.length : "null"));
    byte[] responseBytes = null;
    try {
      @SuppressWarnings("unchecked")
      byte[] bytes = ApiProxy.getDelegate().makeSyncCall(ApiProxy.getCurrentEnvironment(),
          serviceName, methodName, requestBytes);
      responseBytes = bytes;
    } catch (Throwable th) {
      logger.log(Level.WARNING, applicationId + ":" + serviceName + "#" + methodName, th);
      resp.setStatus(HttpURLConnection.HTTP_INTERNAL_ERROR);
      resp.setContentType("text/plain");
      th.printStackTrace(resp.getWriter());
      resp.getWriter().flush();
      return;
    }
    if (responseBytes == null) {
      logger.info(serviceName + "#" + methodName + ": responseBytes == null.");
      responseBytes = new byte[0];
    } else {
      logger.info(serviceName + "#" + methodName + ": responseBytes.length="
          + responseBytes.length);
    }
    resp.setContentType("application/octet-stream");
    resp.getOutputStream().write(responseBytes);
    resp.getOutputStream().flush();
  }

  private boolean validateParameters(HttpServletResponse resp, String serviceName,
      String methodName, String applicationId) throws IOException {
    if (StringUtils.isEmpty(serviceName)) {
      onErrorInParameters(resp, "serviceName was not specified.");
      return false;
    }
    if (StringUtils.isEmpty(methodName)) {
      onErrorInParameters(resp, "methodName was not specified.");
      return false;
    }
    if (StringUtils.isEmpty(applicationId)) {
      onErrorInParameters(resp, "applicationId was not specified.");
      return false;
    }
    // 念のためサーバ環境のApplicationIdと同じかどうか確認する。
    String serverApplicationId = ApiProxy.getCurrentEnvironment().getAppId();
    if (serverApplicationId.equals(applicationId) == false) {
      onErrorInParameters(resp, "applicationId was not equals to " + serverApplicationId
          + ".");
      return false;
    }
    return true;
  }

  private void onErrorInParameters(HttpServletResponse resp, String x) throws IOException {
    resp.setStatus(HttpURLConnection.HTTP_INTERNAL_ERROR);
    resp.setContentType("text/plain");
    resp.getWriter().println(x);
    resp.getWriter().flush();
  }
}

MakeSyncCallServletDelegate

Http通信の手を抜くためにURLFetchServiceを使ってるけど、commons-httpclientに置き換えるつもり。

public class MakeSyncCallServletDelegate implements ApiProxy.Delegate<Environment> {

  @SuppressWarnings("unchecked")
  private final ApiProxy.Delegate<Environment> original = ApiProxy.getDelegate();

  final URL url;

  MakeSyncCallServletDelegate(URL url) throws MalformedURLException {
    this.url = url;
  }

  public void log(Environment environment, LogRecord logRecord) {
    getOriginal().log(environment, logRecord);
  }

  public byte[] makeSyncCall(Environment environment, String serviceName, String methodName,
      byte[] request) throws ApiProxyException {
    if (serviceName.equals("urlfetch")) {
      return original.makeSyncCall(environment, serviceName, methodName, request);
    }
    URLFetchService service = URLFetchServiceFactory.getURLFetchService();
    try {
      HTTPRequest httpRequest = new HTTPRequest(url, HTTPMethod.POST);
      httpRequest.addHeader(new HTTPHeader("serviceName", serviceName));
      httpRequest.addHeader(new HTTPHeader("methodName", methodName));
      httpRequest.addHeader(new HTTPHeader("applicationId", environment.getAppId()));
      httpRequest.setPayload(request);
      HTTPResponse httpResponse = service.fetch(httpRequest);
      if (httpResponse.getResponseCode() == 500) {
        System.out.println(new String(httpResponse.getContent()));
        return new byte[0];
      }
      return httpResponse.getContent();
    } catch (IOException e) {
      e.printStackTrace();
      return new byte[0];
    }
  }

  public ApiProxy.Delegate<Environment> getOriginal() {
    return original;
  }
}

#appengine java night #1( #ajn1 )に参加した

ゲットしたノウハウ

  • 楽観的排他制御にはBigTableの排他制御+独自のチェック(またはJDOのVersion管理+自前の更新前のバージョンチェック)を使う
  • JDOを使う場合は自動でTransactionを開始する機能が邪魔なので止めておく
  • PersistenceManagerを開くタイミング、閉じるタイミング
  • Low-Level APIのパラレルGETは存在しないKeyをパラメタに渡してもエラーにならない
  • スキーマのバージョンをEntityに持たせておくとマイグレーションが必要な場合に便利

感想

実はひがさんも私も予定した事を全ては伝えきれなかったのですが、それでも問題なかったと思います。発表者の発表の合間合間に会場の皆さんを交えた色々な議論が交わされた分だけ、ひがさんも私も予定より押してしまった要因なのですが、個人的にこの進み方は良かったと感じました。おかげさまでより濃い内容になったんだと思います。appengine java nightの次回以降の発表者の方は、資料は普段の半分のボリュームくらいで見積もるのがいいかもしれませんw

懇親会も含めて、とにかく充実していたなぁという感想で、今後も定期的に継続してやっていけるととても面白いと思います。自分も協力できるトコを積極的に協力していこうと思います。楽しみです。

shin1ogawaの発表資料

説明無しでスライドだけだとちょっと伝わらんなぁと感じますが、とりあえずアップロードしておきますた。会場では発表できなかった点について補足しておきます。

「親キー指定のQueryでJDOのようにOneToManyを持ったBeanを構築する」話は、仕組みをわかった人なら納得できるし、JDOの中の動きも垣間見えると思います(EntityGroupはKeyだけでry)。

テストの話や「ApiProxy, Delegate」の話を見てみると、例えば最近どなたかが話していた「UserServiceへのアクセスはコストが低い」という理由も納得できると思います(UserServiceだけは他のサービスと違って外にアクセス(makeSyncCall)する必要がない)。

「Delegate#makeSyncCall()」の話を見てみると、アレコレ妄想が膨らんで便利なツールを作りたくなると思います。

感謝

色々世話をしてくださったGoogleの横田さん、サイオスの松尾さん、appengine java night開催に関して色々と仕切ってくださったスティルハウス佐藤さん楠本さん、貴重なお話をしてくださったひがさん、皆様ありがとうございました!マクブクを貸してくれた嫁ちゃんもありがとう!