Hatena::ブログ(Diary)

on the center line.

2008-10-07

Eclipseプロジェクト内に、Validate対象外のディレクトリを作る方法

| 16:55

Eclipseプロジェクト内に、Tomcatランタイムを格納すると便利だと思ってやってみたのですが、TomcatのサンプルのWebアプリが検証にひっかかってしまって、Eclipse上に大量のエラーがでてしまいました。

Tomcatのサンプルを捨ててしまうという解決策も考えたのですが、そもそも「Eclipseで特定のディレクトリ以下を検証の対象外とする方法はないのか?」という点が気になったので調べてみました。

結論からいうと、Eclipseの機能としてはない、が少しトリッキーな方法で実現できました。

まず、Eclipseの検証機能ですが、これはプロジェクト単位で有効/無効を切り替えることはできますが、ディレクトリ単位では無理なようです。一方で、Eclipseの機能として「.(ドット)」で始まるディレクトリは管理対象(検証対象)としない、という機能があります。これを利用しました。

つまり、Eclipseプロジェクト内に「.Tomcat」というディレクトリを作りました。そして、その下にTomcat一式を置くと、、、

検証エラーを発生させることなく、Tomcat一式をEclipseプロジェクト内に置くことができました。

ちなみに、「.(ドット)」から始まるディレクトリは管理対象(=検証対象)にはならないのですが、ライブラリ選択や、サーバ選択では普通にでてきました。万事解決。

2008-10-03

XMLHttpRequestオブジェクトでリダイレクトをハンドリングする方法

| 10:05

以前に、同じタイトルのエントリを書いたのですが、それよりもスマートなやり方がわかったので、書いておきます。

やりたいことについては前のエントリで書いたとおり。

  1. XMLHttpRequestで送ったリクエストが、サーバで認証エラー(セッションタイムアウトなど)となる
  2. このときサーバは認証エラー(=401)を返すのではなく、ログインページへのリダイレクトを返す
  3. クライアントXMLHttpRequest)は、リダイレクト応答をうけとり、自動的にリダイレクト先を追跡する
  4. 結果、クライアントログインページのHTMLを取得する(このときステータスコード=200)
  5. しかし、そもそもクライアントJSON形式のデータを期待しているためスクリプトエラーとなる

前のエントリでは、XMLHttpRequestでレスポンスヘッダが使えることに着目して、"Content-Type"ヘッダが"text/html"だったらページを強制的に遷移させる方法でした。しかし、この方法は「"Content-Type"ヘッダが"text/html"である正常なレスポンス」があったら使えません。そこで以下の方法を思いつきました。4. および 6. のところが今回紹介する方法になります。

  1. XMLHttpRequestで送ったリクエストが、サーバで認証エラー(セッションタイムアウトなど)となる
  2. このときサーバは認証エラー(=401)を返すのではなく、ログインページへのリダイレクトを返す
  3. クライアントXMLHttpRequest)は、リダイレクト応答をうけとり、自動的にリダイレクト先を追跡する
  4. ログインページでは、レスポンスヘッダに "AjaxRedirect: /Login.jsp" を追加する
  5. 結果、クライアントログインページのHTMLを取得する(このときステータスコード=200)
  6. クラインアンは、レスポンスヘッダをチェックし、"AjaxRedirect" ヘッダが設定されているので、その値のURLを追跡する

以上で、XMLHttpRequestを使ったリダイレクトの完成。サンプルコードも載せておきます。

<%
response.setHeader("AjaxRedirect", request.getContextPath() + "/Login.jsp");
%>
httpRequest.onreadystatechange = function() {
  if (httpRequest.readyState == 4 && httpRequest.status == 200) {
    var redctUrl = httpRequest.getResponseHeader("AjaxRedirect");
    if (redctUrl != null) 
      // リダイレクトURLが設定されていれば追跡
      window.location.href = redctUrl;

世捨て人世捨て人 2017/09/05 22:22 「しかし、この方法は「"Content-Type"ヘッダが"text/html"である正常なレスポンス」があったら使えません。」??何故!?
サーバーがtext/htmlを返してきたのだから、そのままページを表示すればいいのでは!?

2008-09-21

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

| 15:53

アプリケーションサーバを、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 %>

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

2008-06-27

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

| 12:06

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の関係になるので、競合を回避できます。ただし、複数のロガーに共通の親ロガーを作成できない場合は、いまのところ打つ手なしです。

2008-05-21

Socketに出力中に Connection reset by peer: socket write error

| 12:21

Javaでプロキシサーバのようなものを作っていたところ、クライアントへレスポンスを返している途中に、以下の例外が多発。

SocketException: Connection reset by peer: socket write error 

プログラムはすごく簡単なサンプルを参考にしてたので間違ってるとは思えなかった。。。

String CrLf = "\r\n";

ServerSocket ss = new ServerSocket(9950);
Socket s = ss.accept();

StringBuffer b = new StringBuffer();
b.append("HTTP/1.1 200 OK" + CrLf);
b.append("Connection: close" + CrLf);
b.append("Date: Tue, 20 May 2008 18:02:41 GMT" + CrLf);
b.append("Content-Type: text/html;charset=UTF-8" + CrLf);
b.append("Transfer-Encoding: chunked" + CrLf);
b.append(CrLf);

OutputStream o = s.getOutputStream();
o.write(b.toString().getBytes());

InputStream contents = any....    ←なんらかのコンテンツ

for (int data = 0; (data = contents.read()) != -1; ) 
	o.write(data);
	
o.flush();
o.close();
s.close();

が、うまく動いているサンプルと比べてみると「Transfer-Encoding: chunked」のところが違っている。うまく動いているサンプルは代わりに「Content-Length」ヘッダーがある。これは怪しそう、と思って調べてみたところ、以下のサイトに完璧な説明がありました。

http://www.cresc.co.jp/tech/java/Servlet_Tutorial/Att_01.htm

「Chunked Transfer-Encoding=厚切りエンコーディング」である、と。なるほど。以下のように修正したらバッチリ動きました。

String CrLf = "\r\n";

ServerSocket ss = new ServerSocket(9950);
Socket s = ss.accept();

StringBuffer b = new StringBuffer();
b.append("HTTP/1.1 200 OK" + CrLf);
b.append("Connection: close" + CrLf);
b.append("Date: Tue, 20 May 2008 18:02:41 GMT" + CrLf);
b.append("Content-Type: text/html;charset=UTF-8" + CrLf);
b.append("Transfer-Encoding: chunked" + CrLf);
b.append(CrLf);

OutputStream o = s.getOutputStream();
o.write(b.toString().getBytes());

InputStream contents = any....    ←なんらかのコンテンツ

byte[] bt = new byte[500];

for (int len = 0; (len = contents.read(bt, 0, 500)) != -1; ) {
	o.write(Integer.toString(len, 16).getBytes());	←←この部分を追加
	o.write('\r');
	o.write('\n');
	o.write(data, 0, len);
}
o.write('0');

o.flush();
o.close();
s.close();