Java版のGoogle App Engine でテンプレートエンジン

Java版のApp Engineをいろいろいじって見ている.Eclipse Plug inの助けがあるので,当初思ったよりはずっと使いやすい.やっぱりJavaのような言語はIDEがあってナンボ,なのか.

Java版のサンプルを実行してみて一番嫌だったのは,JSP (JavaServer Pages)を使う部分.JSPは,HTMLのファイルのなかに,Javaコードでロジックを埋め込むもので,動的にJava Servletコンパイルされて実行される.ある意味,非常に強力なテンプレートエンジンであると考えることもできなくもない.が,ロジックとプレゼンテーションの分離という観点からすると,あきらかになにか間違っている機構だと思う.HTMLファイルの中にimport文がずらずらならぶというだけでもなんかおかしいだろう.10年以上前(1998年)に作られた機構なので仕方が無いのかもしれないが.

テンプレートエンジンを使ってみる

JSPを使うのはいやなので,テンプレートエンジンを使えるか試してみた.結論から言うとまったく問題なく使えることがわかった.テンプレートエンジンとしてはFreeMarkerを使ってみた.たぶんVelocityでもなんでも同じように使えると思う.

  • jarファイルを用意する.
  • WEB-INF/lib に置く.
  • プロジェクトのプロパティ,Java Build Path, Libraryでjarファイルを指定.

FreeMarkerの場合は,FreeMarkerをビルドしたときにできるfreemarker.jarだけでよい.WEB-INF/lib下に置いておくだけで自動的にcloud上のappengineにも配備される.

サンプル

サンプルとして,ここにあるJSPServlet+テンプレートエンジンで置き換えてみる.

まずはServlet. ポイントは

  • テンプレートをサーブレットコンテナ内部のリソースとして取得するために,TemplateLoaderを指定すること
cfg.setTemplateLoader(
  new WebappTemplateLoader(this.getServletContext()));
  • テンプレートをServletコンテナトップ(/war)からの相対で指定すること.ここでは war/WEB-INF/greetings.tmplに置いているので,
    Template temp = cfg.getTemplate("WEB-INF/greetings.tmpl");

としている.

package greetings;

import freemarker.cache.WebappTemplateLoader;
import freemarker.template.Configuration;
import freemarker.template.DefaultObjectWrapper;
import freemarker.template.Template;
import freemarker.template.TemplateException;

import java.io.IOException;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.logging.Logger;

import javax.jdo.PersistenceManager;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import com.google.appengine.api.users.User;
import com.google.appengine.api.users.UserService;
import com.google.appengine.api.users.UserServiceFactory;


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

  public void doGet(HttpServletRequest req, HttpServletResponse resp)
                throws IOException {
    Map<String, Object> root = new HashMap<String, Object>();
    UserService userService =   
      UserServiceFactory.getUserService();
    User user = userService.getCurrentUser();

    PersistenceManager pm = PMF.get().getPersistenceManager();
    String query = "select from " + Greeting.class.getName();
    List<Greeting> greetings = 
      (List<Greeting>) pm.newQuery(query).execute();

    root.put("user", user);
    root.put("signOutUrl", 
             userService.createLogoutURL(req.getRequestURI())); 
    root.put("signInUrl", 
             userService.createLoginURL(req.getRequestURI())); 
    root.put("greetings", greetings);
	    
    Configuration cfg = new Configuration(); 
    cfg.setTemplateLoader(
      new WebappTemplateLoader(this.getServletContext()));
    cfg.setObjectWrapper(new DefaultObjectWrapper()); 
    Template temp = cfg.getTemplate("WEB-INF/greetings.tmpl");

    resp.setContentType("text/html");
    resp.setCharacterEncoding("utf-8");
    try {
       temp.process(root, resp.getWriter());
    } catch (TemplateException e) {
       resp.getWriter().println(e.getMessage());
    }
  }
}

テンプレート本体はこんな感じ.

<html>
<body>

<#if user??  >
  <p> Hello, ${user.nickname}! 
(You can <a href="${signOutUrl}">sign out</a>.)</p>
<#else>
  <p> Hello! <a href="${signInUrl}"> 
Sing in</a> to include your name with greetings you post.</p>
</#if>

<#if greetings?size == 0 >
  <p>The guestbook has no messages.</p>
<#else>
  <#list greetings as g >
    <#if g.author?? >
      <p><b>${g.author.nickname}</b> wrote:</p>
    <#else>
      <p>An anonymous person wrote:</p>			
    </#if>
    <blockquote>${g.content}</blockquote>	
  </#list>	
</#if>

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

JSPよりは実行速度はだいぶ遅いだろうが,こちらのほうが,遥かにロジックが分かりやすいと思うのだけど,どうだろうか.