GAE/jサンプルプログラムの読解

サンプルプログラムの読解

 今回は、Google公式のGAE/jサンプルプログラム、「Gusetbook Sample App」のうち、twitter botを作成するのに使えそうな所を読んでいく。本来は、スタートガイドに沿って自分で打ち込んでいく内容である。
スタート ガイド: Java - Google App Engine - Google Code
 実際に自分は一度そうしたのだが、ここではあえて、出来上がったコードを読み下して行く。これは、全体像が見えてからの方が、部分が理解しやすいと思うからだ。
 なお、自分は「Java Servlet」「JSP」って何? という段階にあったので、そういったレベルから読み解いている事をご承知頂きたい。
 どうして、そんなレベルなのに、java版を選んだのだろうか……。

「Gusetbook Sample App」の動作

 さて、SDKに含まれるGuesbook Sampleは「ユーザがポストする為のフォーム」「ポストされたデータを保存するサーブレット」「保存されたデータを表示するJSP」「グーグルアカウントでのログイン管理」といった要素から構成されている。
 このうち、「グーグルアカウントでのログイン管理」はtwitterbotには不要な為、ここでは無視する。以下には、それ以外の機能を実現する「Java Servlet」と「JSP」、「JDO」についてまとめる。

純粋なプログラムとして動く「Java Servlet

 Java Servletは、プロジェクト中では guestbook -> Source Packages -> guestbook の位置に置かれている。サンプルプログラム中では、最終的に四つのJava Servletが作られ、そのうちの一つ「SignGuestbookServlet.java」は直接呼び出されている。また「Greeting.java」と「PMF.java」の二つは間接的に呼び出されている。(最後の一つ「GuestbookServlet.java」は、サンプル機能の実装過程で打ち捨てられた亡骸である。カワイソス)
 間接的に呼び出されているServletは後のJDOの所で見るので、ここでは、直接呼び出されるServlet「SignGuestbookServlet.java」を見てみる。
 ファイルを開いてみると、これは HttpServlet を継承したクラスである事が分かる。

public class SignGuestbookServlet extends HttpServlet {
  ...
}

 内部動作としては、二つのサーブレットを呼び出すにとどまり、この段階ではなにをしているのか分からない。保留。
 ここで、Serveletを直接呼び出す方法を見てみる。これは、guestbook -> Webページ ->web.xml 内でServletとURLの対応を記述する事で実現している。

...
    <servlet>
        <servlet-name>sign</servlet-name>
        <servlet-class>guestbook.SignGuestbookServlet</servlet-class>
    </servlet>
    <servlet-mapping>
        <servlet-name>sign</servlet-name>
        <url-pattern>/sign</url-pattern>
    </servlet-mapping>
...

 まず、タグを使ってを対応させている。その上で、タグを使ってを対応させている。どうしてまた、こんな面倒な仕様なのだろうか……。

この節のまとめ
  • Java Servletの正体は、HttpServletクラスを継承した普通のクラス
  • Java Servletはクラスなので、内部的に別のServletから利用できる
  • Java Servletを直接呼び出すには、web.xmlで classとname、nameとurlの二段階に対応させる

HTMLの中にコードを埋め込む「JSP

 JSPは、プロジェクト中の guestbook -> Webページ に置かれている。サンプルでは唯一、「guestbook.jsp」だけが存在している。
 ファイルを開いてみると、これはHTML文書の合間にコードを埋め込んだ、PHP的プログラムである事が分かる。

<%@ page contentType="text/html;charset=UTF-8" language="java" %>

...

<html>
  <head>
    <link type="text/css" rel="stylesheet" href="/stylesheets/main.css" />
  </head>

  <body>

<%
    UserService userService = UserServiceFactory.getUserService();
    User user = userService.getCurrentUser();
    if (user != null) {
%>
<p>Hello, <%= user.getNickname() %>! (You can
<a href="<%= userService.createLogoutURL(request.getRequestURI()) %>">sign out</a>.)</p>
<%
    } else {
%>
<p>Hello!
<a href="<%= userService.createLoginURL(request.getRequestURI()) %>">Sign in</a>
to include your name with greetings you post.</p>
<%
    }
