タグ「Struts」が付けられているもの

HibernateでNoSuchMethodErrorが発生する問題

Struts + Hibernate Ver3を用いたWebアプリケーション実装中、特定のページの特定のユーザのみエラーが発生し、ページが表示されない現象に見舞われました。

ログを確認してみると、

java.lang.NoSuchMethodError: org.hibernate.hql.antlr.HqlBaseParser.recover(Lantlr/RecognitionException;Lantlr/collections/impl/BitSet;)V
が発生しておりました。

しかし、ググってもなかなか解決方法までたどり着けないんですよね。

日本のサイトで唯一情報源に成り得るのは、守破離 - Hibernate 3.2.1.gaでJPQL解釈時にNoSuchMethodErrorで落ちる事の対処法くらいでした。

(Googleを見るとHibernateネタは日本語サイトより中国語サイトの方が多いようなので)知り合いの中国人にも頼んで調べて貰ったのですが、HQL(Hibernate Query Language)の文字列において「from」を「form」の様に誤字すると発生するという情報くらいしか得られませんでした。

しかし、英語サイトも含めて調べていくと原因と対処法がやっと分かってきました。

Note to self - java.lang.NoSuchMethodError: org.hibernate.hql.antlr.HqlBaseParser.recover
Java Programming - hibernate issue

上記に挙げたサイトの情報をまとめる限り、やはり、HQLの構文に何らかの問題が存在するからなのです。

String HQL = " from User user where user.userId ='" + userId+"' and where user.fname ='" + fname +"' and where user.lname='" +lname + "'and where user.email='" + email +"'"; List result = session.createQuery(HQL);
の場合、「lname + "'and」の場所に注目していただけると理由が分かると思います。 「'」(シングルクォーテーション)と「and」の間に半角スペースが抜けているんです。

しかし、今回、私が体験した場合、もっと根が深く気付きにくい問題でした。
例えば、以下のようなコードです:

String id = "darekanoid";
String pass = "darekanopass";
// ↑上は説明の都合上
String HQL = " from User as user where user.userid = :userId"
                + "and user.password = :password";
Query query = session.createQuery(HQL);
query.setString("userId", id);
query.setString("password", pass);
List result = query.list();

HibernateでPreparedStatement(もどき?)を実施する場合は以上のようなソースコードになりますが、この場合、HQLの2行目が「"and」となっており、一行にまとめると「 from User as user where user.userid = :userIdand user.password = :password」になることでエラーが発生します。
本来なら必要ないと思われるHQLを2行に分割した理由は、開発中のプログラムのコーディング規約による制限があったからです。
ただ、エラーが発生しない場合もあるんですよ。
例えば、「String id = "aregeid "」みたく右側にユーザIDの右側にスペースが詰められている存在する場合です。CHAR型のデータを用いると、特定の文字に見たいない場合はスペースで補われますし、そのようなユーザIDを用いて検索するとNoSuchMethodErrorが発生しないようです。

この問題を解決する方法は、2点あります。

1点目は、antlr.jarのバージョンを2.7.6以上にする。
java.lang.NoSuchMethodError: org.hibernate.hql.antlr.HqlBaseParser.recoverが発生する原因は、HQLが正しくないことが原因です。antlr.jarは文字列の正規表現や文字列から数値への解析に用いられるJARだそうです。Hibernate Ver3では正しくないHQLを回復させるためにantlr.Parser.recover()メソッドを用いようとしているもとの思われます。しかし、antlr.jarが古い環境では読み込めず、今回のようなエラーが発生してしまうと考えれば納得が行きます。
もし、antlr.jarを新しいバージョンに変更できる環境であるならば、2008/02/11現在、Antlr Ver2の最新版である2.7.7に置き換えると良いと思います。

