- PR -

Tomcatでのダイナミックプロキシの使用について

投稿者投稿内容
かつのり
ぬし
会議室デビュー日: 2004/03/18
投稿数: 2015
お住まい・勤務地: 札幌
投稿日時: 2005-03-14 14:48
ダイナミックプロキシとTomcatのクラスローダについて質問させていただきます。

環境 j2sdk1.4.6(win32)
開発 Eclipse3.0.1
APサーバ Tomcat5.0.28

ダイナミックプロキシクラス(java.lang.reflect.Proxyで生成したクラス)の
インスタンスをセッションに格納して使用しているのですが、
リロード処理が行われる際にClassNotFoundExceptionが発生します。
ダイナミックプロキシを生成する為のインターフェイスが、
リロード処理のセッション復元時に見つからないという例外です。

これは、直列化されたオブジェクトを復元する際にクラスの解決を行いますが、
例外を見る限り、クラスの解決をStandardClassLoaderで行おうとしてしています。
Webアプリ側で作成したインスタンスですので、
本来ならWebappClassLoaderで解決しなければいけません。

実際に見つからないと言われているクラスをjarにして
tomcatのcommon/libフォルダに配置すると、例外は発生しません。
(StandardClassLoaderから見える位置にクラスが存在すれば問題はない)

例外が発生する条件は、リロード処理が行われセッションの復元が行われる時ですが、
Webアプリの処理の中で直列化・復元を行う場合には一切問題ありません。
(プロキシクラスを解決するクラスローダがWebappClassLoaderであるため)
また通常のクラスのインスタンスであれば、復元処理の問題は発生しません。

コード:
2005/03/14 14:23:16 org.apache.catalina.session.StandardManager start
致命的: 永続記憶装置からセッションをロード中の例外です
java.lang.ClassNotFoundException: jp.overdose.buggle.beans.UserBean
	at org.apache.catalina.loader.StandardClassLoader.loadClass(StandardClassLoader.java:854)
	at org.apache.catalina.loader.StandardClassLoader.loadClass(StandardClassLoader.java:721)
	at java.lang.ClassLoader.loadClassInternal(ClassLoader.java:302)
	at java.lang.Class.forName0(Native Method)
	at java.lang.Class.forName(Class.java:219)
	at java.io.ObjectInputStream.resolveProxyClass(ObjectInputStream.java:630)
	at java.io.ObjectInputStream.readProxyDesc(ObjectInputStream.java:1469)
	at java.io.ObjectInputStream.readClassDesc(ObjectInputStream.java:1432)
	at java.io.ObjectInputStream.readOrdinaryObject(ObjectInputStream.java:1626)
	at java.io.ObjectInputStream.readObject0(ObjectInputStream.java:1274)
	at java.io.ObjectInputStream.readObject(ObjectInputStream.java:324)
	at org.apache.catalina.session.StandardSession.readObject(StandardSession.java:1342)
	at org.apache.catalina.session.StandardSession.readObjectData(StandardSession.java:885)
	at org.apache.catalina.session.StandardManager.doLoad(StandardManager.java:416)
	at org.apache.catalina.session.StandardManager.load(StandardManager.java:343)
	at org.apache.catalina.session.StandardManager.start(StandardManager.java:657)
	at org.apache.catalina.core.StandardContext.start(StandardContext.java:4294)
	at org.apache.catalina.core.StandardContext.reload(StandardContext.java:3043)
	at org.apache.catalina.core.StandardContext.backgroundProcess(StandardContext.java:4658)
	at org.apache.catalina.core.ContainerBase$ContainerBackgroundProcessor.processChildren(ContainerBase.java:1619)
	at org.apache.catalina.core.ContainerBase$ContainerBackgroundProcessor.processChildren(ContainerBase.java:1628)
	at org.apache.catalina.core.ContainerBase$ContainerBackgroundProcessor.processChildren(ContainerBase.java:1628)
	at org.apache.catalina.core.ContainerBase$ContainerBackgroundProcessor.run(ContainerBase.java:1608)
	at java.lang.Thread.run(Thread.java:534)



これがTomcatの仕様なのか、作り方がまずいのか
はたまたTomcatのバグなのか、判断がつきません。
common/libに配置することなく例外が発生しないようにするには
どのように対処すればいいのでしょうか?
シュン
ぬし
会議室デビュー日: 2004/01/06
投稿数: 328
お住まい・勤務地: 東京都
投稿日時: 2005-03-14 15:37
ObjectInputStream#resolveProxyClass()はprotectedメソッドなので、
オーバーライドが効きます。
これを自作オーバーライドしたObjectInputStreamサブクラスでオブジェクト
の読み込みを行うのが一番手っ取り早そうだと思いますが…
かつのり
ぬし
会議室デビュー日: 2004/03/18
投稿数: 2015
お住まい・勤務地: 札幌
投稿日時: 2005-03-14 15:51
返事ありがとうございます。

ObjectInputStream#resolveProxyClass()の検討もしてみたのですが、
自分で行う場合ではなくTomcatの自動リロード処理の中で行われている為、
自分で生成したObjectInputStreamを使う事ができないですね。

ちなみに書き忘れていましたが、通常のインスタンスとダイナミックプロキシの
インスタンスがセッション内に混在していても例外が発生します。
シュン
ぬし
会議室デビュー日: 2004/01/06
投稿数: 328
お住まい・勤務地: 東京都
投稿日時: 2005-03-14 16:20
ぬぬぬ…