%>
    ...
  </body>
</html>

 どうやら<%と%>で挟まれた部分はコードとして扱われる様だ。(コードがあまりにも読みにくいので、Python版を選ばなかった事を少し後悔した)
 コード部分に関しては、やはり前述の二つのクラス「Greeting.java」と「PMF.java」を呼び出しているだけなので、保留。
 注目すべきは、多分ここ。

    <form action="/sign" method="post">
      <div><textarea name="content" rows="3" cols="60"></textarea></div>
      <div><input type="submit" value="Post Greeting" /></div>
    </form>

 JSPと言えど、通常のHTML同様formタグを使い、method="post"で情報を受け渡している。Webアプリケーションである以上、これが自然な方法なのだろう。

この節のまとめ
  • JSPはHTMLの中にコードを埋め込んだ物
  • 他画面への情報伝達には、formタグを利用する

GAEデータストアにアクセスする「JDO」

 さてさて、ようやくここで、これまで保留してきたクラス「Greeting.java」と「PMF.java」に斬り込んでいく。
 と、言いたいが、その前に一つ注意がある。GAEデータストアへJDOを使ってアクセスする場合、その旨を設定する必要がある。EclipseNetBeansとそのプラグインを使用していれば、自動で設定されるので問題ない。それらIDEを使用していない場合は、こちらを参照願いたい。
JDO を利用したデータストアの使用 - Google App Engine - Google Code
 また、そもそもJDOとは何であるのか。JDOはJava Data Objectの略語であり、プログラム中のオブジェクトを、オブジェクトのまま読み書きする機構の様である。つまり、保存の為にデータを加工する事なく、クラスのインスタンスをそのまま保存してしまうのである。
 以上、JDOの使用準備ができ、なんとなくその正体もつかめたら、コード実行順に沿ってその利用法を見ていく。

guestbook.jsp

 このサンプルプログラムを利用していった時、初めにJDOを使用するのは、guestbook.jspに置ける、読み込み部分である。
 該当するコードは次の部分である。

<%
    PersistenceManager pm = PMF.get().getPersistenceManager();
    String query = "select from " + Greeting.class.getName() + " order by date desc range 0,5";
    List<Greeting> greetings = (List<Greeting>) pm.newQuery(query).execute();
    if (greetings.isEmpty()) {
%>
<p>The guestbook has no messages.</p>
<%
    } else {
        for (Greeting g : greetings) {
            if (g.getAuthor() == null) {
%>
<p>An anonymous person wrote:</p>
<%
            } else {
%>
<p><b><%= g.getAuthor().getNickname() %></b> wrote:</p>
<%
            }
%>
<blockquote><%= g.getContent() %></blockquote>
<%
        }
    }
    pm.close();
%>

 上から見ていくと、まず、PersistenceManagerクラスのインスタンスpmを作っている。
 その後に、Greetingクラスの静的メソッドを使ってデータベースクエリを作り、pmからそのクエリを実行している様だ。又、そのデータベースクエリの返り値はGreetingクラスのListにキャストして受け取っている。
 その後はfor-each分でGreetingクラスを一つずつ取り出し、中身を描画している。
 全体としては、窓口を作り、そこにデータを要求して、帰ってきた値を利用する、シンプルな構造だと言える。次に、その構成要素を順番に見ていこう。

PMF.java

 データベースへの窓口となるPersistenveManager、それを作る為に利用されているPMF.javaを見てみると、その中は非常にシンプルである。

package guestbook;

import javax.jdo.JDOHelper;
import javax.jdo.PersistenceManagerFactory;

public final class PMF {
    private static final PersistenceManagerFactory pmfInstance =
        JDOHelper.getPersistenceManagerFactory("transactions-optional");

    private PMF() {}

