WebLogic をバージョンアップしたときに、(JSPで)空白が「null」となってしまう問題

アプリケーションサーバを、WebLogic6からWebLogic10にバージョンアップするという仕事で、問題が発生しました。問題となったのは、JSPスクリプトレットを評価した結果が null のときです。


<% String s = null; %><%= s %>
この実行結果は、JSPの仕様からすると「null」と出力されて当然なのですが、WebLogic6ではこれを空白に変換します。担当したアプリケーションは、このWebLogic6の仕様に基づいて作られていたので、<%= null %>となる箇所が随所にありました。JSPの仕様にあわせて修正するとなると、

<%= s == null? "": s %>
としなければならないので、これは大変な修正量だ、と途方にくれていたのですが、簡単な解決策がありました。
http://edocs.beasys.co.jp/e-docs/wls/docs70/upgrade/upgrade6xto70.html#327303
ちゃんと下位互換性を保てるようにできてました。「printNulls」というフラグを false に設定することで、

<%= null %>
このコードは空白に変換されるようになります。一件落着。

JavaScriptでブラウザの判別(各種JavaScriptライブラリの判定方法を抜粋)

JavaScriptのライブラリを使わずに開発しているときに、ブラウザの判定をするのって意外と悩むので、調べた結果をまとめておきます。Prototype.jsJQuery、MooTools、Dojo について調べました。

Prototype.js の場合

  Browser: {
    IE:     !!(window.attachEvent && !window.opera),
    Opera:  !!window.opera,
    WebKit: navigator.userAgent.indexOf('AppleWebKit/') > -1,
    Gecko:  navigator.userAgent.indexOf('Gecko') > -1 && navigator.userAgent.indexOf('KHTML') == -1,
    MobileSafari: !!navigator.userAgent.match(/Apple.*Mobile.*Safari/)
  },

JQuery の場合

var userAgent = navigator.userAgent.toLowerCase();

jQuery.browser = {
	version: (userAgent.match( /.+(?:rv|it|ra|ie)[\/: ]([\d.]+)/ ) || [])[1],
	safari: /webkit/.test( userAgent ),
	opera: /opera/.test( userAgent ),
	msie: /msie/.test( userAgent ) && !/opera/.test( userAgent ),
	mozilla: /mozilla/.test( userAgent ) && !/(compatible|webkit)/.test( userAgent )
};

●MooTools の場合

var Browser = new Hash({
  Engine: {name: 'unknown', version: ''},
  Platform: {name: (navigator.platform.match(/mac|win|linux/i) || 
                    ['other'])[0].toLowerCase()},
  Features: {xpath: !!(document.evaluate), air: !!(window.runtime)},
  Plugins: {}
});

if (window.opera)
  Browser.Engine = {name: 'presto', version: (document.getElementsByClassName) ? 950 : 925};
else if (window.ActiveXObject)
  Browser.Engine = {name: 'trident', version: (window.XMLHttpRequest) ? 5 : 4};
else if (!navigator.taintEnabled)
  Browser.Engine = {name: 'webkit', version: (Browser.Features.xpath) ? 420 : 419};
else if (document.getBoxObjectFor != null)
  Browser.Engine = {name: 'gecko', version: (document.getElementsByClassName) ? 19 : 18};
Browser.Engine[Browser.Engine.name] = Browser.Engine[Browser.Engine.name + Browser.Engine.version] = true;

Dojo の場合

var dua = navigator.userAgent;
var dav = navigator.appVersion;
var tv = parseFloat(dav);

d.isOpera = (dua.indexOf("Opera") >= 0) ? tv : 0;

var idx = Math.max(dav.indexOf("WebKit"), dav.indexOf("Safari"), 0);
if(idx) {
  d.isSafari = 
    parseFloat(dav.split("Version/")[1]) || 
    ( ( parseFloat(dav.substr(idx+7)) >= 419.3 ) ? 3 : 2 ) || 2;
}
d.isAIR = (dua.indexOf("AdobeAIR") >= 0) ? 1 : 0;
d.isKhtml = (dav.indexOf("Konqueror") >= 0 || d.isSafari) ? tv : 0;
d.isMozilla = d.isMoz = (dua.indexOf("Gecko") >= 0 && !d.isKhtml) ? tv : 0;
d.isFF = d.isIE = 0;
if (d.isMoz) {
  d.isFF = parseFloat(dua.split("Firefox/")[1]) || 0;
}
if (document.all && !d.isOpera) {
  d.isIE = parseFloat(dav.split("MSIE ")[1]) || 0;
}