動作とソースコードを見る限り、
ObjectInputStream#resolveProxyClass()内部でのlatestUserDefinedLoader()
の呼び出しを回避しないとイカンですよね?

それが無理となると、Proxyオブジェクトをセッションに格納するのは辞めてしまう、
というのが良さそうですね。そこを回避するわけにはいかないのでしょうか?
Proxyをセッションに格納するというのは、どのような理由によるものなのでしょう?
シュン
ぬし
会議室デビュー日: 2004/01/06
投稿数: 328
お住まい・勤務地: 東京都
投稿日時: 2005-03-14 16:29
お、今良いことを思いつきました。これ、イケませんか?

1.セッションに直接Proxyオブジェクトをぶら下げるのではなくて、
ラッパーオブジェクトをかますようにする。

public class ProxyWrapper implements Serializable{
 private ProxyInterface impl;
 //getter+setterを実装
}

2.ラッパーオブジェクトのreadObject()、writeObject()
を適当にオーバーライドしてしまう。
かつのり
ぬし
会議室デビュー日: 2004/03/18
投稿数: 2015
お住まい・勤務地: 札幌
投稿日時: 2005-03-14 16:34
あまりスマートな方法ではないですが、
catalina.jarのorg.apache.catalina.util.CustomObjectInputStreamを
修正してうまく動くようになりました。

CustomObjectInputStreamというのはTomcatのWebappClassLoaderを使用して
オブジェクトの復元をしてくれるのですが、通常クラスしか解決してくれません。

そこでresolveProxyClass()をオーバーライドして、
スーパークラスの#resolveProxyClass()を呼び出し、
例外が発生したらCustomObjectInputStreamに存在する
WebappClassLoaderの参照を使ってProxyクラスのロードを行うようにしました。


根本解決と言えるのか微妙な所ですが・・・

ちなみにソースです。
コード:
/*
 * Copyright 1999,2004 The Apache Software Foundation.
 * 
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 * 
 *      http://www.apache.org/licenses/LICENSE-2.0
 * 
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package org.apache.catalina.util;

import java.io.InputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectStreamClass;
import java.lang.reflect.Proxy;

/**
 * Custom subclass of <code>ObjectInputStream</code> that loads from the class
 * loader for this web application. This allows classes defined only with the
 * web application to be found correctly.
 * 
 * @author Craig R. McClanahan
 * @author Bip Thelin
 * @version $Revision: 1.3 $, $Date: 2004/02/27 14:58:50 $
 */

public final class CustomObjectInputStream extends ObjectInputStream {

	/**
	 * The class loader we will use to resolve classes.
	 */
	private ClassLoader classLoader = null;

	/**
	 * Construct a new instance of CustomObjectInputStream
	 * 
	 * @param stream
	 *            The input stream we will read from
	 * @param classLoader
	 *            The class loader used to instantiate objects
	 * 
	 * @exception IOException
	 *                if an input/output error occurs
	 */
	public CustomObjectInputStream(InputStream stream, ClassLoader classLoader)
			throws IOException {

		super(stream);
		this.classLoader = classLoader;
	}

	/**
	 * Load the local class equivalent of the specified stream class
	 * description, by using the class loader assigned to this Context.
	 * 
	 * @param classDesc
	 *            Class description from the input stream
	 * 
	 * @exception ClassNotFoundException
	 *                if this class cannot be found
	 * @exception IOException
	 *                if an input/output error occurs
	 */
	public Class resolveClass(ObjectStreamClass classDesc)
			throws ClassNotFoundException, IOException {
		return Class.forName(classDesc.getName(), false, classLoader);
	}

	/**
	 * このメソッドを追加
	 */
	protected Class resolveProxyClass(String[] interfaces) throws IOException,
			ClassNotFoundException {
		try {
			return super.resolveProxyClass(interfaces);
		} catch (ClassNotFoundException e) {
			Class[] classObjs = new Class[interfaces.length];
			for (int i = 0; i < interfaces.length; i++) {
				Class cl = Class.forName(interfaces[i], false, classLoader);
				classObjs[i] = cl;
			}
			try {
				return Proxy.getProxyClass(classLoader, classObjs);
			} catch (IllegalArgumentException ex) {
				throw new ClassNotFoundException(null, ex);
			}
		}
	}
}


かつのり
ぬし
会議室デビュー日: 2004/03/18
投稿数: 2015
お住まい・勤務地: 札幌
投稿日時: 2005-03-14 16:41
投稿と閲覧のタイミングがずれてしまいました。

Proxyをセッションに格納したい経緯です。
DBから値を取得する処理を動的に行っているのですが、
Beanではなくインターフェイスに値を格納したい為です。

別にBeanでもいいのですが、カスタマイズとテストの容易性から
インターフェイスを選びました。

#遅いから止めろとか、そういう突っ込みは無しで・・・

[ メッセージ編集済み 編集者: かつのり 編集日時 2005-03-14 16:42 ]
シュン
ぬし
会議室デビュー日: 2004/01/06
投稿数: 328
お住まい・勤務地: 東京都
投稿日時: 2005-03-14 16:42
なるほどです。

DynamicProxyは比較的新しい機能であり、どマイナーです
ので、Tomcatが対応していなかった。という結論に見えますね。

Jakarta Tomcatに対するソースコミット提案のチャンスかも?

スキルアップ/キャリアアップ(JOB@IT)