    public static PersistenceManagerFactory get() {
        return pmfInstance;
    }
}

 公式には、以下の様に説明されている。

 データストアを使用するリクエストのたびに、PersistenceManager クラスの新しいインスタンスが生成されます。生成には、PersistenceManagerFactory クラスのインスタンスが使用されます。
 PersistenceManagerFactory インスタンスは初期化に時間がかかりますが、アプリケーションに必要なインスタンスは幸い 1 つのみです。インスタンスを静的変数に格納すれば、複数のリクエストや複数のクラスから使用できます。簡単なのは、静的インスタンスのシングルトン ラッパー クラスを作成する方法です。

 なるほど。つまりは、この通りに使えという事だ。

Greeting.java

 続いて、今回JDOによって読み書きされるオブジェクト、Greetingクラスを見てみる。

package guestbook;

import java.util.Date;
import javax.jdo.annotations.IdGeneratorStrategy;
import javax.jdo.annotations.IdentityType;
import javax.jdo.annotations.PersistenceCapable;
import javax.jdo.annotations.Persistent;
import javax.jdo.annotations.PrimaryKey;
import com.google.appengine.api.users.User;

@PersistenceCapable(identityType = IdentityType.APPLICATION)
public class Greeting {
    @PrimaryKey
    @Persistent(valueStrategy = IdGeneratorStrategy.IDENTITY)
    private Long id;

    @Persistent
    private User author;

    @Persistent
    private String content;

    @Persistent
    private Date date;

    public Greeting(User author, String content, Date date) {
        this.author = author;
        this.content = content;
        this.date = date;
    }

    ...

}

 長いので、getメソッド、setメソッドは省略した。
 さて、このコードでまず重要なのは(importは当然として)、

@PersistenceCapable(identityType = IdentityType.APPLICATION)

の部分である。この宣言を行う事で、GreetingクラスをJDOで読み書き可能である事を示している。
 また、このクラスはauhtor,content,dataという三つのプロパティを持ち、それには @Persistent アノテーションが付加されている。このアノテーションにより、サーバへ保存した時に、値を保存するプロパティを明示する。尚、@NotPersistentアノテーションを付加する事で、プロパティを意図的に保存しない事も可能である。
 また@PrimaryKeyと@Persistent(valueStrategy = IdGeneratorStrategy.IDENTITY)という、二つのアノテーションが付加されたLong型プロパティidもある。これは、GAEのデータストアに保存する時に自動的に設定される主キーを入れる為の物である。公式によれば

データ クラスには、対応するデータストア エンティティの主キーを格納するための専用のフィールドが 1 つ必要です。キー フィールドは 4 つの種類から選択でき、種類ごとに異なる値型とアノテーションを使って指定します(詳しくは、データの作成、取得、削除: キーをご覧ください)。最も単純なキー フィールドは長整数値です。JDO の場合は、オブジェクトを初めてデータストアに保存するときに、そのクラスのすべてのインスタンスの間で一意な値がこのキー フィールドに自動的に設定されます。長整数型のキーでは、@PrimaryKey アノテーションと、@Persistent(valueStrategy = IdGeneratorStrategy.IDENTITY) アノテーションを使用します:

との事。

再びguestbook.jsp

 構成要素を理解した上で、もう一度guestbook.jspを見ると、結局未だ分からないのは、次の部分だけだろう。

    String query = "select from " + Greeting.class.getName() + " order by date desc range 0,5";
    List<Greeting> greetings = (List<Greeting>) pm.newQuery(query).execute();

 ここで使われているクエリは、「JDOQL」による物だ。クエリ文字列だけを抜き出すと次のようになる

select from guestbook.Greeting order by date desc range 0,5

 多少MySQLをかじった事があれば、このクエリは、「(GAEデータストアから)guestbook.Greeting型で保存されているオブジェクトを選択」し、「dataプロパティの降順でソート」して「上から五番目までを」取り出すという命令だと解釈できる。
 注意すべきは、selectの直後に「from」が来る事、rangeは「開始位置,データ数」で指定する事だろう。
 二つの目の命令に関しては、公式に以下の説明がある。