Prototype.jsJQueryはわりとシンプルな判定方法を使っているのに対し、Dojoはだいぶ複雑ですね。その代わりDojoでは、isMoz、isFFが区別できたり、isKhtml、isAIRなんてのがあったり、そもそもブラウザの判別がtrue/falseの2択ではなくてバージョン番号までわかったりと、かなり高機能になってます。
ま、用途に応じて使い分けましょう、と。

Javaで文字列の暗号化/複合化(※外部ライブラリを使わずに)

僕がいま係っているシステムで、別システムのログインパスワードを文字列でデータベースに格納することになりました。これを実現するために、Javaで文字列の暗号化/復合化を行う方法を調べたので、その結果を記しておきます。(※既に別の方々が解説している内容をまとめただけですが。)

まず、大きく以下の工程にわけて考えます。

  1. 文字列を暗号化する(バイト配列を得る)
  2. 暗号化されたバイト配列を文字列化する(暗号化文字列を得る)
  3. 暗号化された文字列をバイト配列化する(暗号化バイト配列を得る)
  4. バイト配列を復号化する(文字列を得る)

「1. 文字列を暗号化する(バイト配列を得る)」
Java Tips:手軽に暗号化・復号化するには? このページにばっちり解説してあるとおりです。

import javax.crypto.Cipher;
import javax.crypto.spec.SecretKeySpec;

public static byte[] encrypt(String key, String text)
    throws Exception {
		
    SecretKeySpec sksSpec = 
        new SecretKeySpec(key.getBytes(), "Blowfish");
		
    Cipher cipher = Cipher.getInstance("Blowfish");
    cipher.init(javax.crypto.Cipher.ENCRYPT_MODE, sksSpec);
		
    return cipher.doFinal(text.getBytes());
}

「4. バイト配列を復号化する(文字列を得る)」
工程2,3は飛ばして「4. バイト配列を復号化する(文字列を得る)」です。これも先ほどのページに解説してあるとおりです。

import javax.crypto.Cipher;
import javax.crypto.spec.SecretKeySpec;

public static String decrypt(String key, byte[] encrypted) 
    throws Exception {
		
    SecretKeySpec sksSpec = 
        new SecretKeySpec(key.getBytes(), "Blowfish");

    Cipher cipher = Cipher.getInstance("Blowfish");
    cipher.init(Cipher.DECRYPT_MODE, sksSpec);
		
    return new String(cipher.doFinal(encrypted)); 
}

「2. 暗号化されたバイト配列を文字列化する(暗号化文字列を得る)」
これが以外に悩みました。普通に new String(byte[]) とやると一部の情報が失われてしまいます。たぶん、文字コードとのマッピングの問題なんだと思います。では別のやり方を探してみたところ、、、Java/暗号化/Blowfish方式 このページで紹介されているやり方はその問題はクリアしているのですが、commonsライブラリを使っているのが僕にとってはちと障害でした。
で、なるべく外部ライブラリを使わずにバイト配列から文字列を作っている方法を調べてみたところ、、、やまろうのJavaなわけ このページがまさにぴったりの内容でしたので、さっそく参考にさせて頂きました。

import java.io.ByteArrayOutputStream;
import java.io.OutputStream;
import javax.mail.internet.MimeUtility;

public static String encodeBase64(byte[] data) throws Exception {
  	
    ByteArrayOutputStream forEncode = new ByteArrayOutputStream();
  	
    OutputStream toBase64 = MimeUtility.encode(forEncode, "base64");
    toBase64.write(data);	
    toBase64.close();
  	
    return forEncode.toString("iso-8859-1");
}

「3. 暗号化された文字列をバイト配列化する(暗号化バイト配列を得る)」
これも先ほどのページに書いてあるとおりのやり方で解決しました。

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.InputStream;
import javax.mail.internet.MimeUtility;

public static byte[] decodeBase64(String base64) throws Exception {

    InputStream fromBase64 = MimeUtility.decode(
        new ByteArrayInputStream(base64.getBytes()), "base64");

    byte[] buf = new byte[1024];
    ByteArrayOutputStream toByteArray = new ByteArrayOutputStream();
  	
    for (int len = -1;(len = fromBase64.read(buf)) != -1;) 
        toByteArray.write(buf, 0, len);
  	
    return toByteArray.toByteArray();
}

