JExcelApiを使ってみた。

どうもJExcelApiというやつライブラリは良いらしい…との噂。


と、言う訳で使ってみました。
普通にサンプルコードを作るのは、他でも見れるので、
S2DataSetの一部であるExcelファイルを読み書きする部分をJExcelApiで実装してみました。


機能的な違いも分ると良いなぁとは、思ったもののS2では只のデータストアとしてしかExcelを使って無いので、
細かいAPI的な違いしか分りませんでした。


プログラマで、POIを使うかJExcelApiを使うか考えてる人向けのメモ書きです。

まずは外側から比較。

ちなみに、僕が試してみたバージョンは、2.5.5


ライセンスはLGPL。POIはASL2.0。まぁ、この違いは余り問題にはならない感じ。


jarファイルのサイズは587KB。POI(poi-2.5.1-final-20040804.jar)は、784KB。
POIがExcel以外のファイルも操作出来る事を考えると、JExcelApiはサイズが大きいかもしんない。
まぁExcel以外のファイルなんぞ操作しないケドネ。


Mavenizeは一応されているけど、http://www.ibiblio.org/maven/から取れるのは、jxl-2.4.2.jar(2004-04-19 21:00リリース)。かなり古い。


外部のライブラリに対する依存は、log4jに対する依存位。POIはcommons-logging+log4jだけど。
どちらもjarファイル1つ追加するだけで使えるので、ライブラリとしては使い易いかと。

コードを見てみる。

太一は、オープンソースなライブラリを使う時には、必ずコードを一通り読みます。
問題が起きた時に、自分で解決する為です。
よって内容が理解出来無い程、複雑もしくは難解なライブラリを太一は使いません。
今の所、例外は、サーバ系のアプリケーションのみ。


解凍すると、.classpathファイルが入ってる、ほぅ。eclipseのプロジェクトになってるみたい。
でも、コンパイル通らないし…。もう1つ重要な事が。テストコードが無いのです。
更に、サンプルコードがjxl.demoと言うパッケージに入っているものの、テストと言うより、ほんとにサンプルコード。
うぅむ…。大丈夫かいな…。
コンパイルが通らない理由は、コチラ
要は、ロギングAPIを切り替える為に実装クラスのファイル名をAntで変える訳ですな…。
いや、commons-logging使っておくれよ…。
配布版のjarファイルだと、System.outにログを出力するです。
つまり、ログ出力を抑制しないのであれば、ビルドし直す必要がある。と言う事になるます。
JExcelApiでは、環境設定の類をシステムプロパティで行う様になっており、他にも幾つか設定が可能です。
jxl.WorkbookSettingsと言うAPIを使って、プログラムから設定を操作する事も出来ます。
それぞれのデフォルト値に関するドキュメントは存在しないので、コードを読んでデフォルト値が何なのかチェックしておく事をお勧めします。
特に、JExcelApiではSystem#gcを処理中にガリガリ呼んだりするので、要チェックです。


API自体は、interfaceを中心に設計されているので、好感が持てます。
只、1つだけ。jxl.CellTypeはイマイチ…。enumeration実装としては良く無いかと。
「==」演算子前提にするのは如何なものかと…。シリアライズしなければ、実害は無いのデスガ。


java.langパッケージにあるクラスと同じ名前のクラスを定義するのもチト勘弁して欲しいなぁ…と思ったり。
jxl.write.Boolean、jxl.write.Number とか。微妙にハマり所だと思います。

そして使ってみる。

HSSFが付いてない事を除けば、POIとほとんどAPI上の違いは無いです。
POIのAPIを知っているのであれば、特に問題なく使える筈です。
明らかな違いは、POIにはある行を表すオブジェクト(HSSFRow)が無い事。Cell[]として行を取る事が出来ます。
それから、セルを表すオブジェクトの扱いが微妙に違う事。
POIでは、HSSFCellからExcel上での型に合わせてアクセッサが用意されていますが、
JExcelApiでは、Excel上の型を表すCellのサブクラスにキャストして値を取り出します。
当該部分を抜粋するとこんな感じ。


まずはPOI。S2のXlsReaderからコードを抜粋してます。

public Object getValue(HSSFCell cell) {
  if (cell == null) {
    return null;
  }
  switch (cell.getCellType()) {
    case HSSFCell.CELL_TYPE_NUMERIC :
      if (isCellDateFormatted(cell)) {
        return TimestampConversionUtil.toTimestamp(
          cell.getDateCellValue());
      }
      return new BigDecimal(cell.getNumericCellValue());
    case HSSFCell.CELL_TYPE_STRING :
      String s = cell.getStringCellValue();
      if (s != null) {
        s = StringUtil.rtrim(s);
      }
      if ("".equals(s)) {
        s = null;
      }
      if (isCellBase64Formatted(cell)) {
        return Base64Util.decode(s);
      }
      return s;
    case HSSFCell.CELL_TYPE_BOOLEAN :
              boolean b = cell.getBooleanCellValue();
              return Boolean.valueOf(b);
    default :
      return null;
  }
}