クエリを準備するには、PersistenceManager インスタンスの newQuery() メソッドを呼び出します。文字列としては、クエリのテキストを渡します。メソッドを実行すると、クエリ オブジェクトが返されます。クエリ オブジェクトの execute() メソッドを呼び出すとクエリが実行され、適切な型の結果オブジェクトの List<> リストが返されます。クエリ文字列には、クエリの対象となるクラスの完全な名前を、パッケージ名も含めて指定する必要があります。

 (List<Greeting>)

 でキャストしているのは、この為だろう。
 読み込みと表示に関する動作は、以上である。

SignGuestbookServlet.java

 SignGuestbookServlet.javaは、guestbook.jspから以下のタグを介してデータを受け取り、それをGAEデータストアに保存している。

<form action="/sign" method="post">

 実際のコードは、次の様になっている。

public class SignGuestbookServlet extends HttpServlet {
    private static final Logger log = Logger.getLogger(SignGuestbookServlet.class.getName());

    public void doPost(HttpServletRequest req, HttpServletResponse resp)
                throws IOException {
        UserService userService = UserServiceFactory.getUserService();
        User user = userService.getCurrentUser();

        String content = req.getParameter("content");
        Date date = new Date();
        Greeting greeting = new Greeting(user, content, date);

        PersistenceManager pm = PMF.get().getPersistenceManager();
        try {
            pm.makePersistent(greeting);
        } finally {
            pm.close();
        }

        resp.sendRedirect("/guestbook.jsp");
    }
}

 突然doPostメソッドが出てきているが、これはこのクラスの継承元である抽象クラスHttpServletクラスが持つメソッドとなっている。そしてこれは、別のページからmethod="post"でデータを受け取った時、自動的に呼ばれる関数となっている。その引数には、HttpServletRequest型とHttpServletResponse型が渡され、前者がこのページに渡されるデータを保持し、後者がこのページから返すデータや動作を指定する。その点だけ理解できれば、もはや、このコードを読む事は難しくない。reqを通して,postされた"content"を取得し、それを保存しているだけである。実際の保存は、PersistenceManagerのmakePersistentメソッドで行われている。

この節のまとめ
  • GAEではJDOという仕組みを通して、データの保存と読み取りを行う
  • 実際にGAEデータストアをデータをやり取りするのは、PersistenveManagerクラスである
  • JDOではオブジェクトをそのまま保存する
    • 保存したいデータには各種アノテーションを付加し、それを明示する必要がある
  • データの取り出しには、JDOQLを利用する
  • postで渡されたデータは、HttpServletのdoPostソメッドで扱う事ができる

ログについて

 最終的なサンプルプログラムでは無くなっているが、GAEjavaではプログラムの動作ログを取ることもできる。
 その為に設定しなければならないのは、以下二つのファイルである。

  • guestbook -> Webページ -> WEB-INF -> appengine-web.xml
  • guestbook -> Webページ -> WEB-INF -> logging.properties

 どちらも、IDEを通してプロジェクトを生成した場合適切に設定される為、あまり気にする必要は無い。もしも手動でプロジェクトを管理している場合はこちらを参考にして欲しい。
 ログを取るためには、java.util.logging.Logger クラスが使用される。実は先程示したSignGuestbookServlet.javaのコードにも、Loggerクラスの使用が宣言されていた。

    private static final Logger log = Logger.getLogger(SignGuestbookServlet.class.getName());

 実際にサーバにログを残す場合には、次の様にinfoメソッドなどを利用する。

log.info("Hogehoge");
この節のまとめ
  • GAEではプログラムの動作ログを取る事もできる
  • 動作ログの記録には、java.util.logging.Logger クラスを利用する

まとめ

 非常に長くなったが、これにて、(Userserviceを除いて)GAEサンプルプログラムの読解は終了である。改めてプログラムを読み下した事で、GAE上で実行するプログラムの全体の流れ、及びそこで使われるAPI群が理解できた様に思う。

次回

 Java、特にGAEjavaから、twitter APIを利用する方法を調査する