以上で完成です。例外処理をいれなければコードもかなりシンプルです。情報を提供してくださっている方々に感謝、感謝です。

MySQLで階層化されたデータを扱う(再帰的に検索しないで済む方法)

Oracleの場合は、Start With〜Connect By Prior〜というSQLで階層化されたデータを取り扱うことができます。では、MySQLではどうか?下記のサイトでしっかり解説してありました。

Managing Hierarchical Data in MySQL
階層化されたデータをMySQLで扱う(上記の日本語訳)

要約すると、

  • 階層化データを扱うには、Adjacency List モデルとネストセットモデルの2つがある
  • Adjacency List モデルは直感的でわかりやすい。更新も簡単。
  • しかし、Adjacency List モデルで表されたデータを検索するのはやや面倒(MySQLでは。)
  • 一方、ネストセットモデルで表されたデータは検索しやすい
  • しかし、ネストセットモデルは更新が面倒

といったところです。どちらの方法にもそれぞれメリット・デメリットがありますが、Adjacency List モデルには、階層が深くなる(または深さの制限がない)と階層データとして取得することはできない、という致命的な問題があるようです。
今回、僕がやりたかったのは「メールのスレッド表示データ」の実現だったので、階層が深くなることが十分にありえます。なので、必然的にネストセットモデルを選ぶことにしました。


ネストセットモデルの問題
ネストセットモデルでは親子関係を表すために、左右の番号を使用します。が、その管理が非常に面倒です。上記サイトで紹介されているやり方では、末端にノードが追加されると、それだけで全体の番号の振り直しが必要になる場合があります。これでは、「メールのスレッド表示」をする上で問題です。という訳で、自分なりに解決策を考えました。

  • 左右の番号を連番にせずに、あらかじめ大きな値を設定しておく

上記サイトでは、左右の番号(lft,rgt)を[1,20][2,9][10,19]、、としていますが、これは必ずしも連番でなくても良さそうなので[1,10000][2,2499][2500,4999]、、というように番号を振ります。これは、右の値として予め大きな値を設定しておくことで、後でノードが追加になったときに他のノードに影響がでないようにする、という狙いです。

番号の振り方についてもう少し補足します。例えば、以下のようなデータがあったとします。


[乗り物]
+-- [車]
+-- [セダン]
+-- [バス]
+-- [トラック]
+-- [電車]
+-- [在来線]
+-- [新幹線]
ここで、左右の値として使用できる番号の範囲を 0〜10001 とします。とすると当然、[乗り物]=[0,10001]になります。次に[乗り物]の子(車,電車)ですが、[車]=[1,5000]、[電車]=[5001,10000]としてしまうと、次に[乗り物]の子が追加になったときに番号を振りなおさなければなりません。なので、あらかじめ1つ分だけ余裕を残しておき [車]=[1,3333]、[電車]=[3334,6666]とします(※66667〜10000が余裕分)。[車]の子についても同様に[セダン]=[2,832]、[バス]=[833,1665]、[トラック]=[1666,2498] とします(※2499〜3332が余裕分)。

このように番号を振ることで、例えば[セダン]の下に子ノードが追加になったときにも、他のノードには一切影響を与えることなく子ノードのみを追加することができます。これでネストセットモデルの番号の管理が大分楽になりました。一方で、以下の点には注意が必要です。

  • 番号の余裕分は、ツリーを初めに作った段階とき割り当てるため、後から追加していくと番号が足りなくなる可能性がある
例えば上記の例でいくと、後から[乗り物]の子ノード[二輪車]が追加になった場合を想定してみます。この場合、余裕分としてみていたのは6667〜10000なので、[二輪車]=[6667,8333]となります(8334〜10000が余裕分)。ここで、さらに後から[乗り物]の子ノード[飛行機]が追加になった場合を考えると、余裕分は8334〜10000しか残ってないので、[飛行機]=[8334,9167]となります(9168〜10000が余裕分)。[車]=[1,3333]と比べると、[飛行機]はずいぶんと値の幅が小さくなってしまっているのが分かります(1/4になっています)。すなわち、[飛行機]の下にぶら下がることのできる子ノードの数は、[車]と比べて 1/4 となってしまいます。