同等のコードをJExcelApiで実装するとこんな感じになります。

public Object getValue(Cell cell) {
  if (cell == null) {
    return null;
  }
  CellType type = cell.getType();
  if(CellType.DATE == type || CellType.DATE_FORMULA == type) {
      Date date = ((DateCell) cell).getDate();
      // Excelから読み取った値に対して、デフォルトタイムゾーンとGMTとの差分を評価する必要が無い為。
      date.setTime(date.getTime() - TimeZone.getDefault().getRawOffset());
      return TimestampConversionUtil.toTimestamp(date);
  } else if(CellType.NUMBER == type || CellType.NUMBER_FORMULA == type) {
      return new BigDecimal(((NumberCell) cell).getValue());
  } else if(CellType.BOOLEAN == type || CellType.BOOLEAN_FORMULA == type) {
      return Boolean.valueOf(((BooleanCell) cell).getValue());
  } else if(CellType.LABEL == type || CellType.STRING_FORMULA == type) {
      String s = cell.getContents();
    if (s != null) {
      s = StringUtil.rtrim(s);
    }
    if ("".equals(s)) {
      s = null;
    }
    if (isCellBase64Formatted(cell)) {
      return Base64Util.decode(s);
    }
    return s;
  } else {
      return cell.getContents();
  }
}

ポイントは、セルが日付型だった時の扱い。
JExcelApiでは、Excelファイルから読み込んだ値を、java.util.Dateに直接引き渡してしまう為、
VMのデフォルトタイムゾーンGMTとの時差が出てしまいます。
僕の実行環境はモチロン日本なので、さっくりと9時間差がでます。*1
なんでCalenderクラス使ってくんねぇかなぁ……と。ちなみに、こんな実装になってます。

jxl.read.biff.DateRecordのコンストラクタより抜粋。

// Convert this to the number of days since 01 Jan 1970
int offsetDays = nf ? 24107 : 25569;
double utcDays = numValue - offsetDays;

// Convert this into utc by multiplying by the number of milliseconds
// in a day
long utcValue = Math.round(utcDays * 24 * 60 * 60 * 1000);

date = new Date(utcValue);


ついでにPOI側の実装も。HSSFDateUtil#getJavaDateから抜粋。

int startYear = 1900;
int dayAdjust = -1; // Excel thinks 2/29/1900 is a valid date, which it isn't
int wholeDays = (int)Math.floor(date);
if (use1904windowing) {
    startYear = 1904;
    dayAdjust = 1; // 1904 date windowing uses 1/2/1904 as the first day
} else if (wholeDays < 61) {
    // Date is prior to 3/1/1900, so adjust because Excel thinks 2/29/1900 exists
    // If Excel date == 2/29/1900, will become 3/1/1900 in Java representation
    dayAdjust = 0;
}
GregorianCalendar calendar = new GregorianCalendar(startYear,0,wholeDays + dayAdjust);
int millisecondsInDay = (int)((date - Math.floor(date)) * (double) DAY_MILLISECONDS + 0.5);
calendar.set(GregorianCalendar.MILLISECOND, millisecondsInDay);
return calendar.getTime();

実装の量と内容が全然チガウですよ…。
JExcelApiでは、ヒョイヒョイと計算してるのに対して、POI側は、GregorianCalendarを使ってます。
まぁ、ココに差がある事を知っていれば、対処のしようもあると思います。


ああと、最後に1つだけ。jxl.Workbook#close超重要。
ファイルを出力する時には、必ず呼び出して下さい。
外側から渡したOutputStreamを、外側で閉じてもファイルは出来ません。


それ以外の部分は、Seasar Sample Projectにコードをアップしておきますです。
興味のある方は、見比べてみて下さい。
と、思ったけどアップロードの仕方が分らないや…。この日記に気付いた誰か、教えて下さい…。

総括。

元々は、S2本体のPOIをJExcelApiで置き換えるつもりだったのだけれども、今の所はイマイチかなぁ…と。
理由は以下の通り。

  • テストコードが無い。
  • 日付型の扱いがイマイチ。
  • ロギングAPIの実装がイマイチ。
  • ドキュメントが少ない。と言うか、サンプルしか無いので、人にはオススメ出来ない…。

*1:これにハマって諦めかけたのは秘密だ。