2点目はHQLが正しいか確認することw
今回、開発がある程度進んでしまった為、安定性を保証する為にantlr.jarを入れ替えることができなかったため、こちらの方を選択しました。
不思議なことに、"and"のように「and」の前にスペースがなくともなぜか無事に取得できる場合があるんですね。
HibernateはコンソールにHQLが正しいか出力する方法が存在します。開発中は、コンソールを確認し、HQLに問題が発生しないか確認する必要があると思います。

ぶっちゃげ、ちゃんと見てなかったからこのような事態が発生し、解決まで時間が掛かったということですがorz
そんなわけで、同じ悩みで開発が止まっている人の参考になれば( ̄∇ ̄)ノ♪

StrutsのActionServletからiTextのPDFを出力する

iTextを用いてJava ServletからPDFを作成するに関連する話題です。

iTextを用いてServletからPDFを出力することは、上記の記事で学習いたしました。
しかし、Strutsの場合、作成したPDFを出力させる機構がすぐに思いつきませんでした。

Strutsのアクションサーブレットは、サーブレットファイルに以下のようなメソッドの中でビジネスロジックを記述しますよね。

public ActionForward execute(ActionMapping mapping, ActionForm form, HttpServletRequest request, HttpServletResponse response) throws Exception {
  ...
  return mapping.findForward("ok");
}
「mapping#findForward(文字列)」の部分でstruts-config.xmlの指定した文字列のJSPに飛ばすことで、MVC(Model-View-Controller)モデルを実現しているわけです。

しかし、今回、StrutsでiTextのPDFを出力する場合はそうも言ってられません。
だからと言って、StrutsのActionサーブレットなのにdoPost()やらdoGet()やらを呼び出す方法にしてしまうのはStrutsの意味がなくなってしまいます。

そこでちょっと調べてみましたが、ActionServletからファイルを出力する方法は普通に存在するみたいです。

天使やカイザーと呼ばれて - Strutsでのファイルダウンロードの方法

なるほど。
ActionServletの「return mapping#findForward(文字列);」を「return null;」にし、その前の行に、HTMLソースやバイトコードを出力するようにすれば、HTMLだろうと、ファイルだろうとStrutsから出力できるようです。
CodeZineのiTextを利用してJavaからPDF形式の帳票を出力するのソースを参考に、StrutsからiTextのPDFを出力するための基本体系を書いておきます。

public ActionForward execute(ActionMapping mapping, ActionForm form, HttpServletRequest request, HttpServletResponse response) throws Exception {

  //出力用のStreamをインスタンス化します。
  ByteArrayOutputStream byteout = new ByteArrayOutputStream();

  //文書オブジェクトを生成
  //ページサイズを設定します。
  Document doc = new Document(PageSize.A4, 50, 50, 50, 50);

  try {
    //アウトプットストリームをPDFWriterに設定します。
    PdfWriter pdfwriter = PdfWriter.getInstance(doc, byteout);
  }

  ...

  // PDFの出力を終了します。
  doc.close();

  response.setContentType("application/pdf");
  response.setContentLength(byteout.size());
  OutputStream out = response.getOutputStream();
  out.write(byteout.toByteArray());
  out.close();

  return null;
}

赤字が今回学習の範囲ですw
非常に簡単ですね。

そんなわけで、今後のPDF出力プログラム作成時に使用させていただきます。

Strutsの<bean:write>で数値フォーマットに整形した値を出力するの続編として、お送りします。

Date型を持つ値を「2007/11/22」の様に、整形した値で出力したいときがありますよね。
Strutsの場合、Date型の変数を日付フォーマットで整形した値を出力する方法があるようです。

忘れっぽいエンジニアのJakarta Strutsリファレンス - 日付フォーマットを指定してプロパティを出力する<bean:write>

実は、前回の記事からも上記のリンクはたどり着けるのですが、残念なことに、現在、参考先サイトのコードにはミスがあったので、それも含めてコードの解説をします。