以上を踏まえた上で、左右の値として使用できる番号の範囲の検討が必要です。例えば、0〜1024とした場合は、1024=2^10 なので、子ノードは最大でも10階層しかもつことができません。今回の「メールのスレッド表示」では、階層が最大12、階層あたりの子ノードが最大6と見積もりましたので、値の範囲としては 0〜16777215(=2^24)としました。

以上、「MySQLで階層化されたデータを扱う」方法についての考察でしたが、Oracleの「Start With〜Connect By Prior〜」の便利さがよくわかりました。Adjacency List モデルなら何も考えずにリンクだけつないどけばいいですからね〜。

java.util.logging.Loggerを使う上での注意点

java.util.logging.Loggerを初めて使ったのですが、ちょっとハマってしまった点などあったので紹介しておきます(主に設定ファイル=logging.propertiesまわり)。
○問題

  1. logging設定ファイルはシステムクラスローダによりロードされる
  2. logging設定ファイルでHandlerの設定をする際、クラス単位でしか設定できない
  3. ユーザコードで設定を変更しても、VM起動時に読み込んだ設定が有効になったまま
  4. ファイルにログを出力する際、Logger単位でファイルをロックしてしまう。

1. logging設定ファイルはシステムクラスローダによりロードされる


これは何が問題かというと、サーバ上で動かすアプリケーションで、Formatterを自作しようとしたとき問題となります(そして、こういった局面は結構あると思います)。
サーバ上で動かすアプリケーションの場合、通常、アプリケーションサーバが独自でクラスローダを持っています。そして、自分が作ったクラス(=ユーザクラス)というのはアプリケーションサーバのクラスローダによりロードされます。一方、logging設定ファイルは(デフォルトのものであろうと、起動パラメータで指定した場合であろうと)システムクラスローダによりロードされます。
このため、logging設定ファイルにユーザクラス(=Formatter)を記述している場合に、システムクラスローダがユーザクラスをロードできず、エラーとなってしまいます。ユーザクラス(=Formatter)をシステムライブラリに追加してやると、ロードできるのですが。

2. logging設定ファイルでHandlerの設定をする際、クラス単位でしか設定できない


例えば、FileHanderクラスを使ってログをファイルに出力する場合、logging設定ファイルに次のような指定をします。


java.util.logging.FileHandler.pattern = %h/application.log
これでは複数のファイルには出力できないですね、クラスに対して指定してしまってるので。
ちなみに、logging設定ファイルを使わずにアプリケーションコードから出力ファイルを指定する場合は、FileHandlerのインスタンスごとに指定できます(というかそれが普通ですね)。

○1. 2. の解決策

サーバアプリの場合は、設定ファイルは使用せず、コードで行う。
サーバ上で動作するコードで、ロガーのコンフィギュレーションをしましょう。そうすれば、クラスローダの都合でユーザクラスが読み込めない、といった問題もなく、また、Handlerの設定はインスタンスごとに行うことができます。

3. ユーザコードで設定を変更しても、VM起動時に読み込んだ設定が有効になったまま


JREを普通にインストールした状態だと、ルートロガーの出力レベルは、INFO以上となっています。そのため、ユーザコードでカスタムロガーのを作成したときに、うっかり出力レベルの指定を忘れていると、ルートの指定(=INFO以上)が有効になります。
僕はカスタムロガーの設定をする際に、Handlerの出力レベルはALLにしたのですが、ロガーの出力レベルを設定するのを忘れていたために、FINE以下のログが出力されず、2時間ぐらいハマってしまいました。

○3. の解決策
ロガーのコンフィギュレーションは注意深く行う。
期待した動作にならない場合は、デフォルトの設定が有効になっている可能性が高いです。そんなときはもう一度、自分が行った設定を見直してみましょう。

4. ファイルにログを出力する際、Logger単位でファイルをロックしてしまう


ロガーを複数定義し、それぞれから同じファイルにログを出力しようとすると、競合が発生してしまうみたいです。こうなった場合、ロガーは自動的に連番を振って、別ファイルにログを出力します。監視することを考えると、なるべくファイルに出力したいのですが。
回避策としては、ログを出力するたびに Handler.close() メソッドを呼び出すという方法もあるのですが、それはそもそもの使い方とは違っているような。

○4. の解決策

ファイルに出力するときは、共通親ロガーを使う。
複数のロガーから単一のファイルにログを出力することはできないので、代わりに複数のロガーの共通の親ロガーを作成します。そして、子の方のロガーでは何も指定せずに親に処理を委譲し、親の方では、通常どおりログをファイルに出力します。
こうすることで、ロガー:ファイルが1:1の関係になるので、競合を回避できます。ただし、複数のロガーに共通の親ロガーを作成できない場合は、いまのところ打つ手なしです。

JavaでFriendクラスを実現するためのパターン(フレンドアクセスブリッジパターン?)

以前、JavaでFriendクラスのような事を実現する方法というエントリを書いたのですが、もう少しすっきりした書き方を見つけたので紹介&再掲載しておきます。

【やりたいこと】Fooクラスのfooメソッドを、別パッケージのBarクラスにのみ公開したい

まず、Fooクラス、Barクラスを作成。

Foo.java

public class Foo {
  private void foo() { System.out.println("foooooo"); }
}

Bar.java

package b;
public class Bar {
  private void bar() {
    new Foo().foo(); // ←コンパイルエラー
  }
}

上記のBarクラスは、当然コンパイルエラーとなる。そこでまず、Fooクラスに特定のFriendからのアクセスを許可するためのBridge的な内部クラス(=FriendFoo)を追加する。
Foo.java

public class Foo {
  public static class FriendFoo {
    public void foo(Foo f) {
      f.foo();
    }
  }
  private void foo() { System.out.println("foooooo"); }
}

そして、BarクラスではFriendFooクラス経由で foo() メソッドを呼び出すようにする。

Bar.java

package b;
public class Bar {
  private static class FriendFoo friend = new Foo.FriendFoo();

  private void bar() {
    Foo f = new Foo();
    friend.foo(f); // FriendFoo.foo→Foo.foo
  }
}

以上でBarクラスからの呼び出しは成功。ただしこれだけだと、Barクラス以外からのアクセスできてしまうので、それを禁止するためのコードを、Fooクラス側に追加。

Foo.java

public class Foo {
  public static class FriendFoo {
    protected FriendFoo() {
      if (!this.getClass().getName().startWith("b.Bar"))
        throw new RuntimeException();
    }
    public void foo(Foo f) {
      f.foo();
    }
  }
  private void foo() { System.out.println("foooooo"); }
}

以上で完成。ここでは、FriendFooクラスのコンストラクタで、インスタンス化したクラスのクラス名をチェックすることで、公開範囲を限定しています。つまり、b.Barの内部クラスとして作成されたサブクラスからでなければ、FriendFooのインスタンスは作成できない、という具合に。

補足ですが、さらにここの条件を startWith("b") || startWith("c.dd") に変更すれば、"b"パッケージ以下すべて、および"c.dd"パッケージ以下すべてに公開、などといった細かい制御もできます。これはC++のフレンドクラスにはないメリットかも。

MySQLで大きなデータを登録しようとするとcom.mysql.jdbc.PacketTooBigException: 発生

MySQLで、3M程度のデータを登録しようとすると、以下のような例外が発生。


com.mysql.jdbc.PacketTooBigException: Packet for query is too large (3526504 > 1048576).
You can change this value on the server by setting the max_allowed_packet' variable.
at com.mysql.jdbc.MysqlIO.send(MysqlIO.java:3068)
at com.mysql.jdbc.MysqlIO.sendCommand(MysqlIO.java:1834)
at com.mysql.jdbc.MysqlIO.sqlQueryDirect(MysqlIO.java:1976)
at com.mysql.jdbc.ConnectionImpl.execSQL(ConnectionImpl.java:2503)
at com.mysql.jdbc.PreparedStatement.executeInternal(PreparedStatement.java:1737)
at com.mysql.jdbc.PreparedStatement.executeUpdate(PreparedStatement.java:2022)
at com.mysql.jdbc.PreparedStatement.executeUpdate(PreparedStatement.java:1940)
at com.mysql.jdbc.PreparedStatement.executeUpdate(PreparedStatement.java:1925)

エラーメッセージが言っているように、「max_allowed_packet」を変更すれば直りました。すなわち、my.cnf(Windows の場合は my.ini)に、以下の記述を追加して完了。


[mysqld] ←←← [mysql]でないことに注意!
max_allowed_packet = 16M
MySQLのマニュアルページはこちら

それにしても親切なエラーメッセージです。僕も見習わなくては。