<bean:write name="usrdata" property="dateData" format="yyyy/MM/dd" />
<bean:write name="usrdata" property="dateData" format="yyyy/MM/dd HH:mm:ss" />
赤字にした部分は月を出力するようフォーマットの設定ですが、そのMMの部分が参考先サイトでは小文字でmmとなっています。2行目の時間出力まで含めたformat要素を見ていただけるとお分かりかとは思いますが、月を意図してmmとしてしまうと、その部分には分が表示されてしまいます。

単純なミスだとは思うのですが、試しに参考先サイトのformatのまま記述してしまうと、「2007/30/22」の様なもの凄い出力がされるということでして、楽しんでみてください(ぇ?w

以上、備忘録でした。

相変わらず放置具合が素敵な感じになっていますが、それでもブログ運営を続けます(≧∇≦)
サンプルが書かれているサイトを参考に製作していたため、思わぬ落とし穴に気付かなかった話でも書いてみようかと思います。

Struts+HibernateのWebアプリケーションを製作していたら、データベースに接続するページがやけに遅い気がしました。そこで、実際にLog4Jを用いて計測してみたところ、データベースの接続(Hibernateのセッション接続)のところで0.5秒前後掛かってました。
これはいくらなんでも掛かりすぎだろ...

データベースのコネクションプールが問題なのかと思い、hibernate.cfg.xmlのhibernate.connection.pool_sizeの値を変更してみましたが、状況としては全く変わらずorz

しかし、調べてみると原因は思わぬところにありました。
例えば、何かの情報について検索するDAOに以下のようなメソッドを作成するとします。

public List searchAll() {
Configuration config = new Configuration().configure();
SessionFactory sessionFactory = config.buildSessionFactory();
Session session = sessionFactory.openSession();
// 罠にはまった壁
List list = session.createCriteria(クラス名.class).list();
return list;
}
よくHibernateのサンプルを公開しているHibernate初心者向けサイトに行くと、メソッドごとに「//罠にはまった壁」から上の3行が書かれています。それが問題でした。 上の3行をメソッドごとに記述してしまうと、コネクションプールが設定されていたとしても、それを切断して新たなコネクションプールを作成してしまいます。コネクションプールを用いて接続までの時間を短縮させる設定が全く活かされなかったんですね。

ではどうするかと言いますと、「//罠にはまった壁」の上3行のうち、上位2行をプラグインとしてTomcat起動前に読み込ませる必要があります。

package hibernate;

import javax.servlet.ServletContext;
import javax.servlet.ServletException;
import org.apache.struts.action.ActionServlet;
import org.apache.struts.action.PlugIn;
import org.apache.struts.config.ModuleConfig;
import org.hibernate.SessionFactory;
import org.hibernate.cfg.Configuration;

public class SamplePlugIn implements PlugIn {

public void init(ActionServlet servlet, ModuleConfig config)
throws ServletException {
// Webアプリケーションの開始処理
System.out.println("*** SamplePlugIn initメソッド 起動 ***");

// Hibernateのセッションを取得
Configuration hibernateConfig = new Configuration().configure();
SessionFactory sessionFactory = hibernateConfig.buildSessionFactory();

// メッセージをServletContextに保存
ServletContext context = servlet.getServletContext();
context.setAttribute("sessionFactory", sessionFactory);

System.out.println("*** SamplePlugIn initメソッド 終了 ***");
}

public void destroy() {
// Webアプリケーションの終了処理
System.out.println("*** SamplePlugIn destory()メソッド ***");
}
}


次に、DAOやStrutsのActionクラス内でServletContext(Appilcationスコープ)に格納した「sessionFactory」を取得し、セッションを作成します。

ServletContext context = servlet.getServletContext(); Session session = context.getAttribute("sessionFactory").openSession();
その後は、Hibernateによりデータベースの処理を記述するだけです。

これでHibernateに0.05〜0.1秒で接続するようになり、体感的に遅く感じなくなりました。

※上の例ではアプリケーションスコープに格納する方法を採用しましたが、init()メソッドにSessionFactoryの作成までを行うstaticメソッドを作成し、同様にHibernateのセッションを静的メソッドで行う方法もあるようです。
そんなわけで、めでたしめでたしということで。

※StrutsのActionでHiberateのセッションを取得したい方は、StrutsからServletContextクラスを取得するにはも参考にどうぞ。

「5300」のような値を「5,900」と出力したい場合、色々と面倒ですよね。
しかし、Strutsの場合、long型やdouble型の基本型を数値フォーマットで整形した値を出力する方法があるようです。

忘れっぽいエンジニアのJakarta Strutsリファレンス - 数値フォーマットを指定してプロパティを出力する<bean:write>

とりあえず、ここで説明しなくても分かりやすい内容が上記のリンクにあるのでご参考に。

<bean:write name="usrdata" property="intData" format="###,###,###" />
<bean:write name="usrdata" property="doubleData" format="##0.0##" />

とか、本当便利ですね。
format属性作った人偉い!
むしろエロい!

以上、備忘録でした。

StrutsのActionFormを使っていると、validator()メソッドが結構便利です。
しかし、普通のActionServletでActionErrorを呼び出してエラーを表示させたい場合もあるじゃないですか。

そのための方法が、忘れっぽいエンジニアのJakarta Strutsリファレンス -
アクションクラスで生成したエラーのエラーメッセージを表示する<html:errors>に書かれていました。

ポイントは、

ActionMessages errors = new ActionMessages();
errors.add("userId", new ActionMessage("errors.required","ユーザーID"));
errors.add("userName", new ActionMessage("errors.invalid","名称"));

のようにActionMessageを用いてエラーを作成し、
saveErrors(request, errors);
return mapping.findForward("error");

のように、saveErrors(HttpServletRequest, ActionMessage)のメソッドを呼び出してから、findForward()メソッドを実行してやれば良いと。

簡単ですね。
そんなわけで備忘録としておいておきます∠( ̄∧ ̄)

Strutsのタグライブラリを用いたページを作っている最中に起きたトラブルへの備忘録です。

Strutsタグがない全てのページの基となるテンプレートページに、Strutsのタグライブラリを付け足しつつ編集していきました。
一通り完成したので、実際に動作させたところ、ページが表示されません。
「org.apache.struts.taglib.html.BEAN という名前のbeanが見つかりません」とエラーがでたものの、どこが原因になっているか分からない...orz

ググってみたところ、Java Look: formの要素のページが見つかりました。
ただし、なぜかindex.rdfの形で( ̄□ ̄;)!!
※上記のリンクはちゃんと普通のページにリンクしているのでご安心をw

ここを見て激しく高速に解決できました。
本来なら<html:form>タグの中に記述しなくてはいけない、<html:radio>、<html:checkbox>、<html:text>などのタグが<html:form>タグの外側に記述されているか、そのページに<html:form>タグがそもそも記述されていないかのどちらかです。

ちなみに、俺の場合、全てのページの基となるテンプレートをいじりながら製作したことで、<html:form>タグを<form>タグのままにしていたことが原因でしたorz
テンプレートをいじりつつ、本来のHTMLタグをActionフォーム用のStrutsタグに置き換えるときには注意ってことで。

StrutsからServletContextクラスを取得するには

MVCモデルで知られているJSP&ServletのStrutsフレームワークですが、Contorollerの部分にあたるActionを作成しているときにちと困ったことになりました。
普通のサーブレットの場合、サーブレットの中でServletContextを呼び出すには、

ServletContext sc = getServletContext();

で良いわけですが、Strutsの場合は上記のような構文をexecuteメソッドの中に記述しても動きません。
せっかくStrutsのプラグインを用いてあるクラスをアプリケーションスコープに格納したのに取得できないなんてかなりorzな訳ですが。

ググってみたところ、上記のコードにもうひと味付けることで解決できました。

ServletContext sc = getServlet().getServletContext();
Foo bar = (Foo)sc.getAttribute("bar");

例えば、上記のコードのようにすれば、StrutsのActionサーブレットにおいてもApplicationスコープから値を取得できます。

めでたし。めでたし。